JUC下的锁

Lock接口

synchronized关键字固化了锁的获取和释放,锁的获取和释放的过程对开发者是不可见的,不具备扩展性,对于复杂的加锁场景synchronized难以实现,Lock接口就解决了这一问题,提供了系列API用于根据需求自定义锁
Lock接口使用方式

// 以ReentrantLock为例
Lock lock = new ReentrantLock();
lock.lock();
try{
//临界区
}finally{
	lock.unlock();
}

Lock接口的API

方法名称描述
void lock()获取锁,调用该方法当前线程会去阻塞式加锁,加锁成功后从该方法返回
void lockInterruptibly() throws InterruptedException可中断的获取锁,和lock的不同之处在于获取锁的过程中可以响应中断从而停止获取锁
boolean tryLock()尝试非阻塞的获取锁,调用之后立刻返回true为加锁成功,false为加锁失败
boolean tryLock(long time,TimeUnit unit) throws InterruptedException超时的获取锁,如果在超时时间内获取锁,在超时时间内被中断,超时结束这三种情况将会返回
void unlock()释放锁
Condition newCondition()获取等待通知组件,该组件和当前的锁绑定,只有获取了锁才能够调用组件的等待通知方法

AQS—队列同步器

AQS全称是AbstractQueuedSychronizer,是阻塞式锁和相关的同步器工具的框架(类似servlet,由开发者通过继承的方式来自定义锁),内部使用了FIFO队列来实现线程阻塞队列,是实现锁的关键

特点

  • 用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
  • 提供了基于FIFO的等待队列实现公平锁,类似于Monitor的EntryList(EntryList不是严格FIFO的)
  • 条件变量来实现等待、唤醒机制;支持多个条件变量,类似于Monitor的WaitSet(某一个条件变量对应一个WaitSet)
  • AQS中的阻塞和唤醒使用LockSupport中的park和unpark来实现

同步器中可重写的方法

方法名称描述
protected boolean tryAcquire(int arg)独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryRelease独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg)共享式获取同步状态,返回大于等于0的值表示获取成功,反之加锁失败
protected int tryReleaseShared(int arg)共享式释放同步状态
proteced boolean isHeldExclusively()当前同步器是否在独占模式下被占用,一般该方法表示是否被当前线程所占用

AQS自定义锁

/***
 * @author shaofan
 * @Description 自定义锁,不可重入、独占锁
 */
public class MyLock implements Lock {

    private MySync mySync = new MySync();

