上次我大致介绍了一下锁这个抽象概念是个啥,java语言是如何实现互斥锁的,互斥锁的工作原理,以及java对互斥锁的优化,锁的四种状态
无锁编程(其实和上期没太多联系)
cas是啥?~~之前看面经常看到的问题
juc又是啥?
aqs又是啥,具体有啥例子能说说嘛?有一说一按都说不明白,所以花时间恶补在这班门弄斧啦~
假设现在有多个线程想要操作同一个资源对象,很多人包括我第一反应就是加锁呀,但是互斥锁的同步方式是悲观的也就是重锁但是在一些只是读操作的线程使用悲观锁就太重了,咱没必要在每次调用的时候都去锁定资源。所以我们就不要过多使用互斥锁。所以诞生了一种非常巧妙的算法**CAS**,比较然后交换
对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
注:t1,t2线程是同时更新同一变量56的值
因为t1和t2线程都同时去访问同一变量56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。
(上图通俗的解释是:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)
就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。(因为是操作系统假设是x86的 就是cmpxchg指令支持cas 而 ARM下则是LL/SC实现cas 不需要操作系统的同步原语mutx相对效率更高)
现在我提供了一个简单的需求~假设你需要使用三条线程,将一个值从0累加到100你会怎么做?
以下是错误用例,我们发现多条线程打印了相同的值,则说明线程之间没有正确通信。
//假设你需要使用三条线程,将一个值从0累加到100你会怎么做?
public class test01 {
static Integer num = 0;
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (num<100){
System.out.println("thread name"+Thread.currentThread().getName()+":"+num++);
}
}
});
t.start();
}
}
}
thread nameThread-1:0
thread nameThread-2:1
thread nameThread-0:0
thread nameThread-0:4
最常规的我们可以通过互斥锁来进行线程同步
public class test01 {
static Integer num = 0;
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
synchronized (test01.class){
while (num<100){
System.out.println("thread name"+Thread.currentThread().getName()+":"+num++);
}
}
}
});
t.start();
}
}
}
此时线程同步了
但是这并不是我们这次讲的重点,我们应该讲的是无锁,如何无锁实现呢? 感谢前人造的轮子(使用AtomicInteger)实现了线程通信。
//假设你需要使用三条线程,将一个值从0累加到100你会怎么做?
public class test01 {
// static Integer num = 0;
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// synchronized (test01.class){
while (num.get()<100){
System.out.println("thread name"+Thread.currentThread().getName()+":"+num.incrementAndGet());
}
}
// }
});
t.start();
}
}
}
可我们关注的不是实现过程,而是他们的底层是如何通过cas来做到无锁同步的
又到了我最喜欢(不是)的扒源码的时候了
AtomicInteger的主要成员变量就是一个unsafe类型的实例和一个long类型的offset
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
我们进入incrementAndGet方法可以看到直接调用了unsafe对象的getAndAddInt方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
再点进去
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));
return var5;
}
果然是用到了unsafe的compareAndSwapInt也就是cas方法
while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
我们注意到上面出现了一个循环,实际上这个就是自旋
那么有没有可能会一直死循环自旋呀,实际上可以通过调参的方式配置的默认是10,所以不会出现死循环的
借张图 ~
AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了类sun.misc.Unsafe库里面的 CAS算法,用CPU指令来实现无锁自增。所以,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。