多线程进阶 Java的锁及其实现

sychronized实现原理

java中sychronized基于monitor管程实现,提现到字节码上就是monitorenter和monitorexit指令。每个对象都与一个monitor关联,进行sychronized就是在monitorenter前获取对象的monitor。
在这里插入图片描述

Java对象头和Monitor

在锁标识为01的对象的Mark word中,前30位是指向monitor的指针。它可以:

  • 与对象一起创建销毁
  • 当前线程试图获取对象锁时自动生成

当一个monitor被某个线程持有时就处于锁定状态。Monitor被JVM的C++代码实现,名为ObjectMonitor,其中重要的几个变量是

  1. _WaitSet 存储处于wait状态的线程
  2. _EntryList,存储处于block状态的线程。
  3. _Owner:拥有该monitor的线程
  4. _Count:线程计数器,初始为0

monitor的工作流程

当有线程进入synchronized代码段时,进入_EntryList而阻塞。等待并获取到monitor(循环CAS获取指针)后,:

  • 将_Owner设置为自己线程
  • _Count++

如果线程调用wait()

  • 让_Owner变为null
  • _Count–
  • 进入_WaitSet

如果线程离开同步块,

  • 让_Owner变为null
  • _Count–进入
  • _WaitSet

编译器会自动产生异常处理器的monitorexit,来保证enter和exit总能相配

Java6后的优化

锁升级机制

偏向锁

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次CAS请求锁时,无需再做任何同步操作。

偏向锁加锁后,不会自动释放,只有其他线程尝试竞争的时候,才会释放,并且必须等到全局安全点(没有字节码执行)。它进行如下动作

  • 暂停拥有偏向锁的线程
  • 检查持有偏向锁的线程是否存活,否则将对象头设为无无锁状态;是则拥有偏向锁对象的锁栈会被遍历
  • 进行遍历后,锁对象的MarkWord要么偏向其它进程,要么恢复到无锁,要么膨胀升级

轻量级锁

有对象进行CAS失败时,就会膨胀为轻量级锁,它的思想为“在整个同步代码块内,都不会存在竞争”,也即线程交替执行同步块的场合。
如果有线程在其他线程执行同步块代码时请求进入同步块,锁救回膨胀为重量级锁

Lock接口

基本API:
在这里插入图片描述

队列同步器(AQS)

AQS是构建锁或者其他同步组建的基础框架,它使用一个volatile int成员变量表示同步状态,内置一个队列来完成资源获取线程的排队工作。
对状态的修改主要使用getState()setState(int newState)compareAndSetState(int expect,int update),从而保证状态改变安全。 支持共享和独占的同步

AQS主要可重写的方法:
在这里插入图片描述
同时,同步器也提供了模板方法,用于在重写同步状态获取模式后,提供同步器的使用接口。

AQS的实现分析

同步队列

AQS以来内部的FIFO双向同步队列完成同步状态的管理,当前线程获取同步状态失败时,同步器会将其及相关信息构造成Node加入同步Queue,并阻塞当前线程,同步状态结束时会将首节点中的线程唤醒,使其在此尝试获取同步状态。
队列结构如下:
在这里插入图片描述
加入尾结点时有可能产生竞争,所以用CAS操作保证原子性:
在这里插入图片描述
弹出首节点获得锁时,由弹出节点代表的线程执行首节点更新操作,显然它是由一个进程来做,不用同步

独占式同步状态获取与释放

通过同步器的acquire(int arg)方法可以获取同步状态,失败后会进入同步队列。

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

它首先调用自定义同步器实现的tryAcquire(int arg),失败则进行acquireQueued(Node node,int arg),使其以死循环方式获取同步状态。
被阻塞线程的唤醒主要依靠前驱节点的出兑或阻塞线程被中断来实现。
addWaiter方法通过CAS添加尾部的方法强行将代表当前进程的Node添加入等待队列。

节点入队之后会进行自旋过程,每个线程都会观察,一旦获取到同步状态就从中退出,否则就要一直执行并且阻塞线程:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

而node.predecessor()==head注定了只有前驱节点是头结点才能常识获取同步状态,这因为:

  1. 头结点显然已经成功获取到同步状态,因而之后头结点的线程释放了同步状态之后,将会唤醒后继节点,后继节点的县城北唤醒后要检查自己的predecessor是否为头结点
  2. 维护队列FIFO原则

总结
在这里插入图片描述

显然从acquire返回时,同步器代表获得互斥锁

之后release(int arg)可以用tryRelease释放同步状态,并唤醒后继节点

共享式同步状态获取与释放

共享之间可以并发,独占之间、独占与共享之间不能并发

acquireShared(int arg)方法中:

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

同步器用tryAcquireShared(子类应当重写)尝试获取共享状态,大于等于0则成功,否则用doAcquireShared(int arg)方法进入自旋获取流程:

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

同样,共享同步也需要release

独占式超时获取同步状态

它与同步状态的区别是,在指定时间内获取同步状态,如果得到1则返回true,否则返回false,整体流程如下:
在这里插入图片描述

可重入锁ReentrantLock

读写锁ReentrantReadWriteLock

支持:

  • 公平与非公平选择
  • 可重入
  • 所降级

实现分析

  1. 读写状态的设计
    将state的高16位定位读状态,低16位定位写状态

  2. 写锁的获取与释放
    写锁是支持充入的排它锁,如当前线程已经获得了写锁,则增加写状态(重入性质);
    如果获取写锁是,已有读锁被获取,或者该线程不是以获取写锁的进程,则其进入等待状态

  3. 读锁的获取与释放
    在没有其他鞋线程访问时,读锁总会被成功获取。

  4. 锁降级
    所降级是指持有写锁,再获取读锁,再释放写锁的过程,这个顺序是必须的,进而能保证数据可见性

LockSupport工具

主要用于阻塞或唤醒进程
在这里插入图片描述

Condition接口

主要是与Lock接口配合,实现等待/通知模式,它依赖于一个Lock对象。
Condition由一个Lock对象产生,要调用Condition对象的方法(awaitsignal等),一个进程必须先获取这个Condition对象对应的锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值