我们知道保证线程安全的三个要素是原子性,可见性,有序性
CAS(Compare And Swap),指令级别保证某一内存地址V上的值的更新修改是一个原子操作
需要三个值:一个内存地址V,一个该线程拿到的值A,一个期望更新后的值B
思路:如果地址V上的实际值和该线程拿到的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。
循环(死循环,自旋)里不断的进行CAS操作
JDK里为我们提供了这些原子操作类
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
我们看看AtomicInteger的源码知道里面的compareAndSet方法是由Unsafe去实现的,点进去看全是native方法..
我们不知道compareAndSwapInt本地方法的实现,我们就退一步自己实现一个AtomicInteger类吧
我们定义一个Test类,和AtomicInteger一样先获取Unsafe对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
问题来了,会有个报错Caused by: java.lang.SecurityException: Unsafe
查了资料后知道Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// 仅在引导类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
那如若想使用这个类,该如何获取其实例?有如下两个可行方案。
其一,从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
其二,通过反射获取单例对象theUnsafe.
我们就用第二种
public class Test3 {
static volatile long valueOffset;
static Unsafe unsafe;
static {
initUnsafe();
try {
valueOffset = unsafe.objectFieldOffset
(Test3.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private static void initUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe1 = (Unsafe) f.get(null);
unsafe = unsafe1;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
volatile int value;
public void test() {
System.out.println(unsafe.compareAndSwapInt(this, valueOffset , value, 10));
System.out.println("value:" + value);
System.out.println(unsafe.compareAndSwapInt(this, valueOffset , 1, 20));
System.out.println("value:" + value);
}
public static void main(String[] args) {
new Test3().test();
}
}
打印结果
true
value:10
false
value:10
关于valueOffset,我们从static代码块里面可以看到,这个值是取的Test3这个class对象里面的value域,我们就先把它理解成前面说的三个值中的V吧,第一个打印操作拿valueOffset里的值和该线程拿到的value比较,发现相等就把value赋值成10,第二个cas操作不等,就是false了
我们现在写个value的自增方法然后多线程去跑,看看会不会有问题
public void increment() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// for(;;) {
// boolean b = unsafe.compareAndSwapInt(this, valueOffset, value, value + 1);
// if (b) {
// break;
// }
// }
value ++;
}
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(100);
Test3 test3 = new Test3();
for (int i = 0; i < 100; i ++) {
new Thread(()->{
test3.increment();
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("value-----" + test3.value);
}
先用value++的方法跑了几次,基本在98,99,100之间
然后increment()方法里注释打开,value++注释掉,打印的结果总是100;
所以说,我们用CAS实现的自增方法是线程安全的
关于CountDownLatch这个工具类后面再讲,包括CyclicBarrier,Semaphore,Exchange还有Fork/Join思想都是jdk提供的并发包里的工具类
在这里CountDownLatch是用来保证主函数的countDownLatch.await()后的打印一定是我们for循环里的子线程执行完成之后执行
我们实现个引用类型的原子操作
volatile Cat value = new Cat(1);
private void test() {
System.out.println(unsafe.compareAndSwapObject(this,valueOffset,value,new Cat(3)));
System.out.println(value.age);
value.age = 13;
System.out.println(unsafe.compareAndSwapObject(this,valueOffset,new Cat(1),new Cat(15)));
System.out.println(value.age);
}
public static void main(String[] args) {
Test3 test3 = new Test3();
test3.test();
}
class Cat{
int age;
Cat(int age){
this.age = age;
}
}
打印结果
true
3
false
13
一些思考:
查阅CAS相关资料的时候总是听到ABA问题,回顾下之前CAS定义
需要三个值:一个内存地址V,一个该线程拿到的值A,一个期望更新后的值B
思路:如果地址V上的实际值和该线程拿到的值A相等,就给地址V赋给新值B,如果不是,不做任何操作。
我们的目的是把A改成B,这里如果地址V上的值在我们获取改线程拿到的值A之前,已经经历过一次CAS操作被改成了B,然后又一次CAS操作改成了A,我们第三次去把A改成B的时候虽然成功了,但是我们不清楚他之前有没有进行过CAS操作,这就是ABA问题.这里jdk也提供了两个类
AtomicMarkableReference,boolean 有没有动过
AtomicStampedReference 动过几次
看看源码,理一下实现的思路
AtomicMarkableReference先看构造函数,只有一个,需要传入初始的引用对象和一个默认boolean值mark(假设此时引用对象为null,boolean为false)
里面定义了一个内部类,把两个初始值赋值给该new出该内部类对象pair,他的compareAndSet()要传入4个值,不仅要判断引用对象是否相同,还要判断mark是否相同
如果只最求结果一致,不关心中途有没有改变过或者改过多少次,后面两个类不用了解