Lock

避免死锁

可能发生死锁的条件中不可剥夺条件指的是

线程已经获得资源,在未使用完之前,不能被剥夺,只能在使用完时自己释放!

要想破坏这个条件,就需要具有申请不到资源,就释放已占有资源的能力使用synchronized,如果线程申请不到资源就会进入阻塞状态,做什么也改变不了它的状态,这是synchronized的致命弱点!

可以从下面三个方面来解决synchronized的局限(破坏不可剥夺条件):

特性 描述 API
能响应中断 如果不能自己释放,可以响应中断跳出阻塞等状态 lockInterruptibly()
非阻塞式的获取锁 尝试获取,获取不到不会阻塞,直接返回false tryLock()
支持超时 如果一段时间没获得锁,不是进入阻塞状态而是返回false tryLock(time, timeUnit)

显式锁Lock除了能完成synchronized关键字的功能外,还能通过显式锁对象提供的方法查看哪些线程被阻塞了;可以创建Condition对象进行线程间通信;可以中断由于获取锁而被阻塞的线程设置获取锁的超时时间

Lock与synchronized

synchronized Lock
释放锁 隐式锁,出了作用域自动释放 显示锁(手动开启和关闭锁)必须手动释放锁
类型 内置的Java关键字 Java接口
获取状态 无法判断获取锁的状态 可以判断是否获取到了锁
是否等待 线程1获得锁,线程2阻塞一直等待 不一定会等待(lock.tryLock())
重入/公平 可重入锁,不可以中断的,非公平 可重入锁,可以中断,是否公平可以自己设置
适用场景 适合锁少量的代码同步问题 适合锁大量的同步代码

Lock接口

public interface Lock {

    /**
     * 尝试获取锁
     * 1.如果此刻锁未被其他线程持有,会立即返回,并设置锁的hold计数为1;
     * 2.如果当前线程已经持有该锁,再次申请,hold计数加1,并立即返回;
     * 3.如果该锁当前被另一个线程持有,那么线程会进入阻塞,直到获得该锁
     * 注意:由于调用lock方法而进入阻塞状态的线程,同样不会被中断,会被放入AQS队列中阻塞挂起等待
     */
    void lock();

    /**
     * Acquires the lock unless the current thread is interrupted.
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 尝试非阻塞获取锁
     * 如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。
     * 该方法不会引起当前线程阻塞。
     */
    boolean tryLock();

    /**
     * 设置了超时时间,如果超时时间到了还没有获取到该锁则返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * Releases the lock.
     *
     */
    void unlock();

    /**
     * 创建一个与该lock相关联的Condition对象
     */
    Condition newCondition();
}
lock
public void lock()
获取锁。
如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。
如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一
直处于休眠状态,此时锁保持计数被设置为 1。

指定者:
接口 Lock 中的 lock
lockInterruptibly
public void lockInterruptibly() throws InterruptedException
1)如果当前线程未被中断,则获取锁。 

2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。 

3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。 

4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
前,该线程将一直处于休眠状态: 
     1)锁由当前线程获得;或者 

     2)其他某个线程中断当前线程。 

5)如果当前线程获得该锁,则将锁保持计数设置为 1。 
   如果当前线程: 
       1)在进入此方法时已经设置了该线程的中断状态;或者 

       2)在等待获取锁的同时被中断。 

   则抛出 InterruptedException,并且清除当前线程的已中断状态。 


6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
重入获取。 

指定者: 接口 Lock 中的 lockInterruptibly
抛出:   InterruptedException   如果当前线程已中断。
tryLock    public boolean tryLock()

仅在调用时锁未被另一个线程保持的情况下,才获取该锁。 

1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),
而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公
平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS) 
,它几乎是等效的(也检测中断)。 

2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。 

3)如果锁被另一个线程保持,则此方法将立即返回 false 值。 

指定者:
   接口 Lock 中的  tryLock
返回: 
   如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回 true;否则返回false

Lock使用范式

Lock lock = new ReentrantLock();
lock.lock();
// 在try{}外面获取锁

try {
    // ...
} finally {
	lock.unlock();  // 在finally中释放锁,目的是保证在获取到锁之后,最终能被释放
}

try{} 外获取锁主要考虑两个方面:

  • 如果没有获取到锁就抛出异常,最终释放锁肯定是有问题的,因为还未曾拥有锁谈何释放锁呢;
  • 如果在获取锁时抛出了异常,也就是当前线程并未获取到锁,但执行到finally代码时,如果恰巧别的线程获取到了锁,则会被释放掉。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 创建50个线程更新counter
 */

public class TestLock {
    public static void main(String[] args) throws InterruptedException {
        final int[] counter = {0};

        ReentrantLock lock = new ReentrantLock();

        for (int i = 0; i < 50; i++) {
            new Thread(
                    () -> {
                        lock.lock();
                        try {
                            int a = counter[0];
                            counter[0] = a + 1;
                        } finally {
                            lock.unlock();
                        }
                    }
            ).start();
        }

        TimeUnit.SECONDS.sleep(2);
        System.out.println(counter[0]);  // 50
    }
}

Lock是怎样起到锁的作用

使用synchronized时,在程序编译成CPU指令后,在临界区会有moniterentermoniterexit指令的出现,可以理解成进出临界区的标识。

锁类型 获取锁 释放锁
synchronized JVM指令monitor enter JVM指令monitor exit
Lock lock() unlock()

Lock和synchronized关键字一样都具备可重入性,Lock内部维护一个hold计数器,而synchronized内部则维护了monitor计数器。若成功获取锁的初始值为1,那么持有该锁时再次获取锁,除了会立即成功外,对应的计数器也会随之自增。

无论是synchronized关键字还是Lock,其主要作用都是保证代码指令的原子操作。

在ReentrantLock内部维护了一个volatile修饰的变量state,通过CAS来进行读写(最底层还是交给硬件来保证原子性和可见性),如果CAS更改成功,即获取到锁,线程进入到try代码块继续执行;如果没有更改成功,线程会被【挂起】,不会向下执行。

但Lock是一个接口,里面根本没有state这个变量的存在。它怎么处理这个state呢?

Lock接口的实现类基本都是通过【聚合】了一个队列同步器AQS的子类完成线程访问控制的!

posted @ 2021-06-27 11:02  chenzufeng  阅读(153)  评论(0编辑  收藏  举报