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许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构。