Synchronized 和AQS实现的要点

参考了其他一些博文的片段

Synchronized

synchronized 是基于 Java 对象头和 Monitor 机制来实现的。

一个对象在内存中包含三部分:对象头,实例数据和对齐填充。其中 Java 对象头包含两部分:

 

Monitor的操作机制如下:

 

 

 

  • Class Metadata Address (类型指针)。存储类的元数据的指针。虚拟机通过这个指针找到它是哪个类的实例。
  • Mark Word(标记字段)。存出一些对象自身运行时的数据。包括哈希码,GC 分代年龄,锁状态标志等。
  •  
  • Java对象头

    在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,以32位的JDK为例:

     

    每一个对象都会有一个monitor,monitor是由C++实现的一个ObjectMonitor类,可以理解为一个实现线程同步的对象;

  • Mark Word 有一个字段指向 monitor 对象。monitor 中记录了锁的持有线程,等待的线程队列等信息。前面说的每个对象都有一个锁和一个等待队列,就是在这里实现的。 monitor 对象由 C++ 实现。其中有三个关键字段:

  • _owner 记录当前持有锁的线程
  • _EntryList 是一个队列,记录所有阻塞等待锁的线程
  • 多个线程竞争锁时,会先进入 EntryList 队列。竞争成功的线程被标记为 Owner。其他线程继续在此队列中阻塞等待。
  • 如果 Owner 线程调用 wait() 方法,则其释放对象锁并进入 WaitSet 中等待被唤醒。Owner 被置空,EntryList 中的线程再次竞争锁。
  • 如果 Owner 线程执行完了,便会释放锁,Owner 被置空,EntryList 中的线程再次竞争锁。
  • _WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程。

jdk 1.6对做了很多优化:

  • 锁消除:在代码上加了锁,但是虚拟机判断出这一块代码不可能被多线程竞争,就会把这个锁消除掉;虚拟机判断的依据是逃逸分析
  • 锁粗化:如果虚拟机检测到一串操作都对一个对象加锁,释放锁,将会把加锁的范围粗化到整个操作的外部;
  • 自适应自旋:自旋时间由前一次在同一个锁的自旋时间和锁的拥有者状态来决定,如果虚拟机判断获得这个锁的可能性很大,就会增加自旋时间,如果觉得很难获得锁,可能会省去自旋这一步节约CPU;
  • 偏向锁:这个锁会偏向于第一个持有它的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。

    轻量级锁:由偏向锁转化来,相对于传统的重量级锁,不会阻塞线程,而是通过自旋进行等待;以CPU为代价,避免线程的上下文切换,追求响应速度;

  • AQS

    锁机制一般是有两种基本形式,1是阻塞,2是自旋(CAS自旋)

  • 常见的锁的实现算法有TAS,TTAS ,CLH,MCS,java里的AQS是CLH的一个变种,CLH适用于SMP数据结构,MCS是适用于NUMA结构,CLH和MCS不同在于,CLH是轮询前驱节点的状态判断前驱节点是否释放锁,MCS是轮询自身节点状态来判断是否可以获得锁,前驱节点释放锁的时候更新后一节点的状态。

  •  

  • Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

  • AQS 全称 AbstractQueuedSynchronizer。AQS 中有两个重要的成员:

  • 成员变量 state。用于表示锁现在的状态,用 volatile 修饰,保证内存一致性。同时所用对 state 的操作都是使用 CAS 进行的。state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。
  • AQS维护着两个队列:一个是由AQS类维护的CLH队列(用于运行CLH算法),用于锁机制的实现,另一个是由AQS的内部类ConditionObject维护的Condition队列(用于支持线程间的同步,提供await,signal,signalAll方法)。

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。CLH算法是轮询前驱节点的状态,这样会耗费处理器资源,AQS没有一直轮询,而是用了阻塞等待唤醒机制来节省处理器资源。

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

主要原理图如下:

AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。

AQS 在线程竞争锁失败后,会讲线程信息包装成一个节点,加入同步队列。多个线程加入同时竞争锁失败时,都要加入同步队列,如何保证加入同步队列操作是原子和互斥的呢,AQS使用了CAS方式来设置同步队列首尾节点的引用。

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */

//将节点加入到同步队列的尾部

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure

//快速尝试在尾部添加

        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {

//如果CAS尝试成功,就说明"设置当前节点node的前驱"与"CAS设置tail"之间没有别的线程设置tail成功

//只需要将"之前的tail"的后继节点指向node即可

                pred.next = node;
                return node;
            }
        }
        enq(node);//否则,通过死循环来保证节点的正确添加

        return node;
    }

   /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

   /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

多个线程使用CAS来加入到同步队列中时,CAS失败时会不断重试,这样会耗费处理器资源。

出队列时,首节点的线程在释放同步状态后,将会唤醒后继节点的线程,并且后继节点的线程在获取到同步状态后将会将自己设置为首节点。因为设置首节点是通过获取同步状态成功的线程来完成的,因此设置头结点的方法并不需要使用CAS来保证,因为只有一个线程能获取到同步状态。

 

AQS 也提供等待通知机制,可以用来代替传统的Object的wait()、notify()实现线程间的协作。他的作用是使得某些线程一起等待某个条件(Condition),只有当该条件具备(signal 或者 signalAll方法被调用)时,这些等待线程才会被唤醒,从而重新争夺锁。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值