在使用技术时也一定看看底层,这样在以后的学习中才能触类旁通,举一反三
开始呢,我们先来思考一个问题,在多个线程来访问一个资源对象的时候我们来如何保证线程安全呢?相信我们第一反应就是加互斥锁,问题这就来了,如果大部分的操作都是读的操作,加上互斥锁会在每次调用的时候都锁定线程,这不是傻了么,还有一种情况当同步代码块的耗时远小于线程切换时,加上锁不是更有些本末倒置么,会严重拖慢程序的性能,这也是上锁的弊端,这种上锁的方式也就是悲观锁,看到这个文章的小伙伴一定对锁有一定的概念了这里就不多说了,回到正题。
大家一定都知道synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高,针对这种情况JAVA也尝试对synchronized进行了优化,对锁之间的切换增加了过度,大家一定都知道,无锁,偏向锁,轻量级锁,重量级锁的概念,所之间转换时很耗时的,优化之后也没能明显的改善,
问题就来了:假如我们不想使用互斥锁,还想对线程调用进行协调?
和你一样牛的前辈也想到了这个问题于是CAS诞生了
还是上面的大前提——多个线程来访问一个资源对象
给大家举个例子:
假设我们的资源对象是一辆摇摇车,有A,B两个小孩想要玩儿,摇摇车有两个状态有空,没空,分别用0,1代表A,B两个小孩看待摇摇车上没人,就抢着往摇摇车跑去,小孩A先跑到并且开启摇摇车,此时摇摇车状态是1,小孩B跑到旁边时发现已经有人在玩了,虽然感觉挺不服气,但是秉着先到先得原则,只能在旁边等着, 现在回头想一想,当资源对象的状态为0的一瞬间,A,B两个线程同时读到状态值0,此时两个线程会各自产生一个值(资源对象的状态值——old Value),当线程得到资源对象更新状态后新的状态值(new Value),old Value一开始都是0new Value都是1,两个线程争抢着屈改变资源对象的状态值,假设线程A运气比较好,率先获得时间片,他讲自己的old Value与资源对象的状态值进行比较(Compare )发现一致,于是将资源对象的状态值改(Swap )为new Value,线程B慢了一步,这是资源对象的状态值已经被线程A改为1,所以线程B在比较(Compare )时会发现和自己预期的old value不一样,所以放弃了Swap操作 ,在实际应用中,我们不会让线程B直接放弃,我们会让线程B进行自旋(不断进行C/S操作),小孩B第一次没有坐上摇摇测也不可能轻易放弃,会在旁边等待(自旋),某一瞬间摇摇车状态为0时再次参与争抢,当小孩B没耐心也会离开不再争抢(设置初始值,达到初始值时停止尝试)
int cas( long addr, long oldvalue, long newValue)
{
if(addr != oldValue)
return 0;
addr = newValue;
return 1;
}
上面的代码很容易看出问题,没有任何同步操作线程一定是不安全的
解决办法:CAS必须是原子性的,比较数值是否一致和更新状态值两个动作一定要有一条线程进行操作,加锁?
似乎问题又回去了,又到了加锁的问题
不同架构的CPU给我们提供了指令级别的指令操作
X86架构下通过cmpxchg指令支持CAS
arm架构下通过LL/SC
实现不依赖锁来进行线程同步,但并不意味无锁代替无锁,这些通过CAS来实现的同步的工具由于不会锁定资源,而且当线程需要修改共享资源的对象时总是乐观的认为对象的状态值没有被修改过,而只是每次都会主动的尝试去比较状态值,这种机制也被狭义的理解为乐观锁,由于没有用到过锁是一种无锁的同步机制*
看文章的小伙伴们在学习线程的时候一定写过这个代码
static int num = 0;
public static void main(String[] args) {
for(int i = 0; i<3 ; i++ ) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(num < 1000) {
System.out.println("thredname"+Thread.currentThread().getName()+":"+num++);
}
}
});
thread.start();
}
}
结果到994就停掉了
于是我们给他上了锁
static int num = 0;
public static void main(String[] args) {
for(int i = 0; i<3 ; i++ ) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Main.class){
while(num < 1000) {
System.out.println("thred-name"+Thread.currentThread().getName()+":"+num++);
}
}
}
});
thread.start();
}
}
我们会发现上锁之后,实现了线程同步,完了!
完了?我们今天讲的时无锁同步(乐观锁)
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
for(int i = 0; i<3 ; i++ ) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(num.get()< 1000) {
System.out.println("thred-name"+Thread.currentThread(). getName()+":" +num.incrementAndGet());
}
}
});
thread.start();
}
}
我们可以看到在不加锁的情况下也实现了线程同步,是怎么实现的呢?
我们在这里是利用java.util.concurrent.atomic包下的 AtomicInteger ,这是Integer的原子性操作
包下还有 AtomicBoolean,AtomicLong …
我们来看看他的底层
我们可以看到在它的底层调用的是一个unsafe类,unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作,我们就是通过了unsafe类调用的底层的CAS,实现了无锁线程同步
感兴趣的小伙伴可以自己研究一下
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题 (这个问题引申出来的东西比较多,后面单独一篇来讲)
CAS 相当于一种乐观锁机制(此处无锁胜有锁)
我觉得这更是一种编程思想,很多的优化都可以运用这种思想举一个简单地例子
我们在做数据库值得修改时正常的语句应该是
update stock set num=$num_new where sid=$sid
如果别的事务A对值进行了修改,这时执行了上面的修改语句读到的值是事务A修改后的值,进行了二次修改,此时
事务A还未结束时发生了未知异常,执行了回滚,数据就会异常,
update stock set num=$num_new where sid=$sid and num=$num_old
这种写法就很好的解决了这个问题 ,在修改前执行验证(Compare ),再执行修改,再看看CAS,机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B,是不是感觉有些类似呢
即:Compare and Swap
学习吧,万物相通,很多理论思想都可以串起来想一想,会有不一样的感觉