第九章 多线程
前言
2024年最新版零基础Java笔记,最全最详细笔记,适用于零基础小白、软件设计师、就业、提升等,笔记会持续更新,共同进步,希望可以帮助更多的小伙伴!
感谢杜老师的超级无敌详细讲解,感谢动力节点,真的受益匪浅!第三次看老杜Java了,每次都有不一样的收获!由于老杜最新版Java没有md笔记,因此鄙人把所学所写的更新于此,共同进步,希望对大家有帮助!
笔记根据老杜《动力节点》视频进行编写,视频地址:动力节点Java零基础视频(下)
笔记有写的不好的地方,恳请在评论区指正批评,谢谢!
IO流目录
9.1 线程概述
- 什么是进程?什么是线程?它们的区别?
- 进程是指操作系统中的一段程序,它是一个正在执行中的程序实例,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。一般来说,一个进程可以包含多个线程。
- 线程是指进程中的一个执行单元,是进程的一部分,它负责在进程中执行程序代码。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同的操作,从而提高了程序的执行效率。
- 现代的操作系统是支持多进程的,也就是可以启动多个软件,一个软件就是一个进程。称为:多进程并发。
- 通常一个进程都是可以启动多个线程的。称为:多线程并发。
- 多线程的作用?
- 提高处理效率。(多线程的优点之一是能够使 CPU 在处理一个任务时同时处理多个线程,这样可以充分利用 CPU 的资源,提高 CPU 的利用效率。)
- JVM规范中规定:
- 堆内存、方法区 是线程共享的。
- 虚拟机栈、本地方法栈、程序计数器 是每个线程私有的。
- 关于Java程序的运行原理
- “java HelloWorld”执行后,会启动JVM,JVM的启动表示一个进程启动了。
- JVM程会首先启动一个主线程(main-thread),主线程负责调用main方法。因此main方法是在主线程中运行的。
- 除了主线程之外,还启动了一个垃圾回收线程。因此启动JVM,至少启动了两个线程。
- 在main方法的执行过程中,程序员可以手动创建其他线程对象并启动。
9.2 并发与并行
- 并发(concurrency)
-
使用单核CPU的时候,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行。
-
如上图所示,假设只有一个CPU资源,线程之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。
-
在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。
-
- 并行(parallellism)
- 使用多核CPU的时候,同一时刻,有多条指令在多个CPU上同时执行。
- 如图所示,在同一时刻,ABC都是同时执行(微观、宏观)。
并发编程与并行编程
- 在CPU比较繁忙(假设为单核CPU),如果开启了很多个线程,则只能为一个线程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
- 在CPU资源比较充足的时候,一个进程内的多个线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。
- 至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。
- 总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。
- 不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率。
线程的调度模型
- 如果多个线程被分配到一个CPU内核中执行,则同一时刻只能允许有一个线程能获得CPU的执行权,那么进程中的多个线程就会抢夺CPU的执行权,这就是涉及到线程调度策略。
- 分时调度模型
- 所有线程轮流使用CPU的执行权,并且平均的分配每个线程占用的CPU的时间。
- 抢占式调度模型
- 让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就会随机选择一个线程获得CPU的执行权,而Java采用的就是抢占式调用。
9.4 实现线程
- 第一种方式:继承Thread
- 编写一个类继承java.lang.Thread,重写run方法。
- 创建线程对象:
Thread t = new MyThread();
- 启动线程:
t.start();
/** * 在Java语言中,实现线程,有两种方式,第一种方式: * 第一步:编写一个类继承 java.lang.Thread * 第二步:重写run方法 * 第三步:new线程对象 * 第四部:调用线程对象的start方法来启动线程。 */ public class ThreadTest02 { public static void main(String[] args) { // 创建线程对象 //MyThread mt = new MyThread(); Thread t = new MyThread(); // 直接调用run方法,不会启动新的线程。 // java中有一个语法规则:对于方法体当中的代码,必须遵循自上而下的顺序依次逐行执行。 // run()方法不结束,main方法是无法继续执行的。 //t.run(); // 调用start()方法,启动线程 // java中有一个语法规则:对于方法体当中的代码,必须遵循自上而下的顺序依次逐行执行。 // start()方法不结束,main方法是无法继续执行的。 // start()瞬间就会结束,原因这个方法的作用是:启动一个新的线程,只要新线程启动成功了,start()就结束了。 t.start(); // 这里编写的代码在main方法中,因此这里的代码属于在主线程中执行。 for (int i = 0; i < 100; i++) { System.out.println("main--->" + i); } } } // 自定义一个线程类 // java.lang.Thread本身就是一个线程。 // MyThread继承Thread,因此MyThread本身也是一个线程。 class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("MyThread--->" + i); } } }
- 第二种方式:实现Runnable接口
- 编写一个类实现Runnable接口,实现run方法。
- 创建线程对象:
Thread t = new Thread(new MyRunnable());
- 启动线程:
t.start();
/** * 在Java语言中,实现线程,有两种方式,第二种方式: * 第一步:编写一个类实现 java.lang.Runnable接口(可运行的接口) * 第二步:实现接口中的run方法。 * 第三步:new线程对象 * 第四部:调用线程的start方法启动线程 * * 总结:实现线程两种方式: * 第一种:编写类直接继承Thread * 第二种:编写类实现Runnable接口 * * 推荐使用第二种,因为实现接口的同时,保留了类的继承。 */ public class ThreadTest03 { public static void main(String[] args) { // 创建Runnable对象 //Runnable r = new MyRunnable(); // 创建线程对象 //Thread t = new Thread(r); // 创建线程对象 Thread t = new Thread(new MyRunnable()); // 启动线程 t.start(); // 主线程中执行的。 for (int i = 0; i < 100; i++) { System.out.println("main----->" + i); } } } // 严格来说,这个不是一个线程类 // 它是一个普通的类,只不过实现了一个Runnable接口。 class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("t----->" + i); } } }
-
优先选择第二种方式:因为实现接口的同时,保留了类的继承。
-
第二种方式也可以使用匿名内部类。
-
t.start()和t.run()的本质区别?
- 本质上没有区别,都是普通方法调用。只不过两个方法完成的任务不同。
- t.run()是调用run方法。执行run方法中的业务代码。
- t.start()是启动线程,只要线程启动了,start()方法就执行结束了。
-
线程常用的三个方法:
- 实例方法:获取线程对象的名字
String getName();
,修改线程的名字void setName(String name);
- 静态方法:获取当前线程对象的引用
static Thread currentThread();
- 实例方法:获取线程对象的名字
9.5 线程生命周期
-
线程生命周期指的是:从线程对象新建,到最终线程死亡的整个过程。
-
线程生命周期包括七个重要阶段:
- 新建状态(NEW)
- 就绪状态(RUNNABLE)
- 运行状态(RUNNABLE)
- 超时等待状态(TIMED_WAITING)
- 等待状态(WAITING)
- 阻塞状态(BLOCKED)
- 死亡状态/终止状态(TERMINATED)
-
线程生命周期包括五个重要阶段:
- 新建状态
- 就绪状态(可运行状态)
- 运行状态
- 阻塞状态
- 死亡状态
9.6 线程的休眠与终止
- 关于线程的sleep方法:
- static void sleep(long millis)
静态方法,没有返回值,参数是一个毫秒。1秒 = 1000毫秒 - 这个方法作用是:
让当前线程进入休眠,也就是让当前线程放弃占有的CPU时间片,让其进入阻塞状态。
意思:你别再占用CPU了,让给其他线程吧。
阻塞多久呢?参数毫秒为准。在指定的时间范围内,当前线程没有权利抢夺CPU时间片了。 - 怎么理解“当前线程”呢?
Thread.sleep(1000); 这个代码出现在哪个线程中,当前线程就是这个线程。 - run方法在方法重写的时候,不能在方法声明位置使用 throws 抛出异常。
- sleep方法可以模拟每隔固定的时间调用一次程序。
- 怎么中断一个线程的睡眠
interrupt()
。(怎么解除线程因sleep导致的阻塞,让其开始抢夺CPU时间片。)public class ThreadTest { public static void main(String[] args) { // 创建线程对象并启动 Thread t = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "===> begin"); try { // 睡眠一年 Thread.sleep(1000 * 60 * 60 * 24 * 365); } catch (InterruptedException e) { // 打印异常信息 //e.printStackTrace(); System.out.println("知道了,这就起床!"); } // 睡眠一年之后,起来干活了 System.out.println(Thread.currentThread().getName() + " do some!"); } }); // 启动线程 t.start(); // 主线程 // 要求:5秒之后,睡眠的Thread-0线程起来干活 try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { throw new RuntimeException(e); } // Thread-0起来干活了。 // 这行代码的作用是终止 t 线程的睡眠。 // interrupt方法是一个实例方法。 // 以下代码含义:t线程别睡了。 // 底层实现原理是利用了:异常处理机制。 // 当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterruptedException,然后捕捉异常,终止睡眠。 t.interrupt(); } }
- 一个线程 t 一直在正常的运行,如何终止 t 线程的执行,
stop()
从java2开始就不建议使用了,因为这种方式是强行终止线程。容易导致数据丢失。没有保存的数据,在内存中的数据一定会因为此方式导致丢失。 - 如何合理的,正常的方式终止一个线程的执行?
- 一般我们在实际开发中会使用打标记的方式,来终止一个线程的执行。
public class ThreadTest { public static void main(String[] args) { // 创建线程 MyRunnable mr = new MyRunnable(); Thread t = new Thread(mr); t.setName("t"); // 启动线程 t.start(); // 5秒之后终止线程t的执行 try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } //终止线程t的执行。 mr.run = false; } } class MyRunnable implements Runnable { /** * 是否继续执行的标记。 * true表示:继续执行。 * false表示:停止执行。 */ boolean run = true; @Override public void run() { for (int i = 0; i < 10; i++) { if(run){ System.out.println(Thread.currentThread().getName() + "==>" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } }else{ return; }}}}
9.7 守护线程
- 在Java语言中,线程被分为两大类:
- 第一类:用户线程(非守护线程)
- 第二类:守护线程(后台线程)
- 在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。
- 守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。
- 如何将一个线程设置为守护线程?
- t.setDaemon(true);
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("t");
// 在启动线程之前,设置线程为守护线程
myThread.setDaemon(true);
myThread.start();
// 10s结束!
// main线程中,main线程是一个用户线程。
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}}}}
class MyThread extends Thread {
@Override
public void run() {
int i = 0;
while(true){
System.out.println(Thread.currentThread().getName() + "===>" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}}}}
9.8 定时任务
- JDK中提供的定时任务:
- java.util.Timer 定时器
- java.util.TimerTask 定时任务
- 定时器 + 定时任务:可以帮我们在程序中完成:每间隔多久执行一次某段程序。
- Timer的构造方法:
Timer()
Timer(boolean isDaemon)
isDaemon是true表示该定时器是一个守护线程。
public class ThreadTest {
public static void main(String[] args) throws Exception{
// 创建定时器对象(本质上就是一个线程)
// 如果这个定时器执行的任务是一个后台任务,是一个守护任务,建议将其定义为守护线程。
Timer timer = new Timer(true);
// 指定定时任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2024-01-27 10:22:00");
//timer.schedule(new LogTimerTask(), firstTime, 1000);
// 匿名内部类的方式
timer.schedule(new TimerTask() {
int count = 0;
@Override
public void run() {
// 执行任务
Date now = new Date();
String strTime = sdf.format(now);
System.out.println(strTime + ": " + count++);
}
}, firstTime, 1000 * 5);
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
}
}
}
定时任务类:专门记录日期的定时任务类。
public class LogTimerTask extends TimerTask {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
int count = 0;
@Override
public void run() {
// 执行任务
Date now = new Date();
String strTime = sdf.format(now);
System.out.println(strTime + ": " + count++);
}
}
9.9 线程的调度
9.9.1 线程的合并
- 调用
join()
方法完成线程合并 join()
方法是一个实例方法。(不是静态方法)t.join- 假设在main方法(main线程)中调用了 t.join(),后果是什么?
- t线程合并到主线程中。主线程进入阻塞状态。直到 t 线程执行结束。主线程阻塞解除。
t.join()
方法其实是让当前线程进入阻塞状态,直到t线程结束,当前线程阻塞解除。- 和sleep方法有点类似,但不一样:
- 第一:sleep方法是静态方法,join是实例方法。
- 第二:sleep方法可以指定睡眠的时长,join方法不能保证阻塞的时长。
- 第三:sleep和join方法都是让当前线程进入阻塞状态。
- 第四:sleep方法的阻塞解除条件?时间过去了。 join方法的阻塞解除条件?调用join方法的那个线程结束了。
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.setName("t");
t.start();
System.out.println("main begin");
// 合并线程
// t合并到main线程中。
// main线程受到阻塞(当前线程受到阻塞)
// t线程继续执行,直到t线程结束。main线程阻塞解除(当前线程阻塞解除)。
//t.join();
// join方法也可以有参数,参数是毫秒。
// 以下代码表示 t 线程合并到 当前线程,合并时长 10 毫秒
// 阻塞当前线程 10 毫秒
//t.join(10);
// 调用这个方法,是想让当前线程受阻10秒
// 但不一定,如果在指定的阻塞时间内,t线程结束了。当前线程阻塞也会解除。
t.join(1000 * 10);
// 当前线程休眠10秒。
//Thread.sleep(1000 * 10);
// 主线程
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
System.out.println("main over");
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}}}
9.9.2 线程的优先级
关于线程生命周期中的JVM调度:
- 优先级
- 线程是可以设置优先级的,优先级较高的,获得CPU时间片的总体概率高一些。
- JVM采用的是抢占式调度模型。谁的优先级高,获取CPU时间片的总体概率就高。
- 默认情况下,一个线程的优先级是 5.
- 最低是1,最高是10.
public class ThreadTest {
public static void main(String[] args) {
/*System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
System.out.println("默认优先级:" + Thread.NORM_PRIORITY);
// 获取main线程的优先级
Thread mainThread = Thread.currentThread();
System.out.println("main线程的优先级:" + mainThread.getPriority()); // 5
// 设置优先级
mainThread.setPriority(Thread.MAX_PRIORITY);
System.out.println("main线程的优先级:" + mainThread.getPriority()); // 10*/
// 创建两个线程
Thread t1 = new MyThread();
t1.setName("t1");
Thread t2 = new MyThread();
t2.setName("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}}}
9.9.3 让位
关于JVM的调度:
- 让位
- 静态方法:Thread.yield()
- 让当前线程让位。
- 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
- 只能保证大方向上的,大概率,到了某个点让位一次。
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.setName("t1");
Thread t2 = new MyThread();
t2.setName("t2");
t1.start();
t2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 500; i++) {
if(Thread.currentThread().getName().equals("t1") && i % 10 == 0){
System.out.println(Thread.currentThread().getName() + "让位了,此时的i下标是:" + i);
// 当前线程让位,这个当前线程一定是t1
// t1会让位一次
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}
9.10 线程安全问题
9.10.1 线程安全概念
- 什么情况下需要考虑线程安全问题?
- 条件1:多线程的并发环境下
- 条件2:有共享的数据
- 条件3:共享数据涉及到修改的操作
- 一般情况下:
- 局部变量不存在线程安全问题。(尤其是基本数据类型不存在线程安全问题【在栈中,栈不是共享的】,如果是引用数据类型,就另说了!)
- 实例变量可能存在线程安全问题。实例变量在堆中。堆是多线程共享的。
- 静态变量也可能存在线程安全问题。静态变量在堆中。堆是多线程共享的。
- 大家找一个现实生活中的例子,来说明一下,线程安全问题:比如同时取钱!
- 以上多线程并发对同一个账户进行取款操作的时候,有安全问题?怎么解决?
- 让线程t1和线程t2排队执行。不要并发。要排队。
- 我们把线程排队执行,叫做:线程同步机制。(t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。只要t1和t2之间发生了等待,就认为是同步。)
- 如果不排队,我们将其称为:线程异步机制。(t1和t2各自执行各自的,谁也不需要等对方。并发的,就认为是异步)
- 异步:效率高。但是可能存在安全隐患。
- 同步:效率低。排队了。可以保证数据的安全问题。
9.10.2 同步机制
使用线程同步机制,来保证多线程并发环境下的数据安全问题:
- 线程同步的本质是:线程排队执行就是同步机制。
- 语法格式:
synchronized(必须是需要排队的这几个线程共享的对象){ // 需要同步的代码 }
- “必须是需要排队的这几个线程共享的对象” 这个必须选对了。
- 这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
- 原理是什么?
synchronized(obj){ // 同步代码块 }
- 假设obj是t1 t2两个线程共享的。
- t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
- 假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
- 当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
- 同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
- 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。
在实例方法上也可以添加 synchronized 关键字:
- 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
- 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
这种方式相对于之前所讲的局部同步代码块的方式要差一些:synchronized(共享对象){ // 同步代码块 }
- 这种方式优点:
- 灵活
- 共享对象可以随便调整。
- 同步代码块的范围可以随便调整。
9.10.3 同步机制面试题
/**
* 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
*/
public class ThreadTest {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new Thread(new MyRunnable(mc));
Thread t2 = new Thread(new MyRunnable(mc));
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
class MyRunnable implements Runnable {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){
mc.m1();
}
if("t2".equals(Thread.currentThread().getName())){
mc.m2();
}
}
}
class MyClass {
public synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("m1 over");
}
public void m2(){
System.out.println("m2 begin");
System.out.println("m2 over");
}
}
答案:不需要
/**
* 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
*/
public class ThreadTest {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new Thread(new MyRunnable(mc));
Thread t2 = new Thread(new MyRunnable(mc));
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
class MyRunnable implements Runnable {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){
mc.m1();
}
if("t2".equals(Thread.currentThread().getName())){
mc.m2();
}
}
}
class MyClass {
public synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("m1 over");
}
public synchronized void m2(){
System.out.println("m2 begin");
System.out.println("m2 over");
}
}
答案:需要
/**
* 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
*/
public class ThreadTest {
public static void main(String[] args) {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new Thread(new MyRunnable(mc1));
Thread t2 = new Thread(new MyRunnable(mc2));
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
class MyRunnable implements Runnable {
private MyClass mc;
public MyRunnable(MyClass mc) {this.mc = mc;}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){mc.m1();}
if("t2".equals(Thread.currentThread().getName())){mc.m2();}
}
}
class MyClass {
public synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("m1 over");
}
public synchronized void m2(){
System.out.println("m2 begin");
System.out.println("m2 over");
}
}
答案:不需要
**
* 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
* 需要等待。
*
* 在静态方法上添加synchronized之后,线程会占有类锁。
* 类锁是,对于一个类来说,只有一把锁。不管创建了多少个对象,类锁只有一把。
*
* 静态方法上添加synchronized,实际上是为了保证静态变量的安全。
* 实例方法上添加synchronized,实际上是为了保证实例变量的安全。
*/
public class ThreadTest {
public static void main(String[] args) {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new Thread(new MyRunnable(mc1));
Thread t2 = new Thread(new MyRunnable(mc2));
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}}
class MyRunnable implements Runnable {
private MyClass mc;
public MyRunnable(MyClass mc) {this.mc = mc;}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){
mc.m1();
}
if("t2".equals(Thread.currentThread().getName())){
mc.m2();
}}}
class MyClass {
public static synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("m1 over");
}
public static synchronized void m2(){
System.out.println("m2 begin");
System.out.println("m2 over");
}
}
9.11 线程间的通信
- 内容是关于:线程通信。
- 线程通信涉及到三个方法:wait()、notify()、notifyAll()
wait()
: 线程执行该方法后,进入等待状态,并且释放对象锁。
2.notify()
: 唤醒优先级最高的那个等待状态的线程。【优先级相同的,随机选一个】。被唤醒的线程从当初wait()的位置继续执行。notifyAll()
: 唤醒所有wait()的线程- 需要注意的:
- 以上三个方法在使用时,必须在同步代码块中或同步方法中。
- 调用这三个方法的对象必须是共享的锁对象。
- 这三个方法都是Object类的方法。
- 其中wait()方法重载了三个:
wait()
:调用此方法,线程进入“等待状态”wait(毫秒)
:调用此方法,线程进入“超时等待状态”wait(毫秒, 纳秒)
:调用此方法,线程进入“超时等待状态”
- 调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
- 例如调用了:obj.wait(),什么效果?
- obj是多线程共享的对象。
- 当调用了
obj.wait()
之后,在obj对象上活跃的所有线程进入无期限等待。直到调用了该共享对象的obj.notify()
方法进行了唤醒。而且唤醒后,会接着上一次调用wait()
方法的位置继续向下执行。
obj.wait()
方法调用之后,会释放之前占用的对象锁。- 关于notify和notifyAll方法:
共享对象.notify()
; 调用之后效果是什么?唤醒优先级最高的等待线程。如果优先级一样,则随机唤醒一个。共享对象.notifyAll()
; 调用之后效果是什么?唤醒所有在该共享对象上等待的线程。
- wait()和sleep的区别?
- 相同点:都会阻塞。
- 不同点:
- wait是Object类的实例方法。sleep是Thread的静态方法。
- wait只能用在同步代码块或同步方法中。sleep随意。
- wait方法执行会释放对象锁。sleep不会。
- wait结束时机是notify唤醒,或达到指定时间。sleep结束时机是到达指定时间。
9.12 单例模式的线程安全问题
class SingletonTest {
// 静态变量
private static Singleton s1;
private static Singleton s2;
public static void main(String[] args) {
// 获取某个类。这是反射机制中的内容。
/*Class stringClass = String.class;
Class singletonClass = Singleton.class;
Class dateClass = java.util.Date.class;*/
// 创建线程对象t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});
// 创建线程对象t2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
s2 = Singleton.getSingleton();
}
});
// 启动线程
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 判断这两个Singleton对象是否一样。
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}
/**
* 懒汉式单例模式
*/
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.println("构造方法执行了!");
}
// 非线程安全的。
/*public static Singleton getSingleton() {
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
return singleton;
}*/
// 线程安全的:第一种方案(同步方法),找类锁。
/*public static synchronized Singleton getSingleton() {
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
return singleton;
}*/
// 线程安全的:第二种方案(同步代码块),找的类锁
/*public static Singleton getSingleton() {
// 这里有一个知识点是反射机制中的内容。可以获取某个类。
synchronized (Singleton.class){
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
return singleton;
}*/
// 线程安全的:这个方案对上一个方案进行优化,提升效率。
/*public static Singleton getSingleton() {
if(singleton == null){
synchronized (Singleton.class){
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
}
return singleton;
}*/
// 使用Lock来实现线程安全
// Lock是接口,从JDK5开始引入的。
// Lock接口下有一个实现类:可重入锁(ReentrantLock)
// 注意:要想使用ReentrantLock达到线程安全,假设要让t1 t2 t3线程同步,就需要让t1 t2 t3共享同一个lock。
// Lock 和 synchronized 哪个好?Lock更好。为什么?因为更加灵活。
private static final ReentrantLock lock = new ReentrantLock();
public static Singleton getSingleton() {
if(singleton == null){
try {
// 加锁
lock.lock();
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
} finally {
// 解锁(需要100%保证解锁,怎么办?finally)
lock.unlock();
}
}
return singleton;
}
}
9.13 Callable实现线程
- 实现线程的第三种方式:实现Callable接口,实现call方法。
- 这种方式实现的线程,是可以获取到线程返回值的。
public class ThreadTest {
public static void main(String[] args) {
// 创建“未来任务”对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 处理业务......
Thread.sleep(1000 * 5);
return 1;
}
});
// 创建线程对象
Thread t = new Thread(task);
t.setName("t");
// 启动线程
t.start();
try {
// 获取“未来任务”线程的返回值
// 阻塞当前线程,等待“未来任务”结束并返回值。
// 拿到返回值,当前线程的阻塞才会解除。继续执行。
Integer i = task.get();
System.out.println(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*class MyRunnable implements Runnable {
@Override
public void run() {
}
}
class MyThread extends Thread {
@Override
public void run() {
}
}*/
9.14 线程池实现线程
- 创建线程的第四种方式:使用线程池技术。
- 线程池本质上就是一个缓存:cache
- 一般都是服务器在启动的时候,初始化线程池,
- 也就是说服务器在启动的时候,创建N多个线程对象,
- 直接放到线程池中,需要使用线程对象的时候,直接从线程池中获取。
public class ThreadTest {
public static void main(String[] args) {
// 创建一个线程池对象(线程池中有3个线程)
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 将任务交给线程池(你不需要触碰到这个线程对象,你只需要将要处理的任务交给线程池即可。)
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
});
// 最后记得关闭线程池
executorService.shutdown();
}
}
- 每章一句:“ 人生没有如果,只有后果和结果。少问别人为什么,多问自己凭什么。”
- 恭喜你已阅读完第九章!点个赞证明你已经挑战成功,进入第十章关卡【反射机制】吧【更新中……】!