    /***
     * 独占锁,同步器类
     */
    static class MySync extends AbstractQueuedSynchronizer{
        /**
         * 尝试加锁
         * @param arg 定义可重入锁时使用到该参数
         * @return
         */
        @Override
        protected boolean tryAcquire(int arg) {
            // 通过cas对state进行修改,cas锁的形式实现独占锁
            if(compareAndSetState(0,1)){
                // 如果cas修改成功,即加锁成功,将Owner设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 尝试解锁
         * @param arg 定义可重入锁时使用
         * @return
         */
        @Override
        protected boolean tryRelease(int arg) {
            // 保证顺序,先将当前加锁线程置空再修改state解锁;state使用volatile修饰、exclusiveOwnerThread没有,保证这个顺序来保证可见性
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        /**
         * 是否持有独占锁
         * @return
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }

        /**
         * 获取条件变量
         * @return
         */
        protected Condition newCondition(){
            return new ConditionObject();
        }
    }
    /**
     * 加锁,加锁失败会放入队列中等待加锁
     */
    @Override
    public void lock() {
        mySync.acquire(1);
    }

    /**
     * 加锁,通过这个方法加锁过程中可打断,synchronized加锁不可打断,容易死锁
     * @throws InterruptedException
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {
        // 底层会调用tryAcquire
        mySync.acquireInterruptibly(1);
    }

    /**
     * 尝试加锁,如果加锁不成功直接返回false
     * @return
     */
    @Override
    public boolean tryLock() {
        return mySync.tryAcquire(1);
    }

    /**
     * 尝试加锁,带有超时时间
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return mySync.tryAcquireSharedNanos(1,unit.toNanos(time));
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        mySync.release(0);
    }

    /**
     * 创建一个条件变量
     * @return
     */
    @Override
    public Condition newCondition() {
        return mySync.newCondition();
    }
}

ReentrantLock基本使用

ReentrantLock是JUC并发包提供的锁工具,它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与synchronized的共同点:都支持可重入

可重入

可重入是指同一个线程如果首次获得了这把锁,因为它是锁的拥有者,有权利再次获取这把锁;如果是不可重入锁,那么第二次获得锁时,自己也会被锁住

@Slf4j
public final class Demo{
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args){
        lock.lock();
        try{
        	lock.lock();
        	try{
            	log.debug("可重入锁");
        	}finally{
        		lock.unlock();
        	}
        }finally {
            lock.unlock();
        }
    }
}

可打断

通过lockInterruptibly方法加锁的线程可以被打断,从而退出阻塞队列,而通过lock方法和synchornized加锁的线程被打断没有任何效果,仍然会等待锁

@Slf4j
public final class Demo{
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(()->{
            try{
                // 可打断的加锁,如果没有加锁成功会进入阻塞队列,可以被打断
                lock.lockInterruptibly();
                try{

                }finally {
                    lock.unlock();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
                log.debug("被打断");
                return;
            }

        },"t");
        lock.lock();
        t.start();
        
        TimeUnit.SECONDS.sleep(1);
        // 打断线程
        t.interrupt();
    }
}

锁超时

@Slf4j
public final class Demo{
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(()->{
            try {
                // tryLock方法中可以传入超时时间,会在指定时间内重复尝试获取锁,超时没有获取返回
                // 打断后仍然会继续获取锁,不会直接退出
                if(!lock.tryLock(1,TimeUnit.SECONDS)){
                    log.debug("没有成功获取锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"t");

        lock.lock();
        t.start();

        TimeUnit.SECONDS.sleep(2);
    }
}

哲学家就餐问题

几个哲学家围着一张桌子就餐,每两个哲学家之间有一只筷子,即每个哲学家的右筷子是右边哲学家的左筷子;他们要就餐需要集齐两只筷子;但当每个哲学家手里都有一只筷子时,都会等待右筷子,陷入死锁

/***
 * @author shaofan
 * @Description 哲学家就餐问题
 */
@Slf4j
public final class Demo{
    public static void main(String[] args) throws InterruptedException {
        Chopstick chopstick1 = new Chopstick("1");
        Chopstick chopstick2 = new Chopstick("2");
        Chopstick chopstick3 = new Chopstick("3");
        Chopstick chopstick4 = new Chopstick("4");
        Chopstick chopstick5 = new Chopstick("5");

        new Philosopher("苏格拉底",chopstick1,chopstick2).start();
        new Philosopher("亚里士多德",chopstick2,chopstick3).start();
        new Philosopher("柏拉图",chopstick3,chopstick4).start();
        new Philosopher("阿基米德",chopstick4,chopstick5).start();
        new Philosopher("赫拉克利特",chopstick5,chopstick1).start();
    }
}

/***
 * 哲学家类,继承Thread,每个哲学家对应一个线程,执行就餐动作
 */
@Slf4j
class Philosopher extends Thread{

    Chopstick left;
    Chopstick right;

    public Philosopher(String name,Chopstick left,Chopstick right){
        super(name);
        this.left = left;
        this.right = right;
    }

    @SneakyThrows
    @Override
    public void run() {
        while(true){
            // 尝试获取左手筷子
            synchronized (left){
                // 尝试获取右手筷子
                synchronized (right){
                    eat();
                }
            }
        }

    }

    private void eat() throws InterruptedException {
        log.debug(getName()+"is eating");
        TimeUnit.SECONDS.sleep(1);
    }
}

/***
 * 筷子,每个哲学家有一个左筷子和一个右筷子,左筷子是左边哲学家的右筷子
 */
class Chopstick{
    String name;

    public Chopstick(String name){
        this.name = name;
    }
}

使用ReentrantLock超时锁解决哲学家就餐问题

指定获取筷子的时间,当获取筷子超时就不去获取筷子了,并且把已经获取到的筷子放下,这样别的哲学家就可以拿到筷子,打破死锁环境

/***
 * @author shaofan
 * @Description 哲学家就餐问题
 */
@Slf4j
public final class Demo{
    public static void main(String[] args) throws InterruptedException {
        Chopstick chopstick1 = new Chopstick("1");
        Chopstick chopstick2 = new Chopstick("2");
        Chopstick chopstick3 = new Chopstick("3");
        Chopstick chopstick4 = new Chopstick("4");
        Chopstick chopstick5 = new Chopstick("5");

        new Philosopher("苏格拉底",chopstick1,chopstick2).start();
        new Philosopher("亚里士多德",chopstick2,chopstick3).start();
        new Philosopher("柏拉图",chopstick3,chopstick4).start();
        new Philosopher("阿基米德",chopstick4,chopstick5).start();
        new Philosopher("赫拉克利特",chopstick5,chopstick1).start();
    }
}

/***
 * 哲学家类,继承Thread,每个哲学家对应一个线程,执行就餐动作
 */
@Slf4j
class Philosopher extends Thread{

    Chopstick left;
    Chopstick right;

    public Philosopher(String name,Chopstick left,Chopstick right){
        super(name);
        this.left = left;
        this.right = right;
    }

    @SneakyThrows
    @Override
    public void run() {
        while(true){
            // 尝试获取左手筷子
            if(left.tryLock(1,TimeUnit.SECONDS)){
                try{
                    if(right.tryLock(1,TimeUnit.SECONDS)){
                        try{
                            eat();
                        }finally {
                            right.unlock();
                        }
                    }
                }finally {
                    left.unlock();
                }

            }
        }

    }

    private void eat() throws InterruptedException {
        log.debug(getName()+"is eating");
        TimeUnit.SECONDS.sleep(1);
    }
}

/***
 * 筷子,每个哲学家有一个左筷子和一个右筷子,左筷子是左边哲学家的右筷子
 */
class Chopstick extends ReentrantLock{
    String name;

    public Chopstick(String name){
        this.name = name;
    }
}

公平锁

构造ReentrantLock时,可以传入布尔值标志该锁是否是公平锁;公平锁会按照进入EntryList的顺序获取锁,每个线程之间没有优先级,可以解决饥饿问题,但是会降低并发度,一般没有必要

条件变量

  1. await前需要获得锁
  2. await执行后,会释放锁,进入conditionObject等待
  3. await线程被唤醒(或打断、或超时),需要重新竞争锁
  4. 竞争锁成功后,从await后面继续执行
@Slf4j
public final class Demo{
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition1 = lock.newCondition();
    static Condition condition2 = lock.newCondition();
    static boolean flag1 = true;
    static boolean flag2 = true;
    public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(()->{
                lock.lock();
                try{
                    // 一个锁可以对应多个进入等待队列的条件,不同条件对应不同的waitSet
                    // 而synchronized只能对应对象锁的waitSet
                    while(flag1){
                        condition1.await();
                    }
                    log.debug("条件1满足");
                    while(flag2){
                        condition2.await();
                    }
                    log.debug("条件2满足");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            },"t");

            t.start();
        TimeUnit.SECONDS.sleep(1);
        lock.lock();
        try{
            flag1 = false;
            flag2 = false;

            condition1.signalAll();
            condition2.signalAll();
        }finally {
            lock.unlock();
        }

    }
}

ReentrantLock实现原理

在这里插入图片描述

非公平锁原理

通过默认构造器构造的ReentrantLock得到的就是非公平锁的实现

public ReentrantLock() {
	sync = new NonfairSync();
}

第一个线程加锁成功:
在这里插入图片描述
在这里插入图片描述

Thread-0一直占有锁,第二个线程加锁失败:

  1. cas将state从0改为1,当前state已经是1,修改失败,进入acquire逻辑
    在这里插入图片描述

  2. 调用tryAcquire再次尝试加锁,仍然失败
    在这里插入图片描述

  3. 进入addWaiter逻辑,构造Node阻塞队列
    在这里插入图片描述
    在这里插入图片描述

    • 图中黄色三角表示该Node的waitStatus,其中0为默认的正常状态
    • Node的创建是懒惰的
    • 第一个Node称为Dummy(哑元)或哨兵,用来占位,并不关联线程
  4. 构造Node后当前线程进入acquireQueued逻辑:
    在这里插入图片描述

    • acquireQueued会在一个死循环中不断尝试获得锁,失败后进入park阻塞
    • 如果当前结点是紧邻着head的结点,那么再次tryAcquire尝试加锁,Thread-0仍然没有解锁,加锁失败
    • 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1,第一次返回false,不会立即让当前线程进入阻塞
    • shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquired尝试获取锁
    • 当再次进入shouldparkAfterFailedAcquire时,前驱node已经是-1,这次返回true
    • 进入parkAndCheckInterrupt,Thread-1被park

在这里插入图片描述

在这里插入图片描述
再有多个线程经历上述失败:
在这里插入图片描述
Thread-0调用unlock释放锁:
在这里插入图片描述

  1. 设置exclusiveOwnerThread为null,state为0
  2. 如果当前阻塞队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程,找到队列中离head最近并且没有取消的一个Node,unpark恢复其运行,本例中即为Thread-1
  3. 被恢复运行的Thread会回到acquireQueued流程,如果此时没有新的线程来竞争,则Thread-1会获得锁;如果此时来了一个新的线程,两个线程会开始竞争锁,可能新的线程竞争到锁,那么Thread-1又会回到阻塞队列中(非公平锁的体现)

锁重入原理

在这里插入图片描述
在这里插入图片描述

可打断原理

不可打断模式
在这里插入图片描述
被打断会修改打断标记,但是还是会继续循环加锁
可打断模式
在这里插入图片描述
当被打断时,会直接抛出异常,不再继续加锁

公平锁原理

通过带参构造器指定构造公平锁;
在这里插入图片描述
新线程加锁时,会先判断在阻塞队列中是否有线程在等待;没有才会加锁,优先让阻塞的线程加锁
在这里插入图片描述

条件变量原理

每个条件变量对应一个等待队列,其实现类是ConditionObject
await流程
开始Thread-0持有锁,调用await,进入ConditionObject的addConditionWaiter流程创建新的Node状态为-1(Node.CONDITION),关联Thread-0,加入等待队列尾部
在这里插入图片描述
single流程
假设Thread-1来唤醒Thread-0,取得等待队列的第一个Node,即Thread-0所在的Node,加入阻塞队列的尾部,修改前驱结点的waitStatus为-1
在这里插入图片描述

读写锁

读操作对数据没有修改,如果大量的读操作的情况,每个读操作都要对数据加锁,就会造成很多不必要的性能问题;当读操作远远高于写操作时,这是使用读写锁让读-读可以并发,提高性能

ReentrantRreadWriteLock

基本使用

@Slf4j
public final class Demo{
    public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer(new Object());
        Thread t1 = new Thread(()->{
            dataContainer.read();
//            dataContainer.write();
        },"t1");
        Thread t2 = new Thread(()->{
//            dataContainer.read();
            dataContainer.write();
        },"t2");

        t1.start();
        t2.start();
    }
}

/***
 * 数据容器
 * 将数据的读写操作分离,读操作和写操作使用两个不同的锁进行
 */
@Slf4j
class DataContainer{
    private Object data;
    /**
     * 读写锁
     */
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    /**
     * 读锁
     */
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    /**
     * 写锁
     */
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    public DataContainer(Object data){
        this.data = data;
    }
    /**
     * 读取操作
     * @return
     */
    public Object read(){
        log.debug("正在获取读锁");
        readLock.lock();
        log.debug("获取读锁成功");
        try{
            log.debug("正在读取数据");
            TimeUnit.SECONDS.sleep(1);
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            log.debug("正在释放读锁");
            readLock.unlock();
        }
    }

    /**
     * 写入操作
     */
    public void write(){
        log.debug("正在获取写锁");
        writeLock.lock();
        log.debug("获取写锁成功");
        try{
            log.debug("正在写入数据");
            TimeUnit.SECONDS.sleep(1 );
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.debug("正在释放写锁");
            writeLock.unlock();
        }
    }

}

读-读的情况,多个线程能同时获取读锁(非独占锁)
读-写和写-写的情况,多个线程不能同时获取锁(互斥锁)

注意事项

  • 读锁不支持条件变量(非独占锁)
  • 锁重入时,不支持升级;即持有读锁的情况下,不能获取写锁,会造成死锁
  • 锁重入时,支持降级;即持有写锁的情况下可以获取读锁

原理

读写锁用的时同一个Sync同步器,所以阻塞队列和state是同一个

加锁
加锁流程与ReentrantLock没有特殊之处,不同点在于,写锁状态占了state的低16位,读锁的使用的是state的高16位

写锁加锁
在这里插入图片描述
读锁加锁
在这里插入图片描述
如果加锁失败,读锁回创建共享结点,连续的共享结点会被同时唤醒,保证读操作的共享性

写锁解锁
在这里插入图片描述
读锁解锁
在这里插入图片描述

StampedLock

该类自JDK8加入,是为了进一步优化读的性能,特点是在使用读锁、写锁是都必须配合戳使用
加解读锁

long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读
StampLock支持tryOptimisticRead方法,读取完毕后需要做一次戳校验,如果校验通过,表示这段时间没有写操作,数据可以安全使用,如果校验没有通过,需要重新获取读锁,保证数据安全;这样可以在大量读操作的时候,没有出现写操作就不用对读加锁

long stamp = lock.tryOptimistcRead();
if(!lock.validate(stamp)){
	// 锁升级
}

Condition—条件变量

synchronized中也有条件变量,体现在wait/notify的使用中,当条件不满足时线程将进入waitSet等待;而JUC并发包中的Condition条件变量就是为了实现synchornized中的这个特征,而且比synchronized功能更强,可以支持多个条件变量

  • synchronized是不满足条件的线程都在一间休息室(waitSet)中休息
  • 而AQS条件变量支持多间休息室,每个条件对应一个waitSet

Condition和Object监视器对比

对比项Object Monitor MethodsCondition
前置条件获取对象的锁调用Lock.lock获取锁,并调用lock.newCondition获取条件对象
调用方式直接调用,object.wait直接调用,condition.await
等待队列个数一个多个
当前线程释放锁并进入等待状态支持支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态至将来某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值