目录
1.预备知识
1.1 冯诺依曼体系结构:
1.2 现代CPU主要关心指标(和日常开发密切相关的)
1.3 计算机中,一个汉字占几个字节?
1.4 Windows和Linux的区别
1.5 PCB的一些关键要点
2.线程和进程
2.1 创建线程的写法
2.2 thread的几个常见属性
2.3 休眠当前进程
3.线程的状态
4.多线程带来的风险-线程安全
4.1 线程安全问题产生原因
4.2 synchronized关键字
4.3 解决死锁的方法
4.4 Java标准库中的线程安全类
5.volatile关键字
5.1 volatile能保证内存可见性
5.2 volatile不保证原子性
6. wait和notify
1.预备知识
1.1 冯诺依曼体系结构:
CPU,存储器(内存、外存/硬盘),输入/输出设备
内存和硬盘区别:
- 内存访问速度快,硬盘速度慢
- 内存空间小,硬盘空间大
- 内存成本高,硬盘成本低
- 内存数据掉电后丢失,硬盘数据掉电后持续存储
有的设备既是输入设备又是输出设备,比如触摸屏,网卡(上网时和网线连接部分的硬件设备,集成在主板上)...
1.2 现代CPU主要关心指标(和日常开发密切相关的)
- CPU的频率
基频/默频
睿频/加速频率
- CPU的核心数
大小核
CPU的基本工作流程:读取指令、解析指令、执行指令
1.3 计算机中,一个汉字占几个字节?
取决于字符集(汉字怎样编码)
- gbk(中国大陆上曾经广泛使用的)。Windows10/11简体中文版默认gbk.使用VS写代码打印汉字strlen,结果是2;
- utf8(当下全世界最流行的编码方式)
- 一个汉字占3字节。utf8本身是变长编码(1-4);
- unicode(Java的char就是使用unicode)一个汉字2字节
- Java的String就不一定
1.4 Windows和Linux的区别
最直接的区别:两个系统提供的API(系统函数)不同
比如:Windows的Sleep(ms) =>#include <Windows.h>
Linux的sleep(s) usllep(us) =>#include <unistd.h>
Windows一般是使用图形化界面操作;Linux一般是命令行操作(命令)
不同的操作系统之间不兼容,java却有“跨平台”特性,不需要任何修改,就可以在不同的系统上完成同样的功能,原因在于Java虚拟机,不同的主流系统都有各自的Java 虚拟机,Windows有Windows JVM,Linux有Linux JVM,这些JVM是不同的程序,但是上层支持的Java字节码是一致的。
进程是操作系统中,资源分配的基本单位。
1.5 PCB的一些关键要点
- pid(进程id)进程的身份标识符
- 内存指针(一组指针)
- 进程需要知道要执行的指令的地址
- 指令依赖的数据在哪里
- 文件描述符表
- 进程状态
- 进程优先级
- 进程上下文
- 进程的记账信息(统计每个进程在CPU上运行了多久,如果某个进程很久没有得到CPU资源,就给此进程多一些资源)
在一个CPU核心上,按照分时复用,执行多个进程这样的方式,称为“并发执行”;(人看起来是同时执行,微观上,其实是一个CPU在串行执行,切换速度极快)
在多个CPU核心上,同时执行多个进程这样的方式,称为“并行执行”。(实际上就是“同时执行”)
现代CPU在运行这些进程的时候,并发和并行是同时存在的。
因为需要并发执行,所以操作系统需要进行进程的快速切换,即“进程调度”。
线程是CPU上调度执行的基本单位。
2.线程和进程
线程(Thread):
- 概念:线程是进程内部的执行单元,是操作系统能够进行运算调度的最小单位。
- 特点:
- 轻量级:线程的创建、切换和销毁相对较快。
- 共享资源:线程可以共享进程的资源。
- 并发执行:多个线程可以同时执行,提高程序的并发性能。
进程(Process):
- 概念:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位。
- 特点:
- 独立性:每个进程有独立的地址空间、状态和资源。
- 资源分配:进程拥有自己的资源,如内存、CPU 等。
- 隔离性:进程之间相互隔离,互不干扰。
进程是操作系统资源分配的基本单位;线程是操作系统调度执行的基本单位。
区别:
- 独立性:进程是独立的执行实体,每个进程有自己的地址空间、资源和状态;而线程是进程中的一部分,共享进程的地址空间和资源。
- 资源分配:进程分配资源(如内存);线程共享进程的资源。
- 调度:进程调度涉及到进程的切换;线程调度更细粒度,切换速度快。
- 进程的创建和销毁开销较大,而线程更灵活、高效。
- 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
2.1 创建线程的写法
- 1. 继承Thread,重写run
package thread;class MyThread extends Thread{@Overridepublic void run(){// run相当于线程的入口(新线程启动就自动执行)while(true){System.out.println("<UNK>");}}
}
public class Demo1 {public static void main(String[] args) {Thread t=new MyThread();while(true){t.start();//真正在系统中创建出一个线程(JVM调用操作系统的API完成线程创建操作)}}
}
- 2. 实现Runnable,重写run(能够更好的解耦合)
package thread;class MyRunnable implements Runnable{@Overridepublic void run() {while(true){System.out.println("<UNK1>");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Runnable runnable = new MyRunnable();Thread t = new Thread(runnable);t.start();while(true){System.out.println("<UNK2>");Thread.sleep(1000);}}
}
- 继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
- 实现 Runnable 接⼝, this 表⽰的是 MyRunnable 的引⽤. 需要使⽤ Thread.currentThread()
- 3. 匿名内部类创建 Thread ⼦类对象
package thread;public class Demo3 {public static void main(String[] args) {Thread t=new Thread(){// 匿名内部类// 1.创建一个Thread子类,是匿名的// 2.{}里面编写子类的定义代码,子类里面的属性、方法、重写父类的方法// 3.创建了这个匿名内部类的实例,并把实例的引用赋值给t@Overridepublic void run(){while(true){System.out.println("hello thread");try{Thread.sleep(2000);}catch (InterruptedException e){throw new RuntimeException(e);}}}};t.start();while(true){System.out.println("hello main");try{Thread.sleep(2000);}catch (InterruptedException e){throw new RuntimeException(e);}}}
}
- 4. 匿名内部类创建 Runnable ⼦类对象
package thread;public class Demo4 {public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread thread = new Thread(runnable);thread.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
- 5. lambda 表达式创建 Runnable ⼦类对象(本质上是“匿名函数”,主要用途是作为“回调函数”)
package thread;public class Demo5 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while (true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
2.2 thread的几个常见属性
- ID是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- JVM会在一个进程的所有前台线程结束后,才会结束运行
- 是否存活,即run方法是否运行结束了
守护进程(Deamon):
前台线程的存在能够影响到进程继续存在;
后台线程的存在不影响进程结束,这些是JVM自带的线程,即使他们继续存在,如果进程要结束了,他们也随之结束。
package thread;public class Demo7 {public static void main(String[] args) {Thread t = new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//线程默认是前台线程t.setDaemon(true);//在start之前进行,将此线程设为后台线程,无力阻止进程结束t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
package thread;public class Demo8 {public static void main(String[] args) {Thread t = new Thread(()->{for(int i=0;i<3;i++){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//这个结果一定是false//此时还没有调用start,没有真正创建线程System.out.println(t.isAlive());t.start();while(true){System.out.println(t.isAlive());try {Thread.sleep(1000);} catch (InterruptedException e) {}}}
}
每个Thread对象,都只能start一次
每次想创建一个新的线程,都得创建一个新的Thread对象(不能重复利用)
package thread;public class Demo9 {public static void main(String[] args) {Thread t=new Thread(()->{System.out.println("hello thread");});t.start();t.start();//抛出异常}
}
Thread对象和内核中的线程一一对应,可能出现内核中的线程已经结束销毁了,但是Thread对象还在。
package thread;public class Demo10 {public static void main(String[] args) throws InterruptedException {boolean isFinished=false;Thread t =new Thread(()->{while (!isFinished) {//报错//lambda里面,希望使用外面的变量,触发“变量捕获”这样的语法。// lambda是回调函数,操作系统真正创建出线程之后才会执行。//很有可能,后续线程创建好了之后,当前main里的方法都执行完了,对应的isFinished就销毁了//为了解决问题,Java把被捕获的变量拷贝一份,拷贝给lambda//外面的变量是否销毁,就不影响lambda里面的执行了System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread is finished");});t.start();Thread.sleep(3000);isFinished = true;}
}
package thread;public class Demo11 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{//这是在lambda中(即在t线程的入口方法中)调用的//返回结果是t
// System.out.println("t:"+Thread.currentThread().getName());while(!Thread.currentThread().isInterrupted()){//静态方法,哪个线程调用,获取到的就是哪个线程的Thread引用System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {
// break;//针对上述代码,//正常来说,调用Interrupt方法就会修改isInterrupted方法内部的标志位,设为true//由于上述代码把sleep唤醒了,//这种提前唤醒的情况下,sleep就会在唤醒之后把isInterrupted标志位设置为false//因此在这样的情况下,如果继续执行到循环条件判定,就会发现能继续执行
// throw new RuntimeException(e);}}System.out.println("thread exit");});t.start();Thread.sleep(3000);System.out.println("main线程尝试终止t线程");t.interrupt();//这个代码是在main中调用的,返回结果是main
// System.out.println("main:"+Thread.currentThread().getName());}
}
- 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志 。当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
- 否则,只是内部的⼀个中断标志被设置,thread 可以通过Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到。
package thread;public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{for (int i = 0; i < 3; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t线程结束");});t.start();Thread.sleep(2000);//虽然可以通过sleep休眠的时间控制线程结束的顺序,但是这样的设定并不科学//通过设置时间的方式不一定靠谱System.out.println("main线程结束");}
}
在main线程中调用t.join,主线程等待t先结束,main线程就会“阻塞等待”
join提供的参数指定“超时时间”,即等待的最大时间
2.3 休眠当前进程
线程调度不可控,因此只能保证实际休眠时间大于等于参数设置的休眠时间。
代码调用sleep,相当于当前进程让出CPU资源,后续时间到了,需要操作系统内核,再把这个线程调到CPU上,才能继续执行。(不是立即执行)
sleep(0)是使用sleep的特殊写法,意味着当前线程立即放弃CPU资源,等待操作系统重新调度。
3.线程的状态
- NEW:安排了工作,还未开始行动。new了Thread对象,还没start.
package thread;public class Demo13 {public static void main(String[] args) {Thread t=new Thread(()->{System.out.println("hello thread");});System.out.println(t.getState());//NEWt.start();}
}
- TERMINATED:工作完成了。内核中的线程已经结束了,但是Thread对象还在.
package thread;public class Demo13 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{System.out.println("hello thread");});System.out.println(t.getState());//NEWt.start();Thread.sleep(1000);System.out.println(t.getState());//TERMINATED}
}
- RUNNABLE:可工作的。又可分为正在工作和即将开始工作.
就绪:线程正在CPU上执行;线程随时可以去CPU上执行。
- TIMED_WAITING:指定时间的阻塞(阻塞的时间有上限,sleep时间到了就回到RUNNABLE)
package thread;public class Demo13 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});System.out.println(t.getState());//NEWt.start();Thread.sleep(1000);System.out.println(t.getState());//TIMED_WAITING}
}
package thread;public class Demo14 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();t.join(6000*1000);//main线程所处的状态为TIMED_WAITING}
}
- WAITING:死等,没有超时时间的阻塞等待。(线程执行完才能回到RUNNABLE)
package thread;public class Demo14 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();t.join();//main线程的状态为WAITING}
}
- BLOCKED:是一种特殊的阻塞,由于锁导致的阻塞。
4.多线程带来的风险-线程安全
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}});t1.start();t2.start();System.out.println(count);//0//main先执行打印}
}
加入t.join之后的结果:(在t1和t2执行完后打印)
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t1结束");});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t2结束");});t1.start();t2.start();t1.join();t2.join();//两个线程谁先join无所谓//总的阻塞时间是t1和t2较长的时间//区别在于是分两个join各自阻塞一会//还是在一个join全部阻塞完System.out.println(count);//63060(多线程并发执行引起的问题)}
}
上述代码是由于多线程的并发执行代码引起的bug,称为“线程安全问题”,或者叫做“线程不安全”。
如果代码在多线程并发执行的环境下也不会出现类似上述的bug,就称代码“线程安全”。
实现预期结果:(串行执行)
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t1结束");});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count++;}System.out.println("t2结束");});t1.start();t1.join();t2.start();t2.join();//两个线程谁先join无所谓//总的阻塞时间是t1和t2较长的时间//区别在于是分两个join各自阻塞一会//还是在一个join全部阻塞完System.out.println(count);//100000}
}
count++实际对应3个CPU指令:
- load,把内存中的值(count变量)读取到CPU寄存器
- add,把指定寄存器中的值进行+1操作(结果还是在寄存器中)
- save,把寄存器中的值写回到内存中
CPU执行这三条指令的过程中,随时可能触发线程的调度切换
4.1 线程安全问题产生原因
- 1. [根本]操作系统对于线程的调度是随机的,抢占式执行
- 2. 多个线程同时修改同一个变量(出现了中间结果相互覆盖的情况)
解决方法:和代码的结构直接相关,调整代码结构,规避一些线程不安全的代码。
有些情况下,需求就是需要多线程修改同一个变量。
- 3. 修改操作,不是原子的。
如果修改操作只对应一个CPU指令,就认为是原子的,CPU不会出现“一条指令执行一半”的情况。
解决方法:加锁。通过加锁,让不是原子的操作,打包成一个原子的操作。
加锁操作,不是把线程锁死到CPU上,禁止线程被调度走;而是禁止其他线程重新加这个锁,避免其他线程在当前线程执行过程中插队。
【事务的4个特性:原子性,一致性,持久性,隔离性】
Java中的String就是采取“不可变”特性确保线程安全。(String没有提供public的修改方法)
String的final用来实现“不可继承”。
两个线程,针对同一个对象加锁,才会产生互斥效果。(一个线程加锁,另一个线程就阻塞等待,等第一个线程释放锁才有机会)
package thread;public class Demo15 {private static int count=0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1=new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){count++;}}System.out.println("t1结束");});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){count++;}}System.out.println("t2结束");});t1.start();t2.start();t1.join();t2.join();//两个线程谁先join无所谓//总的阻塞时间是t1和t2较长的时间//区别在于是分两个join各自阻塞一会//还是在一个join全部阻塞完System.out.println(count);//100000}
}
- 4. 内存可见性问题引起的线程不安全
- 5. 指令重排序引起的线程不安全
Java中使用synchronized+代码块,很少使用lock+unlock函数的方式,是因为unlock容易遗漏。
lock和unlock中间如果有return或者异常处理,后面的unlock会执行不到。
synchronized就避免了这种情况。
package thread;class Counter{public int count=0;public void add(){synchronized (this){count++;}}public int get(){return count;}
}
public class Demo18 {public static void main(String[] args) throws InterruptedException {Object locker=new Object();Counter counter=new Counter();Thread t1=new Thread(()->{for(int i=0;i<50000;i++){counter.add();}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.get());}
}
注意:
public void add(){synchronized (this){count++;}}//可以变形为:synchronized public void add(){count++;}
StringBuffer和Vector这些对象方法上就是带有synchronized(针对this加锁)
有一种特殊情况:
static修饰的方法不存在this,此时,synchronized修饰static方法,相当于针对类对象加锁:
public synchronized static void func(){synchronized(Counter.class){}}
synchronized修饰普通方法,相当于是给this加锁;
synchronized修饰静态方法,相当于是给类对象加锁。
4.2 synchronized关键字
synchronized的特性:互斥;可重入
- 对一个线程连续加锁两次,会出现死锁:
Thread t1=new Thread(()->{for(int i=0;i<50000;i++){synchronized (locker){synchronized (locker){//阻塞等待,等到前一次的锁被释放,第二次加锁的阻塞才会解除counter.add();}}}});
synchronized的可重入特性可以解决:
当某个线程针对一个锁加锁成功后,后续该线程再次针对这个锁进行加锁,不会触发阻塞,而是直接往下走。但是如果是其他线程尝试加锁就会正常阻塞。
可重入锁的实现原理,关键在于让锁对象内部保存当前是哪个线程持有这把锁。
后续有线程针对这个锁加锁的时候,对比一下锁持有者的线程是否和当前加锁的线程是同一个。
如何自己实现一个可重入锁?
- 在锁内部记录当前是哪个线程持有的锁,后续每次加锁,都进行判定
- 通过计数器,记录当前加锁的次数,从而确定何时真正进行解锁
- 两个线程两把锁,每个线程获取到一把锁之后,尝试获取对方的锁会引起死锁:
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1线程两个锁都获取到");}}});Thread t2=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2线程两个锁都获取到");}}});t1.start();t2.start();t1.join();t2.join();}
}
如果不加sleep,有可能t1把locker1和locker2都拿到了,t2还没开始,自然无法构成死锁。
- 死锁的第三种情况:N个线程M把锁
一个经典的模型:哲学家就餐问题
构成死锁的四个必要条件:
- 锁是互斥的。一个线程拿到锁之后,另一个线程再次尝试获取锁,必须要阻塞等待。
- 锁是不可抢占的。
- 请求和保持。一个线程拿到锁1之后,不释放锁1的前提下获取锁2
- 循环等待。多个线程多把锁之间的等待过程构成了“循环”
4.3 解决死锁的方法
- 1.破坏“请求和保持”
代码中加锁不要嵌套:
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}}synchronized (locker2){System.out.println("t1线程两个锁都获取到");}});Thread t2=new Thread(()->{synchronized (locker2){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}}synchronized (locker1){System.out.println("t2线程两个锁都获取到");}});t1.start();t2.start();t1.join();t2.join();}
}
- 2.破坏“循环等待”
约定好加锁的顺序:(先获取序号小的,后获取序号大的)
package thread;public class Demo20 {public static void main(String[] args) throws InterruptedException {Object locker1=new Object();Object locker2=new Object();Thread t1=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1线程两个锁都获取到");}}});Thread t2=new Thread(()->{synchronized (locker1){try{Thread.sleep(1000);}catch (InterruptedException e){throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2线程两个锁都获取到");}}});t1.start();t2.start();t1.join();t2.join();}
}
4.4 Java标准库中的线程安全类
数据结构集合类自身没有进行任何加锁限制,线程不安全:
ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder
但是还有一些是线程安全的,使用了一些锁机制来控制:
Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap(推荐),StringBuffer
代码中使用锁,意味着代码可能因为锁的竞争产生阻塞,从而程序的执行效率降低。
String虽然没有加锁,但是不涉及“修改”,仍然是线程安全的。
5.volatile关键字
5.1 volatile能保证内存可见性
线程安全问题。一个线程读取,一个线程修改,修改线程修改的值并没有被读线程读取到。
package thread;import java.util.Scanner;public class Demo21 {private static int flg=0;public static void main(String[] args) {Thread t1=new Thread(()->{while(flg==0){}System.out.println("t1线程结束");});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("请输入flg的值:");flg=sc.nextInt();});t1.start();t2.start();}
}
在t1线程中while循环里面,JVM执行读flg的操作,发现始终是0(用户输入时间相较于读取时间太长),于是把读取内存的操作优化为读取寄存器的操作,后续load不再重新读内存,直接从寄存器中取。当用户输入值修改flg,此时t1线程就感知不到了。
package thread;import java.util.Scanner;public class Demo21 {private static int flg=0;public static void main(String[] args) {Thread t1=new Thread(()->{while(flg==0){try{Thread.sleep(1);//加了sleep之后,sleep消耗的时间相比于上面load flg的操作,高了很多}catch(InterruptedException e){throw new RuntimeException(e);}}System.out.println("t1线程结束");});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("请输入flg的值:");flg=sc.nextInt();});t1.start();t2.start();}
}
针对内存可见性问题,也不能指望sleep解决,因为sleep大大影响到程序的效率。
因此,在语法中,引入volatile关键字来修饰某个变量,此时编译器对变量的读取操作,就不会优化成读寄存器。
package thread;import java.util.Scanner;public class Demo21 {private volatile static int flg=0;public static void main(String[] args) {Thread t1=new Thread(()->{while(flg==0){}System.out.println("t1线程结束");});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("请输入flg的值:");flg=sc.nextInt();});t1.start();t2.start();}
}
JMM Java内存模型
每个线程有一个自己的“工作内存”,同时这些线程共享一个“主内存”。当一个线程循环进行上述读取变量操作的时候,就会把主内存中的数据,拷贝到该线程的工作内存中,后续另一个线程修改,也是先修改自己的工作内存,拷贝到主内存中。由于第一个线程仍然在读自己的工作内存,因此感知不到主内存的变化。
5.2 volatile不保证原子性
6. wait和notify
public class Demo23 {public static void main(String[] args) throws InterruptedException {Object obj=new Object();System.out.println("1");obj.wait();//抛出异常//wait,会先执行解锁操作,给其他线程获取锁的机会//前提是已经加上锁System.out.println("2");}
}
加上锁:
public class Demo23 {public static void main(String[] args) throws InterruptedException {Object obj=new Object();System.out.println("1");synchronized (obj){//加锁obj.wait();//进入wait,释放锁,阻塞等待//如果其他线程做完了必要的工作,调用notify唤醒这个wait线程//wait就会解除阻塞,重新获取到锁,继续执行并返回(又一次加锁)//要求synchronized的锁对象必须和wait的对象是同一个}System.out.println("2");}
}
package thread;import java.util.Scanner;public class Demo24 {public static void main(String[] args) {Object locker=new Object();Thread t1=new Thread(()->{try {System.out.println("wait前");synchronized (locker) {locker.wait();//wait先释放锁}System.out.println("wait后");} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("输入任意内容,通知唤醒t1");sc.next();//next就是一个带有阻塞的操作,等待用户输入synchronized (locker){locker.notify();//这里需要先拿到锁,再notify}});t1.start();t2.start();}
}
wait操作必须搭配锁进行,wait会先释放锁;
notify操作,原则上不涉及加锁解锁操作,在Java中,强制要求notify搭配synchronized.
要确保先wait后notify,如果先notify后wait,此时wait无法被唤醒。notify的这个线程也没有副作用(notify一个没有在wait的对象,不会报错)。
搭配synchronized,锁对象得和调用wait/notify的对象一致。
如果多个线程在同一对象上wait,进行notify的时候是随机唤醒其中一个线程,再一次notify唤醒另一个线程:
package thread;import java.util.Scanner;public class Demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1=new Thread(()->{try{System.out.println("t1 wait前");synchronized (locker){locker.wait();}System.out.println("t1 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t2=new Thread(()->{try{System.out.println("t2 wait前");synchronized (locker){locker.wait();}System.out.println("t2 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t3=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("输入任意内容,唤醒其中一个线程:");sc.next();synchronized (locker){locker.notify();}System.out.println("输入任意内容,唤醒另一个线程:");sc.next();synchronized (locker){locker.notify();}});t1.start();t2.start();t3.start();}
}
notifyAll一次唤醒所有线程:
虽然同时唤醒t1和t2,由于wait唤醒之后要重新加锁。其中某个线程先加上锁开始执行,另一个线程因为加锁失败再次阻塞等待。等先走的线程解锁,后走的线程才能加上锁,继续执行。
package thread;import java.util.Scanner;public class Demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1=new Thread(()->{try{System.out.println("t1 wait前");synchronized (locker){locker.wait();}System.out.println("t1 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t2=new Thread(()->{try{System.out.println("t2 wait前");synchronized (locker){locker.wait();}System.out.println("t2 wait后");} catch (InterruptedException e){throw new RuntimeException(e);}});Thread t3=new Thread(()->{Scanner sc=new Scanner(System.in);System.out.println("输入任意内容,唤醒所有线程:");sc.next();synchronized (locker){locker.notifyAll();}});t1.start();t2.start();t3.start();}
}
wait和join类似,提供了“死等”版本和“超时时间”版本。
wait和sleep都有等待时间。wait可以使用notify提前唤醒,sleep也可以使用Interrupt提前唤醒。
wait和sleep最主要的区别在于对锁的操作:
- wait必须要搭配锁,先加锁,才能用wait,sleep不需要
- 如果都是在synchronized内部使用,wait会释放锁,sleep不释放锁
有三个线程,分别只能打印A,B,C,要求按顺序打印ABC,打印10次:
package thread;public class Demo26 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Thread t1=new Thread(()->{try {for(int i=0;i<10;i++){synchronized (locker1){locker1.wait();}System.out.print("A");synchronized (locker2){locker2.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2=new Thread(()->{try {for(int i=0;i<10;i++){synchronized (locker2){locker2.wait();}System.out.print("B");synchronized (locker3){locker3.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t3=new Thread(()->{try {for(int i=0;i<10;i++){synchronized (locker3){locker3.wait();}System.out.println("C");synchronized (locker1){locker1.notify();}}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();t3.start();//主线程中,通知一下locker1,让上述逻辑从t1开始执行//需要确保上述三个线程都执行到wait,再进行notifyThread.sleep(1000);synchronized (locker1){locker1.notify();}}
}