关联博文:
【1】内存可见性与内存可见性错误
内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
那么如果一个线程修改了对象状态,其他线程看不到对象的状态变化怎么办?就会引起内存可见性错误!
可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。实例如下:
public class TestVolatile { public static void main(String[] args){ ThreadDemo td=new ThreadDemo(); new Thread(td).start(); while (true){ if (td.isFlag()){ System.out.println("----------------------"); break; } } } } class ThreadDemo implements Runnable{ private boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } flag=true; System.out.println("flag= "+flag); } public boolean isFlag(){ return this.flag; } public void setFlag(boolean flag){ this.flag=flag; } }
有两个线程,main线程和新起的new Thread(td)
,当主线程while循环中判断td.flag为true时,则打印横线并跳出循环。
测试结果如下:
并不会打印横线并终止main的while循环!但是已经打印了flag= true
说明td.flag
状态已经改变。这就是内存可见性错误!!
流程分析
当程序运行时,JVM为会每一个线程分配独立的缓存提高效率。共享数据flag=false
当然是存在JVM内存中(确切的说,是随对象存储在堆里面)。线程将会将数据读取到自己缓存进行利用,更改后同步更新JVM内存中的数据。
如下图所示:
由图可见,线程一new Thread(td)
会去读取flag并进行修改然后再“反映”到JVM内存中的flag。那么为什么main里面的while循环一直拿不到更新后的true?
这里需要注意两点:
new Thread(td)
的run方法里面先Thread.sleep(200);
让while (true)
先执行了;while (true){}
调用了一系列的底层代码执行效率非常高,高到main没有机会获取内存中更新后的flag。
如何解决呢?
可能会说,使用同步代码块,如下:
public static void main(String[] args){ ThreadDemo td=new ThreadDemo(); new Thread(td).start(); while (true){ //这里使用synchronized 关键字,锁为td synchronized (td){ if (td.isFlag()){ System.out.println("----------------------"); break; } } } }
synchronized 能保证每次都刷新缓存,当然可以获取到对象的最新状态,结果如下:
但是需要注意的是,synchronized 会降低性能,如果有多个线程呢?当前线程拿到锁执行代码,其他线程过来肯定要阻塞等待CPU下次调度。
我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的volatile 变量。
这里推荐使用volatile !
volatile能保证多个线程进行操作共享数据时,内存中的数据对彼此是可见的。
如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。
【2】Volatile与Synchronized
Java提供了一种稍弱的同步机制,即volatile 变量,用来确保将变量的更新操作通知到其他线程。你可以将volatile 看做一个轻量级的锁。
修改代码如下:
public class TestVolatile { public static void main(String[] args){ ThreadDemo td=new ThreadDemo(); new Thread(td).start(); while (true){ // synchronized (td){ if (td.isFlag()){ System.out.println("----------------------"); break; } // } } } } class ThreadDemo implements Runnable{ // 这里,使用volatile修饰flag private volatile boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } flag=true; System.out.println("flag= "+flag); } public boolean isFlag(){ return this.flag; } public void setFlag(boolean flag){ this.flag=flag; } }
测试结果如下,可以看到完美符合我们的预期。
那么是否说明volatile比Synchronized更好,能替代Synchronized?
完全错误!!!
如上所说,volatile是一种轻量级的锁,synchronized是重量级锁(悲观锁),volatile与synchronized区别如下:
对于多线程,不是一种互斥关系;
synchronized是保证互斥的,即拿到锁的线程可以执行,其他线程过来只能等待;volatile不能保证这种关系。
不能保证变量状态的“原子性操作”。
不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁),仅仅set或者get的场景是适合volatile的。那么什么是“原子性操作”?volatile不能保证变量的“原子性”操作会出现什么问题?
【3】原子性操作
① 经典例子i++
代码如下:
@Test public void test1(){ int i=10; i=i++; System.out.println(i);//i=10 }
编译后class文件中对应代码如下:
@Test public void test1() { int i = 10; //可以看到i++被拆分为了如下三步 byte var10000 = i; int var2 = i + 1; i = var10000; System.out.println(i); }
使用javap -verbose Test.class
查看对应指令如下:
public void test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=1 0: bipush 10 2: istore_1 3: iload_1 4: iinc 1, 1 7: istore_1 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_1 12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 15: return LineNumberTable: line 22: 0 line 23: 3 line 24: 8 line 25: 15 LocalVariableTable: Start Length Slot Name Signature 0 16 0 this Lcom/jane/TestRex; 3 13 1 i I RuntimeVisibleAnnotations: 0: #17()
也就是说i++
的操作实际上分为三个步骤“读-改-写”(对应指令中的 3 ,4 ,7)。原子性操作就是这三个步骤为一个"单元",在执行这个"单元"过程中,其他线程不能过来干涉。
② volatile不能保证变量的原子操作
示例如下:
public class TestAtomicDemo { public static void main(String[] args){ AtomicDemo ad = new AtomicDemo(); for (int i=0;i<10;i++){ new Thread(ad).start(); } } } class AtomicDemo implements Runnable{ // 这里使用volatile修饰 private volatile int serialNum = 0; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" : "+getSerialNum()); } public int getSerialNum() { // 这里 serialNum++ return serialNum++; } }
多次运行测试,会出现多线程问题,如下图:
由于volatile对于多线程不具有互斥性,不能保证变量的原子操作,故而如线程一读取并修改serialNum时,线程二、线程三也可能要修改!
那么如何在不使用synchronized的情况下实现变量的原子性操作?
这就涉及到了原子变量和CAS算法。
【4】原子变量
JDK1.5后提供了java.util.concurrent工具包。该包为类的小工具包,支持在单个变量上解除锁的线程安全编程。
事实上,此包中的类可将volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:
类AtomicBoolean、AtomicInteger、AtomicLong 和AtomicReference 的实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。
AtomicIntegerArray、AtomicLongArray 和AtomicReferenceArray 类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。
参考博文:JUC中原子类总结
修改代码如下:
public class TestAtomicDemo { public static void main(String[] args){ AtomicDemo ad = new AtomicDemo(); for (int i=0;i<10;i++){ new Thread(ad).start(); } } } class AtomicDemo implements Runnable{ // private volatile int serialNum = 0; private AtomicInteger ai=new AtomicInteger(); @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" : "+getSerialNum()); } public int getSerialNum() { // return serialNum++; return ai.getAndIncrement(); }
测试多次运行测试,不会出现多线程的错误!
为什么使用原子变量就可以避免如上所示的多线程错误?有两点需要注意:
- 变量使用volatile修饰保证内存可见性;
- CAS算法保证数据的原子性。
以AtomicInteger为例,除了final修饰的常量外,变量使用volatile修饰:
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
那么,什么是CAS算法?
【5】CAS算法
CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。其实,CAS 是一种无锁的非阻塞算法的实现。Java中的CAS操作依赖于底层CPU的CAS指令。
CAS 包含了3 个操作数:
- 需要读写的内存值V
- 进行比较的值A
- 拟写入的新值B
当且仅当V 的值等于A 时,CAS 通过原子方式用新值B 来更新V 的值,否则不会执行任何操作。
这也就是为什么【4】中的代码不会出现多线程的错误。
当某个线程尝试进行更新主存中的数据时,会再次读取数据–A,然后将V与A比较,V==A则执行更新,否则放弃。在同一个时刻,将会只有一个线程更新JVM中的数据成功!使用如下代码模拟计算机底层CAS操作:
public class TestCompareAndSwap { public static void main(String[] args){ final CompareAndSwap compareAndSwap = new CompareAndSwap(); for (int i=0;i<10;i++) { new Thread(new Runnable() { @Override public void run() { // 获取主存值 int expectedValue = compareAndSwap.getValue(); boolean b = compareAndSwap.compareAndSet(expectedValue, (int) (Math.random() * 101)); System.out.println(b); } }).start(); } } } class CompareAndSwap{ private int value; //获取主存值 public int getValue() { return value; } // 比较并设置,返回主存值 public synchronized int compareAndSwap(int expectedValue,int newValue){ int oldValue = value; //如果主存值==预估值,则进行更新 if (oldValue==expectedValue){ this.value=newValue; } return oldValue; } //调用设置值方法,并返回成功失败 public synchronized boolean compareAndSet(int expectedValue,int newValue){ return expectedValue==compareAndSwap(expectedValue,newValue); } }
那为什么CAS比锁性能要高呢?
如果使用了锁,该次不成功的情况下将会阻塞,等待下一次CPU调度。但是CAS如果该次失败了,会立即尝试进行下一次!
它允许多线程非阻塞式地对共享资源进行修改,但同一时刻只有一个线程能够成功,其他线程被告知失败但并不会挂起,而是重新尝试。这是一种非阻塞式的同步方式。
【6】CAS指令
java中给我们提供了本地方法来获得和CAS指令一样的执行效果。比如Unsafe类中:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
因为这些方法会被编译成平台相关的CAS指令,故而这些CAS操作都具有原子性。AtomicInteger这些原子类其实就依赖于Unsafe。
在JDK并发包的底层实现中,还是可以处处看到它的身影。如下图所示:
【7】CAS结合失败重试机制进行并发控制
还记得【4】中的代码修改之后就不会出现多线程错误:
class AtomicDemo implements Runnable{ // private volatile int serialNum = 0; private AtomicInteger ai=new AtomicInteger(); @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" : "+getSerialNum()); } public int getSerialNum() { // return serialNum++; return ai.getAndIncrement(); }
CAS指令只是提供了一个更新变量的原子操作,要使用它进行并发控制,还需要结合失败重试机制。
查看 ai.getAndIncrement();
源码跟踪到AtomicInteger类中:
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
继续跟踪到Unsafe类中:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); //获取当前变量值 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //CAS失败则重新尝试直到成功为止 return var5; }
可以看到步骤可以概括为
1.获得变量当前的值var5
2.使用CAS操作进行更新:若var5与当前的值不一样,说明1,2操作间有其他线程作了修改,此次更新失败,重新实行步骤1;否则用 var5+var4的值进行更新,并返回更新前的值。
多个线程同时使用CAS指令去更新变量,失败的线程将会不断重新尝试,直到更新成功。
【8】CAS优缺点
① 优点
没有线程阻塞唤醒带来的性能消耗问题。这也是为什么比synchronized性能高的原因!
② 缺点
ABA问题
在CAS操作时,我们以变量的当前值和预期值一致来判定变量未被其他线程修改。这样是不严谨的,因为变量可能被修改成其他值后又被改了回来。
大部分时候这是个可以忽略的小问题,如果要规避这个问题,可以使用AtomicStampedReference,它会额外使用一个时间戳来判断变量是否被修改过。
无法直接使用CAS来进行并发控制,相比同步锁的方式适用范围较窄。
【8】经典例子i++的几种情况
① 单独a++
如下所示:
//源码 @Test public void contextLoads5() { int a=5; a++; System.out.println(a); } //编译后 @Test public void contextLoads5() { int a = 5; a = a + 1; System.out.println(a); }
此时毫无疑问,a==6。
② a++赋值给自身
如下所示:
//源码 @Test public void contextLoads4() { int a=5; a=a++; System.out.println(a); } //编译后 @Test public void contextLoads4() { int a = 5; byte var10000 = a; int var2 = a + 1; a = var10000; System.out.println(a); }
此时 a仍旧等于5。
③ a++赋值给其他
如下所示:
//源码 @Test public void contextLoads6() { int a=5; int b=a++; System.out.println(a); System.out.println(b); } //编译后 @Test public void contextLoads6() { int a = 5; byte var10000 = a; int a = a + 1; int b = var10000; System.out.println(a); System.out.println(b); }
此时a==6,b==5
。
④ a++与其他运算符结合
如下所示:
//源码 @Test public void contextLoads3() { int a=5; int b=a++; int c=b*a++; System.out.println(a); System.out.println(b); System.out.println(c); }
此时 a==7,b==5,c==30
。
⑤ ++a与其他运算符结合
如下所示:
@Test public void contextLoads3() { int a=5; int b=++a; int c=b*++a; System.out.println(a); System.out.println(b); System.out.println(c); }
此时 a==7,b==6,c==42
。
⑥ a++与++a和其他运算符结合
如下所示:
@Test public void contextLoads3() { int a=10>>1; int b=a++; int c=++a; int d=b*a++; System.out.println(a); System.out.println(b); System.out.println(c); System.out.println(d); }
此时 a==8,b==5,c==7,d==35。
本质也就是a++或者++a换行后,a一定会自增1。如果与其他变量赋值,那么a++是先赋值给其他变量,然后a+1。++a则是a先+1,后赋值给其他变量。