目录

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. 内存空间小,硬盘空间大
  3. 内存成本高,硬盘成本低
  4. 内存数据掉电后丢失,硬盘数据掉电后持续存储

有的设备既是输入设备又是输出设备,比如触摸屏,网卡(上网时和网线连接部分的硬件设备,集成在主板上)...

1.2 现代CPU主要关心指标(和日常开发密切相关的)

  1. CPU的频率
    1. 基频/默频
    2. 睿频/加速频率
  2. CPU的核心数
    1. 大小核

CPU的基本工作流程:读取指令、解析指令、执行指令

1.3 计算机中,一个汉字占几个字节?

取决于字符集(汉字怎样编码)

  1. gbk(中国大陆上曾经广泛使用的)。Windows10/11简体中文版默认gbk.使用VS写代码打印汉字strlen,结果是2;
  2. utf8(当下全世界最流行的编码方式)
    1. 一个汉字占3字节。utf8本身是变长编码(1-4);
  3. unicode(Java的char就是使用unicode)一个汉字2字节
    1. 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的一些关键要点

  1. pid(进程id)进程的身份标识符
  2. 内存指针(一组指针)
    1. 进程需要知道要执行的指令的地址
    2. 指令依赖的数据在哪里
  3. 文件描述符表
  4. 进程状态
  5. 进程优先级
  6. 进程上下文
  7. 进程的记账信息(统计每个进程在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);}}
}
对⽐上⾯两种⽅法:
  1. 继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
  2. 实现 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());}
}
thread 收到通知的⽅式有两种:
  1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
  2. 否则,只是内部的⼀个中断标志被设置,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指令:

  1. load,把内存中的值(count变量)读取到CPU寄存器
  2. add,把指定寄存器中的值进行+1操作(结果还是在寄存器中)
  3. 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的可重入特性可以解决:

当某个线程针对一个锁加锁成功后,后续该线程再次针对这个锁进行加锁,不会触发阻塞,而是直接往下走。但是如果是其他线程尝试加锁就会正常阻塞。

可重入锁的实现原理,关键在于让锁对象内部保存当前是哪个线程持有这把锁。

后续有线程针对这个锁加锁的时候,对比一下锁持有者的线程是否和当前加锁的线程是同一个。

如何自己实现一个可重入锁?

  1. 在锁内部记录当前是哪个线程持有的锁,后续每次加锁,都进行判定
  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();}
}

如果不加sleep,有可能t1把locker1和locker2都拿到了,t2还没开始,自然无法构成死锁。

  • 死锁的第三种情况:N个线程M把锁

一个经典的模型:哲学家就餐问题

构成死锁的四个必要条件:

  1. 锁是互斥的。一个线程拿到锁之后,另一个线程再次尝试获取锁,必须要阻塞等待。
  2. 锁是不可抢占的。
  3. 请求和保持。一个线程拿到锁1之后,不释放锁1的前提下获取锁2
  4. 循环等待。多个线程多把锁之间的等待过程构成了“循环”

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最主要的区别在于对锁的操作:

  1. wait必须要搭配锁,先加锁,才能用wait,sleep不需要
  2. 如果都是在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();}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93382.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93382.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/93382.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用互联网思维扩展电商后台的 CRUD 功能

一、自定义实现MyBatis-Plus逆向工程 多数据源的问题解决了&#xff0c;接下来开始进行实际开发时&#xff0c;你会发现&#xff0c;最麻烦的一件事情就是要创建与数据库表对应的POJO了。这些没什么难度&#xff0c;但是繁琐的内容会占据大量的开发时间。比如一个PmsProducr对…

无代码测试平台ATECLOUD全场景测试方案

ATECLOUD 智能云测试平台是有纳米软件开发的一款以无代码架构与弹性扩展体系为核心的自动化测试平台&#xff0c;通过数据模型驱动的创新设计&#xff0c;为研发、产线等多场景提供高效可控的测试解决方案。​无代码架构 ATECLOUD 打破传统技术壁垒&#xff0c;构建完全可视化的…

当 AI 重构审计流程,CISA 认证为何成为破局关键

在南京审计大学最新发布的《面向审计行业 DeepSeek 大模型操作指南》中&#xff0c;一组数据引发行业深思&#xff1a;通过自动化数据处理、智能风险识别和定制化报告生成&#xff0c;AI 大模型能帮助审计人员降低 40% 以上的人工成本&#xff0c;同时将风险识别准确率提升至 9…

NAT技术、代理服务器

NAT/NAPT技术NAT的全称是network address translation&#xff0c;网络地址转换。NAT 能在对外通信时够将源 IP 转为新源 IP&#xff0c;对内通信时将目的ip转换成新目的ip&#xff0c;实现这个操作&#xff0c;靠的是地址转换表但NAT的说法其实是不准确的&#xff0c;因为多个…

【硬件-笔试面试题】硬件/电子工程师,笔试面试题-45,(知识点:负反馈的作用,基础理解,干扰和噪声的抑制)

目录 1、题目 2、解答 步骤一&#xff1a;明确负反馈的作用原理 步骤二&#xff1a;逐一分析选项 3、相关知识点 一、负反馈的基本原理 二、负反馈对干扰和噪声的抑制机制 三、选项分析与答案 四、扩展思考&#xff1a;如何抑制不同位置的干扰&#xff1f; 总结 题目…

Flutter蓝牙BLE开发完全指南(内含高级功能扩展)

Flutter蓝牙BLE开发完全指南 我将为您提供一个完整的Flutter蓝牙BLE实现方案,包含UI设计、权限处理、设备扫描、连接通信等完整功能。 完整实现方案 1. 添加依赖与权限配置 pubspec.yaml dependencies:flutter:sdk: flutterflutter_blue_plus: ^1.10.0permission_handler…

