Java多线程
Java多线程基础知识,包括:
- 创建多线程的四种方式
- 线程的生命周期
- 线程安全和线程同步
- 死锁
- 线程通信
参考视频:B站-黑马【多线程】知识
1. 基本概念
1.1 并发和并行
- 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- 并行:同一时刻,多个任务都在执行。
操作系统通过对进程的调度以及CPU的快速上下文切换实现并发:每个进程执行一会就先停下来,然后CPU切换到下个被操作系统调度到的进程上使之运行。因为切换的很快,使得用户认为操作系统一直在服务自己的程序。
1.2 进程和线程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。可以在任务管理中查看
- 线程:是进程中的一个独立执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程可以并发多个线程的,这个应用程序也可以称之为多线程程序。
1.3 线程调度
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 - 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java中使用的是抢占式调度
1.4 Thread类
Java使用java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
构造方法:
-
public Thread()
:分配一个新的线程对象。 -
public Thread(String name)
:分配一个指定名字的新的线程对象。 -
public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。 -
public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法:
-
public String getName()
:获取当前线程名称 -
public int getPriority()
:获取当前线程的优先级,1-10,默认为5 -
public void start()
:开始执行此线程,Java虚拟机调用此线程的run方法。 -
public void run()
:此线程要执行的任务 -
public static void sleep(long millis)
:暂停正在执行的线程指定的毫秒数 -
public static Thread currentThread()
:返回对当前正在执行的线程对象的引用
2. 线程的创建方式
在Java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
Java中线程有四种创建方式继承Thread类、 实现Runnable接口、实现Callable接口和线程池。
2.1 继承Thread类
步骤:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把 run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
自定义线程类,继承Thread类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
System.out.println(currentThread().getName()+"正在运行"+i);
}
}
}
测试类:
public class MultiThreadTest {
public static void main(String[] args){
//创建自定义线程
MyThread thread = new MyThread("新的线程!");
//调用start方法会启动线程
thread.start();
//主线程循环打印
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"正在运行"+i);
}
}
}
2.2 实现Runnable接口
步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程
自定义线程类,实现Runnable接口:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"正在运行"+i);
}
}
}
测试类:
public class MultiThreadTest {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(), "MyRunnable");
thread.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "正在运行" + i);
}
}
}
也可以直接用匿名内部类,或者Lambda表达式:
// 匿名内部类
Thread thread = new Thread(new MyRunnable(){
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"正在运行"+i);
}
}
}, "MyRunnable");v
//Lambda表达式
new Thread(() ->System.out.println("多线程任务执行!")).start();
2.3 实现Callable接口
实现Callable接口,需要通过FutureTask包装器来创建线程,FutureTask类结构如下:
Futrue接口的几个方法:
- 判断任务是否完成:isDone()
- 中断任务:cancel()
- 获取任务执行结果:get()
创建线程步骤:
- 定义Callable接口的实现类,并重写该接口的call()方法,该方法可以有返回值,并且可以抛出异常
- 以该实现类为参数,创建FutureTask实例,泛型与MyCallable中的泛型一致
- 以此实例作为Thread的target来创建Thread对象
- 调用线程对象的start()方法来启动线程
自定义线程类,实现Callable接口,指定返回值的类型:
public class MyCallable implements Callable<String> {
/**
* 重写call方法,完成该线程执行的逻辑,方法可以有返回值,并且可以抛出异常
*/
@Override
public String call() throws Exception {
for (int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName()+"正在运行"+i);
}
//返回值
return "MyCallable执行完毕!";
}
}
测试类:
public class MultiThreadTest {
public static void main(String[] args) {
//创建FutureTask实例,泛型与MyCallable中的泛型一致
FutureTask<String> task = new FutureTask<>(new MyCallable());
//创建Thread对象并开启线程
new Thread(task).start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"正在运行"+i);
}
try {
//获取并打印MyCallable的执行结果
System.out.println(task.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 线程池
01 Executor框架
- 两级调度模型:
在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架
)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。
2. Executor框架的结构与成员:
下面是这些类和接口的简介:
-
Executor接口
:是Executor框架的基础,将任务的提交与任务的执行分离开来。 -
ThreadPoolExecutor
:是线程池的核心实现类,执行被提交的任务。 -
ScheduledThreadPoolExecutor
是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。 -
Future
接口和FutureTask
类:代表异步计算的结果。 -
Runnable
接口和Callable
接口的实现类:可以被ThreadPoolExecutor 或ScheduledThreadPoolExecutor执行。
即:
任务:被执行任务需要实现的接口:Runnable接口 或 Callable接口
任务的执行: 任务执行机制的核心接口Executor
异步计算的结果: 接口Future和实现Future接口的FutureTask类
- 使用示意图:
- execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
- execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
- execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
02 使用线程池创建线程
步骤:
- 使用
java.util.concurrent.Executors
线程工厂类创建线程池对象 - 用过线程池对象获取线程对象
newFixedThreadPool()
方法,并执行Runable接口的实现,execute()或submit()方法
//创建线程池对象,包含10个线程对象
ExecutorService service = Executors.newFixedThreadPool(10);
//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.execute(new MyRunnable());
2.5 问题小结
01 start() 方法和 run() 方法的区别
- 新建的线程调用 start() 方法时,会使线程进入就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
- 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
02 继承Thread类和实现接口的区别
- 适合多个相同的程序代码的线程去共享同一个资源;
- 可以避免Java中的单继承的局限性;
- 代码可以被多个线程共享,代码和线程独立,增加程序的健壮性,实现解耦操作。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
03 Runable接口和Callable接口比较
相同点:
- 两者都是接口;
- 两者都可用来编写多线程程序;
- 两者都需要调用Thread.start()启动线程;
不同点:
- Runable接口的run()方法不允许抛出异常,没有返回值;而Callable接口的call()方法允许抛出异常,有返回值
- Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取结果;当不调用此方法时,主线程不会阻塞!
- 实现Callable接口的线程可以调用Future.cancel()取消执行 ,而实现Runnable接口的线程不能
3. 线程的生命周期
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(《Java 并发编程艺术》4.1.4 节)。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
3.1 新建NEW
- new关键字创建了一个线程之后,该线程就处于新建状态
- JVM为线程分配内存,初始化成员变量值
3.2 运行RUNNABLE
- 当线程对象了调用
start()
方法后,线程这时候处于 READY(就绪) 状态,等待线程调度器调度 - 就绪状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态,执行run()方法
3.3 阻塞BLOCKED
线程调用同步方法时,当发生如下情况时,线程将会进入阻塞状态:
- 线程调用sleep()方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步锁,但该同步锁正被其他线程所持有。
- 线程在等待某个通知(notify)
- 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁
3.4 等待WAITING
当线程执行 wait()
方法之后,线程进入等待状态。进入这个状态后是不能自动唤醒的,必须依靠其他线程的通知才能够返回到运行状态。
3.5 超时等待TIMED_WAITING
在等待状态的基础上增加了超时限制,通过 Thread.sleep(long millis)
方法或 Object.wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态
3.6 终止TERMINATED
线程会以如下3种方式结束,结束后就处于终止状态:
- run()或call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的Exception或Error
- 调用该线程stop()方法来结束该线程,该方法容易导致死锁
4. 线程安全问题
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
4.1 产生线程安全问题的原因
- 有多线程环境
- 有共享数据(全局变量及静态变量)
- 有多条语句在操作共享数据(执行写操作)
卖票案例:
可能出现的问题:1. 同一张票卖多次;2. 负数票
public class MultiThreadTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread1=new Thread(runnable,"窗口1");
Thread thread2=new Thread(runnable,"窗口2");
Thread thread3=new Thread(runnable,"窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class MyRunnable implements Runnable {
private int ticketsNum = 100; //票数100张
@Override
public void run() {
while (true) {
if (ticketsNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticketsNum-- + "张票");
}
}
}
}
4.2 解决线程安全问题
在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,这样就可以保证数据的同步性,解决线程不安全的问题。
Java引入了7种线程同步机制:
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(ReenreantLock)
- 特殊域变量(volatile)
- 局部变量(ThreadLocal)
- 阻塞队列(LinkedBlockingQueue)
- 原子变量(Atomic)
01 同步代码块synchronized
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
- 格式:
synchronized(同步锁){
需要同步操作的代码
}
- 同步锁:
同步锁对象可以是任意类型,多个线程对象 要使用同一把锁。在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。 - 使用同步代码块解决问题:
class MyRunnable implements Runnable {
private int ticketsNum = 100; //票数100张
Object obj = new Object(); //锁对象
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticketsNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticketsNum-- + "张票");
}
}
}
}
}
02 同步方法synchronized
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
- 格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
- 同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。 - 使用同步代码块解决问题:
class MyRunnable implements Runnable {
private int ticketsNum = 100; //票数100张
// Object obj = new Object();
@Override
public void run() {
while (true) {
sellTicket();
}
}
private synchronized void sellTicket() {
if (ticketsNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticketsNum-- + "张票");
}
}
}
03 同步锁Lock
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法:
-
public void lock()
:加同步锁。 -
public void unlock()
:释放同步锁。
使用可重入锁ReentrantLock解决问题:
class MyRunnable implements Runnable {
private int ticketsNum = 100; //票数100张
// Object obj = new Object();
Lock lock = new ReentrantLock(); //定义锁对象,构造函数参数为线程是否公平获取锁true-公平;false-不公平,即由某个线程独占,默认是false
@Override
public void run() {
while (true) {
lock.lock(); //上锁
try {
if (ticketsNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + ticketsNum-- + "张票");
}
} finally {
lock.unlock(); //释放锁
}
}
}
}
公平锁和非公平锁:
- 如果一个线程组里,能保证每个线程都能拿到锁,那么这个锁就是公平锁。
- 如果保证不了每个线程都能拿到锁,也就是存在有线程饿死,那么这个锁就是非公平锁
4.3 synchronized和Lock区别
- synchronized是Java内置关键字,在jvm层面;Lock是个Java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果线程1获得锁,线程2线程等待;如果线程1阻塞,线程2会一直等待下去。而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平;而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
5. 线程死锁
5.1 什么是死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
模拟死锁代码:
public class DeadLockTest {
public static void main(String[] args) {
DeadLockDemo deadLock1 = new DeadLockDemo();
DeadLockDemo deadLock2 = new DeadLockDemo();
deadLock2.flag=1;
Thread thread1 = new Thread(deadLock1);
Thread thread2 = new Thread(deadLock2);
thread1.start();
thread2.start();
}
}
class DeadLockDemo implements Runnable {
private static Object obj1 = new Object(); //资源1
private static Object obj2 = new Object(); //资源2
public int flag = 0; //决定线程走向的标记
@Override
public void run() {
if (flag == 0) {
//线程1执行代码
System.out.println("flag:" + flag);
synchronized (obj1) {
System.out.println(Thread.currentThread() + "获取了资源1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "等待获取资源2");
synchronized (obj2) {
System.out.println(Thread.currentThread() + "获取了资源2");
}
}
}
if (flag == 1) {
//线程2执行代码
System.out.println("flag:" + flag);
synchronized (obj2) {
System.out.println(Thread.currentThread() + "获取了资源2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "等待获取资源1");
synchronized (obj1) {
System.out.println(Thread.currentThread() + "获取了资源1");
}
}
}
}
}
运行结果:
flag:1
flag:0
Thread[Thread-0,5,main]获取了资源1
Thread[Thread-1,5,main]获取了资源2
Thread[Thread-1,5,main]等待获取资源1
Thread[Thread-0,5,main]等待获取资源2
5.2 产生死锁的必要条件
以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
01 互斥条件
进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
02 不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,只能由获得该资源的进程主动释放。
03 请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放,只有自己使用完毕后才释放资源。
04 循环等待条件
若干进程之间形成一种头尾相接的循环等待资源关系。
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图所示。
5.3 死锁处理
- 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
- 避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
- 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
- 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
01 死锁预防
只要破坏产生死锁的四个条件中的其中一个就可以了:
- 破坏互斥条件 :这个条件没有办法破坏,因为用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 :
不允许进程在已获得某种资源的情况下,申请其他资源。即阻止进程在持有资源的同时申请其他资源。
- 方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,或什么也不给它。
- 方法二:要求每个进程提出新的资源申请前,释放它所占有的资源,即使它可能很快又要用到该资源。
- 破坏不剥夺条件 :允许对资源实行抢夺。
- 方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
- 方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
- 破坏循环等待条件 :将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出,释放资源则反序释放。
02 死锁避免
避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。
- 有序资源分配法:为所有资源统一编号,同类资源必须一次申请完,不同类资源必须按顺序申请
- 银行家算法:
- REQUEST [i]:请求资源;NEED[i,j]:所需资源;AVAILABLE[i]:可用资源;ALLOCATION[i]:已分配资源
- 顺序加锁:所有线程按照相同的顺序获得锁,这种方式需要事先知道所有可能会用到的锁,只适合特定场景。
- 限时加锁:线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。
03 死锁检测
预防和避免死锁系统开销大且不能充分利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段。
死锁检测数据结构:
E是现有资源向量(existing resource vector),代表每种已存在资源的总数
A是可用资源向量(available resource vector),那么Ai表示当前可供使用的资源数(即没有被分配的资源)
C是当前分配矩阵(current allocation matrix),C的第i行代表Pi当前所持有的每一种类型资源的资源数
R是请求矩阵(request matrix),R的每一行代表P所需要的资源的数量
死锁检测步骤:
- 寻找一个没有结束标记的进程Pi,对于它而言,R矩阵的第i行向量小于或等于A,即请求资源≤可用资源
- 如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
- 如果没有这样的进程,那么算法终止
算法结束时,所有没有标记过的进程都是死锁进程
04 死锁恢复
- 利用抢占恢复:
临时将某个资源从它的当前所属进程转移到另一个进程。这种做法很可能需要人工干预,主要做法是否可行需取决于资源本身的特性。 - 利用回滚恢复:
周期性的将进程的状态进行备份,当发现进程死锁后,根据备份将该进程复位到一个更早的,还没有取得所需的资源的状态,接着就把这些资源分配给其他死锁进程。 - 通过杀死进程恢复:
最直接简单的方式就是杀死一个或若干个进程。尽可能保证杀死的进程可以从头再来而不带来副作用。
6. 线程通讯
多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作, 避免对同一共享变量的争夺。需要通过一定的手段使各个线程能有效的利用资源
线程间通信常用方式如下:
- 等待唤醒机制:
- Object类的wait()、notify()、notifyAll()方法
- Condition类的await()、signal()、signalAll()方
JDK1.5后引入,java.util.concurrent包下:
- CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行
- CyclicBarrier:一组线程等待至某个状态之后再全部同时执行
- Semaphore:用于控制对某组资源的访问权限
6.1 线程控制
01 休眠线程
public static void sleep(long millis)
:指定线程暂停一定时间,不会释放锁
02 加入线程
public final void join()
:等待该线程终止
在start()方法后使用,表示必须等待该线程运行完后,才向下运行
03 礼让线程
public static void yield()
:静态方法,暂停当前正在执行的线程对象,并执行其他线程。让多个线程的执行更和谐,但是不能保证一人一次
04 守护线程
public final void setDaemon(boolean on)
:将该线程标记为守护线程或用户线程,当正在执行的都是守护线程时,Java虚拟机退出
该方法必须在启动线程前调用。 其他线程执行完后,守护线程就不执行了。
05 中断线程
public final void stop()
:已过时。让线程停止,并且线程后面的代码都不执行了
public void interrupt()
:中断线程,并抛出一个IterruptedException异常,线程后面的代码继续执行
6.2 等待唤醒方式
01. Object的wait、notify、notifyAll
- wait(): 线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。需要等待别的线程通知(notify) 这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
- notify(): 选取所通知对象的 wait set 中的一个线程释放;
- notifyAll(): 释放所通知对象的 wait set 上的全部线程。
【注意】:
- 上述方法必须依赖于synchronized关键字,必须在同步代码块或者同步方法中
- 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;否则,从 wait set 出来,又进入 entry set,线程从 WAITING 状态又变成 BLOCKED 状态
案例–依次打印奇数或偶数:
public class WaitNotifyRunnable {
private Object object = new Object(); //锁对象
private Integer i = 0;
//打印奇数
public void odd() {
while (i < 10) {
synchronized (object) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName()+"正在执行,奇数:" + i);
i++;
object.notify(); //打印完毕,唤醒其他线程
} else {
try {
object.wait(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//打印偶数
public void even() {
while (i < 10) {
synchronized (object) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+"正在执行,偶数:" + i);
i++;
object.notify(); //打印完毕,唤醒其他线程
} else {
try {
object.wait(); //等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
new Thread(()->runnable.even(),"线程1").start();
new Thread(()->runnable.odd(),"线程2").start();
// new Thread(runnable::even,"线程1").start();
// new Thread(runnable::odd,"线程2").start();
}
}
结果:
线程1正在执行,偶数:0
线程2正在执行,奇数:1
线程1正在执行,偶数:2
线程2正在执行,奇数:3
线程1正在执行,偶数:4
线程2正在执行,奇数:5
线程1正在执行,偶数:6
线程2正在执行,奇数:7
线程1正在执行,偶数:8
线程2正在执行,奇数:9
02. Condition的await、signal、signalAll
与Object的wait、notify、notifyAll的功能类似,只是依赖于Lock锁
public class WaitNotifyRunnable {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition(); //根据Lock实例生成Condition对象
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 WaitNotifyRunnable runnable = new WaitNotifyRunnable();
new Thread(() -> runnable.even(), "线程1").start();
new Thread(() -> runnable.odd(), "线程2").start();
}
6.3 一些问题
01 sleep() 和 wait() 比较
- 同步:wait()方法只能在同步上下文中调用;sleep()不用
- 作用对象:wait()方法定义在Object类中,是实例方法,作用于对象本身;sleep()定义在java.lang.Thread中,作用于当前线程,是静态方法
- 释放锁资源:sleep() 方法没有释放锁,而 wait() 方法释放了锁
- 唤醒条件:wait() 方法被调用后,线程不会自动苏醒,需要其他线程调用同一个对象上的 notify() 或者 notifyAll() 方法;sleep() 方法被调用后,超时或者调用interrupt()方法后才会唤醒线程
- wait() 方法通常被用于线程间交互/通信,sleep() 方法通常被用于暂停执行。
02 Object和Condition休眠唤醒区别
- object wait()必须在synchronized(同步锁)下使用
- object wait()必须要通过notify()方法进行唤醒
- condition await() 必须和Lock(互斥锁/共享锁)配合使用
- condition await() 必须通过 signal() 方法进行唤醒
6.4 CountDownLatch
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
构造方法:public CountDownLatch(int count)
:指定需要等待的线程个数
成员方法:public void await()
:等待
public void countDown()
:线程计数-1
运动员教练案例:教练必须等待运动员线程执行完毕后才开始执行
public class CoachRacerDemo {
private CountDownLatch countDownLatch = new CountDownLatch(3); //设置需要等待的线程个数为3个
//运动员方法,由运动员线程调用
public void racer() {
String name = Thread.currentThread().getName();
//运动员开始准备
System.out.println(name + "正在准备。。。");
//模拟准备时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//运动员准备完成,打印信息并且计数-1
System.out.println(name + "准备完成!");
countDownLatch.countDown();
}
//教练方法,由教练线程调用
public void coach() {
String name = Thread.currentThread().getName();
//教练等待运动员准备
System.out.println(name + "等待运动员准备。。。");
//教练准备,必须等待其他线程执行完毕
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//运动员已就绪,教练开始训练
System.out.println("所有运动员已就绪,"+name + "开始训练!");
}
public static void main(String[] args) {
final CoachRacerDemo coachRacerDemo = new CoachRacerDemo();
new Thread(()->coachRacerDemo.coach(),"教练").start();
new Thread(()->coachRacerDemo.racer(),"运动员1").start();
new Thread(()->coachRacerDemo.racer(),"运动员2").start();
new Thread(()->coachRacerDemo.racer(),"运动员3").start();
}
}
结果:
运动员3正在准备。。。
教练等待运动员准备。。。
运动员2正在准备。。。
运动员1正在准备。。。
运动员3准备完成!
运动员2准备完成!
运动员1准备完成!
所有运动员已就绪,教练开始训练!
6.5 CyclicBarrier
让所有线程都等待完成后才会继续下一步行动。底层是基于ReentrantLock和Condition实现。
构造方法:
public CyclicBarrier(int parties) //parties 是参与线程的个数
public CyclicBarrier(int parties, Runnable barrierAction) // Runnable 参数是最后一个到达线程要做的任务
成员方法:
public int await() //线程调用 await()表示自己已经到达栅栏,所有的线程必须都到达栅栏位置,才能继续执行
案例:模拟三个线程,三个都调用cyclicBarrier.await()方法后,才能继续向下执行
public class ThreeThreadDemo {
//三个线程都完成后才继续下一步行动
final CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
public void startThread(){
String name = Thread.currentThread().getName();
try {
//1.线程准备启动
System.out.println(name+"正在准备。。。");
System.out.println(name+"就快准备好了。。");
//2.调用cyclicBarrier.await()方法,表示所有的线程必须都到达栅栏位置,才能继续执行
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
//3.线程启动完毕
System.out.println(name+"启动完毕");
}
public static void main(String[] args) {
final ThreeThreadDemo threeThreadDemo = new ThreeThreadDemo();
new Thread(()->threeThreadDemo.startThread(),"线程1").start();
new Thread(()->threeThreadDemo.startThread(),"线程2").start();
new Thread(()->threeThreadDemo.startThread(),"线程3").start();
}
}
结果:
线程1正在准备。。。
线程3正在准备。。。
线程1就快准备好了。。
线程3就快准备好了。。
线程2正在准备。。。
线程2就快准备好了。。
线程1启动完毕
线程3启动完毕
线程2启动完毕
6.6 Semaphore
Semaphore 是一个计数信号量,必须由获取它的线程释放。用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。
Semaphore 只有3个操作:
- 初始化:构造方法
public Semaphore(int permits)
,初始化许可大小 - 增加:
void acquire()
,从该信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。 - 减少:
void release()
,释放一个许可,将其返回给信号量
案例:可使用的资源
public class SemaphoreTest {
final private Semaphore semaphore=new Semaphore(2); //限制可以访问某些资源的线程数量为2个
public void useMachine() {
try {
//1.获取到资源
semaphore.acquire();
//2.打印信息,开始使用
System.out.println(Thread.currentThread().getName()+"请求机器,正在使用资源");
Thread.sleep(3000);
//3.使用完毕,释放资源
System.out.println(Thread.currentThread().getName()+"使用完毕,已经释放资源");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SemaphoreTest semaphoreTest=new SemaphoreTest();
//5个线程去使用
for(int i=0;i<5;i++) {
new Thread(()->semaphoreTest.useMachine(),"线程"+i).start();
}
}
}
结果:只能有2个线程在使用资源。
线程0请求机器,正在使用资源
线程1请求机器,正在使用资源
线程0使用完毕,已经释放资源
线程1使用完毕,已经释放资源
线程2请求机器,正在使用资源
线程3请求机器,正在使用资源
线程3使用完毕,已经释放资源
线程2使用完毕,已经释放资源
线程4请求机器,正在使用资源
线程4使用完毕,已经释放资源