一、多线程理论
1.1、操作系统的发展
在计算机发明之前,人们处理大量的计算是通过人工处理的,耗费人力,成本很大而且错误较多。为了处理大量的数学计算问题,人们发明了计算机。
最初的计算机只能接受一些特定的指令,用户输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。显然这样效率低下,在很多时候,计算机都处在等待状态。
1.1.1、批处理操作系统
既然传统计算机那么慢,那么能不能把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机,计算机通过不断得读取指令进行相应的操作。
就这样,批处理操作系统诞生了。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。
1.1.2、如何提高 CPU 利用率
虽然批处理操作系统的诞生提高了任务处理的便捷性(省略了用户输入的时间),但是仍然存在一个很大的问题:
假如有两个任务 A 和 B, 需要读取大量的数据输入(I/O 操作),而其实 CPU 只能处在等待状态,等任务 A 读取完数据再能继续进行,这样就白白浪费了 CPU 资源。于是人们就想,能否在任务 A 读取数据的过程中,让任务 B 去执行,当任务 A 读取完数据之后,暂停任务 B,让任务 A 继续执行?
这时候又出现了几个问题:内存中始终都只有一个程序在运行,而想要解决上述问题,必然要在内存中装入多个程序,如何处理呢?多个程序使用的数据如何辨别?当一个程序暂停后,随后怎么恢复到它之前执行的状态呢?
1.1.3、进程来了
这时候,人们就发明了进程,用一个进程对应一个程序,每个进程都对应一定的内存地址和内存空间,并且只能自己使用自己的内存空间,多个进程之间的内存互不共享,且进程之间彼此不打扰。
进程同时也保存了程序每时每刻的运行状态,为进程切换提供了如可能。
当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的 2 状态进行恢复,接着继续执行。
1.2、并发和并行
1.2.1、并发
并发是能够让操作系统从宏观上看起来同一时间段执行多个任务。 换句话说,进程让操作体统的并发成为了可能,至此出现多任务操作系统。
虽然并发从宏观上看是有多个任务在执行,但是实际上对于单核 CPU 来说,任意具体时刻都只有一个任务在占用 CPU 资源,操作系统一般通过 CPU 时间片轮转来实现并发。
总的来说,并发就是在一段时间内多个进程轮流使用同一个 CPU,多个进程形成并发。
1.2.2、并行
在同一时刻多个进程使用各自的 CPU,多个进程形成并行。并行需要多个 CPU 支持。
1.3、线程
1.3.1、线程出现的原因
出现了进程之后,操作系统的性能 (CPU 利用率) 得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们不满足,逐渐对实时性有了要求。因为一个进程在一个时间段内只能做一个事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
举一个例子:对于监控系统这个进程来说,不仅要与服务器端进行通信获取图像数据并将图像信息显示在画面上,还要处理与用户的交互操作。如果在一个时刻该系统正在与服务器通信获取图像数据,而用户在监控系统上点击了一个按钮,那么系统只能等获取完图像后才能与用户进行交互操作。如果获取图像需要 10s,用户就得等待 10s。显然这样的系统,无法满足人们的需求。
1.3.2、线程
为了让子任务可以分开执行,即上个例子说的,在与服务器通信获取图形数据的同时相应用户,为了处理这种情况,人们发明了线程,一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。在用户点击按钮的时候,可以暂停获取图像数据的线程,让出 CPU 资源,让 UI 线程获取 CPU 资源,响应用户的操作,响应完后再切换回来,获取图像数据的线程重新获取 CPU 资源。让用户感觉系统在同时做很多事,满足用户对实时性的要求。线程的出现是为了解决实时性的问题
总的来说,线程是进程的细分,通常,在实时性操作系统中,进程会被划分为多个可以独立运行的子任务,这些子任务被称为线程,多个线程配合完成一个进程的任务。
注意
一个进程包含多个线程,但是这些线程共享进程占有的内存地址空间和资源。进程是操作系统进行资源分配的基本单位(进程之间互不干扰),而线程是操作系统进行 CPU 调度的基本单位(线程间互相切换)。
1.3.3、线程工作的原理
假设 P 进程抢占 CPU 后开始执行,此时如果 P 进行正在进行获取网络资源的操作时,用户进行 UI 操作,此时 P 进程不会响应 UI 操作。可以把 P 进程可以分为 Ta、Tb 两个线程。Ta 用于获取网络资源,Tb 用于响应 UI 操作。此时如果 Ta 正在执行获取网络资源时、用户进行 UI 操作,为了做到实时性,Ta 线程暂时挂起,Tb 抢占 CPU 资源,执行 UI 操作,UI 操作执行完成后让出 CPU,Ta 抢占 CPU 资源继续执行请求网络资源。
总结
- 线程再一次提高了 CPU 的利用率
- 线程是包含在进程中,是对进程任务的细分,线程共享进程资源(内存资源等)
- 线程细分后称为 CPU 调度的基本单位。进程称为操作系统资源分配的基本单位。
1.4、线程和进程的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是 CPU 调度和执行的基本单位
- 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过 CPU 调度,在每个时间片中只有一个线程执行)
- 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
- 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分。
1.5、线程调度
1.5.1、分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
1.5.2、抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个 (线程随机性),Java 使用的为抢占式调度。
二、实现线程的方式
在 Java 中实现线程的方式有 2 种,一种是继承 Thread,一种是实现 Runnable 接口。
如果一个进程没有任何线程,我们成为单线程应用程序;如果一个进程有多个线程存在,我们成为多线程应用程序。进程执行时一定会有一个主线程 (main 线程) 存在,主线程有能力创建其他线程。多个线程抢占 CPU,导致程序的运行轨迹不确定。多线程的运行结果也不确定。
2.1、继承 Thread 类
线程开启我们需要用到了java.lang.Thread
类,API 中该类中定义了有关线程的一些方法,具体如下:
构造方法
public Thread()
: 分配一个新的线程对象。public Thread(String name)
: 分配一个指定名字的新的线程对象。public Thread(Runnable target)
: 分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
: 分配一个带有指定目标新的线程对象并指定名字。
常用方法
public String getName()
: 获取当前线程名称。public void start()
: 导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。public void run()
: 此线程要执行的任务在此处定义代码。public static void sleep(long millis)
: 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
: 返回对当前正在执行的线程对象的引用。
继承 Thread 实现多线程,必须重写 run 方法,启动的时候调用的也是调用线程对象的 start() 方法来启动该线程,如果直接调用 run() 方法的话,相当于普通类的执行,此时相当于只有主线程在执行。
package day16_thread.classing.thread;
/**
* @author Xiao_Lin
* @date 2020/12/20 11:40
*/
public class MyThread extends Thread{
@Override
public void run() {
for (int i =1;i<501;i++){
System.out.println("A Thread"+i);
}
}
}
package day16_thread.classing.thread;
/**
* @author Xiao_Lin
* @date 2020/12/20 11:41
*/
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i=1;i<501;i++){
System.out.println("MainThread"+i);
}
}
}
从结果我们可以看出,每一次抢占 CPU 资源的线程是不同的,多个线程轮流使用 CPU,谁先抢占到谁使用 CPU 并执行线程。所以执行结果不确定。
2.1.1、继承 Thread 类的优点
编码简单
2.1.2、继承 Thread 类的缺点
线程类已经继承了 Thread 类了就无法再继承其他类了,功能不能通过其他类继承拓展,功能没有那么强大。
2.2、实现 Runnable 接口
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写 run 方法即可。
步骤如下:
- 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。
- 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
- 调用线程对象的 start() 方法来启动线程。
package day16_thread.classing.thread;
/**
* @author Xiao_Lin
* @date 2020/12/20 13:49
*/
public class MyRun implements Runnable {
@Override
public void run() {
for (int i =1;i<501;i++){
System.out.println("A Thread"+i);
}
}
}
package day16_thread.classing.thread;
/**
* @author Xiao_Lin
* @date 2020/12/20 13:49
*/
public class TestMyRun {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
for (int i=1;i<501;i++){
System.out.println("MainThread"+i);
}
}
}
2.2.1、实现 Runnable 的接口的优点
- 线程任务类只是实现了 Runnable 接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现 Runable 或 Callable 线程任务对象。
- 其实 Thread 类本身也是实现了 Runnable 接口的。
- 唯一的遗憾是不能直接得到线程执行的结果!
2.3、实现 Callable 接口(拓展)
实现多线程还有另一种方式,那就是实现Callable
接口,前面的两种方式都没办法拿到线程执行返回的结果,因为 run() 方法都是 void 修饰的。但是这种方式是可以拿到线程执行返回的结果。
步骤
- 定义一个线程任务类实现 Callable 接口 , 申明线程执行的结果类型。
- 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果。
- 创建一个 Callable 的线程任务对象。
- 把 Callable 的线程任务对象包装成一个未来任务对象。
- 把未来任务对象包装成线程对象。
- 调用线程的 start() 方法启动线程
package day16_thread.classing.thread;
/**
* @author Xiao_Lin
* @date 2020/12/20 13:49
*/
// 1.创建一个线程任务类实现Callable接口,申明线程返回的结果类型
class MyCallable implements Callable<String>{
// 2.重写线程任务类的call方法!
@Override
public String call() throws Exception {
// 需求:计算1-10的和返回
int sum = 0 ;
for(int i = 1 ; i <= 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => " + i);
sum+=i;
}
return Thread.currentThread().getName()+"执行的结果是:"+sum;
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 3.创建一个Callable的线程任务对象
Callable call = new MyCallable();
// 4.把Callable任务对象包装成一个未来任务对象
// -- public FutureTask(Callable<V> callable)
// 未来任务对象是啥,有啥用?
// -- 未来任务对象其实就是一个Runnable对象:这样就可以被包装成线程对象!
// -- 未来任务对象可以在线程执行完毕之后去得到线程执行的结果。
FutureTask<String> task = new FutureTask<>(call);
// 5.把未来任务对象包装成线程对象
Thread t = new Thread(task);
// 6.启动线程对象
t.start();
for(int i = 1 ; i <= 10 ; i++ ){
System.out.println(Thread.currentThread().getName()+" => " + i);
}
// 在最后去获取线程执行的结果,如果线程没有结果,让出CPU等线程执行完再来取结果
try {
String rs = task.get(); // 获取call方法返回的结果(正常/异常结果)
System.out.println(rs);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.3.1、实现 Callable 接口优点
- 线程任务类只是实现了 Callable 接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现 Runable 或 Callable 线程任务对象。
- 能直接得到线程执行的结果!
- 唯一的遗憾就是编码比较复杂,写的代码会比较多。
2.4、两种实现方式的区别
需求:模拟售票窗口买票的过程,共有五张票
2.4.1、Thread 实现
package day16_thread.classing.thicks;
/**
* @author Xiao_Lin
* @date 2020/12/20 13:53
*/
public class MyThread extends Thread{
private static int count = 5;
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i=0;i<5;i++){
if (count>0){
count--;
System.out.println(super.getName()+"卖了一张票。还剩下"+count+"张票");
}
}
}
}
package day16_thread.classing.thicks;
/**
* @author Xiao_Lin
* @date 2020/12/20 13:55
*/
public class TestThread {
public static void main(String[] args) {
MyThread t1 = new MyThread("窗口A");
MyThread t2 = new MyThread("窗口B");
MyThread t3 = new MyThread("窗口C");
MyThread t4 = new MyThread("窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2.4.2、Runable 实现
package day16_thread.classing.thicks;
/**
* @author Xiao_Lin
* @date 2020/12/20 14:15
*/
public class MyRun implements Runnable {
private int count = 5;
@Override
public void run() {
for (int i=0;i<5;i++){
if (count>0){
count--;
System.out.println(Thread.currentThread().getName()+"卖了一张票。还剩下"+count+"张票");
}
}
}
}
package day16_thread.classing.thicks;
/**
* @author Xiao_Lin
* @date 2020/12/20 14:17
*/
public class TestRun {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun,"窗口A");
Thread t2 = new Thread(myRun,"窗口B");
Thread t3 = new Thread(myRun,"窗口C");
Thread t4 = new Thread(myRun,"窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2.4.3、两者实现的区别
- 继承 Thread 类后,不能再继承其他类,而实现了 Runnable 接口后还可以继承其他类。
- 实现 Runnable 接口更方便共享资源,同一份资源,多个线程并发访问,如果多个线程需要访问共享资源,优先考虑 Runnable 方式,如果线程不访问共享资源,可以考虑继承 Thread。
- Thread 类本身也是实现类 Runnable 接口的。
实现 Runnable 接口比继承 Thread 类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免 Java 中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池可以放入实现 Runable 或 Callable 类线程。
2.5、存在的问题
多线程访问共享资源的同时,存在一个十分严重的问题,那就是会导致共享资源数据错乱。
2.6、多线程执行轨迹分析
假设我们拿一种执行情况来分析
2.7、总结
- 线程通过抢占 CPU 的方式工作,在执行过程中,随时可能 CPU 时间片的时间到了,然后被挂起,在程序的任何地方都有可能被切换出去
- 由于随时被挂起或者切换出 CPU,导致访问共享资源会出现数据错乱,解决方法为加锁
三、线程常用的方法
3.1、设置线程优先级
我们可以设置线程的优先级调用,优先级越高 ,被 CPU 调动的可能性越大,但不一定是优先级越高就一定先执行。,有可能设置了最高的优先级但是确实最后调用。
//系统的默认三种优先级
System.out.println(Thread.MAX_PRIORITY);//数字是10
System.out.println(Thread.MIN_PRIORITY);//数字是1
System.out.println(Thread.NORM_PRIORITY);//数字是5
package day16_thread.classing.PriorityTest;
/**
* @author Xiao_Lin
* @date 2020/12/20 19:30
*/
public class TestPriority {
public static void main(String[] args) {
PriorityThrea p1 = new PriorityThrea("线程1");
PriorityThrea p2 = new PriorityThrea("线程2");
p1.setPriority(PriorityThrea.MAX_PRIORITY);
p2.setPriority(PriorityThrea.MIN_PRIORITY);
p1.start();
p2.start();
}
}
3.2、线程的强制执行
强制执行(join 方法)会导致其他线程阻塞,当线程执行完以后,其他线程阻塞原因消除,进入就绪状态
package day16_thread.classing.join;
/**
* @author Xiao_Lin
* @date 2020/12/20 15:45
*/
public class TestMyJoinThread {
public static void main(String[] args) {
MyJoinThread myJoinThread = new MyJoinThread();
myJoinThread.start();
for (int i =0;i<5;i++){
System.out.println("main -> " + i);
if (i==2){
try {
myJoinThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
3.3、线程休眠
线程调用(sleep 方法)方法,传入一个毫秒值,会导致当前线程进入阻塞状态,阻塞时间到了以后线程进入就绪状态,sleep 方法会抛出一个编译时异常InterruptedException
3.3.1、正常执行
package day16_thread.classing.sleep;
/**
* @author Xiao_Lin
* @date 2020/12/20 19:47
*/
public class SleepThread extends Thread{
public SleepThread() {
}
public SleepThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("线程A开始执行");
try {
sleep(2000);
System.out.println("休眠结束");
} catch (InterruptedException e) {
System.out.println("外界有程序中断线程 A");
}
System.out.println("线程A即将结束");
}
}
package day16_thread.classing.sleep;
/**
* @author Xiao_Lin
* @date 2020/12/20 19:50
*/
public class TestSleepThread {
public static void main(String[] args) {
SleepThread s = new SleepThread();
s.start();
}
}
3.3.2、异常情况
package day16_thread.classing.sleep;
/**
* @author Xiao_Lin
* @date 2020/12/20 19:47
*/
public class SleepThread extends Thread{
public SleepThread() {
}
public SleepThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println("线程A开始执行");
try {
sleep(20000);
System.out.println("休眠结束");
} catch (InterruptedException e) {
System.out.println("外界有程序中断线程 A");
}
System.out.println("线程A即将结束");
}
}
package day16_thread.classing.sleep;
/**
* @author Xiao_Lin
* @date 2020/12/20 19:50
*/
public class TestSleepThread {
public static void main(String[] args) {
SleepThread s = new SleepThread();
s.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程中断
s.interrupt();
System.out.println("主线程结束");
}
}
3.3.3、总结
-
线程休眠导致当前线程进入阻塞状态,休眠时间结束后,线程进入就绪状态,抢占 CPU,抢到后继续运行
-
线程休眠过程中可以被中断,所以存在一个编译时异常:
InterruptedException
,外界程序中断该线程时,休眠时间提前结束,进入就绪状态,等待 CPU 调度执行。
3.4、线程的礼让
package day16_thread.classing.yield;
/**
* @author Xiao_Lin
* @date 2020/12/20 20:00
*/
public class YieldThread extends Thread{
public YieldThread() {
}
public YieldThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(super.getName() + "=>" + i);
}
}
}
package day16_thread.classing.yield;
/**
* @author Xiao_Lin
* @date 2020/12/20 20:01
*/
public class TestThreadYield {
public static void main(String[] args) {
System.out.println("主线程开始执行");
YieldThread y1 = new YieldThread();
y1.start();
for (int i = 0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"->"+i);
if (i%2 == 0){
Thread.yield();
}
}
}
}
当前线程礼让后,线程进入就绪状态。
3.5、线程结束
stop 表示强制停止一个线程,停止一个线程的风险较大,不建议使用,通过interrupt
发送中断信号中断线程,线程就会在在那个时间点结束
interrupt
中止正在运行的线程,该线程不会立即结束,而是继续执行,在适当的时机选择结合异常处理机制结束,异常处理机制可以保证线程继续执行,通过异常处理机制让一个线程正常结束。
四、线程生命周期(状态)
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在 API 中java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用 start 方法。MyThread t = new MyThread 只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了 t.start() 方法 :就绪(经典教法) |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用 notify 或者 notifyAll 方法才能够唤醒。 |
Timed Waiting(计时等待) | 同 waiting 状态,有几个方法有超时参数,调用他们将进入 Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡。 |
4.1、新生状态
用 new 关键字建立一个线程后,该对象就处于新生状态,处于新生状态的多线程有自己的内存空间,通过调用 start() 方法进行就绪状态。
4.2、就绪状态
处于就绪状态的线程具备了运行的条件,但是还没有分配到 CPU,处于线程就绪队列,等待系统为其分配 CPU,当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为 “CPU 调度”,等待状态还有一个名字也叫作就绪状态。
4.3、运行状态
在运行状态的线程执行自己的 run 方法中的代码,直到因为等待某资源而阻塞或者完成任务而死亡,如果在给定的时间内没有执行结束,就会被系统换下来回到等待执行的状态。
4.4、阻塞状态
处于运行状态的线程在某种情况下,比如说执行了 sleep(睡眠)方法,或者是等待 I/O 设备等资源,将让出 CPU 并暂时停止自己的运行,进入阻塞状态
在阻塞状态的的线程不会马上进入就绪队列,只有当引起阻塞状态的原因消除时,如睡眠时间已到或者等待的 I/O 设备空闲下来,线程便进入了就绪状态,重新进入到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。冻结状态、静止状态都是阻塞状态
4.5、死亡状态
死亡状态是线程生命周期中最后的一个状态。
引起线程死亡的原因有三种:
- 正常运行的线程执行完了他的全部工作。
- 线程被强制性地终止(stop 方法)。
- 线程抛出了未捕获的异常。
五、线程安全问题
5.1、问题引入
在多线程环境下,什么时候会出现数据错乱的问题?
多个线程并发访问共享资源,并对共享资源进行破坏性操作(增删改)的时候,一定会出现数据错乱的问题
如何解决
在多线程环境下,如果对共享资源进行破坏性操作的时候,需要同步操作。
5.2、同步操作
如果希望一系列操作 (在代码中可以认为是很多句语句),要么都执行,要么都不执行,我们把这种操作叫做原子性操作,原则性操作可以认为是业务上不可分割的单元。
Java 实现原子性操作的过程叫做同步操作,常见的有两种方式实现同步:
- 同步代码块
- 同步方法
- Lock 锁
5.3、同步代码块
把原子性操作放到一个代码块中,就是同步代码块,使用关键字synchronized
synchronized (mutex) {//mutex 称为同步锁,也叫互斥锁。
// 原子性操作
}
改造之前的火车卖票的代码
package day16_thread.classing.thicks;
/**
* @author Xiao_Lin
* @date 2020/12/20 14:15
*/
public class MyRun implements Runnable {
private int count = 500;
@Override
public void run() {
for (int i=0;i<1000;i++){
synchronized (this){
if (count>20){
count--;
System.out.println(Thread.currentThread().getName()+"卖了一张票。还剩下"+count+"张票");
}
}
}
}
}
package day16_thread.classing.thicks;
/**
* @author Xiao_Lin
* @date 2020/12/20 14:17
*/
public class TestRun {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun,"窗口A");
Thread t2 = new Thread(myRun,"窗口B");
Thread t3 = new Thread(myRun,"窗口C");
Thread t4 = new Thread(myRun,"窗口D");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
原则上,锁对象建议使用共享资源,但是遵循以下两个点:
- 在实例方法中建议使用
this
作为锁对象,此时this
正好是共享资源。 - 在静态方法中建议使用
类名.calss
字节码作为锁对象。
5.3.1、同步监视器
synchronized(obj){}
中的 obj 称为同步监视器,同步代码块中的同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器,且同步监视器不能是基本数据类型,同时也不推荐使用包装类型(会有自动拆箱和装箱)
5.3.2、总结
- 如果需要实现原子性操作,必须对共享资源加锁。
- 如果线程运行时,发现不是加锁的那个线程,那么此时会导致该线程阻塞,进入阻塞状态。
- 如果是需要对共享资源进行破坏性操作的时候,推荐使用实现
Runnable
接口会比较方便。
5.4、同步方法
当原子性操作代码很长且需要重复调用的时候,可以考虑将同步代码块中的代码抽取出来变成同步方法。
修饰符 synchronized 返回值类型 方法名称(){
//原子性操作
}
同步方法中无需指定同步监视器,因为同步方法的监视器就是 this,也就是对象本身。
5.5、Lock 锁
java.util.concurrent.locks.Lock
机制提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作,同步代码块 / 同步方法具有的功能 Lock 都有,除此之外更强大。
Lock 锁也称同步锁,加锁与释放锁方法化了,他是显示的,需要我们手动加,方法如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
public class Ticket implements Runnable{
private int ticket = 100;
//创建锁对象
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();//上锁操作
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();//解锁操作
}
}
}
5.6、线程通信
多个线程由于处在同一个进程,所以互相通信是比较容易的。
线程通信的核心方法:
public void wait()
: 让当前线程进入到等待状态 此方法必须锁对象调用。public void notify()
: 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用。public void notifyAll()
: 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用。
线程通信的经典模型:生产者与消费者问题。
- 生产者负责生成商品,消费者负责消费商品。
- 生产不能过剩,消费不能没有。
模拟一个案例:小明和小红有一个共同账户 (共享资源),他们有 3 个爸爸(亲爸,岳父,干爹)给他们存钱。小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们 3 个爸爸们来存钱他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。做整存整取:10000 元。
package com;
// 账户对象
public class Account {
private String cardId ;
private double money ; // 余额。
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
// 亲爸,干爹,岳父
public synchronized void saveMoney(double money) {
try{
// 1.知道是谁来存钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够
if(this.money > 0){
// 5.等待自己,唤醒别人!
this.notifyAll();
this.wait();
}else{
// 3.钱没有,存钱
this.money += money;
System.out.println(name+"来存钱,存入了"+money+"剩余:"+this.money);
// 4.等待自己,唤醒别人!
this.notifyAll();
this.wait();
}
}catch (Exception e){
e.printStackTrace();
}
}
// 小明 小红
public synchronized void drawMoney(double money) {
try{
// 1.知道是谁来取钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够
if(this.money > 0){
// 3.账户有钱,有钱可以取
this.money -= money;
System.out.println(name+"来取钱"+money+"取钱后剩余:"+this.money);
// 4.没钱,先唤醒别人,等待自己,。
this.notifyAll();
this.wait();
}else{
// 5.余额不足,没钱,先唤醒别人,等待自己,。
this.notifyAll();
this.wait();
}
}catch (Exception e){
e.printStackTrace();
}
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
package com;
/**
取钱的线程类
*/
public class DrawThread extends Thread {
private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象!
public DrawThread(Account acc , String name){
super(name); // 为当前线程对象取名字
this.acc = acc ;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(4000);
acc.drawMoney(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
package com;
/**
存钱的线程类
*/
public class SaveThread extends Thread {
private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象!
public SaveThread(Account acc , String name){
super(name); // 为当前线程对象取名字
this.acc = acc ;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(4000);
acc.saveMoney(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
package com;
/**
目标:线程通信(了解原理,代码几乎不用)
线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。
线程通信的经典模型:生产者与消费者问题。
生产者负责生成商品,消费者负责消费商品。
生产不能过剩,消费不能没有。
模拟一个案例:
小明和小红有一个共同账户:共享资源
他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。
模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱
他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。
做整存整取:10000元。
分析:
生产者线程:亲爸,岳父,干爹
消费者线程:小明,小红
共享资源:账户对象。
注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。
线程通信必须先保证线程安全,否则毫无意义,代码也会报错!
线程通信的核心方法:
public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用.
public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用
public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用
小结:
是一种等待唤醒机制。
必须是在同一个共享资源才需要通信,而且必须保证线程安全。
*/
public class ThreadCommunication {
public static void main(String[] args) {
// 1.创建一个账户对象。
Account acc = new Account("ICBC-1313113",0);
// 2.创建2个取钱线程。
new DrawThread(acc , "小明").start();
new DrawThread(acc , "小红").start();
// 3.创建3个存钱线程。
new SaveThread(acc , "亲爹").start();
new SaveThread(acc , "干爹").start();
new SaveThread(acc , "岳父").start();
}
}
5.6.1、线程通信总结
- 线程通信是一种等待唤醒机制。
- 线程安全必须早同一个共享资源才需要通信,而且必须保证线程安全。
5.7、总结
- 线程安全,性能差。
- 线程不安全性能好,假如开发中不会存在多线程的安全问题,建议使用线程不安全的设计类。
六、 volatile 关键字
6.1、问题引入
public class VolatileThread extends Thread {
// 定义成员变量
private boolean flag = false ;
public boolean isFlag() { return flag;}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将flag的值更改为true
this.flag = true ;
System.out.println("flag=" + flag);
}
}
public class VolatileThreadDemo {// 测试类
public static void main(String[] args) {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
// main方法
while(true) {
if(volatileThread.isFlag()) {
System.out.println("执行了======");
}
}
}
}
6.2、多线程下变量的不可见性
6.2.1、概述
在介绍多线程并发修改变量不可见现象的原因之前,我们先看看另一种 Java 内存模型(和 Java 并发编程有关的模型):JMM。
JMM(Java Memory Model):Java 内存模型是 Java 虚拟机规范中定义的一种内存模型,Java 内存模型是标准化的,他屏蔽了底层不同计算机的硬件的不同
Java 内存模型描述了 Java 程序中各种变量(线程共享变量)的访问规则以及在 JVM 中将变量存储到内存和从内存中读取变量的底层细节。
JMM 有以下规定:
- 所有的共享变量都存储于主内存(这里的变量是指实例变量和类变量,不包含局部变量,因为局部变量的线程是私有的,不存在竞争的问题)
- 每一个线程都有自己独立的工作内存,线程的工作内存保留了被线程使用的变量的工作副本
- 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存的变量。
本地内存和主内存之间的关系:
6.2.2、问题分析
- 子线程 1 从主内存中读取到数据并复制到其对应的工作内存。
- 修改 flag 的值为 true,但是这个时候 flag 的值还并没有写会主内存。
- 此时 main 方法读取到了 flag 的值为 false。
- 当子线程 1 将 flag 的值写回去之后,由于 main 函数中的
while(true)
调用的是系统底层的代码,速度快,快到没有时间再去读取主内存中的值,所以此时while(true)
读取到的值一直是flag = false
。 - 此时我们能想到的办法是,如果 main 线程从主内存中读取到了 flag 最新的值,那么 if 语句就可以执行了。
6.2.3、多线程下变量的不可见性的原因
- 每个线程都有自己的工作内存,线程都是从主内存中拷贝到共享变量的副本值
- 每个线程都是在自己的工作内存中操作共享变量的。
6.2.4、解决方案
6.2.4.1、加锁
while(true){
synchronized(t){
if(t.isFlag()){
System.out.print("主线程进入循环")
}
}
}
第一个线程进入synchronized
代码块前后,执行过程如下:
- 线程获得锁
- 清空工作内存
- 从主内存中拷贝共享变量的最新值变成副本
- 执行代码
- 将修改后的值重新放回主内存中
- 线程释放锁
6.2.4.2、对共享变量使用 volatile 关键字修饰
我们还可以对共享变量用volatile
关键字修饰,volatile
关键字的作用是在多线程并发下修改共享变量实现可见性。,一旦一线程修改了volatile
修饰的变量,另一个线程可以立即读取到最新值。
6.2.5、volatile 和 synchronized
volatile
只能修饰实例变量和类变量,而synchronized
可以修饰方法以及代码块volatile
保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全),而synchronized
是一种排他互斥的机制,可以保证线程安全。
七、原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
7.1、问题引入
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
private int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
public class VolatileAtomicThreadDemo {
public static void main(String[] args) {
// 创建VolatileAtomicThread对象
VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;
// 开启100个线程对count进行++操作
for(int x = 0 ; x < 100 ; x++) {
new Thread(volatileAtomicThread).start();
}
}
}
执行结果:不保证一定是 10000
7.2、问题原理说明
以上问题主要是发生在 count++ 操作上,count++ 操作包含 3 个步骤:
- 从主内存中读取数据到工作内存
- 对工作内存中的数据进行 ++ 操作
- 将工作内存中的数据写回到主内存
count++ 操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
1)假设此时 x 的值是 100,线程 A 需要对改变量进行自增 1 的操作,首先它需要从主内存中读取变量 x 的值。由于 CPU 的切换关系,此时 CPU 的执行权被切换到了 B 线程。A 线程就处于就绪状态,B 线程处于运行状态
2)线程 B 也需要从主内存中读取 x 变量的值, 由于线程 A 没有对 x 值做任何修改因此此时 B 读取到的数据还是 100
3)线程 B 工作内存中 x 执行了 + 1 操作,但是未刷新之主内存中
4)此时 CPU 的执行权切换到了 A 线程上,由于此时线程 B 没有将工作内存中的数据刷新到主内存,因此 A 线程工作内存中的变量值还是 100,没有失效。A 线程对工作内存中的数据进行了 + 1 操作
5)线程 B 将 101 写入到主内存
6)线程 A 将 101 写入到主内存
虽然计算了 2 次,但是只对 A 进行了 1 次修改。
7.3、volition 的原子性验证
// 定义一个int类型的变量
private volatile int count = 0 ;
小结:在多线程环境下 ,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下 volatile 修饰的变量也是线程不安全的)。
在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。
7.4、问题解决办法
7.4.1、使用锁机制(加锁)
我们可以给 count++ 操作添加锁,那么 count++ 操作就是临界区的代码,临界区只能有一个线程去执行,所以 count++ 就变成了原子操作。
缺点:性能差。
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量
private volatile int count = 0 ;
private static final Object obj = new Object();
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
synchronized (obj) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
}
7.4.2、使用原子类
7.4.2.1、概述
Java 从 JDK5 开始提供了java.util.concurrent.atomic
包 (简称 Atomic 包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。我们可以使用原子类来保证原子性操作,从而保证线程安全。
7.4.2.2、常用 API
我们以 Integer 的原子类进行讲解。
方法 | 概述 |
---|---|
public AtomicInteger(): | 初始化一个默认值为 0 的原子型 Integer |
public AtomicInteger(int initialValue): | 初始化一个指定值的原子型 Integer |
int get(): | 获取值 |
int getAndIncrement(): | 以原子方式将当前值加 1,注意,这里返回的是自增前的值。 |
int incrementAndGet(): | 以原子方式将当前值加 1,注意,这里返回的是自增后的值。 |
int addAndGet(int data): | 以原子方式将输入的数值与实例中的值(AtomicInteger 里的 value)相加,并返回结果。 |
int getAndSet(int value): | 以原子方式设置为 newValue 的值,并返回旧值。 |
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量,默认值是0,我们也可以指定长度
private AtomicInteger atomicInteger = new AtomicInteger() ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
int i = atomicInteger.getAndIncrement();
System.out.println("count =========>>>> " + i);
}
}
}
7.5、原子类 CAS 机制
CAS 的全成是: Compare And Swap
(比较再交换); 是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS 可以将 read-modify-check-write 转换为原子操作,这个原子操作直接由处理器保证。
CAS 机制当中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。
7.5.1、CAS 机制详解
- 在内存地址 V 当中,存储着值为 10 的变量。
- 此时线程 1 想要把变量的值增加 1。对线程 1 来说,旧的预期值 A=10,要修改的新值 B=11。
- 在线程 1 要提交更新之前,另一个线程 2 抢先一步,把内存地址 V 中的变量值率先更新成了 11。
- 线程 1 开始提交更新,首先进行 A 和地址 V 的实际值比较(Compare),发现 A 不等于 V 的实际值,说明值已经被更改过了,提交失败。
- 线程 1 重新获取内存地址 V 的当前值,并重新计算想要修改的新值。此时对线程 1 来说,A=11,B=12。这个重新尝试的过程被称为自旋。
- 这一次比较幸运,没有其他线程改变地址 V 的值。线程 1 进行 Compare,发现 A 和地址 V 的实际值是相等的,说明并没有人修改过值。
- 线程 1 进行 SWAP,把地址 V 的值替换为 B,也就是 12。
7.6、乐观锁和悲观锁
CAS 和 Synchronized 都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
7.6.1、悲观锁
Synchronized 是从悲观的角度出发,是一个典型的悲观锁。
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。因此 Synchronized 我们也将其称之为悲观锁。jdk 中的 ReentrantLock 也是一种悲观锁。性能较差!
7.6.2、乐观锁
CAS 是从乐观的角度出发,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
CAS 这种机制我们也可以将其称之为乐观锁。综合性能较好!很多数据库都会使用到乐观锁机制。
八、并发包
在实际开发中如果不需要考虑线程安全问题,大家不需要做线程安全,因为如果做了反而性能不好!但是开发中有很多业务是需要考虑线程安全问题的,此时就必须考虑了。否则业务出现问题。
Java 为很多业务场景提供了性能优异,且线程安全的并发包,程序员可以选择使用!
8.1、ConcurrentHashMap
Map 集合中的经典集合:HashMap 它是线程不安全的,性能好。如果在要求线程安全的业务情况下就不能用这个集合做 Map 集合,否则业务会崩溃。
为了保证线程安全,可以使用 Hashtable。Hashtable 是线程安全的 Map 集合,但是性能较差!(已经被淘汰了,虽然安全,但是性能差)
为什么说 HashTable 的性能差呢?我们看看源码可以得知,HashTable 的每一个方法都用synchronized
修饰了,实在是过于悲观。
ConcurrentHashMap 不止线程安全,而且效率高,性能好,最新最好用的线程安全的 Map 集。
8.1.1、HashMap 线程不安全演示
public class Const {
public static HashMap<String,String> map = new HashMap<>();
}
public void run() {
for (int i = 0; i < 500000; i++) {
Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
}
System.out.println(this.getName() + " 结束!");
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread1A a1 = new Thread1A();
Thread1A a2 = new Thread1A();
a1.setName("线程1-");
a2.setName("线程2-");
a1.start();
a2.start();
//休息10秒,确保两个线程执行完毕
Thread.sleep(1000 * 5);
//打印集合大小
System.out.println("Map大小:" + Const.map.size());
}
}
我们执行后可以发现出来的错误是有以下三种:
- 没有达到预期的效果
- 抛出异常
- 结果错误
8.1.2、Hashtable 演示
public class Const {
public static Hashtable<String,String> map = new Hashtable<>();
}
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
}
long end = System.currentTimeMillis();
System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread1A a1 = new Thread1A();
Thread1A a2 = new Thread1A();
a1.setName("线程1-");
a2.setName("线程2-");
a1.start();
a2.start();
//休息10秒,确保两个线程执行完毕
Thread.sleep(1000 * 5);
//打印集合大小
System.out.println("Map大小:" + Const.map.size());
}
}
8.1.3、ConcurrentHashMap 演示
public class Const {
public static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
}
public void run() {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
Const.map.put(this.getName() + (i + 1), this.getName() + i + 1);
}
long end = System.currentTimeMillis();
System.out.println(this.getName() + " 结束!用时:" + (end - start) + " 毫秒");
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread1A a1 = new Thread1A();
Thread1A a2 = new Thread1A();
a1.setName("线程1-");
a2.setName("线程2-");
a1.start();
a2.start();
//休息10秒,确保两个线程执行完毕
Thread.sleep(1000 * 5);
//打印集合大小
System.out.println("Map大小:" + Const.map.size());
}
}
8.1.4、HashTable 效率低下的原因
我们查看 HashTable 的源码我们可以发现他的每一个方法都用synchronized
修饰了,实在是过于悲观。
public synchronized V put(K key, V value)
public synchronized V get(Object key)
在线程竞争激烈的情况下 HashTable 的效率非常低下。因为当一个线程访问 HashTable 的同步方法,其他线程也访问 HashTable 的同步方法时,会进入阻塞状态。如线程 1 使用 put 进行元素添加,线程 2 不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越低。
8.1.5、ConcurrentHashMap 高效的原因
ConcurrentHashMap 上锁机制:CAS + 局部 (synchronized) 锁定(分段式锁)
8.1.6、总结
- HashMap 是线程不安全的。
- Hashtable 线程安全基于 synchronized,综合性能差, 被淘汰了。
- ConcurrentHashMap:线程安全的,分段式锁,综合性能最好,线程安全开发中推荐使用
8.2、CountDownLatch
CountDownLatch 允许一个或多个线程等待其他线程完成操作,再执行自己。
需求
线程 1 要执行打印:A 和 C,线程 2 要执行打印:B,但线程 1 在打印 A 后,要线程 2 打印 B 之后才能打印 C,所以:线程 1 在打印 A 后,必须等待线程 2 打印完 B 之后才能继续执行。
CountDownLatch 构造器以及方法
public CountDownLatch(int count)// 初始化唤醒需要的down几步。count相当于一个计数器
方法 | 详解 |
---|---|
public void await() throws InterruptedException | 让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待 |
public void countDown() | 计数器进行减 1 (down 1) |
8.2.1、例子
public class ThreadA extends Thread {
private CountDownLatch down ;
public ThreadA(CountDownLatch down) {
this.down = down;
}
@Override
public void run() {
System.out.println("A");
try {
down.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
}
public class ThreadB extends Thread {
private CountDownLatch down ;
public ThreadB(CountDownLatch down) {
this.down = down;
}
@Override
public void run() {
System.out.println("B");
down.countDown();
}
}
public class Demo {
public static void main(String[] args) {
CountDownLatch down = new CountDownLatch(1);//创建1个计数器
new ThreadA(down).start();
new ThreadB(down).start();
}
}
8.2.2、总结
- CountDownLatch 中 count down 是倒数的意思,latch 则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点 “三二一,芝麻开门” 的感觉。
- CountDownLatch 是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用 countDown() 方法让计数器 - 1,当计数器到达 0 时,调用 CountDownLatch 的 wait() 方法的线程阻塞状态解除,继续执行。
8.3、CyclicBarrier
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。这里的屏障相当于需要达到的要求。
CyclicBarrier 构造方法:
public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景
CyclicBarrier 重要方法:
public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
8.3.1、例子
制作员工线程
public class PersonThread extends Thread {
private CyclicBarrier cbRef;
public PersonThread(CyclicBarrier cbRef) {
this.cbRef = cbRef;
}
@Override
public void run() {
try {
Thread.sleep((int) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " 到了! ");
cbRef.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
制作开会线程
public class MeetingThread extends Thread {
@Override
public void run() {
System.out.println("好了,人都到了,开始开会......");
}
}
制作测试类
public class Demo {
public static void main(String[] args) {
CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());//等待5个线程执行完毕,再执行MeetingThread
PersonThread p1 = new PersonThread(cbRef);
PersonThread p2 = new PersonThread(cbRef);
PersonThread p3 = new PersonThread(cbRef);
PersonThread p4 = new PersonThread(cbRef);
PersonThread p5 = new PersonThread(cbRef);
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
}
}
8.3.2、使用场景
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景。例如:使用两个线程读取 2 个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。
8.4、Semaphore
Semaphore(发信号)的主要作用是控制线程的并发数量。他的机制和synchronized
一样都是上锁,但是,但某个时间段内,synchronized
只能有一个线程允许执行。Semaphore 可以设置同时允许几个线程执行。它的作用是控制访问特定资源的线程数目。
Semaphore 构造方法:
public Semaphore(int permits) //permits 表示许可线程的数量
public Semaphore(int permits, boolean fair) //fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
Semaphore 重要方法:
public void acquire() throws InterruptedException //表示获取许可
public void release() //release() 表示释放许可
8.4.1、示范一
我们测试一下只允许一个线程的案例。
制作一个 Service 类
public class Service {
private Semaphore semaphore = new Semaphore(1);//1表示许可的意思,表示最多允许1个线程执行acquire()和release()之间的内容
public void testMethod() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()
+ " 进入 时间=" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ " 结束 时间=" + System.currentTimeMillis());
semaphore.release();
//acquire()和release()方法之间的代码为"同步代码"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
制作线程类
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
测试类
public class Demo {
public static void main(String[] args) {
Service service = new Service();
//启动5个线程
for (int i = 1; i <= 5; i++) {
ThreadA a = new ThreadA(service);
a.setName("线程 " + i);
a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有1个线程执行
}
}
}
8.4.2、示范二
我们测试一下只允许两个线程的案例。
修改 Service 类
public class Service {
private Semaphore semaphore = new Semaphore(2);//2表示许可的意思,表示最多允许2个线程执行acquire()和release()之间的内容
public void testMethod() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()
+ " 进入 时间=" + System.currentTimeMillis());
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()
+ " 结束 时间=" + System.currentTimeMillis());
semaphore.release();
//acquire()和release()方法之间的代码为"同步代码"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
8.5、Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。
这两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
Exchanger 构造方法:
public Exchanger();
Exchanger 重要方法:
public V exchange(V x)
8.5.1、exchange 方法的阻塞特性
- 制作线程 A,并能够接收一个 Exchanger 对象:
public class ThreadA extends Thread {
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger) {
super();
this.exchanger = exchanger;
}
@Override
public void run() {
try {
System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 制作 main() 方法
public class Demo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<String>();
ThreadA a = new ThreadA(exchanger);
a.start();
}
}
8.5.2、exchange 方法执行交换
- 制作线程 A
public class ThreadA extends Thread {
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger) {
super();
this.exchanger = exchanger;
}
@Override
public void run() {
try {
System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值...");
System.out.println("在线程A中得到线程B的值=" + exchanger.exchange("礼物A"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 制作线程 B
public class ThreadB extends Thread {
private Exchanger<String> exchanger;
public ThreadB(Exchanger<String> exchanger) {
super();
this.exchanger = exchanger;
}
@Override
public void run() {
try {
System.out.println("线程B欲传递值'礼物B'给线程A,并等待线程A的值...");
System.out.println("在线程B中得到线程A的值=" + exchanger.exchange("礼物B"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 制作测试类
public class Demo {
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<String>();
ThreadA a = new ThreadA(exchanger);
ThreadB b = new ThreadB(exchanger);
a.start();
b.start();
}
}
8.5.3、exchange 方法超时
exchange 方法我们可以设置不一直等待,可以设置一个超时时间。
- 制作线程 A
public class ThreadA extends Thread {
private Exchanger<String> exchanger;
public ThreadA(Exchanger<String> exchanger) {
super();
this.exchanger = exchanger;
}
@Override
public void run() {
try {
System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
System.out.println("线程A结束!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("5秒钟没等到线程B的值,线程A结束!");
}
}
}
- 制作测试类
public class Run {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<String>();
ThreadA a = new ThreadA(exchanger);
a.start();
}
}
8.5.4、使用场景
我们可以做可以做数据校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用 AB 岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,并对两个文件数据进行校对,看看是否录入一致。
九、线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
于是 Java 提供了一种思想:线程池,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。
9.1、线程池的概念
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下 (每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)。
9.2、线程池的实现
Java 中线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而是一个执行线程的工具。真正的线程池接java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用 Executors 工程类来创建线程池对象。
Executors 类中有个创建线程池的方法有:
-
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池, 也就是池中的线程个数可以指定最大数量) -
public Future<?> submit(Runnable task)
: 获取线程池中的某一个线程对象,并执行Future
接口:用来记录线程任务执行完毕后产生的结果。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建 Runnable 接口子类对象。
- 提交 Runnable 接口子类对象。
- 关闭线程池 (一般不做)。
9.2.1、Runnable
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}
9.2.2、Callable
-
<T> Future<T> submit(Callable<T> task)
: 获取线程池中的某一个线程对象,并执行.Future : 表示计算的结果。
-
V get()
: 获取计算完成的结果。
public class ThreadPoolDemo2 {
public static void main(String[] args) throws Exception {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
Callable<Double> c = new Callable<Double>() {
@Override
public Double call() throws Exception {
return Math.random();
}
};
// 从线程池中获取线程对象,然后调用Callable中的call()
Future<Double> f1 = service.submit(c);
// Futur 调用get() 获取运算结果
System.out.println(f1.get());
Future<Double> f2 = service.submit(c);
System.out.println(f2.get());
Future<Double> f3 = service.submit(c);
System.out.println(f3.get());
}
}
十、死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
举个例子:客户(占用资金,等待经销商的货品资源) 经销商(占用货品资源,等待客户的资金)
10.1、死锁产生的必要条件
- 互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
- 不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失
10.2、死锁代码实现
死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免,且死锁一般存在资源的嵌套请求!
package test;
/**
* @author Xiao_Lin
* @date 2020/12/30 19:10
*/
public class DeadlockDemo {
// 1.至少需要两个资源,每个资源只需要1份。
public static Object resources1 = new Object();
public static Object resources2 = new Object();;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
// 线程1:占用资源1 ,请求资源2
synchronized (resources1){
System.out.println("线程1抢占资源1,等待资源2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resources2){
System.out.println("线程1成功抢占资源2");
}
};
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resources2){
System.out.println("线程2抢占资源2,等待资源1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resources1){
System.out.println("线程2成功抢占资源1");
}
}
}
}).start();
}
}