多线程——多线程安全(synchronized和volatile)、wait和notify
创始人
2025-05-28 01:56:37

目录

一、线程不安全的原因

1. 线程是抢占式执行的,线程间的调度充满的随机性。

2. 修改共享数据

3. 原子性:针对变量的操作不是原子的

解决方法:synchronized  加锁

4. 内存可见性

解决方法:synchronized 和 volatile

5. 指令重排序

解决方法:synchronized

二、synchronized 关键字 —— 监视器锁 monitor lock

1. synchronized 的特性

(1)互斥

(2)刷新内存(保证了内存可见性)

(3)可重入

2. synchronized 的使用方式:

(1)修饰一个普通方法

(2)修饰一个代码块

(3)修饰一个静态方法

三、volatile 关键字

1. JMM (Java Memory Model)(Java 内存模型)

2. volatile 和 synchronized

四、wait 方法 和 notify 方法

1. wait( )  方法

2.  notify( ) 方法

3. 基本用法

4. notifyAll( )  方法


一、线程不安全的原因

1. 线程是抢占式执行的,线程间的调度充满的随机性。

2. 修改共享数据

   多个线程修改同一个共享数据救护出现线程不安全的情况。(若多个线程读同一个数据 或者 修改各自的数据 则不会出现线程不安全的情况)

3. 原子性:针对变量的操作不是原子的

   以 count++ 这条语句举例,在计算机内部,这条操作分为了三个CPU指令:①load:把内存中的 count 的值,加载到 CPU 寄存器中;②add:把寄存器中的值 + 1;③save:把寄存器的值写回到 内存 的 count 中。

   在多线程情况下,是抢占式执行的。假设有两个线程,当两个线程“抢占式执行”,就导致了两个线程同时执行这三条指令时,顺序上充满了随机性

   当两个线程同时对 count++ 时,会出现当线程一还没将它寄存器中计算好的值放回内存时,线程二就将内存中的 count 值拿走了。假设 count = 0,使用线程一和线程二同时对 count++,预期结果是 count = 2,但是如果是上述情况的话,线程一拿走 0 到他的寄存器进行计算得 1,还没将 1 返回到内存中,线程二就像 内存 中 count 的值 0 拿到了他的寄存器中,然后再进行计算得 1,最后两个线程将其寄存器的值返回到内存中 的 count,结果是 1。明明加了两次,但结果还是 1。因此出现了线程不安全的情况。

   如下图所示:

public class Demo8 {//对同一个变量count进行 ++ 操作,使用两个线程同时加,预期正常结果为 10_0000public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5_0000; i++) {count++;}});t1.start();Thread t2 = new Thread(() -> {for (int i = 0; i < 5_0000; i++) {count++;}});t2.start();t1.join();t2.join();System.out.println(count);}
}

 但结果:

 可见是线程不安全的。

解决方法:synchronized  加锁

    在自增之前,先加锁,lock;在自增之后,再解锁,unlock;

    因为在实际开发中,一个线程中有很多任务。在这些任务中,可能只有任务4是线程不安全的,所以只对任务4进行加锁即可,而上面的任务1、任务2、任务3都是并发执行的。

   加锁的方法之一:synchronized 关键字。

   给方法加上 synchronized 关键字,此时进入方法时,就会自动加锁离开方法,就会自动解锁。当一个线程加锁成功时,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程就处于 BLOCKED 状态)。阻塞会一直持续到占用锁的线程把锁释放为止

class Counter{public int count;synchronized public void increase(){count++;}
}public class Demo10 {private static Counter counter = new Counter();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5_0000; i++) {counter.increase();}});t1.start();Thread t2 = new Thread(() -> {for (int i = 0; i < 5_0000; i++) {counter.increase();}});t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

