多线程是一个很宏大的话题。
这一章和下一章,我们会用两章的篇幅来讨论多线程。
在这一章,我们会讨论:
什么是多线程
线程创建
线程传递入参
线程控制
线程调度
线程生命周期
线程安全
线程死锁
线程通信
在下一章《8.多线程 [1/2]》 ,我们会讨论
Java内存模型
多线程特性
ThreadLocal
原子类
Lock类
Volatile关键字
并发容器
线程池
什么是多线程
现在,让我们进入第一个话题,什么是多线程。
很多资料一提到多线程,就拿Windows的那个任务管理器举例子。
就像这样。
然后说,你们看,Code.exe开了多线程吧。
实际上,还真不是,这里Code.exe就是有多个不一样的,开的是多进程。
还有些资料,喜欢用Windows窗体来举例子。
就像这样。
这个在Winform中的确有,只有一个主线程,而且主线程在处理数据的时候,Winform窗体可能无法拖动。
但是呢,如果说这时候无法编辑,而且页面设置那个弹窗在抖动提示。是因为只有一个线程导致的话,这就不太对了。
Winform的窗体设置有一个属性就可以配置这个,具体实现原理我不太了解,但绝不是什么只有一个线程导致的。
我们在《6.网络编程》 讨论的那个BIO的缺陷的多线程解决方案时候,举了一个例子,在游戏《牧场物语:矿石镇的伙伴们》中,玩家经营了一个牧场,养鸡、养牛、种菜等,如果玩家忙不过来了,可以把某些活委托给小矮人。
这是一个比较通俗的多线程的例子。
但是,也存在不恰当之处。我们在上一章也讲过,这个例子会让大家误以为多线程就是快,实际上多线程不一定快,这个我们会在《8.多线程 [2/2]》 做详细的讨论。
同时,我们还说了,当时有一个线程都已经阻塞了,用多线程就是会快。这个我们会在本章讨论线程生命周期的时候就解释。
进程与线程
进程:
是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:
单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
一个线程只能属于一个进程,但是一个进程可以拥有多个线程。
多线程处理就是允许一个进程中在同一时刻执行多个任务。
线程是一种轻量级的进程,与进程相比,线程给操作系统带来创建、维护和管理的负担要轻,意味着线程的代价或开销比较小。
比如,现在大家在看这个博客,这时候会有一个或多个和浏览器相关的进程,然后每个进程内部又有一个或多个的线程。
线程创建
Java中线程有四种创建方式:
继承Thread类
实现Runnable接口
实现Callable接口
线程池
我们分别讨论。
继承Thread类
相关方法
方法名
说明
void run()
在线程开启后,此方法将被调用执行。
void start()
使此线程开始执行,Java虚拟机会调用run()方法。
步骤:
定义一个类MyThread继承Thread类
在MyThread类中重写run()方法
创建MyThread类的对象
启动线程
那么,来吧!
示例代码:
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan;public class MyThread extends Thread { @Override public void run () { for (int i = 0 ; i<10 ; i++){ System.out.println(Thread.currentThread().getName() + " " + i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 package com.kakawanyifan;public class MyThreadDemo { public static void main (String[] args) { System.out.println("主线程名字:" + Thread.currentThread().getName()); MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); my1.run(); my2.run(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 主线程名字:main main 0 main 1 main 2 main 3 main 4 main 5 main 6 main 7 main 8 main 9 main 0 main 1 main 2 main 3 main 4 main 5 main 6 main 7 main 8 main 9
有没有问题?
有!
我们上文的代码有Thread.currentThread().getName()
,现在子线程和主线程同名?而且,这个开多线程了吗?看起来是my1执行完成之后,执行了my2啊。
来,我们把run
换成start
。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 package com.kakawanyifan;public class MyThreadDemo { public static void main (String[] args) { System.out.println("主线程名字:" + Thread.currentThread().getName()); MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); my1.start(); my2.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 主线程名字:main Thread-0 0 Thread-1 0 Thread-1 1 Thread-0 1 Thread-0 2 Thread-0 3 Thread-0 4 Thread-0 5 Thread-0 6 Thread-0 7 Thread-0 8 Thread-0 9 Thread-1 2 Thread-1 3 Thread-1 4 Thread-1 5 Thread-1 6 Thread-1 7 Thread-1 8 Thread-1 9
这回似乎就对了。
解释说明:
为什么要重写run()方法?
因为run()是用来封装被线程执行的代码。
run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用。
start():启动线程;然后由JVM调用此线程的run()方法。
最后解释一下我们线程名的方法,顺带说一下设置线程名的方法。
设置线程名称和获取线程名称。
方法名
说明
void setName(String name)
将此线程的名称更改为等于参数name
String getName()
返回此线程的名称
Thread currentThread()
返回对当前正在执行的线程对象的引用
实现Runnable接口
第二种创建线程的方法是实现Runnable接口。
步骤:
定义一个类MyRunnable实现Runnable接口
在MyRunnable类中重写run()方法
创建MyRunnable类的对象
创建Thread类的对象,把MyRunnable对象作为构造方法的参数
启动线程
Thread构造方法
方法名
说明
Thread(Runnable target)
分配一个新的Thread对象
Thread(Runnable target, String name)
分配一个新的Thread对象,同时设置名称。
方法是新方法,但其实最后殊途同归,还是实例化了一个Thread。但其实这两种方法有区别,我们在下文很快就会进行比较讨论。
示例代码:
1 2 3 4 5 6 7 8 9 package com.kakawanyifan;public class MyRunnable implements Runnable { public void run () { for (int i=0 ; i<10 ; i++){ System.out.println(Thread.currentThread().getName() + " " + i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;public class MyRunnableDemo { public static void main (String[] args) { MyRunnable my = new MyRunnable(); Thread t1 = new Thread(my); Thread t2 = new Thread(my); t1.start(); t2.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Thread-0 0 Thread-1 0 Thread-0 1 Thread-1 1 Thread-0 2 Thread-0 3 Thread-1 2 Thread-0 4 Thread-1 3 Thread-0 5 Thread-1 4 Thread-0 6 Thread-1 5 Thread-0 7 Thread-1 6 Thread-0 8 Thread-1 7 Thread-0 9 Thread-1 8 Thread-1 9
实现Callable接口
Callabel和Runable相比,在名称方面一个是调用,一个是运行。既然强调是调用呢,那么就一定有不同于运行的特点,有返回值。
步骤:
定义一个类MyCallabel继承Callabel接口
两个注意:1、在范型处定义返回值类型;2、被重写的方法是call方法,而不是run方法。
利用FutureTask对MyCallable的对象进行包装。
实例化一个线程,参数是FutureTask对象。
启动线程。
获取返回对象。
来,试一下。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;import java.time.LocalDateTime;import java.util.concurrent.Callable;public class MyCallable implements Callable <String > { public String call () throws Exception { for (int i=0 ; i<10 ; i++){ Thread.sleep(1000 ); System.out.println(Thread.currentThread().getName() + " " + i); } return LocalDateTime.now().toString(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class MyCallableDemo { public static void main (String[] args) throws ExecutionException, InterruptedException { FutureTask futureTask = new FutureTask(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); System.out.println(futureTask.get()); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 Thread-0 0 Thread-0 1 Thread-0 2 Thread-0 3 Thread-0 4 Thread-0 5 Thread-0 6 Thread-0 7 Thread-0 8 Thread-0 9 2021-09-10T16:02:49.870
有没有问题?
我们在MyCallable的call方法中写了sleep(1000)
。但是,我们在MyCallableDemo中,启动线程后,立马去获取返回。那还能获取到吗?必须抛出异常啊。
然而,并没有。
为什么?
因为在futureTask.get()
这里阻塞了。
此方法会阻塞主线程直到获取到结果!
我们可以看一下FutureTask这个类的源码
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package java.util.concurrent;import java.util.concurrent.locks.LockSupport;public class FutureTask <V > implements RunnableFuture <V > { private volatile int state; private static final int NEW = 0 ; private static final int COMPLETING = 1 ; private static final int NORMAL = 2 ; private static final int EXCEPTIONAL = 3 ; private static final int CANCELLED = 4 ; private static final int INTERRUPTING = 5 ; private static final int INTERRUPTED = 6 ; 【部分代码略】 public V get () throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false , 0L ); return report(s); } 【部分代码略】 }
如果没有完成,就await(同步等待)。
特别的,我们还看到FutureTask类的是接口RunnableFuture的实现类。
示例代码:
1 public class FutureTask <V > implements RunnableFuture <V >
再来看一下RunnableFuture的源代码。继承自Runnable, Future。
示例代码:
1 2 3 4 5 6 7 public interface RunnableFuture <V > extends Runnable , Future <V > { void run () ; }
同时继承了两个接口?
这个没问题。
一个接口可以继承多个接口,一个类可以实现多个接口。但是,一个类只能继承一个类。
需要注意的是,继承了两个接口,其中一个就是我们上文讨论的Runnable。
结构如下
FutureTask还有其他方法
判断任务是否完成:isDone()
能够中断任务:cancel()
能够获取任务执行结果:get()
现在问,这些方法由哪个接口继承来,当然是由Future接口提供。
Callable方法与Runnable一样,方法是新方法,但其实最后殊途同归,还是实例化了一个Thread。
线程池
但是,上述三种方法,其实我们都不常用。更多的时候,我们通过线程池来创建线程。
线程池就像一个容器,这个容器中有很多线程,我们要用的时候,直接拿过来用,用完在放回去。
这样就不需要人工去创建和维护线程了。
(关于线程池,我们会在下一章进行更详细的讨论。)
线程池相关接口和类的关系图
这里,我们先来了解一下线程池相关接口和类的关系图。
最顶级的是Executor接口:
声明了execute(Runnable runnable)方法,执行任务代码
ExecutorService接口:
继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等
AbstractExecutorService抽象类:
实现ExecutorService接口,基本实现ExecutorService中声明的所有方法
ScheduledExecutorService接口:
继承ExecutorService接口,声明定时执行任务方法。
ThreadPoolExecutor类:
继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法;是线程池的核心实现类,用来执行被提交的任务
ScheduledThreadPoolExecutor类:
继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法;可以进行延迟或者定期执行任务。
但是呢,通过ThreadPoolExecutor创建线程池的操作比较多。Java提供了一个工具类:Executors类。(注意有s)
在下一章《8.多线程 [2/2]》 ,我们就会见到ThreadPoolExecutor创建线程池的操作比较多,到底多在哪里。
这一章,我们暂时只演示用Executors创建线程的简单方法。
基于线程池创建线程
步骤如下
使用Executors获取线程池对象。
通过线程池对象获取线程,并执行。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ExecutorsDemo { public static void main (String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3 ); executorService.execute(new MyRunnable()); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 pool-1-thread-1 0 pool-1-thread-1 1 pool-1-thread-1 2 pool-1-thread-1 3 pool-1-thread-1 4 pool-1-thread-1 5 pool-1-thread-1 6 pool-1-thread-1 7 pool-1-thread-1 8 pool-1-thread-1 9
特别的,假如我们线程池只有两个线程,但是我们执行三个,会怎么样?
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.kakawanyifan;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ExecutorsDemo { public static void main (String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2 ); executorService.execute(new MyRunnable()); executorService.execute(new MyRunnable()); executorService.execute(new MyRunnable()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;import java.time.LocalDateTime;public class MyRunnable implements Runnable { public void run () { System.out.println(LocalDateTime.now()); try { Thread.sleep(3000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果:
1 2 3 2021-09-10T16:56:42.192 2021-09-10T16:56:42.192 2021-09-10T16:56:45.197
排队。
在下一章《8.多线程 [2/2]》 ,我们就会解释为什么会这样。
现在问,这个程序结束了吗?
没有
线程池还在呢。
线程池关闭方法
executorService.shutdown();
等正在进行任务执行完,进行停止
executorService.shutdownNow();
不用等待正在进行任务执行完,立即停止
除了这种手动的方法,还有其他的自动的线程池关闭方法,我们会在下一章《8.多线程 [2/2]》 讨论。
比较
实现Runnable接口和继承Thread类比较
在上文我们说了,实现Runnable接口,这个方法是新方法,虽然最后殊途同归,还是实例化了一个Thread,但其实这两种方法有区别。
现在我们讨论。
实现Runnable接口和继承Thread类比较
Runnable接口更适合多个相同的程序代码的线程去共享同一个资源。
稍后我们就会看到多个线程共享同一个资源。
Runnable接口可以避免Java中的单继承的局限性。
Runnable接口代码可以被多个线程共享,代码和线程独立。
线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread的类。
所以呢,实现Runnable接口比继承Thread类好。
Runnable和Callable接口比较
相同点:
两者都是接口。
两者都可用来编写多线程程序。
两者都需要调用Thread.start()启动线程。
不同点:
实现Callable接口的线程能返回执行结果;而实现Runnable接口的线程不能返回结果。
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的不允许抛异常。
实现Callable接口的线程可以调用Future.cancel取消执行,而实现Runnable接口的线程不能。
那么,Runnable和Callable,哪个好?Callable的确功能更强大,毕竟是在Runable基础上进行了拓展。但是,这个还是根据具体的需求来吧。
题外话 在java中,每次程序运行至少启动需要启动几个线程? 两个线程。一个是main线程,一个是垃圾回收线程。 (关于垃圾回收,我们在《2.面向对象》 那一章提到过。)
线程传递入参
通过上文的讨论,我们已经知道了怎么获取线程任务执行的返回结果,也就是线程传递出参。
那么,我们怎么给线程传递入参呢?
我们怎么创建线程的?是不是实例化了一个Thread对象,或者Runable接口的实现类的对象,又或者Callable接口的实现类的对象。
所以呢,就有两种方法给线程传递入参。
通过构造方法传递入参。
通过变量的Set方法传递入参。
我们分别举例子说明。
通过构造方法传递入参
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;public class MyThreadPara extends Thread { private String name; public MyThreadPara (String name) { this .name = name; } public void run () { System.out.println(Thread.currentThread().getName() + " hello " + name); } public static void main (String[] args) { System.out.println(Thread.currentThread().getName()); Thread thread = new MyThreadPara("world" ); thread.start(); } }
运行结果:
1 2 main Thread-0 hello world
通过变量的Set方法传递入参
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;public class MyThreadPara implements Runnable { private String name; public void setName (String name) { this .name = name; } public void run () { System.out.println("hello " + name); } public static void main (String[] args) { MyThreadPara myThreadPara = new MyThreadPara(); myThreadPara.setName("world" ); Thread thread = new Thread(myThreadPara); thread.start(); } }
运行结果:
很多资料会说,还有一种传递入参的方式,是通过回调函数。但是我个人不认为在这个过程中进入了入参传递,而且这种方式应该也不常用。
线程控制
在之前的章节中,我们多次使用到了sleep这个方法,这个方法可以让线程停留指定的毫秒数,期间CPU不会去执行这个线程。
这就是一种线程控制的方法。除了这种方法,常见线程控制方法还有还有两种:
join()
setDaemon(boolean on)
方法名
说明
static void sleep(long millis)
使当前正在执行的线程停留(暂停执行)指定的毫秒数
void join()
只有这个线程死亡了,其他线程才可以执行。
void setDaemon(boolean on)
被标记为守护线程的线程,在主线程结束的时候,也会结束运行。
此外,还有suspend()
方法和stop()
方法,但是容易导致线程死锁,不推荐使用。
我们来举例子说明join和setDaemon。
join
示例代码:
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan;public class ThreadJoin extends Thread { @Override public void run () { for (int i = 0 ; i < 10 ; i++) { System.out.println(getName() + ":" + i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;public class ThreadJoinDemo { public static void main (String[] args) { ThreadJoin tj1 = new ThreadJoin(); ThreadJoin tj2 = new ThreadJoin(); ThreadJoin tj3 = new ThreadJoin(); tj1.setName("周天子" ); tj2.setName("魏" ); tj3.setName("齐" ); System.out.println(tj1.getPriority()); System.out.println(tj2.getPriority()); System.out.println(tj3.getPriority()); tj1.start(); try { tj1.join(); } catch (InterruptedException e) { e.printStackTrace(); } tj2.start(); tj3.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 5 5 5 周天子:0 周天子:1 周天子:2 周天子:3 周天子:4 周天子:5 周天子:6 周天子:7 周天子:8 周天子:9 魏:0 魏:1 齐:0 魏:2 齐:1 魏:3 齐:2 魏:4 齐:3 魏:5 齐:4 魏:6 齐:5 魏:7 魏:8 魏:9 齐:6 齐:7 齐:8 齐:9
一定要tj1
结束之后,其他线程才会执行。
setDaemon
示例代码:
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan;public class ThreadDaemon extends Thread { @Override public void run () { for (int i = 0 ; i < 100 ; i++) { System.out.println(getName() + ":" + i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan;public class ThreadDaemonDemo { public static void main (String[] args) { ThreadDaemon td = new ThreadDaemon(); td.setName("毛" ); Thread.currentThread().setName("皮" ); td.setDaemon(true ); td.start(); for (int i = 0 ; i < 10 ; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 皮:0 皮:1 皮:2 皮:3 皮:4 皮:5 毛:0 皮:6 皮:7 皮:8 皮:9 毛:1 毛:2 毛:3 毛:4 毛:5 毛:6 毛:7 毛:8 毛:9 毛:10 毛:11 毛:12 毛:13 毛:14 毛:15 毛:16 毛:17 毛:18 毛:19
线程调度
我们注意看上述第一个方法,当"周天子"这个线程结束之后,"魏"和"齐这两个线程是怎么执行的?是轮流吗?不是,似乎是一种无序的状态,是两个线程在竞争CPU。
这就涉及到线程的两种调度方式
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型
这里提到了CPU的时间片,CPU的执行时间分成了很多个细小的时间窗口,一个细小的时间窗口只执行一个线程。在Java中线程需要去抢CPU的时间窗口,就像我们抢订会议室一样。
还提到了线程的优先级,我们列举一下两个线程优先级相关方法。
方法名
说明
final int getPriority()
返回此线程的优先级。
final void setPriority(int newPriority)
更改此线程的优先级。(线程默认优先级是5;线程优先级的范围是:1-10)
示例代码:
1 2 3 4 5 6 7 8 9 10 package com.kakawanyifan;public class ThreadPriority extends Thread { @Override public void run () { for (int i = 0 ; i < 10 ; i++) { System.out.println(getName() + ":" + i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.kakawanyifan;public class ThreadPriorityDemo { public static void main (String[] args) { ThreadPriority tp1 = new ThreadPriority(); ThreadPriority tp2 = new ThreadPriority(); ThreadPriority tp3 = new ThreadPriority(); tp1.setName("飞机" ); tp2.setName("高铁" ); tp3.setName("汽车" ); System.out.println(tp1.getPriority()); System.out.println(tp2.getPriority()); System.out.println(tp3.getPriority()); System.out.println(Thread.MAX_PRIORITY); System.out.println(Thread.MIN_PRIORITY); System.out.println(Thread.NORM_PRIORITY); tp1.setPriority(10 ); tp2.setPriority(5 ); tp3.setPriority(1 ); tp3.start(); tp2.start(); tp1.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 5 5 5 10 1 5 高铁:0 高铁:1 高铁:2 飞机:0 飞机:1 飞机:2 飞机:3 飞机:4 飞机:5 汽车:0 飞机:6 高铁:3 高铁:4 高铁:5 高铁:6 高铁:7 高铁:8 高铁:9 汽车:1 飞机:7 飞机:8 飞机:9 汽车:2 汽车:3 汽车:4 汽车:5 汽车:6 汽车:7 汽车:8 汽车:9
注意看,线程优先级高,只是说明其获取CPU时间片的几率高。但并不是每次都跑在前面。
线程的生命周期
在讨论了线程控制和线程调度,以及之前的阻塞之后。我们终于可以讨论线程的生命周期了。
解释一下这张图。
首先,新建,在这个过程,我们创建了线程对象。然后我们调用start方法,线程启动,但是并没有立即执行。
进入了就绪状态,这个时候有执行资格,但是没有执行权。
在抢到CPU的执行权之后,进入运行状态,有执行资格也有执行权,CPU开始执行线程。
接下来,如果run方法结束或者通过stop方法停止,或者出现异常等情况,线程就进入了死亡状态。
如果在运行的时候,被其他线程抢走了CPU的执行权,重新回到就绪的状态。为什么会被其他CPU抢占执行权?我们上文讨论了,一次只抢到了CPU的一个时间片。
如果在运行的时候,调用了sleep或者其他的阻塞方法,回到阻塞状态。没有执行资格,没有执行权。直到阻塞方法结束,回到就绪状态。
注意,没有执行资格,也没有执行权。那么CPU这时候在干啥呢?如果没有其他线程进来,CPU啥事也不做。这就解释了在上一章《6.网络编程》 我们讨论的,线程都阻塞了,这时候开多线程肯定更快。
小结一下,一共5个状态。
新建:
new关键字创建了一个线程之后,该线程就处于新建状态。JVM为线程分配内存,初始化成员变量值。
就绪:
当线程对象调用了start()方法之后,该线程处于就绪状态。JVM为线程创建方法栈和程序计数器,等待线程调度器调度。
关于什么是方法栈,什么是程序计数器。我们会在下一章《8.多线程 [2/2]》 做详细的讨论。
运行:
就绪状态的线程获得CPU资源,开始执行run()方法,该线程进入运行状态
阻塞:
当发生如下情况时,线程将会进入阻塞状态:
线程调用sleep()方法主动放弃所占用的处理器资源。
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
线程试图获得一个同步锁,但该同步锁正被其他线程所持有。
线程在等待某个通知(notify)。
程序调用了线程的suspend()方法将该线程挂起。
死亡:
线程会以如下3种方式结束,结束后就处于死亡状态:
run()或call()方法执行完成,线程正常结束。
线程抛出一个未捕获的Exception或Error。
调用该线程stop()方法来结束该线程。
特别注意,suspend()
方法和stop()
容易导致线程死锁,不推荐使用。
那么,什么是线程死锁呢?
在讨论线程死锁之前,我们要先讨论一下线程安全。为了解决线程安全,引入锁,但是如果锁应用不当,就会导致死锁。
线程安全
现象
在上文我们讨论了,线程每次只能抢到CPU的一个时间片,如果下次被抢了或者自己调用了阻塞方法,都会失去CPU的执行权。
接下来,我们来看一个很有趣的现象。
以这么一个例子为例。
卖票。
这个太常见了。
12306或者双十一等等,都有这个场景。
我们假设有100张票,有三个窗口(线程)在卖票。
我们来模拟这个过程。
定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100
在SellTicket类中重写run()方法实现卖票,代码步骤如下
判断票数大于0,就卖票,并告知是哪个窗口卖的
卖了票之后,总票数要减1
定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
创建SellTicket类的对象
创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
启动线程
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;public class SellTicket implements Runnable { private int tickets = 100 ; @Override public void run () { while (true ) { if (tickets > 0 ) { try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票" ); tickets--; } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan;public class SellTicketDemo { public static void main (String[] args) { SellTicket st = new SellTicket(); Thread t1 = new Thread(st,"窗口1" ); Thread t2 = new Thread(st,"窗口2" ); Thread t3 = new Thread(st,"窗口3" ); t1.start(); t2.start(); t3.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 窗口2正在出售第100张票 窗口1正在出售第100张票 窗口3正在出售第100张票 窗口3正在出售第97张票 窗口1正在出售第97张票 窗口2正在出售第97张票 【部分运行结果略】 窗口2正在出售第4张票 窗口1正在出售第4张票 窗口3正在出售第4张票 窗口2正在出售第1张票 窗口3正在出售第1张票 窗口1正在出售第1张票
出问题了。
相同的票出现了多次
出现了负数的票(实际打印的记录行数大于100)
原因
为什么会这样?
我们来分析一下。
首先,肯定会有一个线程优先抢到CPU的执行权,比如就是t1线程抢到了CPU的执行权。然后t1线程执行到Thread.sleep(10),t1线程进入阻塞状态,这时候t1没有执行资格没有执行权,CPU暂时空出来了。马上t2线程抢到了CPU的执行权,t2线程就开始执行,执行到Thread.sleep(10),t2线程进入阻塞状态。t3线程抢到了CPU的执行权,t3线程就开始执行,同样,t3线程进入阻塞。
到这一步没有任何问题。
sleep结束后,线程进入就绪状态,继续抢CPU的执行权。
假如t1首先抢到CPU的执行权,这时候t1会马上执行System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
,输出正在出售第100张票
。
理论上接下来应该执行tickets--
。但是假如,这时候CPU的执行权被抢了?
t2抢到了CPU的执行权,t2也输出正在出售第100张票
,t2接下来应该执行tickets--
,假如又被抢了?
t3同理。
这就是为什么相同的票被卖了多次。
出现了负数票(实际打印的记录行数大于100)的原因类似。
那么为什么会有线程安全问题?
两个原因,第一个原因,线程一次只能抢占CPU的一个时间片。这个是外界原因,
那么内在原因呢?
tickets这个变量,多个线程在共享。在共享没问题,如果大家都只读的话,一点问题都没有。问题在于有线程在写,有线程在写其实也没问题。有线程在写也没问题,如果每个线程都只执行一个赋值的写操作,比如tickets = 99
这种。问题在于线程执行了多个操作。
在这里一个线程执行了三个操作,打印是一个,然后打印结束之后的tickets--
其实是三个操作,tp1 = tickets
、tp2 = tp1-1
、tockets = tp2
。
综上所述,线程安全问题的内在原因或者根本原因是:
多个线程在操作共享的数据。
多个线程对共享数据有写操作。
操作共享数据的线程执行的操作有多个。
解决
如何解决多线程安全问题呢?
让线程们不要共享数据了,这是一个办法,如果可以不共享的话,ThreadLocal,在下一章《8.多线程 [2/2]》 ,我们就会讨论这种。
让线程不要写数据了,这个做不到,业务逻辑就是要写数据。
让线程执行的操作只有一个,这个也做不到。业务逻辑就是要有多个。
但是?
如果某个线程修改共享资源的时候,其他线程不能修改该资源,只有等修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作呢?
这样是不是就解决了线程不安全的现象。
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。
同步代码块(synchronized)
同步方法(synchronized)
同步锁(ReenreantLock)
特殊域变量(volatile)
局部变量(ThreadLocal)
阻塞队列(LinkedBlockingQueue)
原子变量(Atomic*)
这一章我们讨论前面三种,剩下的四种会在下一章《8.多线程 [2/2]》 讨论。
同步代码块
第一种,同步代码块(synchronized)。
同步代码块格式:
1 2 3 synchronized (任意对象) { 多条语句操作共享数据的代码 }
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁。如果某个线程试图获得一个同步锁,但该同步锁正被其他线程所持有,则该线程会进入阻塞状态,直到锁释放。
那么!来吧!
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;public class SellTicket implements Runnable { private int tickets = 100 ; private Object obj = new Object(); @Override public void run () { while (true ) { synchronized (new Object()) { if (tickets > 0 ) { try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票" ); tickets--; } } } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 窗口3正在出售第100张票 窗口2正在出售第100张票 窗口1正在出售第100张票 窗口1正在出售第97张票 窗口2正在出售第97张票 窗口3正在出售第95张票 【部分运行结果略】 窗口3正在出售第4张票 窗口2正在出售第4张票 窗口1正在出售第4张票 窗口1正在出售第1张票 窗口3正在出售第1张票 窗口2正在出售第1张票
这看起来也没解决啊。
注意看synchronized (new Object())
,每个线程运行到这里都去创建一个新的对象,获得了一把新的锁,然后用完就释放。
这样这把锁根本没有效果,并没有把其他线程锁住。
我们要这么写。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.kakawanyifan;public class SellTicket implements Runnable { private int tickets = 100 ; private Object obj = new Object(); @Override public void run () { while (true ) { synchronized (obj) { if (tickets > 0 ) { try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票" ); tickets--; } } } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 窗口1正在出售第100张票 窗口1正在出售第99张票 窗口1正在出售第98张票 窗口1正在出售第97张票 窗口1正在出售第96张票 窗口1正在出售第95张票 【部分运行结果略】 窗口3正在出售第6张票 窗口3正在出售第5张票 窗口3正在出售第4张票 窗口3正在出售第3张票 窗口2正在出售第2张票 窗口2正在出售第1张票
这样的锁,才是同一个SellTicket的对象的同一个成员变量。
如果我们按照如下代码的方式进行调用呢?new Thread(new SellTicket(),"窗口1")
,为每一个线程都实例化一个新的"Runnable"。 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;public class SellTicketDemo { public static void main (String[] args) { Thread t1 = new Thread(new SellTicket(),"窗口1" ); Thread t2 = new Thread(new SellTicket(),"窗口2" ); Thread t3 = new Thread(new SellTicket(),"窗口3" ); t1.start(); t2.start(); t3.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 窗口2正在出售第100张票 窗口3正在出售第100张票 窗口1正在出售第100张票 窗口3正在出售第99张票 窗口1正在出售第99张票 【部分运行结果略】 窗口2正在出售第3张票 窗口1正在出售第1张票 窗口3正在出售第1张票 窗口2正在出售第2张票 窗口2正在出售第1张票
同步方法
第二种,同步方法。
格式如下:
1 2 3 修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }
在上文我们讨论了,同步代码块的锁,需要是同一个对象。
那么,同步方法的锁对象是什么呢?
对于非static方法,同步锁的对象就是this。
对于static方法,同步锁的对象是当前方法所在类的字节码(类名.class)。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.kakawanyifan;public class SellTicket implements Runnable { private static int tickets = 100 ; @Override public void run () { while (true ) { sellTicket(); } } private static synchronized void sellTicket () { if (tickets > 0 ) { try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票" ); tickets--; } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 窗口1正在出售第100张票 窗口1正在出售第99张票 窗口1正在出售第98张票 窗口1正在出售第97张票 窗口1正在出售第96张票 窗口1正在出售第95张票 【部分运行结果略】 窗口3正在出售第6张票 窗口3正在出售第5张票 窗口3正在出售第4张票 窗口3正在出售第3张票 窗口2正在出售第2张票 窗口2正在出售第1张票
Lock锁
第三种方法,Lock锁。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock(重入锁)来实例化。
ReentrantLock构造方法
方法名
说明
ReentrantLock()
创建一个ReentrantLock的实例
ReentrantLock(boolean fair)
创建一个是否是公平锁的ReentrantLock的实例
公平锁:多个线程都公平拥有执行权,每个线程执行一段时间。
非公平锁,那么就需要抢了,默认是false,非公平锁。
加锁解锁方法
方法名
说明
void lock()
获得锁
void unlock()
释放锁
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.kakawanyifan;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class SellTicket implements Runnable { private int tickets = 100 ; private Lock lock = new ReentrantLock(); @Override public void run () { while (true ) { try { lock.lock(); if (tickets > 0 ) { try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票" ); tickets--; } } finally { lock.unlock(); } } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 窗口1正在出售第100张票 窗口1正在出售第99张票 窗口1正在出售第98张票 窗口1正在出售第97张票 窗口1正在出售第96张票 窗口1正在出售第95张票 【部分运行结果略】 窗口3正在出售第6张票 窗口3正在出售第5张票 窗口3正在出售第4张票 窗口3正在出售第3张票 窗口2正在出售第2张票 窗口2正在出售第1张票
线程安全的类
在讨论了锁之后,我们来看看几个线程安全的类。
在这里,我们会看到锁在JDK中的应用。
举几个例子
StringBuffer,线程安全的"StringBuilder"。
Vector,线程安全的"List"。
Hashtable,线程安全的"Hashmap"。
关于Hashmap为什么线程不安全,我们在《4.集合》 就已经讨论过,就是那个并发修改异常。
例如,StringBuffer。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package java.lang;【部分代码略】 public final class StringBuffer extends AbstractStringBuilder implements java .io .Serializable , CharSequence { 【部分代码略】 @Override public synchronized int length () { return count; } @Override public synchronized int capacity () { return value.length; } @Override public synchronized void ensureCapacity (int minimumCapacity) { super .ensureCapacity(minimumCapacity); } @Override public synchronized void trimToSize () { super .trimToSize(); } @Override public synchronized void setLength (int newLength) { toStringCache = null ; super .setLength(newLength); } @Override public synchronized char charAt (int index) { if ((index < 0 ) || (index >= count)) throw new StringIndexOutOfBoundsException(index); return value[index]; } 【部分代码略】 }
解释说明:
相关方法是加了synchronized关键字。
再比如,Hashtable。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package java.util;public class Hashtable <K ,V > extends Dictionary <K ,V > implements Map <K ,V >, Cloneable , java .io .Serializable { 【部分代码略】 public synchronized int size () { return count; } public synchronized boolean isEmpty () { return count == 0 ; } public synchronized Enumeration<K> keys () { return this .<K>getEnumeration(KEYS); } public synchronized Enumeration<V> elements () { return this .<V>getEnumeration(VALUES); } public synchronized boolean contains (Object value) { if (value == null ) { throw new NullPointerException(); } Entry<?,?> tab[] = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true ; } } } return false ; } public boolean containsValue (Object value) { return contains(value); } public synchronized boolean containsKey (Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return true ; } } return false ; } 【部分代码略】 }
其特点也是相关方法加上了synchronized关键字。
还有一种方法是Collections中的Collections.synchronizedList()等方法。
我们来看看源码。
1 2 3 4 5 public static <T> List<T> synchronizedList (List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); }
不是同步方法?
来看SynchronizedList这个类的成员方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 static class SynchronizedList <E > extends SynchronizedCollection <E > implements List <E > { 【部分代码略】 final List<E> list; SynchronizedList(List<E> list) { super (list); this .list = list; } SynchronizedList(List<E> list, Object mutex) { super (list, mutex); this .list = list; } public boolean equals (Object o) { if (this == o) return true ; synchronized (mutex) {return list.equals(o);} } public int hashCode () { synchronized (mutex) {return list.hashCode();} } public E get (int index) { synchronized (mutex) {return list.get(index);} } public E set (int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add (int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove (int index) { synchronized (mutex) {return list.remove(index);} } public int indexOf (Object o) { synchronized (mutex) {return list.indexOf(o);} } public int lastIndexOf (Object o) { synchronized (mutex) {return list.lastIndexOf(o);} } public boolean addAll (int index, Collection<? extends E> c) { synchronized (mutex) {return list.addAll(index, c);} } public ListIterator<E> listIterator () { return list.listIterator(); } 【部分代码略】 }
其相关方法也不是同步方法,但其相关方法中的代码是同步代码块。即是通过同步代码块的方式实现的。
所以这些也被称为同步容器,下一章《7.多线程 [1/2]》 ,我们还会讨论并发容器,其实现原理不一样。
synchronized与Lock的区别
有如下区别:
存在层次
锁的释放
锁的获取
锁状态
锁类型
存在层次
synchronized:Java的关键字,在jvm层面上
Lock:一个类(这么说不准确,在下一章《8.多线程 [2/2] ,我们会对其进行更详细的分析。)
锁的释放
synchronized:获取锁的线程执行完同步代码,释放锁。或者线程执行发生异常,jvm会让线程释放锁
Lock:在finally中必须释放锁,不然容易造成线程死锁。
锁的获取
synchronized:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。
Lock:分情况而定,可以尝试获得锁,得不到可以不用一直等待,例如tryLock。
这里介绍一下tryLock。tryLock方法重载了两种。
tryLock()
: 如果获取锁的时候锁被占用就返回false,否则返回true。
tryLock(long time, TimeUnit unit)
:比起tryLock()
就是给了一个时间期限,等待给定的时间。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.kakawanyifan;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockTest { private Lock lock = new ReentrantLock(); private void method (Thread thread) { if (lock.tryLock()) { try { System.out.println("线程名" + thread.getName() + "获得了锁" ); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("线程名" + thread.getName() + "释放了锁" ); lock.unlock(); } } else { System.out.println("我是" + Thread.currentThread().getName() + "有人占着锁,我就不要啦" ); } } public static void main (String[] args) { LockTest lockTest = new LockTest(); Thread t1 = new Thread(new Runnable() { @Override public void run () { lockTest.method(Thread.currentThread()); } }, "t1" ); Thread t2 = new Thread(new Runnable() { @Override public void run () { lockTest.method(Thread.currentThread()); } }, "t2" ); t1.start(); t2.start(); } }
运行结果:
1 2 3 线程名t1获得了锁 我是t2有人占着锁,我就不要啦 线程名t1释放了锁
锁状态
synchronized:无法判断
Lock:可以判断
锁类型
synchronized:可重入,不可中断,非公平。
Lock:可重入,可中断,可公平(两者皆可),类型更丰富。
关于各种类型的锁,我们会在下一章《8.多线程 [2/2]》 做详细的讨论。
线程死锁
什么是死锁
通过上文的讨论,我们已经知道了解决线程安全一个方法是锁。那么什么是死锁呢?
死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
举个例子。
R1和R2,大家可以理解为资源,或者锁对象。
P1和P2,代表两个线程。
这就是线程死锁。
现象
接下来,我们来看具体的现象。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.kakawanyifan;public class DeadLockRunnable implements Runnable { private static Object obj1 = new Object(); private static Object obj2 = new Object(); public int flag = 0 ; public DeadLockRunnable (int flag) { this .flag = flag; } public void run () { if (flag == 1 ) { synchronized (obj1) { try { System.out.println(Thread.currentThread().getName() + " 已经获取Obj1,正在请求Obj2。" ); Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2) { System.out.println(Thread.currentThread().getName() + " 已经获取Obj2。" ); } } } if (flag == 2 ) { synchronized (obj2) { try { System.out.println(Thread.currentThread().getName() + " 已经获取Obj2,正在请求Obj1。" ); Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj1) { System.out.println(Thread.currentThread().getName() + " 已经获取Obj1。" ); } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;public class DeadLockDemo { public static void main (String[] args) { DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable(1 ); DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable(2 ); Thread thread1 = new Thread(deadLockRunnable1); Thread thread2 = new Thread(deadLockRunnable2); thread1.start(); thread2.start(); } }
运行结果:
1 2 Thread-0 已经获取Obj1,正在请求Obj2。 Thread-1 已经获取Obj2,正在请求Obj1。
死锁产生的必要条件
以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
互斥条件
不可剥夺条件
请求与保持条件
循环等待条件
互斥条件
要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
不可剥夺条件
线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求与保持条件
线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已经获得的资源保持不放。
循环等待条件
存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。如图所示:
特别注意:如果pn等待p0或者pk,这时候虽然有循环等待,但是不一定会死锁。
那么,死锁怎么解决呢?
或者在设计的时候进行避免,或者在已经发生后借助外力方法。这个我们不做太多讨论。
回到上文,我们说线程控制的两个方法不建议使用,suspend()
和stop()
,因为容易导致锁资源没被释放,从而引起死锁。
线程通信
线程间通信常用方式如下:
休眠唤醒方式:
Object的wait、notify、notifyAll
Condition的await、signal、signalAll
CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行。
CyclicBarrier:一组线程等待至某个状态之后再全部同时执行。
Semaphore:用于控制对某组资源的访问权限。
我们分别讨论。
休眠唤醒方式
我们以两个线程打印奇偶数为例。
Object的wait、notify、notifyAll
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.kakawanyifan;public class WaitNotifyDemo { private Object obj = new Object(); private Integer i = 0 ; public void odd () { while (i < 10 ) { synchronized (obj) { if (i % 2 == 1 ) { System.out.println(Thread.currentThread().getName() + " " + i); i++; obj.notify(); } else { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public void even () { while (i < 10 ) { synchronized (obj) { if (i % 2 == 0 ) { System.out.println(Thread.currentThread().getName() + " " + i); i++; obj.notify(); } else { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public static void main (String[] args) { final WaitNotifyDemo runnable = new WaitNotifyDemo(); Thread t1 = new Thread(new Runnable() { public void run () { runnable.odd(); } }, "奇数线程" ); Thread t2 = new Thread(new Runnable() { public void run () { runnable.even(); } }, "偶数线程" ); t1.start(); t2.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 偶数线程 0 奇数线程 1 偶数线程 2 奇数线程 3 偶数线程 4 奇数线程 5 偶数线程 6 奇数线程 7 偶数线程 8 奇数线程 9
Condition的await、signal、signalAll
这种方法依赖Lock。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package com.kakawanyifan;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class WaitNotifyDemo { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private Integer i = 0 ; public void odd () { while (i < 10 ) { lock.lock(); try { if (i % 2 == 1 ) { System.out.println(Thread.currentThread().getName() + " " + i); i++; condition.signal(); } else { condition.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public void even () { while (i < 10 ) { lock.lock(); try { if (i % 2 == 0 ) { System.out.println(Thread.currentThread().getName() + " " + i); i++; condition.signal(); } else { condition.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main (String[] args) { final WaitNotifyDemo runnable = new WaitNotifyDemo(); Thread t1 = new Thread(new Runnable() { public void run () { runnable.odd(); } }, "奇数线程" ); Thread t2 = new Thread(new Runnable() { public void run () { runnable.even(); } }, "偶数线程" ); t1.start(); t2.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 偶数线程 0 奇数线程 1 偶数线程 2 奇数线程 3 偶数线程 4 奇数线程 5 偶数线程 6 奇数线程 7 偶数线程 8 奇数线程 9
生产者消费者模式
上述的线程通信方式,有一个很典型的应用,就是生产者消费者模式。
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为。
理想情况下,生产者生产数据之后,消费者立马消费数据。完美配合,天衣无缝。
但是,可能会生产者生产数据之后,消费者还没来消费,就需要提醒消费者快来消费。或者消费者准备消费数据,发现没有数据,所以消费者需要提醒生产者赶紧生产。
这时候就需要线程通信了。
我们举个例子。生产牛奶和消费牛奶。
包含的类:
奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
创建奶箱对象,这是共享数据区域
创建消费者创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
启动线程
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.kakawanyifan;public class Box { private int milk; private boolean state = false ; public synchronized void put (int milk) { if (state) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this .milk = milk; System.out.println("送奶工将第" + this .milk + "瓶奶放入奶箱" ); state = true ; notifyAll(); } public synchronized void get () { if (!state) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("用户拿到第" + this .milk + "瓶奶" ); state = false ; notifyAll(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;public class Producer implements Runnable { private Box b; public Producer (Box b) { this .b = b; } @Override public void run () { for (int i=1 ; i<=30 ; i++) { b.put(i); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;public class Customer implements Runnable { private Box b; public Customer (Box b) { this .b = b; } @Override public void run () { while (true ) { b.get(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.kakawanyifan;public class BoxDemo { public static void main (String[] args) { Box b = new Box(); Producer p = new Producer(b); Customer c = new Customer(b); Thread t1 = new Thread(p); Thread t2 = new Thread(c); t1.start(); t2.start(); } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 送奶工将第1瓶奶放入奶箱 用户拿到第1瓶奶 送奶工将第2瓶奶放入奶箱 用户拿到第2瓶奶 送奶工将第3瓶奶放入奶箱 用户拿到第3瓶奶 【部分运行结果略】 送奶工将第28瓶奶放入奶箱 用户拿到第28瓶奶 送奶工将第29瓶奶放入奶箱 用户拿到第29瓶奶 送奶工将第30瓶奶放入奶箱 用户拿到第30瓶奶
CountDownLatch方式
CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。
每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
比如,我们需要并发的去表里查数据。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.kakawanyifan;import java.util.UUID;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class CountDownLatchDemo { public static void main (String[] args) { ConcurrentHashMap<String,Object> map = new ConcurrentHashMap<>(); String[] tables = {"a" ,"b" ,"c" ,"d" ,"e" }; CountDownLatch countDownLatch = new CountDownLatch(tables.length); ExecutorService executorService = Executors.newFixedThreadPool(tables.length); for (String table:tables) { Runnable runnable = new Runnable() { @Override public void run () { map.put(table, UUID.randomUUID()); countDownLatch.countDown(); } }; executorService.execute(runnable); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); System.out.println(map.toString()); } }
运行结果:
1 {a=8f2e175b-ecd2-4542-b18c-88a44c8ca0f9, b=93a8e610-93a8-4c84-8b14-e0b67b1d7ccd, c=698ae836-a9e0-474c-bfc0-ecc843e116bc, d=9817c94c-b499-44bb-9a95-de262891c448, e=85030da5-2897-49ab-9cf9-68e732aa2241}
CyclicBarrier方式
CyclicBarrier方式让一组线程等待至某个状态之后再全部同时执行。
类似于,枪响之后,所有的运动员同时起泡。
三个线程同时启动,示例代码如下。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package com.kakawanyifan;import java.util.Date;import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;public class CyclicBarrierDemo { public static void main (String[] args) { final CyclicBarrier cyclicBarrier = new CyclicBarrier(3 ); new Thread(new Runnable() { public void run () { System.out.println(Thread.currentThread().getName()+":准备..." ); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"启动完毕:" +new Date().getTime()); } },"线程1" ).start(); new Thread(new Runnable() { public void run () { System.out.println(Thread.currentThread().getName()+":准备..." ); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"启动完毕:" +new Date().getTime()); } },"线程2" ).start(); new Thread(new Runnable() { public void run () { System.out.println(Thread.currentThread().getName()+":准备..." ); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"启动完毕:" +new Date().getTime()); } },"线程3" ).start(); } }
运行结果:
1 2 3 4 5 6 线程1:准备... 线程2:准备... 线程3:准备... 线程3启动完毕:1631522569844 线程1启动完毕:1631522569844 线程2启动完毕:1631522569844
Semaphore方式
Semaphore用于控制对某组资源的访问权限。
工人使用机器工作,示例代码如下:
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.kakawanyifan;import java.util.concurrent.Semaphore;public class SemaphoreDemo { static class Machine implements Runnable { private int num; private Semaphore semaphore; public Machine (int num, Semaphore semaphore) { this .num = num; this .semaphore = semaphore; } public void run () { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + " 工人" +this .num+"请求机器,正在使用机器" ); Thread.sleep(1000 ); System.out.println(Thread.currentThread().getName() + " 工人" +this .num+"使用完毕,已经释放机器" ); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main (String[] args) { int worker = 8 ; Semaphore semaphore = new Semaphore(3 ); for (int i=0 ; i< worker; i++){ new Thread(new Machine(i, semaphore)).start(); } } }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Thread-1 工人1请求机器,正在使用机器 Thread-2 工人2请求机器,正在使用机器 Thread-0 工人0请求机器,正在使用机器 Thread-0 工人0使用完毕,已经释放机器 Thread-3 工人3请求机器,正在使用机器 Thread-2 工人2使用完毕,已经释放机器 Thread-5 工人5请求机器,正在使用机器 Thread-1 工人1使用完毕,已经释放机器 Thread-6 工人6请求机器,正在使用机器 Thread-5 工人5使用完毕,已经释放机器 Thread-6 工人6使用完毕,已经释放机器 Thread-3 工人3使用完毕,已经释放机器 Thread-7 工人7请求机器,正在使用机器 Thread-4 工人4请求机器,正在使用机器 Thread-4 工人4使用完毕,已经释放机器 Thread-7 工人7使用完毕,已经释放机器