线程安全问题 短命女 2023-10-15 17:32 76阅读 0赞 ## 一、线程安全 VS 线程不安全? ## **线程安全**指的是代码若是串行执行和并发执行的结果完全一致,就称为该代码是线程安全的。 若多个线程串行执行(单线程执行)的结果和并发执行的结果不同,就称为**线程不安全**。 **顺序执行:** public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println("两个搬砖人都已经执行结束"); System.out.println(counter.count); } ![bcac259fdada4038b093a07a5e96adfe.png][] **并发执行:** public class ThreadUnsafeDemo { private static class Counter{ int count = 0; void increase(){ count++; } } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("两个搬砖人都已经执行结束"); System.out.println(counter.count); } } ![2414f0a45d6442c392721b73ba243856.png][] ![238e7f8b139144e091d5d19b6846b164.png][] 顺序执行和并发执行的结果并不一致,而且同一段代码(并发),每次执行的结果也都不相同。 ## 二、JMM--Java内存模型 ## JMM(Java Memory Model):java 内存模型:描述多线程场景下,线程的工作内存(CPU的高速缓存和寄存器)和主内存的关系 每个线程都有自己的工作内存,每次读取共享变量(类中的成员变量、静态变量、常量都属于共享变量,在堆中和方法区中存储的变量),不是线程的局部变量,都是先从主内存中将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写会主内存。 ![ed1f96d90b6245a5a48af2e710d77ba0.png][] ## 三、保证线程安全需满足的三大特性 ## ### (一)原子性 ### 原子性 :该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么全都不被执行,不会存在中间状态。这个操作就是一个原子性操作。 int a = 10 => 直接将常量10赋值给a变量,要么没赋值,要么赋值成功,原子性 a += 10 => a = a+ 10 先要读取当前变量a 的值,再将 a + 10计算,最后将计算得出的值重新赋值给a变量(对应3个原子性操作,这条指令是不原子性的) ### (二)可见性 ### 可见性 :一个线程对共享变量的修改,能够及时的被其他线程看到,这种特性称为可见性(synchronized-上锁 、 volatile关键字 、 final关键字) ### (三)防止指令重排 ### 指令重排: 代码的书写顺序不一定就是最终JVM或者CPU的执行顺序。(编译器和CPU会对指令优化 -——>前提 : 保证代码的逻辑正确) 在单线程场景下指令重排没什么问题,但是在多线程场景下就有可能因为指令重排导致错误(一般就是对象还未初始化完成就被别的线程给用了) ![2603c23691d2430fbc4e7eb8d9d2aa9e.png][] 要确保一段代码的线程安全,需要同时满足可见性、原子性和防止指令重排。 **是否会导致线程不安全,一定要注意,多个线程是否在操作同一个共享变量。** ### (四)解释上述并发代码为什么会发生线程安全问题 ### 1、increase()方法中的count ++ 操作不是一个原子性的操作。 首先,线程会从主内存中读取count的值到自己的工作内存中; 然后,计算 count++; 最后,将 count的值写会主内存。 ![c2ce0b02af164458ab82a1b8650955f9.png][] 2、此时是多个线程同时操作同一个共享变量(count) ![36f74a0c9b0a4255aee64f737853122c.png][] 3、线程对共享变量的修改不能及时被其他线程看到(主内存 和 工作内存)。 4、其中一种可能性(以结果为66211为例) (1) t1 和 t2 在线程启动时,会将主内存中的 count 值读取到自己的工作内存中,t1先于t2启动,先从主内存中读取count的值 t1.count = 0,此时t2还没有启动。 (2)之后t1开始执行自己的run方法,假设 t1 执行循环16211次,此时 t1.count =16211,线程 t1 将16211 写回主内存中,这时t2才启动,从主内存中读到 count = 16211。 (3)然后 t1 、t2都各自在执行自己的run方法,(解设期间t1、t2都没有再写会主内存),t1 在执行直到最后,把t1.count = 5000,写回内存。 (4)但是 t2 在拿到初始值16211后,一直在读取自己工作内存中的值,而 t1.count = 5000这个值对于 t2 来说并不可见,在 t2 执行完之后,就会把最终值66211写回主内存,把之前t1写回的5000给覆盖了。 ## 四、解决线程安全问题 ## ### (一)synchronized关键字 - 监视器锁monitor lock ### **解决线程安全问题**,就是保证原子性和可见性。Synchronized 关键字就能同时满足原子性和可见性。 private static class Counter{ int count = 0; synchronized void increase(){ count++; } } public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("两个搬砖人都已经执行结束"); System.out.println(counter.count); } ![d0ecd80b96ca42299b11b5b2d834f205.png][] (1)synchronized 的三大特性 ##### 1. 互斥 ##### synchronized 会起到互斥效果(metex lock), 某个线程执行到某个对象的 synchronized 中时(获取到该对象的锁), 其他线程如果也要获取同一个对象的锁,就会处在阻塞等待状态. ![b557e0e63eb6453abd16ea7d1b6c7bfa.png][] 当给increase方法加上synchronized关键字,所有进入该方法的线程都需要获取当前counter对象的 “锁”,获取成功才能进入,获取失败,就会进入阻塞状态。 正因为increase方法上上锁处理,多个线程在执行increase方法时,其实是在排队进入,同一时刻只可能有一个线程进入increase方法,执行对count属性的操作。----保证了线程安全 ![6658724b06e94fa59710daa79428523a.png][] ##### 2. 刷新内存 ##### 线程执行synchronized代码块的流程: a. 获取对象锁 b. 从主内存拷贝变量值到工作内存中 c. 执行代码 d. 将更改后的值写会主内存 e. 释放对象锁 因为从a - e 只有一个线程能执行,其他线程都在阻塞。synchronized保证互斥,同一时刻只有一个线程能够获取到这个对象的锁,这就保证了**原子性**(synchronized修饰的代码块全部执行结束后才会释放锁) 和**可见性**(操作完毕将共享变量的值写回主内存后才释放锁,其它线程获取锁的时候,这内存中的值一定是更改后的)。 ##### 3. 可重入 ##### **可重入:**获取到对象锁的线程可以再次加锁,这种操作就称为可重入。(Java中线程安全锁都是可重入的(包括java.concurrent.lock)) 在Java内部,每个Java对象都有一块内存(对象头),描述当前对象的“锁”信息。信息包含当前对象被哪个线程持有,以及一个计数器-记录当前对象被上锁的次数。 I. 若线程1 需要进入当前对象的同步代码块(synchronized),此时当前对象的对象头中没有锁信息,线程1是第一个获取锁的线程,进入同步代码块,对象头修改持有线程为线程1,计数器从 0 -> 1。当线程1 在同步代码块中再次调用当前对象的其他同步方法,计数器的值再次+1,说明此时对象锁被线程1获取了两次。 II. 若线程2需要进入当前对象的同步代码块,此时当前对象的对象头持有线程为线程1,且计数器值不为0,线程2就会进入阻塞状态,一直等到线程1释放锁为止(直到计数器值为0,才叫真正释放锁)。 ![f3cf111d347544c288695b420e538795.png][] #### (2)synchronized使用示例 #### ##### 1、synchronized修饰类中的成员方法 ##### synchronized修饰类中的成员方法,则锁的对象就是当前类的对象。**当前这个方法是通过哪个对象调用的,synchronized锁的就是哪个对象。** public class Reentrant { public static void main(String[] args) { Counter counter1 = new Counter(); Thread t1 = new Thread(() -> { counter1.increase(); }); t1.start(); Counter counter2 = new Counter(); Thread t2 = new Thread(() -> { // counter2.increase(); counter1.increase(); }); t2.start(); } private static class Counter { int val; // 锁的是当前Counter对象 synchronized void increase() { val ++; } } } 情况1: ![d46e2237e92c408ab5aeb6d86981e9ce.png][] 情况2: ![16104087cedc45cab8bf11f90359bc48.png][] ##### 2、synchronized修饰静态方法 ##### synchronized修饰的类中的静态方法,锁的是当前这个类的class对象(**全局唯一**,相当于把这个类锁了,同一时刻只能有一个线程访问这个方法(无论有几个对象)) public class Reentrant { public static void main(String[] args) { Counter counter1 = new Counter(); Counter counter2 = new Counter(); Counter counter3 = new Counter(); Thread t1 = new Thread(() -> { counter1.increase2(); },"t1"); Thread t2 = new Thread(() -> { counter2.increase2(); },"t2"); Thread t3 = new Thread(() -> { counter3.increase2(); },"t3"); t1.start(); t2.start(); t3.start(); } private static class Counter { // 当synchronized修饰静态方法,则相当于将Counter这个类的所有对象都给锁了(其实锁的Counter类的class对象,全局唯一) synchronized static void increase2() { while (true) { System.out.println(Thread.currentThread().getName() + "获取到了锁~~"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } ![ab5be355782b4e36b66b9266c308c415.png][]![33e16f42a6ba4e88a5e96ac2612d398e.png][] ##### 3、synchronized修饰普通代码块 ##### private static class Counter { void increase3() { //.....很多代码 // 同步代码块,进入同步代码块,必须获取到指定的锁 // this表示当前对象引用 ~~,锁的就是当前对象 // 若锁的是class对象,全局唯一 synchronized (this) { while (true) { System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } ![3d0624d295af4343abc46cef4109b174.png][] ### (二)volatile关键字 ### #### (1)volatile的两大特性 #### ##### 1.可见性 ##### volatile 关键字可以保证共享变量可见性(**强制线程读写主内存的变量值**)。相较于普通的共享变量,使用volatile可以保证共享变量的可见性。 a. 当线程读取的是volatile关键字时,线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值) b. 当线程写的是volatile关键字时,将当前修改后的变量值(工作内存中的)立即刷新到主内存中,且其他正在读取此变量值的线程会等待(不是阻塞),直到写回主内存操作完成,保证读的一定是刷新后的主内存值。(对于同一个volatile变量,它的写操作一定发生在它的读操作之前,保证读到数据一定是主内存中刷新后的数据) 线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值) public class Volatile { private static class Counter { volatile int flag = 0; } public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(() -> { // volatile变量每次都读写主内存 while (counter.flag == 0) { // 一直循环.. } System.out.println(counter.flag + "退出循环"); }); t1.start(); Thread t2 = new Thread(() -> { Scanner scanner = new Scanner(System.in); System.out.println("请改变flag的值"); counter.flag = scanner.nextInt(); }); t2.start(); } } ![61e8f4a59d884daa94e1085ab3d8fd0c.png][] **注意:volatile只保证可见性,但无法保证原子性 ,因此,如果代码不是原子性操作,仍然不是线程安全的!!!** ##### 2.内存屏障 ##### 使用volatile关键字修饰的变量,相当于一个内存屏障。 ![b8b0f858c6414e76be892bc533cadf2b.png][] ### (三)final修饰的常量 ### final修饰的常量一定是可见的,因为常量在定义时就要赋值,且赋值后无法修改! ![7fe4de144a6446aea0bcedb3c6e89dd1.png][]![c78b4ec0a3834c319c858193a60455a5.png][] [bcac259fdada4038b093a07a5e96adfe.png]: https://img-blog.csdnimg.cn/bcac259fdada4038b093a07a5e96adfe.png [2414f0a45d6442c392721b73ba243856.png]: https://img-blog.csdnimg.cn/2414f0a45d6442c392721b73ba243856.png [238e7f8b139144e091d5d19b6846b164.png]: https://img-blog.csdnimg.cn/238e7f8b139144e091d5d19b6846b164.png [ed1f96d90b6245a5a48af2e710d77ba0.png]: https://img-blog.csdnimg.cn/ed1f96d90b6245a5a48af2e710d77ba0.png [2603c23691d2430fbc4e7eb8d9d2aa9e.png]: https://img-blog.csdnimg.cn/2603c23691d2430fbc4e7eb8d9d2aa9e.png [c2ce0b02af164458ab82a1b8650955f9.png]: https://img-blog.csdnimg.cn/c2ce0b02af164458ab82a1b8650955f9.png [36f74a0c9b0a4255aee64f737853122c.png]: https://img-blog.csdnimg.cn/36f74a0c9b0a4255aee64f737853122c.png [d0ecd80b96ca42299b11b5b2d834f205.png]: https://img-blog.csdnimg.cn/d0ecd80b96ca42299b11b5b2d834f205.png [b557e0e63eb6453abd16ea7d1b6c7bfa.png]: https://img-blog.csdnimg.cn/b557e0e63eb6453abd16ea7d1b6c7bfa.png [6658724b06e94fa59710daa79428523a.png]: https://img-blog.csdnimg.cn/6658724b06e94fa59710daa79428523a.png [f3cf111d347544c288695b420e538795.png]: https://img-blog.csdnimg.cn/f3cf111d347544c288695b420e538795.png [d46e2237e92c408ab5aeb6d86981e9ce.png]: https://img-blog.csdnimg.cn/d46e2237e92c408ab5aeb6d86981e9ce.png [16104087cedc45cab8bf11f90359bc48.png]: https://img-blog.csdnimg.cn/16104087cedc45cab8bf11f90359bc48.png [ab5be355782b4e36b66b9266c308c415.png]: https://img-blog.csdnimg.cn/ab5be355782b4e36b66b9266c308c415.png [33e16f42a6ba4e88a5e96ac2612d398e.png]: https://img-blog.csdnimg.cn/33e16f42a6ba4e88a5e96ac2612d398e.png [3d0624d295af4343abc46cef4109b174.png]: https://img-blog.csdnimg.cn/3d0624d295af4343abc46cef4109b174.png [61e8f4a59d884daa94e1085ab3d8fd0c.png]: https://img-blog.csdnimg.cn/61e8f4a59d884daa94e1085ab3d8fd0c.png [b8b0f858c6414e76be892bc533cadf2b.png]: https://img-blog.csdnimg.cn/b8b0f858c6414e76be892bc533cadf2b.png [7fe4de144a6446aea0bcedb3c6e89dd1.png]: https://img-blog.csdnimg.cn/7fe4de144a6446aea0bcedb3c6e89dd1.png [c78b4ec0a3834c319c858193a60455a5.png]: https://img-blog.csdnimg.cn/c78b4ec0a3834c319c858193a60455a5.png
相关 线程安全问题 我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就... 绝地灬酷狼/ 2024年04月23日 20:34/ 0 赞/ 91 阅读
相关 线程安全问题 作者简介: zoro-1,目前大二,正在学习Java,数据结构,mysql,javaee等作者主页:??。 朴灿烈づ我的快乐病毒、/ 2024年04月20日 04:48/ 0 赞/ 103 阅读
相关 线程安全问题 目录 1.不安全原因 1.1线程调度无序(抢占式) 1.2多个线程修改同一个变量 1.3修改操作不是原子的 1.4内存可见性 1.5指令重排序 2.解决问题 2 缺乏、安全感/ 2024年03月25日 18:16/ 0 赞/ 70 阅读
相关 线程安全问题 一、线程安全 VS 线程不安全? 线程安全指的是代码若是串行执行和并发执行的结果完全一致,就称为该代码是线程安全的。 若多个线程串行执行(单线程执行)的结果和并发执行的 短命女/ 2023年10月15日 17:32/ 0 赞/ 77 阅读
相关 线程安全问题 定义 > 首先大家需要思考一下何为线程安全性呢??? 《Java并发编程实战》书中给出定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替 旧城等待,/ 2023年10月01日 19:02/ 0 赞/ 29 阅读
相关 线程安全问题 ![在这里插入图片描述][f9d8ad49a2a449b9888b716a9946db5c.gif_pic_center] 文章目录 一、线程安全问题 二、 逃离我推掉我的手/ 2023年09月27日 13:11/ 0 赞/ 16 阅读
相关 线程安全问题 基本概述: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nL àì夳堔傛蜴生んèń/ 2022年11月29日 12:21/ 0 赞/ 249 阅读
相关 线程安全问题 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污 迷南。/ 2022年08月21日 04:15/ 0 赞/ 272 阅读
相关 线程安全问题 线程安装概念 当多个线程,访问某一个类,对象或者方法时 这个类始终都能表现出正确的行为 那么,这个类,对象或者方法,就是线程安全的 Synchronize 朴灿烈づ我的快乐病毒、/ 2022年05月17日 08:06/ 0 赞/ 293 阅读
相关 线程安全问题 a++造成运行结果错误 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly ╰半橙微兮°/ 2021年09月24日 02:46/ 0 赞/ 463 阅读
还没有评论,来说两句吧...