4. 内存可见性

   假设针对同一个变量,一个线程 t1 进行读操作(循环进行很多次),一个线程 t2 进行修改操作(合适的时候进行一次)。

   t1 线程在循环读这个变量,这个变量在内存中。因为读取内存操作相比于读取寄存器操作来说要慢很多(慢 3 ~ 4 个人数量级),而此时 t2 右迟迟不进行修改,导致 t1 每次读到的数值都是同一个数值。因此就出现了Java编译器进行的代码优化,即不再从内存读数据,而是直接从寄存器里读值。一旦 t1 这样做,万一此时 t2 进行了修改,t1 就不能知道了。因此不是内存可见的。

    如下面代码所示,t 线程一直在飞速运转读取 isQuit 的值进行判断,经过优化后,t 线程直接在寄存器中读取了。而当我们输入isQuit 的值,isQuit 的值不再为 0 了,对应的 t 线程中循环判断条件为 false而退出循环,进而打印 “循环结束,t 线程退出”。但实际上并没有,t 线程仍然在运行中,则出现了线程不安全的情况。 

public class Demo9 {public static int isQuit = 0;public static void main(String[] args) {Thread t = new Thread(() -> {while (isQuit == 0) {}System.out.println("循环结束,t线程退出");});t.start();Scanner scanner = new Scanner(System.in);System.out.println("请输入一个 isQuit 的值:");isQuit = scanner.nextInt();System.out.println("main 线程执行完毕");}
}

结果:

解决方法:synchronized 和 volatile

(1)使用 synchronized 关键字

   synchronized 关键字不光能保证指令的 原子性,同时也能保证 内存可见性

   被 synchronized 包裹起来的代码,编译器就不敢轻易做出上述的假设(优化),相当于手动禁止了编译器的优化。

(2)使用 volatile 关键字

   volatile 和 原子性 无关,但是能够保证 内存可见性

   禁止编译器做出上面优化,编译器每次执行 判定相等,都会重新从 内存 中读取 isQuit 的值。

public class Demo9 {public static volatile int isQuit = 0;public static void main(String[] args) {Thread t = new Thread(() -> {while (isQuit == 0) {}System.out.println("循环结束,t线程退出");});t.start();Scanner scanner = new Scanner(System.in);System.out.println("请输入一个 isQuit 的值:");isQuit = scanner.nextInt();System.out.println("main 线程执行完毕");}
}

 结果:

5. 指令重排序

    指令重排序也会影响到线程安全问题。指令重排序也是编译器优化中的一种操作。

   对于我们平常写的代码,谁在前,谁在后无所谓,但是编译器不这样认为。编译器会智能的整理这些代码的前后顺序,从而提高程序的效率。(保证逻辑不变的前提下,去调整)

   对于单线程而言,编译器的判定是很准的;而对于多线程而言,编译器可能会产生误判。

解决方法:synchronized

   synchronized 不光能保证原子性,同时还能够保证内存可见性,同时还能禁止指令重排序

二、synchronized 关键字 —— 监视器锁 monitor lock

   解决线程不安全要从三个方面考虑:原子性、内存可见性、指令重排序。

   使用 synchronized 时,本质是哪个是针对某个对象进行加锁。而在代码的异常信息中,可能会出现 monitor lock 这个词。

   加锁操作是指在对象(实例)的 对象头 里 设置了一个标志位。在Java中,每个类都是继承自Object类。每个 new 出来的实例,里面一方面包含了你自己安排的属性,一方面包含了“对象头”,这个对象头中存储的是对象的一些 元数据 。

1. synchronized 的特性

(1)互斥

   使用 synchronized 会产生互斥的效果。进入 synchronized 修饰的代码块时,自动加锁,退出 synchronized 修饰的代码块时,自动解锁。此时其他的线程才有可能获得锁。

   使用时要注意:多个线程要针对同一个锁对象进行加锁才有用。

(2)刷新内存(保证了内存可见性)

   当一个线程获得了锁之后,它的大致执行流程:

  • 1. 获得互斥锁
  • 2. 从 主内存 中拷贝变量的值到 工作内存
  • 3. 执行代码,在其 工作内存 中改变变量的值
  • 4. 将更改后的值再 刷新 到 主内存 中
  • 5. 释放互斥锁

   将一系列操作进行了捆绑,实现了内存可见性。 

