Java锁机制

1.什么是锁

在并发情况下多个线程会对同一资源进行抢夺从而导致数据不一致性的问题,很多编程语言都会引入锁机制,锁是一种抽象的概念,目的是对资源进行锁定从而达到线程同步的目的

2.原始锁synchronized

为什么把synchronized叫原始锁,因为它是Java一开始就提供的关键字,它是建立在JVM指令的层命来实现的,我知道很多人也在用lock锁,lock锁是在JDK5后由Doug Lea大师伴随着concurrent包引入的,synchronized的底层实现关键点在于monitor监视及配合monitorenter和monitorexit两条字节码指令,任何编程语言都是建立在操作系统的层面上的,Java作为更高级语言也不例外,Java线程其实是对操作系统底层线程的映射,monitor最终也会调用操作系统的mutexlock指令,所以这样的加锁操作要进行操作系统用户态和内核态的切换,我觉得这是锁机制发展历程的核心,虽然在JDK6之后优化引入了无锁、偏向锁、轻量级锁、重量级锁,当然这四种状态你也可以在对象头里的MarkWord里清楚看到它们的标志为信息等等,锁只能不断升级不能降级。
在最初的无锁状态即没有对资源进行锁定,所有线程都可以访问到资源,这样就会有两种情况:1.资源不会出现在多线程的情况或者即使出现在多线程的情况下也不会形成竞争,这样就无需担心同步问题;2.资源会被竞争,但是不想对资源进行锁定,原因就是我上面说的,可以通过一些类如CAS的机制来控制线程//无锁到偏向锁再研究研究
然后变为偏向锁,偏向锁顾名思义即偏向于某一线程的锁,因为HotSpot作者发现大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得,所以引入偏向锁,偏向锁的实现依赖于线程ID,在MarkWord中也可以发现,根据锁标志位是否为01以及是否偏向位为1即可确定为偏向锁,通过确认线程ID确认线程是不是老顾客来把锁直接交出去,不需要通过底层mutex lock以及CAS来获取锁,显然这样是十分高效的,当确定线程ID不符合从而确定当前环境下线程竞争激烈从而去升级为轻量级锁;
与偏向锁不同的是,偏向锁只需要通过线程ID就可以实现线程和锁的绑定,而偏向锁绑定实现在于自己虚拟机栈中的owner指针去指向对象,成功获取偏向锁的线程就可以进入同步代码块,其他线程进行自旋等待,自旋同样也是高效方式的体现,如果对象的锁很快就会释放的话,不断自选轮询的某个线程就会去直接获取锁,而如果是以被操作系统挂起阻塞的方式还需要进行系统中断和现场恢复,自旋其实相当于CPU空转,长时间自选其实也不是什么好事情,根据自选等待的线程以及自选时间系统会将轻量级锁升级为种量级锁;
种量级锁即最初始的没有优化前的需要通过monitor调用底层同步源语的方式,或者说其实前三种方式都不算是真正意义上的加锁,而种量级锁才是真正的加锁

3.完美的CAS

synchronized虽然经过了优化,在线程竞争激烈的情况下这不还是很快就会演变为种量级锁,这不还是老样子,这也是为什么常言道的syn锁在线程竞争不是很激烈的情况下效率高,而lock锁在线程竞争激烈的情况下效率高。回到最初的问题,我认为锁机制的发展还是关键在于解决两个点:1.不要调用操作系统底层源语从而导致用户态和内核态的切换;2.在如今大量的读业务情况下,并且还可能同步代码块里微妙的代码执行时间远小于线程间切换的时间;
如果可以不去调用操作系统底层源语避免用户态和内核态切换的开销,并且不用去锁定资源就可以实现线程的同步,这样的方式一定是最高效的,显然CAS就是这样;
CAS(compare and swap比较并交换),它是一种思想或者说是一种算法,CAS包含三个操作数 —— 内存位置(V)、原预期值(A)、新值(B)。
其实逻辑上很简单,思想跟锁一样,线程如果进入资源就去改变资源的状态,内存V作为资源的原状态值,线程用跟资源状原态值一模一样的预期值A来判断资源是否有别的线程在操作,如果没有就进入资源并用自己的新值B替换掉A,别的线程就无法用预期值A再次判断出内存V,无法进入并且自旋不断地重试CAS
简单点说就是,当更新变量时,只有当变量的预期值A和内存地址V当中的值相同时,才会将内存地址V对应的值修改为B
思想还是跟加锁相似,但实现上CAS是在操作系统层面上,而且我们常用的X86,ARM架构的CPU也都提供了指令级别的CAS原子操作,即不需要通过操作系统的同步原语,CPU可以原生的支持CAS,这样从根本上就可以不再依赖锁来进行线程同步,万丈高楼平地起,计算机世界也是如此,底层开放了新特性,那么上层就会有更多的编程花样,有了CPU原生指令级的CAS支持,就会促进出很多无锁编程的思路,相比于syn锁的悲观锁,用CAS进行无锁编程或者也叫乐观锁会更加高效
在JAVA中,CAS操作依赖于Unsafe类中的方法,Unsafe类存在于sun.misc包中,其内部方法可以像C的指针一样直接操作内存,从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,Unsafe类中的所有方法都是native修饰的,也就是Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:

4.JUC并发包

在有了CAS的基础上,在JDK5后util包下继而引入了concurrent包,主要由并发大师Doug Lea完成,通常所说的concurrent包基本有3个package组成
java.util.concurrent:提供大部分关于并发的接口和类,如BlockingQueue,Callable,ConcurrentHashMap,ExecutorService, Semaphore等
java.util.concurrent.atomic:提供所有原子操作的类, 如AtomicInteger, AtomicLong等;
java.util.concurrent.locks:提供锁相关的类, 如Lock, ReentrantLock, ReadWriteLock, Condition等;
在这里插入图片描述

