1、volatile的实现可见性原理
volatile的是通过加入内存屏障和禁止指令重排序优化来实现的。
对于写而言:volatile关键修饰的变量在被写操作时,会在写操作后加入一条store指令,将当前工作内存中的数据刷新到主存中去。
对于读而言:volatile关键修饰的变量在被读操作时,会再读操作前加入一条load执行,将主存中的数据更新到当前工作内存中。
线程写volatile变量的过程:
1、改变线程工作内存中的volatile变量副本的值;
2、将修改后的副本变量更新到主内存中去。
线程读volatile变量的过程:
1、先从主内存中将最新volatile变量的最新值读取到当前线程的工作内存中;
2、线程从工作内存中获取volatile变量副本的值。
2、volatile不能保证原子性
如下代码:
public static void main(String[] args) {
int number = 1;
number++;
System.out.println(number);
}
在这段代码中,number++是一段线程不安全的代码,这一行代码可以拆分成如下三步操作:
1、读取number的值
2、将number的值加1
3、将加后的number写回内存中
要想实现原子性使用synchronized关键字是可以实现的
synchronized (this) {
number++;
}
如果为number变量增加上volatile关键字显然无法实现原子性。
代码:
public class VolatileDemo3 {
private volatile int number = 0;
private Lock lock = new ReentrantLock();
public int getNumber() {
return this.number;
}
// public synchronized void increase() {
public void increase() {
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// lock.lock();
// try {
this.number++;
// } finally {
// lock.unlock();
// }
}
public static void main(String[] args) {
int a = 1;
long startTime = System.currentTimeMillis();
for (;;) {
int i = volatileTest(a);
a ++;
if (i < 500) {
System.out.println("在第" + a + "次,获得的结果为" + i);
break;
} else if(i >= 10000) {
System.out.println("已经十万次,获得结果为" + i);
break;
}
}
long endTime = System.currentTimeMillis();
System.out.println("共耗时:" + (endTime - startTime) / 1000);
}
public static int volatileTest(int a) {
VolatileDemo3 volatileDemo3 = new VolatileDemo3();
long startTime = System.currentTimeMillis();
for (int i = 0;i < 500;i++) {
new Thread(new Runnable() {
@Override
public void run() {
volatileDemo3.increase();
}
}).start();
}
/**
* 打印当前线程列表
*/
// Thread.currentThread().getThreadGroup().list();
/**
* 这个循环的意思是当所以线程还有大于1个线程在执行,主线程都让出执行权,当所有线程执行完程序才向下执行
* 这里在idea开发工具里面需要设置大于2,在eclipse里面只需要设置1就行了,因为idea还有一个监控线程
*/
while(Thread.activeCount() > 2) {
Thread.yield();
}
long endTime = System.currentTimeMillis();
System.out.println("第"+ a +"循环启用500个线程,共耗时:" + (endTime - startTime));
return volatileDemo3.number;
}
}
上面的代码是启动循环,在循环中启动500个线程同时执行++的操作,由于++是线程不安全的,且volatile无法保证原子性,在循环过程中可能出现小于500的情况,主要原因可以分析下number++的原理
number++线程不安全的情况:
1、线程1获得CPU执行权,需要执行++操作,发现number是volatile修饰的,先从主存中获取number的最新值为0到当前线程工作内存中;
2、此时,线程2获得CPU执行权,线程1进入等待状态,线程2发现number是volatile修饰的,先从主存中获取number的最新值为0到当前线程工作内存中;
3、线程2执行了++操作,并且将number的值更新到主存中,此时线程2的工作内存和主存的number都为1;
4、线程1重新获得执行权,将线程1工作内存中的number值加1后,并刷新到主存中,由于线程1的工作内存还为0,所以刷新到主存后主存的结果为1
这样就出现了加了两次,但是结果还是1的情况。
结果方案:
1、使用Synchronized关键字
使用关键字有两种形式,一个是锁方法,另外一个是锁代码块
方法1:
public synchronized void increase() {
this.number++;
}
方法2:
public void increase() {
synchronized (this) {
this.number++;
}
}
2、使用Lock类
这种方式是使用jdk1.5中的ReentrantLock类
private Lock lock = new ReentrantLock();
public synchronized void increase() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}
3、使用AtomicInteger
AtomicInteger这个类是在jdk1.5以后出现的一个原子性加减的类,使用方式如下:
private AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement();//实现++操作
atomicInteger.get();//获取当前值