内存可见性问题(volatile关键字)
- Java在1.5之后的版本中加入了JUC(
java.util.concurrent
包) - 内存可见性问题:当多个线程操作共享数据时,彼此不可见
- JVM会为每一个线程分配独立的缓存用于提高效率
1 内存可见性问题逐步解决(一个线程读,一个线程写)
1.1 原始问题(内存可见性问题现象)
-
原始程序的问题:运行过程中,
Thread-0
线程进行flag值的更改,main
线程用于访问flag的值,由于while循环更加接近底层代码,因此运行效率极高,最终会导致无法从ThreadDemo
中获取已经被Thread-0
线程更改过的新的flag
值,此时两个线程是不可见的,while
循环变成了死循环。 -
原始代码执行过程
package JUC.Volatile;
/**
* 以下为原始程序:在main方法中共有两个线程,首先是demo线程,其次是main线程
*
* @author Yorick
*
*/
public class VolatileTest {
public static void main(String[] args) {
// Thread-0线程
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
// main线程
while (true) {
if (demo.isFlag()) {
System.out.println(Thread.currentThread().getName() + "----------------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;
System.out.println(Thread.currentThread().getName() + "isflag" + ":" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
1.2 通过加锁的方式解决线程可见性问题
- 效率低下
public static void main(String[] args) {
// Thread-0线程
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
// main线程
while (true) {
// 通过加锁的方式,每次循环都会重新从ThreadDemo中获取新的flag值
synchronized (demo) {
if (demo.isFlag()) {
System.out.println(Thread.currentThread().getName() + "----------------");
break;
}
}
}
}
1.3 通过volatile关键字解决线程可见性问题
- volatile关键字作用:当多个线程进行操作共享数据时,保证内存中的数据可见(底层采用了内存栅栏),相较于
synchronized
是一种较为轻量级的同步策略 volatile
使用注意:- 不具备互斥性
- 不嫩敢保证变量的原子性
class ThreadDemo implements Runnable {
private volatile boolean flag;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;
System.out.println(Thread.currentThread().getName() + "isflag" + ":" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
2 原子性问题(两个线程均存在读写操作)
2.1 原始问题(原子性问题现象)
- 产生原因
package JUC;
public class AtomicTest {
public static void main(String[] args) {
ThreadDemo2 demo2 = new ThreadDemo2();
for (int i = 0; i < 10; i++) {
new Thread(demo2).start();
}
}
}
class ThreadDemo2 implements Runnable {
private int i = 0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i++);
}
}
// 运行结果:每个线程单独对i变量进行自增操作,不应该出现两次0,存在线程安全问题
//Thread-1:0
//Thread-4:3
//Thread-2:1
//Thread-3:2
//Thread-0:0
//Thread-5:4
//Thread-6:6
//Thread-7:5
//Thread-8:8
//Thread-9:7
2.2 原子变量
- jdk1.5之后
java.util.concurrent.atomic
包下提供了常用的原子变量,该变量使用了如下的技术- 使用
volatile
保证内存可见性 - 使用CAS(Compare-and-Swap)算法保证数据的原子性,CAS算法是硬件对于并发操作共享数据的支持,包含了:内存值(V),预估值(A)和更新值(B)。当且仅当V等于A时,才会把B的值赋值给V,否则不会进行任何操作。
- 通过使用以上特性,可以保证每次有且只有一个线程能对共享数据操作成功。没有成功的线程不会被挂起,而是再次尝试更新操作,因此效率会高于加锁的方式
- 使用
class ThreadDemo3 implements Runnable {
// 原子变量(从10开始追加)
private AtomicInteger atomicInteger = new AtomicInteger(10);
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 先获取值再自增,相当于i++
System.out.println(Thread.currentThread().getName() + ":" + atomicInteger.getAndIncrement());
}
}