https://www.cnblogs.com/dolphin0520/p/3920373.html

Adblocker

Volatile关键字

volatile
首先,大佬(马老师)说,这个volatile在工程中能不用就不用,因为这玩意不好掌控,没有什么资料.

  1. 保证线程可见性(synchronize也有这效果
    设一个变量a,如果没有加volatile,多线程情况下,在线程t1修改了a的值后,另一个线程t2读到的仍然是旧值;如果加了volatile修饰,t2就可以马上读到t1修改后的值.
    因为如下:(本质依靠的是MESI,CPU的缓存一致性协议)
    首先变量a保存在heap堆内存中,堆内存是各线程共享内存;而且每个线程都有自己的专属工作内存.
    当两个线程,t1和t2去访问共享内存的变量a时,他们会各自把a复制一份到自己的专属内存.
    如果变量a没有加volatile,这时候如果线程t1修改了变量的值,(t1应该会把变动马上同步回共享内存),但是线程t2什么时候去共享内存再次读取同步变量a,不好控制,如果线程2没有去共享内存再次读取同步变量a,那么就看不见线程1的修改后的结果.
    看一个保证线程可见性的例子:
    在一秒后,main线程修改了变量running的值,
    没有加volatile时,程序会过很久才打印”m end!”;
    加了volatile后,会在一秒后马上打印”m end!”
  1. import java.util.concurrent.TimeUnit;
  2. public class T01_HelloVolatile {
  3. // 对比一下有无volatile的运行结果
  4. /*volatile*/ boolean running = true;
  5. void m() {
  6. System.out.println("m start");
  7. while(running) {
  8. }
  9. System.out.println("m end!");
  10. }
  11. public static void main(String[] args) {
  12. T01_HelloVolatile t = new T01_HelloVolatile();
  13. new Thread(t::m, "t1").start();
  14. try {
  15. TimeUnit.SECONDS.sleep(1);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. t.running = false;
  20. }
  21. }
  1. 禁止指令重排序(也和CPU有关)(synchronize无此效果)
    (其底层原理是加了读屏障loadfence和写屏障storefence原语指令)
    以前的CPU是”串联”执行指令;现代CPU为了提高效率,当第一个指令执行到中间时,就开始执行第二个指令.(原来像是平铺的水泥板,现在像是楼梯).
    为了利用CPU的这种高效架构,编译器(compiler)把源码编译时,可能会将指令重新排序,据说这样会把速度提高很多.
    指令重排序可能带来问题的场景举例:DCL单例模式

DCL双重检查锁实现的单例模式(静态实例变量加volatile)
new一个对象的时候,分为三步:

给这个对象申请内存,给成员变量赋默认值(比如int的默认值为0);
给这个对象的成员变量初始化(比如我们写的int a=8)
把申请的内存赋值给对象的”引用”
正常情况下是1,2,3顺序执行;如果发生指令重排序的话,会1,3,2这样顺序执行.
重排序后,在特别大的并发量下,可能会在第1,3步后,还没有执行第2步前,这时该引用已经!=null了,但是成员变量还没有初始化,这个时候对象被其他线程使用就会出问题.
DCL单例模式举例:(这个属于懒汉模式,一般用不到;直接用恶汉单例就行啦,没必要用懒汉)

  1. public class Manager {
  2. private static volatile Manager INSTANCE = null;
  3. // 私有构造方法
  4. private Manager() {
  5. }
  6. public static Manager getInstance() {
  7. if (INSTANCE == null) {
  8. synchronized (Manager.class) {
  9. if (INSTANCE == null) {
  10. // 初始化INSTANCE,加载所需资源等
  11. INSTANCE = new Manager();
  12. }
  13. }
  14. }
  15. return INSTANCE;
  16. }
  17. }
  1. 不能保证原子性
    做个测试证明一下,如果volatile可以保证原子性,那么下面这段代码应该输出100000;反之则证明volatile不能保证原子性.
    如果想保证原子性,可以在方法m()上加个synchronize,或者把count++这段代码用synchronize包起来.

    1. public class T04_VolatileNotSync {
    2. volatile int count = 0;
    3. void m() {
    4. for(int i=0; i<10000; i++) {
    5. count++;
    6. /*
    7. synchronize(this){
    8. count++;
    9. }
    10. */
    11. }
    12. }
    13. public static void main(String[] args) {
    14. T04_VolatileNotSync t = new T04_VolatileNotSync();
    15. List<Thread> threads = new ArrayList<Thread>();
    16. for(int i=0; i<10; i++) {
    17. threads.add(new Thread(t::m, "thread-"+i));
    18. }
    19. threads.forEach((o)->o.start());
    20. threads.forEach((o)->{
    21. try {
    22. o.join();
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. });
    27. System.out.println(t.count);
    28. }
    29. }

    4.volatile修饰引用类型变量的可见性?

    有些文章会写到,volatile如果修饰引用类型变量,那么”引用”的地址的改变对其他线程是可见的,但是引用的对象的属性变化对其他线程不可见.

如果你写一个例子,经过一些尝试,发现普通对象的属性的改变,volatile能保证其变化是可见的.但是!!!
但是!!!大量的测试后,我发现,不是每次都可见,特别是对象的属性变化很多次的时候!
所以,结论是,volatile的确不能保证变量指向的对象的属性的可见性.
又但是!如果修改对象属性的线程,sleep了一下,哪怕一纳秒,那么就能保证可见了,真是奇怪,回头有时间研究下JVM没准能搞明白.(也有可能是sleep的情况下,测试次数不够多)

下面是测试的例子,可以看出,volatile修饰的引用类型变量,如果修改的线程(生产者)没有sleep,其他线程不是每次都可见.

  1. public class VolatileObject {
  2. volatile static Pet pet = new Pet("dahuang", 1);
  3. public static void main(String[] args) {
  4. // 多运行几次无论是ageChange还是nameChange,会发现有时候t1不会结束(如果t2不sleep)
  5. nameChange();
  6. // ageChange();
  7. }
  8. private static void ageChange() {
  9. new Thread(() -> {
  10. System.out.println("t1 start "/* + Instant.now()*/);
  11. while (true) {
  12. if (pet.getAge() == 5) {
  13. break;
  14. }
  15. }
  16. System.out.println("t1 end "/* + Instant.now()*/);
  17. }, "t1").start();
  18. try {
  19. TimeUnit.SECONDS.sleep(1);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. new Thread(() -> {
  24. Pet myPet = pet;
  25. for (int i = 1; i <= 100; i++) {
  26. int age = myPet.getAge();
  27. myPet.setAge(++age);
  28. /*try {
  29. Thread.sleep(1);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }*/
  33. }
  34. System.out.println("t2 end "/* + Instant.now()*/);
  35. }, "t2").start();
  36. }
  37. private static void nameChange() {
  38. new Thread(() -> {
  39. System.out.println("t1 start "/* + Instant.now()*/);
  40. while (true) {
  41. if ("xiaobai8".equals(pet.getName())) {
  42. break;
  43. }
  44. }
  45. System.out.println("t1 end "/* + Instant.now()*/);
  46. }, "t1").start();
  47. try {
  48. TimeUnit.SECONDS.sleep(1);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. new Thread(() -> {
  53. Pet myPet = pet;
  54. for (int i = 1; i <= 10; i++) {
  55. myPet.setName("xiaobai" + i);
  56. /*try {
  57. TimeUnit.NANOSECONDS.sleep(1);
  58. } catch (InterruptedException e) {
  59. e.printStackTrace();
  60. }*/
  61. }
  62. System.out.println("t2 end "/* + Instant.now()*/);
  63. }, "t2").start();
  64. }
  65. static class Pet {
  66. String name;
  67. int age;
  68. public Pet(String name, int age) {
  69. this.name = name;
  70. this.age = age;
  71. }
  72. public String getName() {
  73. return name;
  74. }
  75. public void setName(String name) {
  76. this.name = name;
  77. }
  78. public int getAge() {
  79. return age;
  80. }
  81. public void setAge(int age) {
  82. this.age = age;
  83. }
  84. }
  85. }

本文来自

多线程与高并发

线程底层原理,多线程使用方式,高并发情况下如何利用多线程来提高程序执行效率,最大化使用CPU资源
关注
精品推荐

Zookeeper

ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
关注
精品推荐

Redis

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构。