5.AQS

在Java已经提供了CAS能力的基础上,如何去利用它去开发一个通用的去对竞争资源同步的框架是一件很重要的事情,那么这样的框架设计思路是怎样的?
首先,谈到框架一定具有通用性,在实现底层同步机制的同时需要开放一定的空间去给上层进行业务逻辑的编写
其次,利用CAS的原子性,将内存V作为状态标记位,以此来阻碍其他线程的调用
最后,在没有获取到资源被阻碍的线程中有两种场景,一是有的线程只是想去快速尝试获取一下共享资源而已,获取不到也没关系它会去做其他的业务处理;二是有的线程必须去获取执行共享资源才能进行下一步处理,必须一直等待;针对第一种场景直接利用CAS去返回true或者false即可,针对第二种场景也可以去给予返回值,然后让它去进行轮询,但是轮询相当于CPU空转,高并发的场景下CPU负载会更加严重,降低系统性能,并且让上层业务去主动处理也会增加上层业务开发的难度这不是我们想要的目的,因而可以设计一个队列将这些线程排队,待共享资源空闲下来队列里的线程就可以依次去获取
显然AQS就是这样的,JUC中的很多工具以及现在许多主流的开源中间件都使用了AQS,AbstractQueuedSynchronizer,队列同步框架。

AQS成员属性:

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;//判断共享资源是否被占用的标记位

AQS成员变量很简单,三个属性,vloatile修饰的state属性来确保线程的可见性,这里的state是用int修饰而不是boolean,因为线程获取锁的有两种模式,独占和共享,在共享模式下,线程获取锁后其他以共享模式获取锁的线程也可以去获取锁并且可以去增强锁,从而一起访问共享资源,因而int可以去代表当前占用资源的线程数量,其余两个属性头节点和尾节点维护了一个Node类型的先进先出(FIFO)的双向链表,队列中的节点同样有独占和共享两种模式

Node的具体结构:

static final class Node {
   
        // 共享模式下等待的标记
        static final Node SHARED = new Node();
        // 独占模式下等待的标记
        static final Node EXCLUSIVE = null;

         // 线程的等待状态 表示线程已经被取消
        static final int CANCELLED =  1;
        // 线程的等待状态 表示后继线程需要被唤醒
        static final int SIGNAL    = -1;
        // 线程的等待状态 表示线程在Condtion上
        static final int CONDITION = -2;
        // 表示下一个acquireShared需要无条件的传播
        static final int PROPAGATE = -3;

        //线程在队列中的等待状态 
        volatile int waitStatus;

        //当前线程节点的前继节点  
        volatile Node prev;

        //当前线程节点的后继节点
        volatile Node next;

        //线程对象
        volatile Thread thread;

        //该属性用于条件队列或者共享锁 
        Node nextWaiter;
        
        //......

就成员属性而言重要的属性大致分为4类:thread线程对象本身、waitStatus线程等待状态、前后节点、4中状态值
如何利用state状态和FIFO的队列来操作管理线程,这些操作都被封装为了方法,按场景来分类,第一种是尝试获取锁,如果不成功直接返回结果即可不想去等待,第二种即愿意去加入队列中等待
其中第一种场景对应tryAcquire方法,第二种场景对应acquire方法

tryAcquire方法:

    protected boolean tryAcquire(int arg) {
   
        throw new UnsupportedOperationException();
    }

源码中的tryAcquire是一个被protected修饰,int作为参数,返回boolea的方法,boolea返回值即代表是否成功获取锁,参数值int代表对state状态的修改,此方法的实现只有1行是抛出一个异常,因而该方法的意图很明显,AQS只是一个框架,它需要一个继承类用户自己去重写实现该方法并且加入自己的业务逻辑,否则就直接抛出异常,例如:

public class Syncer extends AbstractQueuedSynchronizer {
   
    @Override
    protected boolean tryAcquire(int arg) {
   
        //上层业务逻辑,比如
        if(arg != 1)
            return false;
        if(getState() == 1)
            return false;
        return compareAndSetState(0,1);
    }
}

外层调用Syncer即可

acquire方法:

    public final void acquire(int arg) {
   
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire方法的修饰符是public final,即所有的继承类都可以直接去调用并且不允许继承类擅自去重写,即此方法一定可以得到锁
if判断条件中包含两个条件:!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
如果tryAcquire获得锁则 !tryAcquire为false,啪的一下很快啊就跳出整个 if 了,直接执行selfInterrupt方法,如果tryAcquire没有获得锁,则会去执行后面的acquireQueued(Node,arg)方法进行排队等待锁,而其中的Node参数嵌套了addWaiter(Node.EXCLUSIVE)方法

addWaiter方法:

addWaiter方法即将当前线程封装为Node加入到等待队列中

    private Node addWaiter(Node mode) {
   
         //创建出新的Node节点
        Node node = new Node(Thread.currentThread(), mode);
        //绑定到尾节点后
        Node pred = tail;
        //在尾节点不为空的情况下通过CAS将当前节点置为尾节点
        if (pred != null) {
   
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
   
                //再将前置节点的后置绑定到当前节点
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

如果程序没有进入第一个if块,也就是尾节点为空或第一次CAS失败,则会进入完整的入队方法enq

enq方法:

    private Node enq(final Node node) {
   
        for (;;) {
    //无限循环即自旋CAS直到把节点插入为止
            Node t = tail;
            if (t == null) {
    
                //获取尾节点为null直接初始化,再此循环进来进入else
                if (compareAndSetHead(new Node()))
                    tail = head;
            
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值