1.简介
一个线程对主内存的修改能够及时地被其他线程观察到,这种特性被称之为可见性。
2.演示可见性带来的问题
/**
* 描述:演示可见性带来的问题。
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("a=" + a + ";b=" + b + ";");
}
public static void main(String[] args) {
while (true) {
FieldVisibility fieldVisibility = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
fieldVisibility.change();
}
},"thread0").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
fieldVisibility.print();
}
},"thread1").start();
}
}
}
代码运行结果如下所示。
a=3;b=3;
a=1;b=2;
a=3;b=2;
a=1;b=3;
3.可见性分析
上述结果中,第1个和第2个结果按照正常思路便可以分析出来,但是“a=3;b=2;”和“a=1;b=3;”却不容易得出,这是因为发生了可见性问题。可见性会有四种情况,执行逻辑和结果分别如下所示。
当thread0执行完change()方法后,在打印前,修改的“a=3;b=a(3);”都没有刷进主存,此时会输出“a=1;b=2”的结果。
当thread0执行完change()方法后,在打印前,修改的“a=3;b=a(3);”都刷进了主存,此时会输出“a=3;b=3”的结果。
当thread0执行完change()方法后,在打印a时,修改的“a=3”还没有刷进主存,而在打印b时,修改的“a=3”已经刷进主存了,此时便会输出“a=1;b=3”的结果。
当thread0执行完change()方法后,在打印a时,修改的“a=3”刷进主存,而在打印b时,修改的“b=a(3)”还没有刷进主存了,此时便会输出“a=3;b=2”的结果。
4.可见性问题存在的原因
1.所有的共享变量存在于主内存中(Main Memory),每个线程有自己的本地内存(L1-L3)。
2.线程读写共享数据是通过本地内存交换的,线程不能直接读写主内存中的变量,而是只能操作自己本地内存中的变量,然后同步到主内存中。
3.主内存是多个线程共享的,但是线程间不共享本地内存,如果线程间需要通信,必须借助主内存中转来完成,因此读写操作不是实时的,这才导致了可见性问题。
1.高速缓存的速度仅次于寄存器,但是容量比主内存小,所以在CPU和主内存之间就多了三层缓存。
2.线程间对于共享变量的可见性问题不是由多核引起的,而是由多层缓存引起的,如果所有CPU都只用一个缓存,那么就不会存在内存可见性问题。
3.每个核心都会将自己需要的数据读取到独占缓存中,数据修改后需要写入到缓存中,然后等待刷入到主存中,所以会导致有些CPU读取的值是一个过期的值。
5.Happens-before
(1).简介
Happens-before规则是用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A,或者说,两个操作可以用Happens-before来确定他们的执行顺序,如果一个操作Happens-before于另一个操作,那第一个操作对于第二个操作就是可见的,这就是Happens-before。
(2).Happens-before规则
- synchronized和Lock锁操作(线程A在解锁之前的所有操作,对于线程B在加锁之后都是可见的)
- volatile变量(使用volatitle关键字修饰的变量,只要有一个线程对变量进行修改,那么这个变量就一定对其它线程是可见的)
- 单线程规则(语句n,一定可以看见语句n之前的所有操作)
- 线程启动(主线程启动一个子线程,那么子线程一定能看到主线程启动子线程之前的操作)
- 线程join(一个线程一旦使用join方法,那么这个线程的所有操作,对其它线程一定是可见的)
- 传递性(如果一个程序有ABC3行代码,A运行完之后对B是可见的,B运行完之后对C是可见的,那么此时,A对C也一定是可见的,这就是传递性原则)
- 中断(一个线程被其它线程中断interrupt,那么检测中断isInterrupted或者抛出InterruptedException一定能看到这个线程被中断了)
- 工具类的Happens-before原则
- 线程安全的容器get一定能看到在此之前的put操作
- CountDownLatch
- Semaphore
- Future
- CyclicBarrier
- 线程池
6.volatile
(1).简介
- volatile是一种同步机制,相较于synchronized或Lock类是更轻量级的。如果一个变量被volatile修饰,那么JVM就知道这个变量可能会被并发修改。
- volatile是轻量级的,使用它并不会发生线程上下文切换等开销很大的行为,主要是把值刷到主内存中。因为开销小,与之对应的是能力便小,如volatile做不到synchronized那样的原子保护,volatile仅仅在有限的场景下才能发挥作用。
(2).不适用场景1
/**
* 描述:不适用场景1
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realIncrement = new AtomicInteger();
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
a++;
//统计真正计算的次数
realIncrement.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("a的值:"+((NoVolatile) r).a);
System.out.println("累加的次数:"+((NoVolatile) r).realIncrement.get());
}
}
代码运行结果如下所示,其中累计的次数一定是20000,而a的则不定,可以是20000,也可能是其它数值,这说明了volatile不适用于++场景。
a的值:18703
累加的次数:20000
(3).不适用场景2
/**
* 描述:不适用场景2
*/
public class NoVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realIncrement = new AtomicInteger();
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
flipDone();
realIncrement.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("done的值:" + ((NoVolatile) r).done);
System.out.println("累加的次数:" + ((NoVolatile) r).realIncrement.get());
}
}
代码运行结果如下所示,其中累计的次数一定是20000,而done的则不定,可能是true,也可能是false,这说明了volatile不适用于boolean值d的场景。
done的值:true
累加的次数:20000
(4).适用场景1
/**
* 描述:适用场景1
*/
public class UseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realIncrement = new AtomicInteger();
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
setDone();
realIncrement.incrementAndGet();
}
}
private void setDone() {
//和原来的状态没有关系
done = true;
}
public static void main(String[] args) throws InterruptedException{
Runnable r = new UseVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("done的值:" + ((UseVolatile) r).done);
System.out.println("累加的次数:" + ((UseVolatile) r).realIncrement.get());
}
}
代码运行结果如下所示,其中累计的次数一定是20000,done的值一定是true。
done的值:true
累加的次数:20000
如果一个共享变量自始至终只被各个线程赋值而没有其它操作,那么就可以用volatile来代替synchronized或者原子变量,因为赋值自身是有原子性的,而volatile又保证了可见行,所以就足以保证线程安全。
(5).适用场景2
/**
* 描述:演示重排序现象。直到达到某个条件才停止,来测试小概率事件。
*/
public class OutOfOrderExecution {
private volatile static int a = 0, b = 0;
private volatile static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
two.start();
one.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
代码运行结果如下所示,而不会出现“x=0,y=0”的情况。
x=0,y=1
x=1,y=0
x=1,y=1
- volatile修饰符适用于以下场景,某个属性被多个线程共享,其中一个线程修改了此属性,其它线程可以立即得到修改后的值,或者作为触发器实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。正因为无锁,不需要花费时间在获取锁和释放锁上,所以说它的代价比较小。
- volatile只能作用于属性,这样编译器就不会对这个属性做指令重排序。
- 除了volatile可以让变量保证可见行外,synchronized、Lock、并发集合、Thread类的join和start方法等都可以保证可见性,具体参照happens-before原则。
-
synchronized可见性的理解有两点。一是synchronized不仅保证了原子性,还保证了可见性。
二是synchronized不仅让被保护的代码安全,让加锁前的代码也具有可见性了。
- volatile和synchronized的关系,volatile可以看作是轻量版的synchronized,都是用来保证线程安全的。如果一个共享自始至终只被各个线程赋值而没有其它操作,那么就可以用volatile来代替synchronized或者原子变量,因为赋值自身是有原子性的,而volatile又保证了可见行,所以就足以保证线程安全。