(3)可重入

   synchronized 实现的锁为 可重入锁,即 不会自己把自己锁死。

   当一个线程还没有释放锁,然后又尝试获取锁时,会出现如下情况:第一次获取锁成功,第二次获取锁时,锁还没有被释放,则该线程就会一直处于阻塞状态,直到锁被释放。但是此时 锁 已经在该线程中,释放锁也必须由该线程完成,但是此时该线程处于阻塞状态,就会造成 死锁 问题。

死锁 的 四个 必要条件:

互斥使用

   一个锁被一个线程占用了之后,其他线程占用不了。

不可抢占

   一个锁被一个线程占用了之后,其他线程不能把这个锁给抢走。

请求和保持

   当一个线程占用了多把锁之后,除非显示的释放锁,否则这些锁始终都是被该线程持有。

环路等待

   等待关系,成环了。(A 等 B,B 等 C,C 又等 A)

   而对于 synchronized 来说,不会出现这样的现象。在第二次尝试获取锁的时候又加了一次锁,相当于第一次锁没有释放,然后加了两次锁。

   在可重入锁的内部,包含了 “线程持有者” 和 “计数器” 两个信息。此时有两种情况:

  • 当某个线程加锁时,发现锁已经被占用,但占用的线程恰好是自己(线程持有者),那么仍然可以继续获取到锁,同时让 计数器 自增
  • 当解锁的时候,不是单纯的代码执行完就解锁。而是当计数器减为 0 的时候,才真正的释放锁。(此时锁才能被别的线程获取到)

2. synchronized 的使用方式:

(1)修饰一个普通方法

   此时锁对象是 this

   例如上面的例子中:这里的这里的 synchronized 就是针对 this 来加锁,加锁的位置就是在设置 this 的对象头的标志位。

class Counter {public static int count;synchronized public static void increase() {count++;}
}

(2)修饰一个代码块

   要显示指定针对哪个对象加锁,Java中的任意一个对象都可以作为锁对象

   例如:

class Counter {public static int count;public void increase() {synchronized (this){count++;}}
}

(3)修饰一个静态方法

   对当先类的 类对象 加锁。以上面的例子来说,锁对象为 Counter.class,即 类名.class

   类对象:在运行程序中, .class 文件被加载到JVM内存中的模样。——> 反射机制。

   所谓的 “静态方法”  即 “类方法” ,而普通的方法为 “实例方法”。而静态方法中是没有对象实例的。因此锁对象为类对象。

   例如: 

class Demo {synchronized public static void fun() {System.out.println("111");}
}

 等价于

class Demo {public static void fun() {synchronized (Demo.class) {System.out.println("111");}}
}

3. Java 标准库中线程安全的类

  Java 有很多现成的类,有些是线程安全的,有些是线程不安全的。因此在多线程环境下,如果使用线程不安全的类,就需要小心谨慎。

线程不安全的类:

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

线程安全的类:这些关键方法上都有 synchronized,可以保证在多线程下,修改同一个对象没有问题。

  • Vector(不推荐使用)
  • HashTable(不推荐使用)
  • ConcurrentHashMap
  • StringBuffer
  • String :没有 synchronized,但是 String 是不可变对象(内部没有提供public 的修改属性的操作),无法在多个线程中同时修改同一个 String,因此是线程安全的。

三、volatile 关键字

   volatile 主要是阻止编译器优化,保存内存可见性。(例如在频繁读一个值时,依旧每次都从 内存 中读取)。不保证原子性。

1. JMM (Java Memory Model)(Java 内存模型)

   JMM就是将硬件结构,在Java中用专门的术语又重新抽象的封装了一遍。-> 主内存(内存)和工作内存(CPU、寄存器、缓存... 统称为工作内存)。

   因为Java是一个跨平台的变成语言,因此希望程序员在使用时,感知不到 CPU、内存等硬件设备的存在,所以要把硬件的细节封装起来。(假设某个计算机没有CPU,或没有内存,同样可以套在该模型中)

