第五章 Java中的锁

第五章 Java中的锁

5.1Lock接口

获取锁后,发生过异常,锁不会释放。

锁与synchronized关键字的对比:

这里写图片描述

Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

5.2队列同步器

AbstractQueuedSynchronizer

同步器是构建锁和其他同步组件的基础框架。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器来提供的3个方法来进行操作。

这里写图片描述

同步器提供的模版方法分为3类:独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。

5.2.1队列同步器的接口与实例

重写同步器的方法是,需要你使用同步器提供的如下3种方法:

getState():获取当前同步状态

setState(int newState):设置当前同步状态

扫描二维码关注公众号,回复: 1580074 查看本文章

compareAndSetState(int expect,int update):使用CAS设置当前的状态,该方法能保证是值得原子性。

同步器可重写的方法

这里写图片描述

同步器提供的模版方法

这里写图片描述

5.2.2队列同步器的实现分析

1.同步队列

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点并将其加入同步队列,同时会柱塞当前线程,当同步状态释放时,会把首节点的线程唤醒,使其再次尝试获取同步状态。

这里写图片描述

同步器中有两个节点类型的引用,一个指向头节点,另一个指向尾节点,设置节点使用的是CAS操作

这里写图片描述

首节点是获得同步状态成功的节点,在首节点释放同步状态后,将会唤醒后继节点,而后继节点将会将自己设置为首节点。

这里写图片描述

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

​ 通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对于中断不明干,也就是由于线程获取同步轧辊台失败后进入同步队列中,后续对线程的中断操作,线程不会从同步队列移除。

主要的逻辑是:首先调用自定义的同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的同步获取,如果同步状态获取失败,如果获取失败,构造同步节点,加入同步队列尾部,然后使用死循环获取同步状态,如果获取不到阻塞节点中的线程,而别阻塞线程的唤醒主要依靠前驱节点的出队列或者阻塞线程被中断来实现。

为什么只有前驱节点是头节点才能尝试获取同步状态呢?

1.头节点是成功获取同步状态大鳄节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,而后记节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
2.维护同步队列的FIFOyaunze。

节点自旋获取同步状态的行为:

这里写图片描述

独占式同步状态获取流程如下:

这里写图片描述

当前线程获取同步状态并执行相应逻辑后,就需要释放同步状态,使得后续节点能够继续获取同步状态。

这里做一个总结:

在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件时前驱节点为头节点且成功获取同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后续节点。

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

​ 共享式获取与独占式获取最主要的区别是在同一时刻能否有多个线程同时获取到同步状态。

​ 在共享式获取同步状态时候,使用的是tryAcquireShared(int arg)方法尝试获取同步状态,该方法返回的是int类型,如果大于等于0,表示能够获取同步状态。在共享式获取的自旋过程中,成功获取到同步状态并推出自旋的条件就是方法返回值大于等于0

​ 对于能够支持多个线程同时访问的并发组件,他和独占式主要的区别在于try ReleaseShared(int arg)方法必须确保同步状态线程安全释放,一般是主要通过循环和CAS来保证,应为释放同步抓状态的操作同时来自多个线程。

5.2.4独占式超时获取同步状态

​ 调用同步器的doAcquireNanos(int arg,long nanos TimeOut)方法,也就是在指定的时间内获取同步状态,如果获取同步状态那么返回true,否则false。

​ 独占式超时获取同步状态流程

这里写图片描述

5.3重入锁

​ 重入锁ReentrantLock,就是支持一个线程队资源的重复加锁。这个锁还支持公平和非公平选择。

​ 如果一般的锁进行再次加锁,那么将获组塞。

5.3.1实现重入锁

​ 重进入是指任意线程在获取锁以后能够再次获取该锁儿不会被锁组塞。需要解决两个问题

1.线程再次获取锁。

2.锁的最终释放。

5.3.2公平和非公平获取锁的区别

​ 公平性与否是针对获取锁而言,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

​ 公平锁能够保证锁的获取按照FIFO原则,代价更高,主要是线程切换。

5.4读写锁

​ 读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他线程均被阻塞。读写锁维护一对锁,一个读锁和一个写锁,通过分离读锁和写锁,是的并发性相比一般的怕他锁有很大提升。

​ 在没有读写锁之前,Java使用的是等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的多操作才能继续执行,这样做的目的是使读操作能够读取正确的数据,不会出现脏读。

​ 一般情况下,读写锁的性能都会比排他锁好,因为大多数情况系读多于写。

​ 读写锁的特性

这里写图片描述

5.4.1读写锁的接口与实例

