CAS无锁算法:ReentrantLock,synchronized(JDK 1.6),悲观锁/乐观锁

> 悲观锁/乐观锁

 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。乐观锁的一种实现方式-CAS(Compare and Swap 比较并交换).CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

> Java1.6为Synchronized做了优化

尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

 - jdk1.6以后 对synchronized锁做了哪些优化:
 1.适应自旋锁,自旋锁:为了减少线程状态改变带来的消耗 不停地执行当前线程 
 2.锁消除:不可能存在共享数据竞争的锁进行消除
 3.锁粗化:将连续的加锁 精简到只加一次锁
 4.轻量级锁:无竞争条件下 通过CAS消除同步互斥
 5.偏向锁:无竞争条件下 消除整个同步互斥,连CAS都不操作。

-  轻量级锁和偏向锁都是在没有竞争的情况下出现,一旦出现竞争就会升级为重量级锁。对于synchronized,锁的升级情况可能是 偏向锁—>轻量锁—>自适应自旋锁—>重量锁。

> CAS

java高并发:CAS无锁原理及广泛应用- https://blog.csdn.net/liubenlong007/article/details/53761730

  java.util.concurrent(简称JUC)包,ReentrantLock也是基于CAS的。  CAS,全称compare and swap,一个CPU原子指令,在硬件层面实现的机制,体现了乐观锁的思想。Unsafe类是CAS实现的核心。 Unsafe封装了一些类似于C++中指针的东西,该类中的方法都是native的,而且是原子的操作。原子性是通过CAS原子指令实现的,由处理器保证。
  CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。实现非阻塞同步的方案称为“无锁编程算法”( Non-blocking algorithm)。相对应的,独占锁是一种悲观锁,synchronized就是一种独占锁。
 
java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类,这些原子变量都调用了 sun.misc.Unsafe 类库里面的 CAS算法,用CPU指令来实现无锁自增,JDK源码:

public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
}  

public final boolean compareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}
  从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。Atomic操作的底层实现正是利用的CAS机制.
 无锁算法——CAS原理- https://blog.csdn.net/roy_70/article/details/69799845

  与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。
  Java中的原子操作( atomic operations),原子操作指的是在一步之内就完成而且不能被中断。原子操作在多线程环境中是线程安全的,无需考虑同步的问题。那么long型赋值不是原子操作呢?实时上java会分两步写入这个long变量,先写32位,再写后32位。这样就线程不安全了。如果改成下面的就线程安全了:private volatile long foo; 因为volatile内部已经做了synchronized.
  实现无锁(lock-free)的非阻塞算法有多种实现方法,其中 CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是一种 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
   CAS看起来很爽,但是会导致“ABA问题”。CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。
  CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

  优化锁实现的例子 :Java中的ConcurrentHashMap,设计巧妙,用桶粒度的锁和锁分离机制,避免了put和get中对整个map的锁定,尤其在get中,只对一个HashEntry做锁定操作,性能提升是显而易见的。
  Lock-free无锁的例子 :CAS(Compare-And-Swap)的利用和LMAX的disruptor 无锁消息队列数据结构等。例如ConcurrentLinkedQueue。

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
 1.A线程写volatile变量,随后B线程读这个volatile变量。
 2.A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
 3.A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
 4.A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

  AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。

关于Java锁机制- https://yq.aliyun.com/articles/607025?utm_content=m_1000005240#

- CAS的缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