使用 Canvas 替代 <video> 标签加载并渲染视频

在部分浏览器环境或业务场景下&#xff0c;直接使用 <video> 标签加载视频会出现首帧延迟的情况。以下方法通过 WebGPU Canvas 2D 将视频帧绘制到自定义 Canvas 上&#xff0c;让 <video> 只做解码&#xff0c;WebGPU 接管渲染&#xff0c;通过最小化对象创建 精…

基于Flask的智能停车场管理系统开发实践

在现代城市中&#xff0c;停车难已成为一个普遍问题。为了解决这一问题&#xff0c;我开发了一个基于Python Flask框架的智能停车场管理系统。该系统集成了车牌识别、车位状态监控、收费管理等多项功能&#xff0c;为停车场的智能化管理提供了完整的解决方案。系统功能概述该停…

【C#获取高精度时间】

在C#中&#xff0c;有几种方法可以获取高精度时间&#xff08;高分辨率时间戳&#xff09;&#xff0c;适用于性能测量、计时等需要高精度的场景。以下是几种常用方法&#xff1a; 1. 使用 Stopwatch 类&#xff08;推荐&#xff09; Stopwatch 类提供了最高精度的时间测量&…

Spring Boot + React 打造现代化高校成绩管理系统实战记录

作者: 笙囧同学 发布时间: 2025年7月 技术栈: Spring Boot 3.2.3 React 18 TypeScript 华为云GaussDB 项目类型: 全栈Web应用 开发周期: 30天 代码量: 15000 行 &#x1f4d6; 前言 大家好&#xff0c;我是笙囧同学&#xff01;&#x1f64b;‍♂️ 作为一名计算机科学与技…

形参表不匹配(BUG)

在您的代码中&#xff0c;存在两个主要问题导致"形参表中不匹配"的错误&#xff1a;erase() 函数中的成员变量名错误iterator erase(iterator pos) {// ...size--; // ❌ 错误&#xff1a;成员变量名为 _size 而非 sizereturn iterator(next); }修正&#xff1a;ite…

Spring循环依赖以及三个级别缓存

Spring循环依赖以及三个级别缓存 什么是循环依赖&#xff1f; 循环依赖&#xff0c;顾名思义&#xff0c;就是指两个或多个 Spring Bean 之间相互依赖&#xff0c;形成一个闭环。 最常见也是 Spring 能够“解决”的循环依赖是构造器注入 和 setter 注入 混合或单独使用时&…

《零基础入门AI:OpenCV图像预处理进一步学习》

本文全面讲解OpenCV图像预处理的七大核心技术&#xff08;插值方法、边缘填充、图像矫正&#xff08;透视变换&#xff09;、图像掩膜、ROI切割、图像添加水印、图像噪点消除&#xff09;&#xff0c;每个知识点都配有详细解释和实用代码示例&#xff0c;帮助初学者建立系统的图…

MongoDB的内存和核心数对于运行效率的影响

在 MongoDB 线上生产环境中&#xff0c;CPU&#xff08;核心&#xff09; 和 内存 是两大关键硬件资源&#xff0c;它们在不同的操作场景下发挥着核心作用&#xff0c;共同影响着数据库的性能、稳定性和扩展性。理解它们的作用场景至关重要&#xff0c;是容量规划、性能优化和故…

自己的SAPGUI尝试

为满足用户需求&#xff0c;博主做了一个台账管理程序&#xff0c;尝试用自己的程序做GUI&#xff0c;用SAP 系统做数据库。 运行了半年&#xff0c;程序很nice,用户每天都在高效的使用&#xff0c;已经有十几万的数据。 总结一下这次自己的GUI尝试&#xff0c;好处是C# WINFOR…

高效处理 JSON 数据:JsonUtil 工具类全方位解析与实战

在现代软件开发中,JSON(JavaScript Object Notation)已成为数据交换的“通用语言”——从前后端接口通信到微服务数据交互,从配置文件解析到日志格式化,几乎所有场景都离不开JSON的处理。然而,原生JSON框架(如FastJSON、Jackson)的API往往需要大量重复代码,且空指针、…

Python 库手册:xmlrpc.client 与 xmlrpc.server 模块

xmlrpc.client 和 xmlrpc.server 是 Python 标准库中用于构建基于 XML-RPC 协议的远程过程调用&#xff08;RPC&#xff09;通信模块。xmlrpc.client 用于编写客户端程序&#xff0c;向远程服务器发起方法调用。xmlrpc.server 用于编写服务器端&#xff0c;暴露本地方法供远程客…

渲染篇(一):从零实现一个“微型React”:Virtual DOM的真面目

渲染篇(一)&#xff1a;从零实现一个“微型React”&#xff1a;Virtual DOM的真面目 引子&#xff1a;前端性能的“永恒之问” 在前面两章中&#xff0c;我们已经奠定了坚实的架构基础。我们用“任务调度器”建立了声明式和模块化的编程范式&#xff0c;并通过对比MVC等模式论…

SWC 深入全面讲解

一、核心功能与原理 1. 高性能编译 Rust 架构优势&#xff1a;SWC 基于 Rust 编写&#xff0c;利用 Rust 的性能和并发性优势&#xff0c;编译速度比 Babel 快约 20 倍&#xff0c;比 TypeScript 编译器更快。并行编译&#xff1a;支持多线程并行处理&#xff0c;在四核基准测试…

XML Expat Parser:深入解析与高效应用

XML Expat Parser:深入解析与高效应用 引言 XML(可扩展标记语言)作为一种广泛使用的标记语言,在数据交换、存储和表示中扮演着重要角色。XML Expat Parser 是一个高性能、可扩展的XML解析库,广泛应用于各种编程语言中。本文将深入探讨XML Expat Parser 的原理、特性以及…