2. volatile 和 synchronized

   volatile 只保证 内存可见性,不保证原子性。只处理一个线程读,一个线程写的情况。

   synchronized 都能处理。(原子性,内存可见性,指令重排序)

四、wait 方法 和 notify 方法

   因为线程之间是抢占式执行的,充满了随机性。而实际开发中,我们需要让线程按照一定的顺序执行,因此可以使用 wait(等待) 和 notify(唤醒)。

   join 也是一种控制循环的方式,它更倾向于控制线程的结束。

    wait 和 notify 都是 Object 类的方法。调用 wait 方法的线程会陷入阻塞,阻塞到有其他线程通过 notify 来通知。

  • wait( ) /  wait( long timeout ) :让当前线程进入等待状态。
  • notify ( ) / notifyAll ( ) :唤醒在当前对象上等待的线程。

1. wait( )  方法

   调用 wait 方法后,内部会做三件事:

  • 1. 先释放锁;
  • 2. 等待其他线程的通知;
  • 3. 收到通知后,重新获取锁,并继续往下执行。

   可见要调用 wait 方法,前提是已经获取到锁了。即要搭配 synchronized 来进行使用。(notify 也要搭配 synchronized 来使用)

   例如: wait 哪个对象,就要对哪个对象加锁。

public class Demo2 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("wait 前");//代码中调用了 wait 就会发生阻塞object.wait();System.out.println("wait 后");}}
}

2.  notify( ) 方法

   notify 方法是唤醒等待的线程。搭配 wait 方法(和 synchronized)使用。

   如下例,有两个线程,让第一个线程调用 wait 方法,第二个线程调用 notify 方法,观察打印结果。

public class Demo3 {private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized(locker) {System.out.println("wait 前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("wait 后");}});t1.start();Thread.sleep(1000);Thread t2 = new Thread(() -> {synchronized(locker) {System.out.println("notify 前");locker.notify();System.out.println("notify 后");}});t2.start();}
}

结果:

    由上面例子可以看出,在 t1 线程调用 wait 后,t1 线程进入了阻塞状态。然后 main 线程休眠了1s后,开始执行 t2 线程,即开始调用 notify 方法,调用之后,t1 线程阻塞状态结束,继续执行代码,因此打印了 “wait 后”。

3. 基本用法

   如图,假设有两个线程(t1 和 t2),t1 里的任务:a、b、c, t2 里的任务:e、f、g。假设我们需要让两个线程按照:a -> e ; b -> f;c -> g 的顺序执行。

4. notifyAll( )  方法

   wait 和 notify 都是针对同一个对象来操作,而notifyAll 可以一次性唤醒所有的等待线程。而所有唤醒的线程之间仍然需要竞争锁。