- 在JDK1.5新增的java.util.concurrent(J.U.C) 就是建立在CAS操作上的。CAS是一种非阻塞的实现(PS:乐观锁采用一种 “自旋锁”的技术,其原理是:如果存在竞争,则没有获得资源的线程不立即挂起,而是采用让线程执行一个忙循环(自旋)的方式,等待一段时间看是否能获得锁,如果超出规定时间再挂起),所以J.U.C在性能上有很大的提升。下面以J.U.C下的AtomicInteger的部分源码为例,看一下CAS的过程究竟如何:
public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;

    public final int get() {
        return value;
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
  // CAS操作
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

}

 乐观锁、悲观锁是一种思想,CAS是乐观锁的一种实现。前者是非阻塞同步,非独占,而后者是阻塞同步,独占锁。在可预知的情况下,如果竞争冲突发生较少,乐观锁是个不错的选择。而如果竞争激烈,悲观锁应得到考虑。
 关于对象的创建分配内存,因为多线程分配对象空间并不安全,如分配A,B两对象,当给A分配内存时,指针还没修改,就切换到给B分配同一块内存,引发错误。其中一种解决方法就是通过底层的CAS操作来保证分配的原子性。

  AQS是J.U.C包下AbstractQueuedSynchronizer抽象的队列式的同步器的简称,这是一个抽象类,它定义了一套多线程访问共享资源的同步器框架,J.U.C包下的许多同步类实现都依赖于它,比如ReentrantLock/Semaphore/CountDownLatch,可以说这个抽象类是J.U.C并发包的基础。Java同步框架AbstractQueuedSynchronizer提到了两个概念,一个是独占锁,一个是共享锁,所谓独占锁就是只能有一个线程获取到锁,其他线程必须在这个锁释放了锁之后才能竞争而获得锁。而共享锁则可以允许多个线程获取到锁。
  Lock是一个接口。Lock有一个实现类 ReentrantLock (又名可重入锁),这种锁是可以反复多次进入的,其局限性在于同一个线程内。
  ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。

 synchronized关键字可有效解决线程安全问题,其内部原理也是通过对所作用的代码块加锁,synchronized关键字可以作用在:

1、作用于代码块上,=》锁的对象是括号内的对象;

2、作用于成员函数上,=》锁的对象是调用该方法的对象(对象锁);

3、作用与静态方法上,=》锁的对象是该类的字节码文件对象(类锁)

可重入锁:
 谁持有锁==>线程持有锁
 锁的对象是谁==>锁的对象是对象或者类
 什么是可重入锁?==>可以被同一个线程多次进入(多次持有)
 可重入锁又叫做递归锁

可重入锁概念:
 对于可重入锁,每个线程都可以多次去获得该锁(同一个线程多次获得该锁),在线程获得锁后,可再去获得此锁多次,但是释放的时候,一定要释放同样的次数,否则,释放少于获得次数,将导致别的等待该锁的线程长期等待;如果线程多次释放锁(大于获得次数),或者释放别的线程的锁,将会出现异常。
 所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁。实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。

 用lock来保证原子性(this.count++这段代码称为临界区)。

 什么是原子性,就是不可分,从头执行到尾,不能被其他线程同时执行。CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
 CAS主要通过compareAndSwapXXX()方法来实现,而这个方法的实现需要涉及底层的unsafe类;
unsafe类:java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作

介绍原子操作的博客- https://my.oschina.net/xinxingegeya/blog/499223
对unsafe类详解的博客- http://www.cnblogs.com/mickole/articles/3757278.html

public class Count{
      Lock lock = new Lock();
      public void print(){
          lock.lock();
          doAdd();
          lock.unlock();
      }
      public void doAdd(){
          lock.lock();
         //do something
         lock.unlock();
     }
}
当调用print()方法时,获得了锁,这时就无法再调用doAdd()方法,这时必须先释放锁才能调用,所以称这种锁为不可重入锁,也叫自旋锁。

java中实现原子操作的类(记录至http://blog.csdn.net/huzhigenlaohu/article/details/51646455)
 AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
 AtomicLongFieldUpdater:原子更新长整型字段的更新器
 AtomicStampedReference:原子更新带有版本号的引用类型。该类将整型数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
 AtomicReference :原子更新引用类型
 AtomicReferenceFieldUpdater :原子更新引用类型里的字段
 AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和应用类型
 AtomicIntegerArray :原子更新整型数组里的元素
 AtomicLongArray :原子更新长整型数组里的元素
 AtomicReferenceArray : 原子更新引用类型数组的元素
 AtomicBooleanArray :原子更新布尔类型数组的元素
 AtomicBoolean :原子更新布尔类型
 AtomicInteger: 原子更新整型
 AtomicLong: 原子更新长整型

 若有同一线程两调用lock() ,会导致第二次调用lock位置进行自旋,产生了死锁,说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值