0 cas
翻译一下就好理解了,比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
具体的同步过程
从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。
此时存在两种情况
位置 V 处的值还 A ,那就直接将 A 改成 B 。CAS操作成功。
位置 V 处的值被其他线程更改成了 C ,那当前线程不会将 C 改成 B ,而是返回 C (在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)。CAS操作失败。
思考一下,C = A 的时候怎么办?
0.1 为什么要用CAS
我们与synchronized关键字来做比较,synchronized是一种独占锁,独占锁属于悲观锁,也就是每次操作都认为线程是不安全的,他会导致其它所有需要锁的线程被挂起,等待持有锁的线程释放锁。相对应的,乐观锁用到的原理就是CAS。所以,CAS操作要比synchronized这种方式性能有很大提升。
举了例子,同样实现线程安全下的i++操作。
public volatile int i;
public synchronized void add(){
i++;
}
public AtomicInteger i;
public void add(){
i.getAndIncrement();
}
0.2 (CAS) i++源码解析
包名:package java.util.concurrent.atomic;
//AtomicInteger类
//变量value; 相当于i++中的i
private volatile int value;
//创建Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//变量value的偏移量, 具体赋值是在下面的静态代码块中中进行的
private static final long valueOffset;
//在静态代码块中获取变量value的偏移量
static {
try {
//获取value变量的偏移量, 赋值给valueOffset
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//执行value++操作
public final int getAndIncrement() {
//this是当前对象
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// ....
//Unsafe类
//获取内存地址为obj+offset的变量值, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
//通过对象和偏移量获取变量的值
//由于volatile的修饰, 所有线程看到的v都是一样的
v= this.getIntVolatile(obj, offset);
} while(!this.compareAndSwapInt(obj, offset, v, v + delta));
return v;
}
静态方法中对valueOffset变量的赋值,应该是获取了value变量相对于对象内存地址的偏移量,得到的结果是value的内存地址,相当于获取了CAS操作数中的-内存位置(V)。这样后面可以根据这个地址中的值来进行比较赋值的操作。
while中的compareAndSwapInt()方法尝试修改v的值,该方法也会通过obj和offset获取变量的值如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true, 退出循环。
Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断。
另外我们可以看出,CAS使用的正是非阻塞算法,即一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
源码大法好。
0.3 CAS原理
CAS是通过调用JNI的代码实现的,而JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言,使用native修饰,如C++。
涉及cpu底层指令等,这里就先不说了,不太懂。
0.4 CAS操作会产生哪些问题
我们在实现某些功能的时候几乎没有一种完美的解决方案,趋利避害的适应业务的实现方式就是好的方案。
CAS虽然能够高效的解决原子操作,但是仍然存在三大问题。ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。
0.4.1 什么是ABA问题,怎么解决
CAS在执行的过程中,预期原值(A)变成了新值(B),后面又变成了(A),相当于A-B-A。那么这两个A肯定不是同一个A,只不过是值相等罢了。
举个例子,有一个变量 i = 0在主内存中,线程A和B都获取了这个变量 i = 0 到自己的工作内存中。然后线程A先将变量 i 修改成了 1 ,后面又修改成了 0 ,并且线程A执行完毕后将 0 这个值保存到了主内存中。接着,线程B也执行完成了,需要把 i 从工作内存中保存到主内存时,发现主内存中的 i 没有发生变化,这时线程B认为CAS操作成功,并且返回了CAS操作成功的结果。
ABA问题的解决方式就是给变量加一个版本号。
package java.util.concurrent.atomic;
包中有一个AtomicStampedReference
的类就是用来解决ABA问题的。
上源码
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
compareAndSet方法会检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
0.4.2 循环时间长开销大
上面 i++ 的源码里getAndAddInt
方法中有个循环,如果大量线程同时更新的话,所有线程都在循环获取,但是最终只有一个可以成功,会给CPU带来很大的执行开销。
这时候就需要限制自旋次数,防止进入死循环,或者限制循环时间。
从jdk1.8开始LongAdder
和ConcurrentHashMap
就是采用的这种方式,这也就是为什么ConcurrentHashMap
可以替代HashTable
。
0.4.3 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。
常见的解决办法是使用AtomicReference
类来操作对象,保证对象中被封装的多个共享变量的原子性。
此外,还可以使用锁来保证原子性,但是这种方式感觉有点违背CAS的初衷。
0.5 AtomicStampedReference
和AtomicReference
的使用示例
@Data
static
class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
AtomicReference<Dog> atomicReference = new AtomicReference<>();
Dog jinmao = new Dog("糖糖", 2);
atomicReference.set(jinmao);
System.out.println(" 1 >>>>>>>>> : "+atomicReference.get());
Dog labuladuo = new Dog("小七", 3);
boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
System.out.println(" 2 >>>>>>>>> : "+b);
System.out.println(" 3 >>>>>>>>> : "+atomicReference.get());
}
输出
1 >>>>>>>>> : Test.Dog(name=糖糖, age=2)
2 >>>>>>>>> : true
3 >>>>>>>>> : Test.Dog(name=小七, age=3)
compareAndSet方法其实就是比较原值A是否还是jinmao,如果是,那就将jinmao更改为labuladuo。
我们再来看一个多线程的例子
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
AtomicReference<Dog> atomicReference = new AtomicReference<>();
Dog jinmao = new Dog("糖糖", 2);
atomicReference.set(jinmao);
System.out.println(" 1 >>>>>>>>> : " + atomicReference.get());
pool.execute(()->{
Dog samo = new Dog("旺仔", 4);
boolean b = atomicReference.compareAndSet(jinmao, samo);
System.out.println(" 2 >>>>>>>>> : " + atomicReference.get());
System.out.println(" 2 >>>>>>>>> : " + b);
});
pool.execute(()->{
Dog labuladuo = new Dog("小七", 3);
boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
System.out.println(" 3 >>>>>>>>> : " + atomicReference.get());
System.out.println(" 3 >>>>>>>>> : " + b);
pool.shutdown();
});
}
输出
1 >>>>>>>>> : Test.Dog(name=糖糖, age=2)
2 >>>>>>>>> : Test.Dog(name=旺仔, age=4)
2 >>>>>>>>> : true
3 >>>>>>>>> : Test.Dog(name=旺仔, age=4)
3 >>>>>>>>> : false
boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
这一句的jinmao已经变成samo了,所以CAS操作失败。
我们再来看一个改变对象变量的操作,记住,atomicReference对于对象的操作是原子性的。
public static void main(String[] args) {
AtomicReference<Dog> atomicReference = new AtomicReference<>();
Dog jinmao = new Dog("糖糖", 2);
atomicReference.set(jinmao);
System.out.println(" 1 >>>>>>>>> : "+atomicReference.get());
jinmao.setAge(20);
atomicReference.compareAndSet(jinmao, jinmao);
Dog labuladuo = new Dog("小七", 3);
boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
System.out.println(" 2 >>>>>>>>> : "+b);
System.out.println(" 3 >>>>>>>>> : "+atomicReference.get());
}
输出
1 >>>>>>>>> : Test.Dog(name=糖糖, age=2)
2 >>>>>>>>> : true
3 >>>>>>>>> : Test.Dog(name=小七, age=3)
修改成功了,为什么?因为jinmao对象没有变。显然我们要的不是这个结果,怎么办?轮到AtomicStampedReference
出场了。
public static void main(String[] args) {
int version = 0;
Dog jinmao = new Dog("糖糖", 2);
AtomicStampedReference<Dog> atomicReference = new AtomicStampedReference<>(jinmao, version);
System.out.println(" 1 >>>>>>>>> : "+atomicReference.getReference()+ " ...... "+ version);
jinmao.setAge(20);
atomicReference.compareAndSet(jinmao, jinmao, version, version +1);
Dog labuladuo = new Dog("小七", 3);
boolean b = atomicReference.compareAndSet(jinmao, labuladuo, version, version + 1);
System.out.println(" 2 >>>>>>>>> : "+b);
System.out.println(" 3 >>>>>>>>> : "+atomicReference.getReference()+ " ...... "+ version);
}
输出
1 >>>>>>>>> : Test.Dog(name=糖糖, age=2) ...... 0
2 >>>>>>>>> : false
3 >>>>>>>>> : Test.Dog(name=糖糖, age=20) ...... 0
1 synchronized
被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令。
执行时如下流程
“锁”的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。
synchronized又分为是否明确的指定了锁对象,比如synchronized(this)或是synchronized直接修饰方法。
1.1 指定了锁对象
如下三种方式
public final Object lock = new Object();
int i;
public void thisSync() { //第1种锁
synchronized (this) {
i++;
}
}
public void objectSync() { //第2种锁
synchronized (lock) {
i++;
}
}
public void classSync() { //第3种锁
synchronized (Test.class) {
i++;
}
}
前两种对于第三种的区别很好理解,1和2是对象锁:只锁当前对象,每个对象各子一把锁,不影响其他对象调用此方法中的代码块;3是类锁:对于该类的所有实体对象都产生影响,每个对象使用同一把锁。
-
1和2的区别又是什么呢?既然都是对象锁,那为什么要多此一举创建个对象出来呢?区别就在于锁是不同的。
static class Printer { public void print(){ synchronized (this){ System.out.println("打印机打印"); } } } public static void main(String[] args) { Printer printer = new Printer(); ExecutorService pool = Executors.newCachedThreadPool(); pool.execute(()->{ synchronized (printer){ while (true); } }); // 线程1 pool.execute(printer::print); // 线程2 }
这段代码将不会有任何输出!这就是死锁。线程1一直在使用this锁对象,线程2获得不到锁一直处于阻塞状态。
我们修改一下代码
static class Printer { private final Object object = new Object(); public void print(){ synchronized (object){ System.out.println("打印机打印"); } } } public static void main(String[] args) { Printer printer = new Printer(); ExecutorService pool = Executors.newCachedThreadPool(); pool.execute(()->{ synchronized (printer){ while (true); } }); // 线程1 pool.execute(printer::print); // 线程2 }
这时有输出了。区别在于,第一个示例中的锁是printer这个对象,两个线程使用的都是这个锁。而第二个示例线程1使用的锁是printer对象,线程2使用的锁是object对,两个线程两把锁,相互之间不会有任何影响。
1.2 修饰方法
public synchronized static void staticMethod(){
}
public synchronized void method(){
}
这个很好理解,类锁和对象锁之间的区别。
注意,当一个对象被锁住时,对象里面所有用synchronized修饰的方法都将产生堵塞,而对象里非synchronized修饰的方法可正常被调用,不受锁影响。
2 ReentrantLock
2.1 为什么使用ReentrantLock
随着Jdk的发展,synchronize和ReentrantLock在性能上的几乎没有什么差别了,
先看例子
static
class Printer {
private Integer counter = 0;
private final ReentrantLock lock = new ReentrantLock();
public void print(String threadName){
System.out.println("线程:--->"+threadName+"等待获取锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
System.out.println("线程:--->"+threadName+"第一次加锁");
counter++;
System.out.println("线程:--->"+threadName+"做第"+counter+"件事");
//重入该锁,我还有一件事情要做,没做完之前不能把锁资源让出去
lock.lock();
System.out.println("线程:--->"+threadName+"第二次加锁");
counter++;
System.out.println("线程:--->"+threadName+"做第"+counter+"件事");
lock.unlock();
System.out.println("线程:--->"+threadName+"释放一个锁");
lock.unlock();
System.out.println("线程:--->"+threadName+"释放一个锁");
}
}
public static void main(String[] args) {
Printer printer = new Printer();
ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(()->{
String threadName = Thread.currentThread().getName();
printer.print(threadName);
}); // 线程1
pool.execute(()->{
String threadName = Thread.currentThread().getName();
printer.print(threadName);
}); // 线程2
}