   假设现在有一个对象 o ,并且有 10 个线程,都调用了 o. wait ,此时 10 个线程都是阻塞状态。

如果调用了 o.notify ,就会把 10 个其中的一个给唤醒。(唤醒哪个,不确定)

如果调用了 o.notifyAll,就会把所有的10个线程都唤醒,wait 唤醒后,会重新尝试获取锁(产生竞争)

相关内容

热门资讯

必学习的前后端交互框架ajax ajax显然是最重要的框架 无论是c#,java,web程序通通能够解决前后端问题。 现在越来越多的...
北美关税缓和窗口期,独立站卖家... 作者 | 衡之编辑 | 刘景丰北美外贸商家,正在经历90天的关税缓和窗口期。北京时间5月12日,《中...
信创办公–基于WPS的PPT最... 信创办公–基于WPS的PPT最佳实践系列(表格和图标常用动画) 目录应...
逻辑回归全方位认识 目录 1.逻辑回归的原理和推导 2.逻辑回归相关面试题 1.逻辑回归的原理和推导 逻辑回顾假设数...
74岁王石罕见发声,能为万科做... 深陷债务风暴的万科,突然迎来了一个熟悉的身影——74岁的创始人王石站了出来。5月27日,万科创始人、...
朱华荣谈“车圈恒大”:业内存风... 长安汽车未来十年将投入2000亿元布局新汽车科技产业链,三年内发布飞行汽车和人形机器人产品文|周忻儿...
巨头“分合之道” A股约400... 中经记者 秦枭 北京报道5月25日晚间,市值900余亿元的曙光信息产业股份有限公司(603019.S...
css属性学习 css属性 就是我们选择器里面 { } 中的内容 字体样式 font-size 控制字体大小&#...
“灯塔计划”背后,SALOMO... SALOMON正在成为亚玛芬体育的增长新引擎。根据5月20日刚刚发布的2025年第一季度财报数据,亚...
linux系统性能分析(一) linux系统性能分析1 影响 Linux 性能的各种因素1.1 系统硬件资源1. CPU2. 内存...
selenium(4)----... webdriverAPI 一)定位元素的方式,必问 1.1)id来定位元素࿰...
廊坊银行打官司 荣盛发展拿两座... “ 面对廊坊银行的起诉,荣盛发展抛出了以物抵债方案,涉及海南和河北两处酒店资产。据悉,此次牵涉两起金...
药师帮举办2025投资者开放日... 5月27日,药师帮 ( 9885.HK ) 在广州总部举办上市以来首场投资者开放日活动,来自中欧、富...
健康意识崛起,家电企业如何打造... 当下,全球公共卫生体系深度变革,健康议题正在深刻影响消费市场走向。根据世界卫生组织最新数据,我国在推...
QT串口助手开发1之绘制界面 系列文章目录 QT串口助手开发1之绘制界面 QT串口助手开发1系列文章目录一、QT串口助手开发1...
蔚来能源实现天津换电县县通,5... IT之家 5 月 28 日消息,蔚来官方今日发文宣布:蔚来换电站 | 天津和平安泊城市港湾上线投运,...
“跨境大佬占”陈海军将出席易境... 2025年6月19日,由易境通与跨境猫联合主办的第三届跨境电商生态大会暨海外仓发展论坛将在深圳乐荟中...
本轮中美关税大博弈,中国企业如... 文|周君芝、毛晨、谢雨心(研究助理) 核心观点 本轮中美关税大博弈,中国企业如何应对? 企业怎么想:...
奥浦迈:并购项目正在稳步推进中 奥浦迈5月27日发布最新业绩说明会纪要,据公司介绍,截至2025年第一季度,共有258个已确定中试工...
3.15 22111作业  编写一个名为myfirstshell.sh的脚本,它包括一下内容          ...
财政部:4月全国发行新增债券2... 【大河财立方消息】5月28日,财政部发布2025年4月地方政府债券发行和债务余额情况。 2025年4...
原创 油... 鸡蛋市场:果然,蛋价触底反弹,市场呈现理性回调的走势!据悉,在国内鸡蛋市场,上周末,产销市场蛋价连创...
黄金ETF:5月27日融资买入... 融券方面,当日无融券交易。 融资融券余额69.56亿元,较昨日下滑0.09%。 小知识 融资融券:...
2025年一季度债券市场分析报... 一、宏观动态 宏观政策:财政政策更积极,赤字率提至4%,专项债限额4.4万亿,超长期特别国债1.3万...
日本给美国上了一课 日本给美国... 出品 | 妙投APP作者 | 丁萍头图 | AI生图近期,美债收益率再次飙升,30年期美债收益率突破...
机构看好A股市场当前具备赔率优... 5月28日,A股大盘指数横盘震荡,盘面上黄金珠宝、乳业、饮料制造、光模块CPO、核电等概念题材领涨。...
北京证监局对东方时尚驾校公司采... 新京报贝壳财经讯(记者张冰)北京证监局5月27日发布通报,因东方时尚驾驶学校股份有限公司使用公开发行...
【JavaWeb】Tomcat... 目录 Tomcat Tomcat的下载 ​编辑Tomcat的启动 Tomcat部署前端页面 Serv...
vs2022 libevent (1)在上文编译出的 libevent文件夹中有 lib,include...
SignalR+WebRTC技... 一、建立信令服务器 1、后台项目中新建一个对应的集线器类,取名VideoHub...