https://www.cnblogs.com/dolphin0520/p/3920373.html
Volatile关键字
volatile
首先,大佬(马老师)说,这个volatile在工程中能不用就不用,因为这玩意不好掌控,没有什么资料.
- 保证线程可见性(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!”
import java.util.concurrent.TimeUnit;public class T01_HelloVolatile {// 对比一下有无volatile的运行结果/*volatile*/ boolean running = true;void m() {System.out.println("m start");while(running) {}System.out.println("m end!");}public static void main(String[] args) {T01_HelloVolatile t = new T01_HelloVolatile();new Thread(t::m, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}t.running = false;}}
- 禁止指令重排序(也和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单例模式举例:(这个属于懒汉模式,一般用不到;直接用恶汉单例就行啦,没必要用懒汉)
public class Manager {private static volatile Manager INSTANCE = null;// 私有构造方法private Manager() {}public static Manager getInstance() {if (INSTANCE == null) {synchronized (Manager.class) {if (INSTANCE == null) {// 初始化INSTANCE,加载所需资源等INSTANCE = new Manager();}}}return INSTANCE;}}
不能保证原子性
做个测试证明一下,如果volatile可以保证原子性,那么下面这段代码应该输出100000;反之则证明volatile不能保证原子性.
如果想保证原子性,可以在方法m()上加个synchronize,或者把count++这段代码用synchronize包起来.public class T04_VolatileNotSync {volatile int count = 0;void m() {for(int i=0; i<10000; i++) {count++;/*synchronize(this){count++;}*/}}public static void main(String[] args) {T04_VolatileNotSync t = new T04_VolatileNotSync();List<Thread> threads = new ArrayList<Thread>();for(int i=0; i<10; i++) {threads.add(new Thread(t::m, "thread-"+i));}threads.forEach((o)->o.start());threads.forEach((o)->{try {o.join();} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.count);}}
4.volatile修饰引用类型变量的可见性?
有些文章会写到,volatile如果修饰引用类型变量,那么”引用”的地址的改变对其他线程是可见的,但是引用的对象的属性变化对其他线程不可见.
如果你写一个例子,经过一些尝试,发现普通对象的属性的改变,volatile能保证其变化是可见的.但是!!!
但是!!!大量的测试后,我发现,不是每次都可见,特别是对象的属性变化很多次的时候!
所以,结论是,volatile的确不能保证变量指向的对象的属性的可见性.
又但是!如果修改对象属性的线程,sleep了一下,哪怕一纳秒,那么就能保证可见了,真是奇怪,回头有时间研究下JVM没准能搞明白.(也有可能是sleep的情况下,测试次数不够多)
下面是测试的例子,可以看出,volatile修饰的引用类型变量,如果修改的线程(生产者)没有sleep,其他线程不是每次都可见.
public class VolatileObject {volatile static Pet pet = new Pet("dahuang", 1);public static void main(String[] args) {// 多运行几次无论是ageChange还是nameChange,会发现有时候t1不会结束(如果t2不sleep)nameChange();// ageChange();}private static void ageChange() {new Thread(() -> {System.out.println("t1 start "/* + Instant.now()*/);while (true) {if (pet.getAge() == 5) {break;}}System.out.println("t1 end "/* + Instant.now()*/);}, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {Pet myPet = pet;for (int i = 1; i <= 100; i++) {int age = myPet.getAge();myPet.setAge(++age);/*try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}*/}System.out.println("t2 end "/* + Instant.now()*/);}, "t2").start();}private static void nameChange() {new Thread(() -> {System.out.println("t1 start "/* + Instant.now()*/);while (true) {if ("xiaobai8".equals(pet.getName())) {break;}}System.out.println("t1 end "/* + Instant.now()*/);}, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {Pet myPet = pet;for (int i = 1; i <= 10; i++) {myPet.setName("xiaobai" + i);/*try {TimeUnit.NANOSECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}*/}System.out.println("t2 end "/* + Instant.now()*/);}, "t2").start();}static class Pet {String name;int age;public Pet(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}}}
多线程与高并发
线程底层原理,多线程使用方式,高并发情况下如何利用多线程来提高程序执行效率,最大化使用CPU资源
关注
精品推荐
Zookeeper
ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
关注
精品推荐
Redis
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构。
