美文网首页
并发编程三:锁

并发编程三:锁

作者: 饭勺 | 来源:发表于2021-03-09 20:03 被阅读0次

    一、CAS

    1.CAS原理

    CAS全称为Compare And Swap,比较与交换。
    CAS是原子性操作的一种实现方式,类似Synchronized代码块,也属于原子性操作。原子性操作为不可再分的操作。
    但是Synchronized代码块相对较重,粒度比较大,以代码块为粒度,比较粗糙。
    CAS属于更轻量级原子性操作,以变量为粒度。

    CAS基本原理为,检测目标变量T是否为预期变量Y,有两种情况:
    一、如果目标变量T=预期变量Y,则修改目标变量T为新变量N(这个时候就可以获得锁了);
    二、如果目标变量T!=预期变量Y,则循环,知道目标变量T=预期变量Y;
    这里需要注意一点,释放锁的时候,我们会将目标变量T置为预期变量Y,方便其他操作获得锁。

    CAS是乐观锁机制,乐观锁拿不到锁则会进入循环,直到拿到锁,没有进行上下文切换;
    Synchronized是悲观锁机制,悲观锁拿不到锁则进行上下文切换,进入等待,然后被唤醒,再次进行上下文切换。上下文切换相对比较耗时。

    2.CAS的问题

    1.CAS循环时长的开销
    如果循环时间很长,这种情况是比较耗CPU资源的,因为不断的在循环
    2.只能保证一个变量的原子性操作
    如果要保证多个变量的原子性操作,可以将多个变量合并成一个变量。

    二、AQS

    1.AQS概念

    AQS全称为AbstractQueuedSynchronizer,是java提供的抽象类,通过模板方法模式提供了各种类型锁的接口。具体的锁类可以通过继承AQS实现不同的接口方法来定义自己的锁,比如ReentrantLock中Sync类。
    AQS中维护了一个int值代表锁的状态,通过FIFO队列维护获取锁的线程。

    2.AQS与锁的关系

    AQS可以理解为抽象同步器,Sync类可以理解为实际同步器,ReentrantLock才是真正面向使用者的锁。
    所以,锁面向使用者,同步器面向锁的具体实现。

    3.模板方法

    AQS采用模板方法模式,定义一系列锁的实现接口,子类中去实现具体算法。并且封装部分固定操作。

    4.ReentrantLock的实现

     abstract static class Sync extends AbstractQueuedSynchronizer {
            private static final long serialVersionUID = -5179523762034025860L;
    
            /**
             * Performs {@link Lock#lock}. The main reason for subclassing
             * is to allow fast path for nonfair version.
             */
            abstract void lock();
    
            /**
             * Performs non-fair tryLock.  tryAcquire is implemented in
             * subclasses, but both need nonfair try for trylock method.
             */
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    
            protected final boolean tryRelease(int releases) {
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
    
            protected final boolean isHeldExclusively() {
                // While we must in general read state before owner,
                // we don't need to do so to check if current thread is owner
                return getExclusiveOwnerThread() == Thread.currentThread();
            }
    
            final ConditionObject newCondition() {
                return new ConditionObject();
            }
    
            // Methods relayed from outer class
    
            final Thread getOwner() {
                return getState() == 0 ? null : getExclusiveOwnerThread();
            }
    
            final int getHoldCount() {
                return isHeldExclusively() ? getState() : 0;
            }
    
            final boolean isLocked() {
                return getState() != 0;
            }
    
            /**
             * Reconstitutes the instance from a stream (that is, deserializes it).
             */
            private void readObject(java.io.ObjectInputStream s)
                throws java.io.IOException, ClassNotFoundException {
                s.defaultReadObject();
                setState(0); // reset to unlocked state
            }
        }
    

    可以看出可重入锁,原理就是记录第一次获得锁的线程,如果线程一样则继续访问,并且将state+1,释放则-1;主要是通过nonfairTryAcquire()获取锁,tryRelease释放锁。

    5.各种锁的实现基本思路

    公平锁:先来先拿锁,会先判断队列里面有没有等待;
    非公平锁:随机拿锁,可插队抢占,不会判断队列有没有等待,当唤醒时直接拿锁。两者实现上差别不大。
    锁的可重入:在tryAcquire中,判断获取锁的线程是不是当前线程,如果是当前线程就继续执行,并且将State+1,每当释放锁tryRelease的时候,state-1,这样就是一把可重入锁。如果不做当前线程的校验,就是一把不可重入的锁。这也是ReentrantLock的实现原理。

    6.公平锁之CLH队列锁

    CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋(不断检测前一个节点是否释放锁),它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
    CLH队列锁的每个Node只需要混选前一个Node的locked变量,locked变量代表是否需要获得锁,初始都是置为true,代表需要获得锁,当释放锁之后,置为false,这样后一个Node循环到前一个Node的locked为false时则可获得锁了。

    Java中的AQS是CLH队列锁的一种变体实现。对CLH队列锁有所改进,限制自旋次数,超过次数进入等待状态。并且AQS不仅有拿锁的队列,还有一个等待的队列,也是一种改进。

    三、锁的状态

    1.锁的状态

    一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

    2.偏向锁

    引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

    偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

    偏向锁的适用场景
    始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;

    3.轻量级锁

    轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

    4.自旋锁

    自旋锁的优缺点

    自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

    但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。

    自旋锁时间阈值

    自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要

    JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

    5.各种锁的比较

    偏向锁优点:加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

    偏向锁的缺点:如果线程间存在竞争,会带来额外的锁撤销的消耗。

    偏向锁的缺点适用场景:适用于只有一个线程访问同步块的场景。

    轻量级锁的优点:竞争的线程不会阻塞,提供了程序的响应速度。

    轻量级锁的缺点:如果始终得不到锁的竞争的线程使用自旋会消耗CPU。

    轻量级锁的使用场景:追求响应时间。同步快执行速度非常快。

    重量级锁优点:线程竞争不使用自旋,不会消耗CPU。

    重量级锁缺点:线程阻塞,响应时间缓慢。

    重量级锁适用场景:追求吞吐量。同步执行时间较长。

    6.死锁

    两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
    死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。
    两种解决方式1、 内部通过顺序比较,确定拿锁的顺序;2、采用尝试拿锁的机制。

    7.活锁

    两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
    解决办法:每个线程休眠随机数,错开拿锁的时间。

    8.线程饥饿

    低优先级的线程,总是拿不到执行时间。

    四、Synchronized

    synchronized可以保证可见性,也可以保证原子性。

    1.可见性

    可见性,表示其他线程修改了变量,当前线程都能及时看到。synchronized保证可见性,是将变量的设置和获取直接加锁。

    2.原子性

    原子性,表示操作不可再分。通过synchronized加锁的代码块或者方法具有原子性。

    3.Synchronized以Interger为锁的情况

    由于数据自增,Interger对象会不断变换。也就是每个线程实际加锁的是不同的Integer 对象。

    4.synchronized修饰普通方法和静态方法的区别

    synchronized修饰普通方法,那么锁就是执行该方法的对象;
    synchronized修饰静态方法,那么锁就是执行该方法的对象的class对象;

    5.Synchronized实现原理

    对于Synchronized同步代码块,在jvm中MonitorEnter指令会插在同步代码块的前面,表示代码块需要获得对象的Monitor所有权,也就是要获取锁。在代码块结束的地方以及异常处,会插入MonitorExit指令,表示将释放锁。
    对于同步方法,实现原理有所不同。当Synchronized同步方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,需要获取Monitor所有权,也即获取锁才能继续执行。synchronized使用的锁是存放在Java对象头里面。
    synchronized对锁做了状态优化,从无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态的变化,适用不同的场景。
    synchronized还做了逃逸分析优化,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

    五、volatile

    volatile属于轻量级的同步机制,只保证可见性,不保证原子性。volatile 最适用的场景:一个线程写,多个线程读。

    1.volatile的原理

    被volatile关键字修饰的变量会存在一个“lock:”的前缀。Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

    2.volatile抑制CPU重排序

    volatile还有一个抑制CPU重排序的功能,重排序是现代CPU对代码指令打乱执行的一种优化方式,在不影响执行结果的情况下,现代CPU会对执行指令进行重排优化。

    六、其他

    1.ThreadLocal

    ThreadLocal和Synchronized都可以解决线程同步问题,但原理完全不一样。
    Synchronized是锁机制,ThreadLocal只是给每个线程建立变量副本,每个线程拿到的对象根本不是同一个。

    相关文章

      网友评论

          本文标题:并发编程三:锁

          本文链接:https://www.haomeiwen.com/subject/tqfbqltx.html