​ ReadWriteLock获取读锁和写锁的方法,也就是readLock()方法和writeLock()方法

ReetrantReadWriteLock展示内部工作状态的方法

这里写图片描述

这里可以使用这个类实现HashMap线程安全

5.4.2读写锁的实现分析

​ 主要包含读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级

1.读写状态的设计

​ 同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,是的该状态的设计成为读写锁实现的关键。

​ 这里使用按位切割使用这个变量,读写锁将变量切分成为两个部分,高位16位表示读,低16位表示写。

这里写图片描述

​ 通过位运算确定读和写各自的状态。

根据状态的划分得出一个推论:S不等于0时,当写状态等于0时则读状态大于0,也就是读锁已被获取。

2.写锁的获取与释放

​ 写锁时一个支持重进入的排他锁。如果房钱线程已经获取了写锁,则增加写状态。如果当线程获取写锁时,读锁已经被获取活着该线程不是已经获取写锁的线程,则当前进入等待状态。

​ 获取读锁的线程,需要判断读锁是否存在,如果读锁存在,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作

3.读锁的获取与释放

​ 读锁是一个支持重入锁进入的共享锁,它能够被多个线程同时获取,在没有其他写程序访问时,读锁总会被成功地获取,而所做的也只是增加状态。

​ 获取读锁的实现从Java5到Java6变得复杂许多,主要的原因时新增一些功能,作用时放回当前线程获取读锁的次数。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁边的复杂。

4.锁降级

​ 锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再次取读锁,这种分段完成的过程不能锁降级。锁降级是指把持住写锁,再次取到读锁,然后释放写锁的过程。

锁降级主要的为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取写锁并修改数据,那么当前线程无法获取线程T的数据更新。如果当前线程获取写锁,即遵循锁降级的步骤,则线程T将会阻塞,直到当前线程使用数据并释放苏锁之后,线程T才能获取写锁并进行数据更新。

​ 不支持锁升级,目的也是保证数据可见行,如果读锁已被多个线程获取,任意线程成功获取了写锁并更新了数据,其更新对其他获取读锁的线程是不可见的。

5.5LockSupport工具

​ 阻塞或唤醒一个线程都使用LockSupport工具类进行完成。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

​ LockSupport提供的组塞和唤醒方法

这里写图片描述

5.6Condition接口

​ 任意一个Jav对象,都拥有一组监视器方法(定义在java.lang.Object),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待通知模式。

​ Condition也提供了类似的功能,但是要配合Lock使用。

这里写图片描述

5.6.1Condition接口与实例

​ Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取Condition对象关联的锁。

package cn.edu.hust.lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionUseCase {
    Lock lock=new ReentrantLock();
    Condition condition=lock.newCondition();
    //这里相当于对象的wait()方法
    public void waitCondidtion() throws InterruptedException {
        lock.lock();
        try
        {
            condition.await();
        }
        finally {
            lock.unlock();
        }
    }

    //相当于对象的notify()方法
    public void singalCondition()
    {
        lock.lock();
        try
        {
            condition.signal();
        }
        finally {
            lock.unlock();
        }
    }
    public static void main(String[] args)
    {

    }
}

​ Condition的使用方法比较简单,需要注意在调用方法前获取锁。

Condition的方法以及描述

这里写图片描述

5.6.2COndotion的实现与分析

​ ConditionObject时同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取关联的锁,所以作为同步器的内部类比较合理。每个Condition对象都包含着一个队列,该队列时Condition对象实现等待/通知动能的关键。

1.等待队列

​ 队列是一个FIFO的队列,队列的每个节点都含有一个线程引用,该线程就是Condition对象上等待的线程,如果一个此案成调用Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待队列。

​ Condition拥有首节点和尾节点。

这里写图片描述

上述节点引用更新没有CAS保证,原因在于调用await()方法的线程必定获取锁的线程,也就是该过程用锁来保证线程安全。

​ 在Object的监视器模型模型功能,一个队列拥有一个同步队列和等待队列,而并发包中国年Lock拥有一个同步队列和多个等待队列。

这里写图片描述

2.等待

​ 从队列的角度看await()方法,当调用await()方法时,相当于同步队列的首节点移动到Condition的队列中。

3.通知

​ 调用Condition的singal()方法,将会唤醒在等待队列等待时间最长的节点,在唤醒节点之前,会将节点已不到同步队列。

这里写图片描述

​ 当前线程必须获取锁,可以看到singal()方法进行了isHeldExclusively()检查,也就是当前线程必须就是获取锁的线程。

猜你喜欢

转载自blog.csdn.net/oeljeklaus/article/details/80667238