在Java中调用的是Unsafe
的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的):
/**
*
* @param o 对象所在类本身的对象(一般这里是对一个对象的属性做修改,才会出现并发)
* @param offset 属性在对象中的相对偏移量位置(获取偏移量也是通过unsafe的⼀个⽅法: objectFieldOffset(Fieldfield)来获取属性在对象中的偏移量;)
* @param expected 修改前期待的值,原来的值和修改之前的值需要一致
* @param x 修改的目标值
* @return 修改是否成功
*/
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
对象的引用进行比较后交换,交换成功则返回true,交换失败返回false
CAS能够保证性能的同时保证数据的可见性,可以说是一个非阻塞的轻量级锁,性能高于synchronized
执行逻辑类似:
if (this == expect) {
this = update
return true;
} else {
return false;
}
交换过程完全是原子的,基本流程如下:
这明明是好几步的操作,怎么会是原子操作呢?
整体过程:Java通过JNI来调用native方法,而native方法的实现是C,C调用的是CPU底层指令实现。
在CPU底层指令中,是通过加锁的方式实现,实现方式有如下三种:
优点:
缺点:
AtomicStampedReference
就是采用类似的思路解决ABA问题我们拿java.util.concurrent.atomic.AtomicInteger
举例:
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 属性在这个对象⾥⾯的相对偏移量位置
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
// 要更新的变量
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* 如果当前值是期望值expect,则原子地将值设置为给定的更新值update
*
* @param expect 期望值
* @param update 更新值
* @return 如果true表示更新成功,false表示实际值不等于期望值
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* 将当前值原子的加1
*
* @return the 加1之前的值
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
...
}
java.util.concurrent.atomic
包中Atomic*基本都是使用相同的思路来实现原子操作的
volatile的主要作用是:
可见性表示一个变量的值被更新后,是否能够在每个线程中可见
public class ExampleVisibility {
private boolean stop = false;
private void stop() {
this.stop = true;
}
private void run() {
while (!stop) {
// run
}
System.out.println("stop!!");
}
public static void main(String[] args) throws InterruptedException {
ExampleVisibility self = new ExampleVisibility();
new Thread(() -> {
self.run();
}).start();
Thread.sleep(1000 * 4);
new Thread(() -> {
// 停止while
self.stop();
}).start();
}
}
执行多次,你会发现有的时候无法停止while,也就是stop一直为false
问题原因:
在cpu中有多级cache,如果每次在内存中取效率不高,所有cpu中有自己的cache以提高效率(在缓存中,每个变量在缓存行上都有一个2bit的状态,那就是CPU EMSI协议)
有如下两种可能原因:
解决方案:
在stop变量中增加volatile关键字保证可见性
private volatile boolean stop = false;
volatile在一定程度上保证有序性。其中主要就是通过禁止指令重排的方式实现有序性
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
代码转换为机器执行的指令所经过的步骤:
流水线优化问题:
在cpu内部不同的操作是由不同的硬件来做,比如专门ADD操作的硬件、INC的硬件、MOV的硬件,它们之间同时执行多个指令,上一级处理完成流到下一级,所以叫做流水线
在未排序之前需要阻塞等待ADD指令完成写入后INC才能继续执行,INC执行后才能MOV
在重排序之后,先执行了MOV,然后INC,指令之间没有阻塞(一般的重排序不会对结果产生影响)
public class ExampleReordering {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true) {
count++;
x = 0;
y = 0;
a = 0;
b = 0;//clear
Thread one = new Thread(new Runnable() {
public void run() {
shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + count + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
代码正确执行情况下,x和y不会同时等于0,在指令重排序的情况下,就可能发生x和y都等于0的异常情况
解决方案:
在每个变量中增加volatile
关键字禁止指令重排序
private volatile static int x = 0, y = 0;
private volatile static int a = 0, b = 0;
在对比加入volatile关键字与未加入volatile关键字所生成的汇编代码发现,加入volatile关键字时会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(或内存栅栏),内存屏障主要提供如下功能:
synchronized关键字是防止多个线程执行同一段代码,而volatile作用却不同,使用前需要具备如下两个条件:
不能,volatile关键字保证了操作的可见性,但无法保证变量操作的原子性,原因如下:
在多个线程对i变量进行i++操作时,实际上需要做如上4步,在线程A还没有将add后的i值putfield,上下文就切换到了线程B,线程B执行了add操作。
两个线程同时对一个老的i进行add,所有原来应该+2的变成只有+1,最终导致数据的偏差。
解决方案:在incr()方法中增加synchronized关键字
private synchronized void incr() {
i++;
}
是一种同步锁,通过synchronized
对方法或代码块修饰,确保多个线程在同一个时刻,只能有一个线程处于方法/代码块中,它确保了线程对变量访问的可见性和排他性
JVM是通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的;而对象监视器的本质依赖于底层操作系统的互斥锁(Mutex Lock
)实现。
基本过程:
monitor.enter
指令,在退出方法和异常处插入monitor.exit
指令monitor.exit
之后才能继续尝试获取锁在JDK1.6之前,synchronized是一个重量级锁,开销很大,所以经常被建议说少用点。
但在JDK1.6之后,该关键字进行了很多的优化,主要就是锁升级,会自动根据不同的资源竞争情况升级锁(锁膨胀)。
锁升级过程:
synchronized 有如下四种方式修饰对象来实现代码同步:
package com.muse.thread;
import java.util.concurrent.TimeUnit;
public class SynchronizedDemo {
public static void main(String[] args) {
/** case1:无Synchronized,乱序输出 */
// NoneSyncDemo noneSyncDemo = new NoneSyncDemo();
// Thread thread1 = new Thread(noneSyncDemo);
// Thread thread2 = new Thread(noneSyncDemo);
/** case2:synchronized修饰代码块, 对象锁 */
// 加锁有效
// SyncBlockDemo syncBlockDemo = new SyncBlockDemo();
// Thread thread1 = new Thread(syncBlockDemo);
// Thread thread2 = new Thread(syncBlockDemo);
// 加锁无效
// SyncBlockDemo syncBlockDemo1 = new SyncBlockDemo();
// SyncBlockDemo syncBlockDemo2 = new SyncBlockDemo();
// Thread thread1 = new Thread(syncBlockDemo1);
// Thread thread2 = new Thread(syncBlockDemo2);
/** case3:synchronized修饰方法,对象锁 */
// 加锁有效
// SyncMethodDemo syncMethodDemo = new SyncMethodDemo();
// Thread thread1 = new Thread(syncMethodDemo);
// Thread thread2 = new Thread(syncMethodDemo);
// 加锁无效
// SyncMethodDemo syncMethodDemo1 = new SyncMethodDemo();
// SyncMethodDemo syncMethodDemo2 = new SyncMethodDemo();
// Thread thread1 = new Thread(syncMethodDemo1);
// Thread thread2 = new Thread(syncMethodDemo2);
/** case4:synchronized修饰静态方法,类锁 */
// 加锁有效
// SyncStaticMethodDemo syncStaticMethodDemo = new SyncStaticMethodDemo();
// Thread thread1 = new Thread(syncStaticMethodDemo);
// Thread thread2 = new Thread(syncStaticMethodDemo);
// 加锁有效
// SyncStaticMethodDemo syncStaticMethodDemo1 = new SyncStaticMethodDemo();
// SyncStaticMethodDemo syncStaticMethodDemo2 = new SyncStaticMethodDemo();
// Thread thread1 = new Thread(syncStaticMethodDemo1);
// Thread thread2 = new Thread(syncStaticMethodDemo2);
/** case5:synchronized修饰类,类锁 */
// 加锁有效
// SyncClassDemo syncClassDemo = new SyncClassDemo();
// Thread thread1 = new Thread(syncClassDemo);
// Thread thread2 = new Thread(syncClassDemo);
// 加锁有效
SyncClassDemo syncClassDemo1 = new SyncClassDemo();
SyncClassDemo syncClassDemo2 = new SyncClassDemo();
Thread thread1 = new Thread(syncClassDemo1);
Thread thread2 = new Thread(syncClassDemo2);
thread1.start();
thread2.start();
}
}
/**
* [case1:无Synchronized]
*/
class NoneSyncDemo implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(100);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case2:synchronized修饰代码块]
* 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
**/
class SyncBlockDemo implements Runnable {
@Override
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
/**
* [case3:synchronized修饰方法]
* 一个线程访问一个对象中的synchronized修饰的方法时,其他试图访问该对象的线程将被阻塞。
**/
class SyncMethodDemo implements Runnable {
@Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case4:synchronized修饰静态方法]
* 修饰一个静态的方法,类锁,其作用的对象是这个类的所有对象;
**/
class SyncStaticMethodDemo implements Runnable {
@Override
public void run() {
method();
}
public synchronized static void method() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
/**
* [case5:synchronized修饰类]
* 修饰一个类,类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
**/
class SyncClassDemo implements Runnable {
@Override
public void run() {
synchronized(SyncClassDemo.class) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + i);
TimeUnit.MILLISECONDS.sleep(200);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
区别点 | volatile | synchronized |
---|---|---|
修饰 | 只能用于修饰变量 | 可以用于修饰方法、代码块、类 |
线程阻塞 | 不会发生线程阻塞 | 发生阻塞 |
原子性 | 不能保证变量的原子性 | 可以保证变量原子性 |
可见性 | 可以保证变量在线程之间访问资源的可见性 | 可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步 |
同步性 | 能保证变量在私有内存和主内存间的同步 | synchronize是多线程之间访问资源的同步性(同时只能有一个线程访问) |
编译器优化 | 不会被编译器优化 | 标记的变量可以被编译器优化 |
区别点 | Synchronized | ReentrantLock |
---|---|---|
使用方式 | 关键字 | 实现类 |
实现方式 | JVM实现控制 | AQS实现控制 |
是否自动 | yes | no |
锁的获取 | 如果资源被锁,会一直等待 | 如果资源被锁,可以有多种处理方式 详情参见“面试题2” |
锁的释放 | 被锁的代码执行完or发生异常 | finally中手动编程释放 |
锁的状态 | 无法判断 | 可以判断,isLocked() |
锁的类型 | 可重入,不可中断,非公平锁 | 可重入,**可中断(lockInterruptibly),公平锁or非公平锁 |
对CAS、volatile、synchronized原理分析,并对各自的特点、应用场景、使用方式做了举例。在最后比较了这几种保证线程安全机制的区别。希望通过本文能够帮你们揭开它们神秘的面纱。
MESI的四个字⺟分别代表了四个可以被标记在缓存⾏上的独⽴状态。(也就是⽤2bit来编码)
基本过程: