• 多线程带来的的风险——线程安全


    目录

    线程安全

    线程不安全问题

    1. 观察线程不安全

    2.  JMM - Java的内存模型

    保证线程安全的条件

    原子性 

    可见性

    第一种可能性

    第二种可能性 

    现实生活中的超卖现象

    问题汇总

    防止指令重排

    关于synchronized关键字

    1. synchronized——监视器锁 monitor lock(对象锁)

    2. synchronized代码块刷新内存

    3. 可重入

    synchronized支持线程的可重入

    1. synchronized修饰类中的成员方法,锁的对象就是当前类的对象 

    2. synchronized修饰类中的静态方法,锁的是当前这个类的class对象

    3. synchronized代码块,明确锁的是哪个对象

    练习

    Java标准库中的线程安全类

    volatile关键字:可见性,内存屏障 

    1. volatile关键字可以保证共享变量可见性  强制线程读写主内存的变量值

    2. 使用volatile修饰的变量,相当于一个内存屏障


    线程安全

    线程不安全问题

    所谓的线程不安全问题,在多线程并发的场景下,实际运行结果和单线程场景下预期结果不符的问题。

    1. 观察线程不安全

    1. /**
    2. * 观察多线程场景下的线程安全问题
    3. */
    4. public class ThreadUnSafeDemo {
    5. private static class Counter {
    6. int count = 0;
    7. void increase() {
    8. count++;
    9. }
    10. }
    11. public static void main(String[] args) throws InterruptedException {
    12. Counter counter = new Counter();
    13. // t1将count值 + 5w
    14. Thread t1 = new Thread(() -> {
    15. for (int i = 0; i < 50000; i++) {
    16. counter.increase();
    17. }
    18. });
    19. // t2将count值 + 5w
    20. Thread t2 = new Thread(() -> {
    21. for (int i = 0; i < 50000; i++) {
    22. counter.increase();
    23. }
    24. });
    25. t1.start();
    26. t2.start();
    27. t1.join();
    28. t2.join();
    29. // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
    30. System.out.println("两个子线程执行结束");
    31. System.out.println(counter.count);
    32. }
    33. }


    什么是所谓的线程安全:
    线程安全指的是代码若是串行执行并行执行,结果完全一致,就称为该代码是线程安全的;多个线程串行执行的结果和并行执行的结果不同,这就称为线程不安全。

    更改代码

    1. /**
    2. * 观察多线程场景下的线程安全问题
    3. */
    4. public class ThreadUnSafeDemo {
    5. private static class Counter {
    6. int count = 0;
    7. void increase() {
    8. count++;
    9. }
    10. }
    11. public static void main(String[] args) throws InterruptedException {
    12. Counter counter = new Counter();
    13. // t1将count值 + 5w
    14. Thread t1 = new Thread(() -> {
    15. for (int i = 0; i < 50000; i++) {
    16. counter.increase();
    17. }
    18. });
    19. // t2将count值 + 5w
    20. Thread t2 = new Thread(() -> {
    21. for (int i = 0; i < 50000; i++) {
    22. counter.increase();
    23. }
    24. });
    25. t1.start();
    26. t1.join();
    27. t2.start();
    28. t2.join();
    29. // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
    30. System.out.println("两个子线程执行结束");
    31. System.out.println(counter.count);
    32. }
    33. }

    线程不安全:串行执行和并行执行,结果不同。

     

    2.  JMM - Java的内存模型

    描述多线程场景下Java的线程内存(CPU的高速缓存和寄存器和主内存的关系)

    与JVM部分的JVM内存区域划分(JVM将内存划分为6大区域)不是一个概念

    每个线程都有自己的工作内存,每次读取变量(共享变量,不是线程的局部变量)都是先从主内存将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写回主内存。

    共享变量:类中成员变量,静态变量,常量都属于共享变量,即在堆和方法区中存储的变量。

    保证线程安全的条件

    一段代码要保证是线程安全的(多线程场景下和单线程场景下的运行结果保持一致),需要同时满足以下三个特性。

    原子性 

    原子性:该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么都不执行,不会存在中间状态。这种操作是一个原子性操作。


    eg:int a = 10 ——>直接将常量10赋值给a变量原子性,要么没赋值,要么赋值成功。

    eg:a+=10 ——> a = a +10 先要读取当前变量a的值,再将a +10计算,最后将计算得出的值重新赋值给a变量(对应三个原子性操作)

    可见性

    可见性:一个线程对共享变量的修改,能够及时的被其他线程看到,这种特性称为可见性。

    (synchronized-上锁,volatile关键字,final关键字可以保证可见性)。

    count值是Counter类的一个成员变量,这个属性属于共享属性,多个线程同时访问Count类的同一个对象,这个值属于线程共享变量。count值在主内存中存储(堆上)

    这个操作不是一个原子性操作,在某个线程执行count++的时候(分为三步走)其他线程大概率读不到++后的值。

    不可见性产生原因:

    count变量是共享变量,不同线程都有count的工作内存,工作内存修改不可见;
    increase方法内部不是原子性操作;
    不可见性导致+不原子性导致的。


    问题:为何final能保证可见性?final修饰的常量一定是可见的?
    答:常量在定义时就赋值了,且赋值后无法修改。

    第一种可能性

    不可见性+不原子性导致的线程不安全。

    1. 最开始t1和t2在线程启动时,会将主内存中的count值读取到自己的工作内存中,二者count = 0。

    2. 开始执行各自的run方法,假设此时t1先执行5253次循环,此时t1.count = 5253这个值,此时t1线程将5253写回主内存

    然后t2开始执行,从主内存中加载了这个5253这个初始值运行。同时t1也继续执行,t1先执行结束,最后t1线程的值就是50000,写回主内存。

    t2一直读取的自己的工作内存的值,它从5253这个初始值开始运行。在for循环的过程中,一直读取的是自己的工作内存的值(t1在写回主内存的过程中,50000值对t2是不可见的),t2在执行完自己的50000次循环后,将最终值55253写回主内存。


    3. 主线程最终去主内存中加载的count值就是55253


    简述上述过程

    第二种可能性 

    t1和t2并行执行,因为原子性的问题,导致的线程不安全。

    t1、t2一直在并行执行

    t1的工作内存中count = 0,+1操作使count从0变为1,之后将count = 1写回主内存;

    t2的工作内存中count = 0,+1操作使count从0变为1,之后将count = 1写回主内存;

    分别执行一次后,结果count = 1。本来计划要+2次(即count = 2)最终只是+1次。执行5w次后,count = 50000。

    只要执行过程中,t1和t2稍微有一点点时间差,就会得到不是5w的值。

    现实生活中的超卖现象

    问题汇总

    1. 为什么会有这么多内存?
    其实只有一个内存——主内存(硬件中的内存条),JMM讲的工作内存实际上是CPU的高速缓存和寄存器

    2. 为什么CPU要使用缓存和寄存器,不直接读写内存?
    速度:CPU的高速缓存和寄存器的读写速度基本上是内存3到4个数量级(成千上万倍)。
    成本:高速缓存和寄存器价格非常昂贵,造成的结果是其空间不可能太大,所以需要内存。

    CPU价格最贵,速度最快;内存次之,硬盘最便宜,速度也最慢。

    防止指令重排

    了解即可,大部分代码的线程安全问题都是因为不满足可见性和原子性


    代码的书写顺序不一定就是最终JVM或者CPU的执行顺序


    编译器和CPU会对指令优化,这个优化的前提就是保证代码的逻辑没有错误。

    在单线程场景下指令重排没问题,但是在多线程场景下就有可能因为指令重排导致错误,一般就是对象还没初始化完成就被别的线程给用了。

    关于synchronized关键字

    要确保一段代码的线程安全性,需要同时满足可见性,原子性和防止指令重排。
    synchronized一个关键字就能同时满足原子性和可见性。

    1. /**
    2. * 观察多线程场景下的线程安全问题
    3. */
    4. public class ThreadUnSafeDemo {
    5. private static class Counter {
    6. int count = 0;
    7. synchronized void increase() {
    8. count++;
    9. }
    10. }
    11. public static void main(String[] args) throws InterruptedException {
    12. Counter counter = new Counter();
    13. // t1将count值 + 5w
    14. Thread t1 = new Thread(() -> {
    15. for (int i = 0; i < 50000; i++) {
    16. counter.increase();
    17. }
    18. });
    19. // t2将count值 + 5w
    20. Thread t2 = new Thread(() -> {
    21. for (int i = 0; i < 50000; i++) {
    22. counter.increase();
    23. }
    24. });
    25. t1.start();
    26. t2.start();
    27. t1.join();
    28. t2.join();
    29. // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
    30. System.out.println("两个子线程执行结束");
    31. System.out.println(counter.count);
    32. }
    33. }

    1. synchronized——监视器锁 monitor lock(对象锁)

    何为锁?锁什么东西?

    "互斥" mutex lock
    某个线程获取到该对象的锁时,其他线程若也要获取同一个对象的锁,就会处在阻塞等待状态。

    当给increase方法加上synchronized关键字,所有进入该对象的线程都需要获取当前counter对象的"锁",获取成功才能进入,获取失败,就会进入阻塞态。

    1. 进入synchronized代码块就会尝试执行加锁操作。
    2. 退出synchronized代码块,就会释放这个锁。

    正因为increase方法上锁处理,多个线程在执行increase方法时,其实是排队进入,同一时刻只可能有一个线程进入increase方法执行对count属性的操作,保证了线程安全性。


    在Java内部,每个Java对象都有一块内存,描述当前对象"锁"的信息,锁的信息就表示当前对象被哪个线程持有。

    a. 若锁信息没有保存线程,则说明该对象没有被任何线程持有;
    b. 若锁信息保存了线程id,其他线程要获取该锁,就处在阻塞状态。

    等待队列不是FIFO队列,不满足先来后到的特点。


    关于互斥的理解,要理解锁的是谁(对象)

    1. /**
    2. * 观察多线程场景下的线程安全问题
    3. */
    4. public class ThreadUnSafeDemo {
    5. private static class Counter {
    6. int count = 0;
    7. // 以下代码操作的属性和操作都是原子性和可见性的
    8. synchronized void increase() {
    9. System.out.println(Thread.currentThread().getName() + "获取到锁");
    10. count++;
    11. try {
    12. Thread.sleep(1000);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. }
    17. }
    18. public static void main(String[] args) throws InterruptedException {
    19. Counter counter = new Counter();
    20. // t1将count值 + 5w
    21. Thread t1 = new Thread(() -> {
    22. for (int i = 0; i < 50000; i++) {
    23. counter.increase();
    24. }
    25. }, "t1");
    26. // t2将count值 + 5w
    27. Thread t2 = new Thread(() -> {
    28. for (int i = 0; i < 50000; i++) {
    29. counter.increase();
    30. }
    31. }, "t2");
    32. Counter counter1 = new Counter();
    33. Thread t3 = new Thread(() -> {
    34. for (int i = 0; i < 50000; i++) {
    35. counter1.increase();
    36. }
    37. }, "t3");
    38. t1.start();
    39. t2.start();
    40. t3.start();
    41. t1.join();
    42. t2.join();
    43. t3.join();
    44. // 主线程走到此处,说明t1、t2、t3都已经执行结束
    45. System.out.println("两个子线程执行结束");
    46. System.out.println(counter.count);
    47. }
    48. }

     


    a. t2线程在等待t1线程持有的counter对象的锁——t1和t2处在互斥关系;

    b. t3线程获取的是counter1这个对象的锁。没线程和他竞争,所以每次都能成功获取,t3和t1、t2不构成互斥关系。

    到底是不是互斥关系,就要看锁的是不是一个对象。

    2. synchronized代码块刷新内存

    线程执行synchronized代码块的流程

    a. 获取对象锁
    b. 从主内存拷贝变量值到工作内存
    c. 执行代码
    d. 将更改后的值写回主内存

    e. 释放对象锁

    因为synchronized保证互斥,同一时刻只有一个线程获取到这个对象的锁,就保证了可见性和原子性。

    从a到e只有一个线程能执行其他线程都在等待,bcd三步对于其他线程就是天然的可见性和原子性。

    问题:加锁操作与单线程的区别?

    只是在牵扯到变量更改的时候进行上锁操作,对于该类中其他方法,仍然是并发执行。


    此时类中新增getCount方法,方法中没有变量更改操作,所以不存在互斥关系,多个线程可以同时读取。


    所谓的单线程,指的是一个类的所有代码同一时刻只有一个线程在操作。
    上述指的是,只有increase方法(上锁的方法)是单线程操作,其他方法仍然可以多线程并行。

    3. 可重入

    获取到对象锁的线程可以再次加锁,这个操作就称为可重入。

    Java中的线程安全锁都是可重入的(包括java.concurrent.lock)

    1. public class Reentrant {
    2. private class Counter {
    3. int val;
    4. // 锁的是当前Counter对象
    5. synchronized void increase() {
    6. val++;
    7. }
    8. // 也是锁counter对象
    9. synchronized void increase1() {
    10. // 就相当于对同一个Counter对象加了两次锁
    11. increase();
    12. }
    13. }
    14. }

    如果不支持可重入,线程进入了increase1方法,之后进入increase需要再次获取锁,无法进入increase。线程1拿到当前Counter对象锁的线程阻塞在这里,等待线程自己释放锁之后再进入increase()——>这个程序永远不会停止。线程1一直阻塞在这里——死锁。

    synchronized支持线程的可重入

    Java中每个对象都有一个"对象头",其中包括:

    a. 描述当前对象的锁信息,即当前对象被哪个线程持有;

    b. 以及一个"计数器",即当前对象被上锁的次数。


    情况Ⅰ. 若线程1需要进入当前对象的同步代码块(synchronized),此时当前对象的对象头没有锁信息,线程1是第一个获取锁的线程,进入同步代码块,对象头修改持有线程为线程1, 计数器的值由0 + 1 =1。当线程1在同步代码块中再次调用当前对象的其他同步方法可以进入,计数器的值再次+1,说明此时对象锁被线程1获取了两次。

    问题:为什么加两次锁?

    答:

    synchronized代码块语法没问题,考虑的是多线程的安全问题。之所以在increase1上也加锁,说明increase1需要保证线程安全。


    情况Ⅱ. 若线程2需要进入当前对象的同步块,此时当前对象的对象头持有线程为线程1,且计数器值不为0,线程2就会进入阻塞状态,一直等到线程1释放锁为止(直到计数器值为0,才是真正释放锁)。

    1. synchronized修饰类中的成员方法,锁的对象就是当前类的对象 

    synchronized是对象锁,必须得有个具体的对象让他锁

    问题:t1和t2是否互斥?

    答:

    当前t1通过counter1对象调用的increase,则锁的是counter1这个对象,t1获取到的是counter1这个对象的锁;

    当前t2通过counter2这个对象调用的increase,锁的是counter2这个对象,t2获取到的是counter2这个对象的锁;
    t1和t2不互斥,各回各家,各锁各门。

    下面的代码中t1、t2才互斥

    2. synchronized修饰类中的静态方法,锁的是当前这个类的class对象

    class对象全局唯一,相当于我把这个类给锁了,同一时刻只能有一个线程访问这个方法(无论是几个对象)。

    1. public class Reentrant {
    2. public static void main(String[] args) {
    3. Counter counter1 = new Counter();
    4. Counter counter2 = new Counter();
    5. Counter counter3 = new Counter();
    6. Thread t1 = new Thread(() -> {
    7. counter1.increase2();
    8. }, "t1");
    9. Thread t2 = new Thread(() -> {
    10. counter2.increase2();
    11. }, "t2");
    12. Thread t3 = new Thread(() -> {
    13. counter3.increase2();
    14. }, "t3");
    15. t1.start();
    16. t2.start();
    17. t3.start();
    18. }
    19. private static class Counter {
    20. // 当synchronized修饰静态方法,则相当于将Counter这个类的所有对象都给锁了
    21. // 其实锁的Counter类的class对象,全局唯一
    22. synchronized static void increase2() {
    23. while (true) {
    24. System.out.println(Thread.currentThread().getName() + "获取到锁");
    25. try {
    26. Thread.sleep(1000);
    27. } catch (InterruptedException e) {
    28. throw new RuntimeException(e);
    29. }
    30. }
    31. }
    32. }
    33. }

     

     


    这是一个静态方法,锁的Counter.class对象,全局唯一。

    无论通过哪个Counter对象调用increase2(),同一时刻只能有一个线程获取到这个锁,其他线程都在等待,synchronized修饰静态方法需要谨慎。

    Counter.class对象是什么?

    任何一个类的.class对象都是全局唯一的,当JVM加载这个类的时候产生,描述该类的核心信息(具备哪些属性和方法),这个对象是反射的核心对象。

    3. synchronized代码块,明确锁的是哪个对象

    锁的粒度更细,只有在需要同步的若干代码才加上synchronized关键字。

    1. public class Reentrant {
    2. public static void main(String[] args) {
    3. Counter counter1 = new Counter();
    4. Thread t1 = new Thread(() -> {
    5. counter1.increase4();
    6. }, "t1");
    7. Thread t2 = new Thread(() -> {
    8. counter1.increase4();
    9. }, "t2");
    10. Thread t3 = new Thread(() -> {
    11. counter1.increase4();
    12. }, "t3");
    13. t1.start();
    14. t2.start();
    15. t3.start();
    16. }
    17. private static class Counter {
    18. int val;
    19. // 方法上没加synchronized关键字,因此所有线程都可以并发进入increase4方法
    20. void increase4() {
    21. System.out.println(val);
    22. System.out.println("准备进入同步代码块");
    23. // 同步代码块,进入同步代码块,必须获取到指定的锁
    24. // this表示当前对象引用,锁的就是当前对象counter1
    25. synchronized (this) {
    26. while (true) {
    27. System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
    28. try {
    29. Thread.sleep(1000);
    30. } catch (InterruptedException e) {
    31. throw new RuntimeException(e);
    32. }
    33. }
    34. }
    35. }
    36. }
    37. }

    1. public class Reentrant {
    2. public static void main(String[] args) {
    3. Counter counter1 = new Counter();
    4. Counter counter2 = new Counter();
    5. Counter counter3 = new Counter();
    6. Thread t1 = new Thread(() -> {
    7. counter1.increase4();
    8. }, "t1");
    9. Thread t2 = new Thread(() -> {
    10. counter2.increase4();
    11. }, "t2");
    12. Thread t3 = new Thread(() -> {
    13. counter3.increase4();
    14. }, "t3");
    15. t1.start();
    16. t2.start();
    17. t3.start();
    18. }
    19. private static class Counter {
    20. int val;
    21. // 方法上没加synchronized关键字,因此所有线程都可以并发进入increase4方法
    22. void increase4() {
    23. System.out.println(val);
    24. System.out.println("准备进入同步代码块");
    25. // 同步代码块,进入同步代码块,必须获取到指定的锁
    26. // this表示当前对象引用,锁的就是当前对象
    27. synchronized (this) {
    28. while (true) {
    29. System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
    30. try {
    31. Thread.sleep(1000);
    32. } catch (InterruptedException e) {
    33. throw new RuntimeException(e);
    34. }
    35. }
    36. }
    37. }
    38. }
    39. }

    1. public class Reentrant {
    2. public static void main(String[] args) {
    3. Counter counter1 = new Counter();
    4. Counter counter2 = new Counter();
    5. Counter counter3 = new Counter();
    6. Thread t1 = new Thread(() -> {
    7. counter1.increase4();
    8. }, "t1");
    9. Thread t2 = new Thread(() -> {
    10. counter2.increase4();
    11. }, "t2");
    12. Thread t3 = new Thread(() -> {
    13. counter3.increase4();
    14. }, "t3");
    15. t1.start();
    16. t2.start();
    17. t3.start();
    18. }
    19. private static class Counter {
    20. int val;
    21. // 方法上没加synchronized关键字,因此所有线程都可以并发进入increase4方法
    22. void increase4() {
    23. System.out.println(val);
    24. System.out.println("准备进入同步代码块");
    25. // 同步代码块,进入同步代码块,必须获取到指定的锁
    26. // 若锁的是class对象,全局唯一
    27. synchronized (Reentrant.class) {
    28. while (true) {
    29. System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
    30. try {
    31. Thread.sleep(1000);
    32. } catch (InterruptedException e) {
    33. throw new RuntimeException(e);
    34. }
    35. }
    36. }
    37. }
    38. // 当synchronized修饰静态方法,则相当于将Counter这个类的所有对象都给锁了
    39. // 其实锁的Counter类的class对象,全局唯一
    40. synchronized static void increase2() {
    41. while (true) {
    42. System.out.println(Thread.currentThread().getName() + "获取到锁");
    43. try {
    44. Thread.sleep(1000);
    45. } catch (InterruptedException e) {
    46. throw new RuntimeException(e);
    47. }
    48. }
    49. }
    50. // 锁的是当前Counter对象
    51. synchronized void increase() {
    52. val++;
    53. }
    54. // 也是锁counter对象
    55. synchronized void increase1() {
    56. // 就相当于对同一个Counter对象加了两次锁
    57. increase();
    58. }
    59. }
    60. }

    练习

    到底互斥与否,就看多个线程锁的是什么?只有锁的是同一个对象才互斥,不同对象就不互斥。

    1. public class LockNormal {
    2. public static void main(String[] args) {
    3. Object lock = new Object();
    4. Counter c1 = new Counter();
    5. c1.lock = lock;
    6. Counter c2 = new Counter();
    7. c2.lock = lock;
    8. Counter c3 = new Counter();
    9. c3.lock = new Object();
    10. Thread t1 = new Thread(() -> {
    11. for (int i = 0; i < 50000; i++) {
    12. c1.increase();
    13. }
    14. }, "t1");
    15. Thread t2 = new Thread(() -> {
    16. for (int i = 0; i < 50000; i++) {
    17. c2.increase();
    18. }
    19. }, "t2");
    20. Thread t3 = new Thread(() -> {
    21. for (int i = 0; i < 50000; i++) {
    22. c3.increase();
    23. }
    24. }, "t3");
    25. t1.start();
    26. t2.start();
    27. t3.start();
    28. }
    29. private static class Counter {
    30. int val;
    31. Object lock;
    32. void increase() {
    33. // 不需要同步的代码
    34. // synchronized锁任意对象,传什么锁什么
    35. synchronized (lock) {
    36. while (true) {
    37. System.out.println(Thread.currentThread().getName() + "获取了锁");
    38. try {
    39. Thread.sleep(1000);
    40. } catch (InterruptedException e) {
    41. throw new RuntimeException(e);
    42. }
    43. }
    44. }
    45. }
    46. }
    47. }

    问题:这三个线程谁和谁互斥?

    A. 都是互斥的t1,t2,t3互斥

    B. 都是并发的
    C. t1和t2互斥,和t3并发

    D. t2和t3互斥,和t1并发

    E. t1和t3互斥,和t2并发

    答:C

    t3和t1,t2不构成互斥,t3的lock是一个新的对象。

    两次运行结果如下

    Java标准库中的线程安全类

    线程不安全:多线程发修改同一个集合的内容,就有数据安全问题。

    只要多线程访问同一个vector对象,都是互斥的。

    volatile关键字:可见性,内存屏障 

    1. volatile关键字可以保证共享变量可见性  强制线程读写主内存的变量值

    相较于普通的共享变量,使用volatile关键字可以保证共享变量的可见性
    a. 当线程读取的是volatile关键字时,线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值)。
    b. 当线程的是volatle关键字的变量,将当前修改后的变量值(工作内存中)立即刷新到主内存中,且其他正在读此变量的线程会等待(不是阻塞),直到写回主内存操作完成,保证读的一定是刷新后的主内存值。

    对于同一个volatile变量,他的写操作一定发生在他的读操作之前,保证读到的数据一定是主内存中刷新后的数据。

    未添加volatile关键字

    1. import java.util.Scanner;
    2. public class NonVolatile {
    3. private static class Counter {
    4. int flag = 0; // 未添加volatile关键字
    5. }
    6. public static void main(String[] args) {
    7. Counter counter = new Counter();
    8. Thread t1 = new Thread(() -> {
    9. // t1将flag加载到工作内存后一直读取的是当前工作内存的值
    10. // t2的修改对t1是不可见的
    11. while (counter.flag == 0) {
    12. // 一直循环
    13. }
    14. System.out.println(counter.flag + "退出循环");
    15. });
    16. t1.start();
    17. Thread t2 = new Thread(() -> {
    18. Scanner scanner = new Scanner(System.in);
    19. System.out.println("请改变flag的值");
    20. counter.flag = scanner.nextInt();
    21. });
    22. t2.start();
    23. }
    24. }

    t1将flag加载到工作内存后一直读取的是当前工作内存的值,t2的修改对t1是不可见的。

    添加了volatile关键字

    1. import java.util.Scanner;
    2. public class NonVolatile {
    3. private static class Counter {
    4. volatile int flag = 0;
    5. }
    6. public static void main(String[] args) {
    7. Counter counter = new Counter();
    8. Thread t1 = new Thread(() -> {
    9. // volatile变量每次都读写主内存
    10. while (counter.flag == 0) {
    11. // 一直循环
    12. }
    13. System.out.println(counter.flag + "退出循环");
    14. });
    15. t1.start();
    16. Thread t2 = new Thread(() -> {
    17. Scanner scanner = new Scanner(System.in);
    18. System.out.println("请改变flag的值");
    19. counter.flag = scanner.nextInt();
    20. });
    21. t2.start();
    22. }
    23. }

    使用synchronized保证可见性

    1. import java.util.Scanner;
    2. public class Volatile {
    3. private static class Counter {
    4. int flag = 0;
    5. }
    6. public static void main(String[] args) {
    7. Counter counter = new Counter();
    8. Thread t1 = new Thread(() -> {
    9. while (true) {
    10. synchronized (counter) {
    11. if (counter.flag == 0) {
    12. continue;
    13. }else {
    14. break;
    15. }
    16. }
    17. }
    18. System.out.println(counter.flag + "退出循环");
    19. });
    20. t1.start();
    21. Thread t2 = new Thread(() -> {
    22. Scanner scanner = new Scanner(System.in);
    23. System.out.println("请改变flag的值");
    24. counter.flag = scanner.nextInt();
    25. });
    26. t2.start();
    27. }
    28. }

    volatile只能保证可见性,无法保证原子性,因此若代码不是原子性操作,仍然不是线程安全的,volatile != synchronized。 

    1. /**
    2. * 观察多线程场景下的线程安全问题
    3. */
    4. public class ThreadUnSafeDemo {
    5. private static class Counter {
    6. // 使用关键字volatile,保证可见性
    7. volatile int count = 0;
    8. // 以下代码操作的属性和操作只是可见性的,不保证原子性
    9. void increase() {
    10. count++;
    11. }
    12. }
    13. public static void main(String[] args) throws InterruptedException {
    14. Counter counter = new Counter();
    15. // t1将count值 + 5w
    16. Thread t1 = new Thread(() -> {
    17. for (int i = 0; i < 50000; i++) {
    18. counter.increase();
    19. }
    20. }, "t1");
    21. // t2将count值 + 5w
    22. Thread t2 = new Thread(() -> {
    23. for (int i = 0; i < 50000; i++) {
    24. counter.increase();
    25. }
    26. }, "t2");
    27. Counter counter1 = new Counter();
    28. t1.start();
    29. t2.start();
    30. t1.join();
    31. t2.join();
    32. // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
    33. System.out.println("两个子线程执行结束");
    34. System.out.println(counter.count);
    35. }
    36. }

    2. 使用volatile修饰的变量,相当于一个内存屏障

    未使用volatile时

    1. class {
    2. int x = 1; // 1
    3. int y = 2; // 2
    4. boolean z = true;// 3
    5. x = × + 1; // 4
    6. y = y + 2; // 5
    7. }

    指令重排:CPU会在不影响结果的前提下、执行时不一定按照书写顺序执行。

    a. 1 2 3 4 5
    b. 1 3 4 2 5
    c. 2 3 5 1 4
    d. 1 4 2 5 3

    这四个排法最终结果都一样。


    使用volatile修饰时

    1. class {
    2. int x = 1; // 1
    3. int y = 2; // 2
    4. volatile boolean z = true;// 3
    5. x = × + 1; // 4
    6. y = y + 2; // 5
    7. }

    CPU在执行到第3行时,一定保证1和2已经执行结束(1和2的顺序可以打乱,1、2必须在3之前),且1和2的结果对后面的结果可见;
    此时执行到第三行,4和5一定还没开始;
    当把第三行执行结束,4和5顺序也能打乱。

    a. 1 2 3 4 5 √
    b. 1 3 4 2 5 ×
    c. 2 3 5 1 4 ×
    d. 1 4 2 5 3 ×

    e. 2 1 3 4 5 √

    f.  2 1 3 5 4 √

  • 相关阅读:
    基于java+SpringBoot+HTML+MySQL服在线销售的设计与实现
    解读2022年度敏捷教练行业现状报告
    Task01|GriModel统计分析(下)|方法论与一元数值检验|假设检验1
    【Android】源码中的工厂方法模式
    centos7 dubbo安装
    如何掌握项目管理的5个阶段?
    2022年最新Python大数据之Python基础【六】函数与变量
    Java泛型和类型擦除
    14. 对有状态组件和无状态组件的理解及使用场景?
    交换机和路由器技术-25-OSPF多区域配置
  • 原文地址:https://blog.csdn.net/XHT117/article/details/125396864