并发编程之Lock接口

Lock 接口简述

Lock 接口是 java.util.concurrent.locks 包下的接口,它是 jdk 1.5 后新增的
Lock 接口的类图如下
在这里插入图片描述

LockReadWriteLock 是两大锁的根接口。Lock 接口代表的实现类是 ReentrantLock(可重入锁),ReadWriteLock 接口的代表实现类是 ReentrantReadWriteLock

Lock 接口源码

public interface Lock {
    
    

    // 获取锁
    void lock();

    // 如果当前线程未被中断,则获取锁,可以响应中断  
    void lockInterruptibly() throws InterruptedException;

    // 仅在调用时锁为空闲状态才获取该锁,可以响应中断
    boolean tryLock();

    // 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // 释放锁
    void unlock();

    // 返回绑定到此 Lock 实例的新 Condition 实例
    Condition newCondition();
}

lock() 方法

lock() 方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。如果采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用 Lock 必须在 try…catch… 块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生

Lock lock = ...;
lock.lock();

try{
    
    
    //处理任务
}catch(Exception ex){
    
    

}finally{
    
    
    lock.unlock();   //释放锁
}

tryLock() & tryLock(long time, TimeUnit unit) 方法

  • tryLock() 方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true;如果获取失败(即锁已被其他线程获取),则返回 false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)
  • tryLock(long time, TimeUnit unit) 方法和 tryLock() 方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回 false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true
Lock lock = ...;

if(lock.tryLock()) {
    
    
     try{
    
    
         //处理任务
     }catch(Exception ex){
    
    

     }finally{
    
    
         lock.unlock();   //释放锁
     } 
}else {
    
    
    //如果不能获取锁,则直接做其他事情
}

lockInterruptibly() 方法

  • lockInterruptibly() 方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。例如,当两个线程同时通过 lock.lockInterruptibly() 想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等待,那么对线程 B 调用 threadB.interrupt() 方法能够中断线程 B 的等待过程
  • 由于 lockInterruptibly() 的声明中抛出了异常,所以 lock.lockInterruptibly() 必须放在 try 块中或者在调用 lockInterruptibly() 的方法外声明抛出 InterruptedException
public void method() throws InterruptedException {
    
    
    
    lock.lockInterruptibly();
    try {
    
      
     //.....
    }
    finally {
    
    
        lock.unlock();
    }  
}

SynchronizedReentrantLock

SynchronizedReentrantLock 的相似点

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)

SynchronizedReentrantLock 的功能区别

  • 这两种方式最大区别就是对于 Synchronized 来说,它是 java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现。而 ReentrantLock 它是 JDK 1.5 之后提供的 API 层面的互斥锁,需要 lock()unlock() 方法配合 try/finally 语句块来完成
  • 便利性:很明显 Synchronized 的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而 ReenTrantLock 需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在 finally 中声明释放锁
  • 锁的细粒度和灵活度:很明显 ReenTrantLock 优于 Synchronized

SynchronizedReentrantLock 的性能区别

Synchronized 优化以前,synchronized 的性能是比 ReenTrantLock 差很多的,但是自从 Synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized,其实 synchronized 的优化我感觉就借鉴了 ReenTrantLock 中的 CAS 技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞

Synchronized

Synchronized 经过编译,会在同步块的前后分别形成 monitorentermonitorexit 这个两个字节码指令

  • 在执行 monitorenter 指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加 1
  • 相应的,在执行 monitorexit 指令时会将锁计算器就减 1,当计算器为 0
    时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止

ReentrantLock

由于 ReentrantLockjava.util.concurrent 包下提供的一套互斥锁,相比 SynchronizedReentrantLock 类提供了一些高级功能,主要有以下 3 项:

  • 等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized 来说可以避免出现死锁的情况。通过 lock.lockInterruptibly() 来实现这个机制
  • 锁绑定多个条件:一个 ReentrantLock 对象可以同时绑定多个对象。ReenTrantLock 提供了一个 Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程
  • 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平锁,ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好
// 创建一个非公平锁,默认是非公平锁
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);
 
// 创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);

ReentrantLock 的实现原理

简单来说,ReenTrantLock 的实现是一种自旋锁,通过循环调用 CAS 操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙

ReentrantLock 的简单使用示例

public class SynDemo{
    
    
 
	public static void main(String[] arg){
    
    
		Runnable t1 = new MyThread();
    	new Thread(t1, "t1").start();
    	new Thread(t1, "t2").start();
	}
}

class MyThread implements Runnable {
    
    
 
	private Lock lock = new ReentrantLock();
	
	public void run() {
    
    
		lock.lock();
		try{
    
    
			for(int i = 0; i < 5; i++) {
    
    
				System.out.println(Thread.currentThread().getName()+":"+i);
			}
		}finally{
    
    
			lock.unlock();
		}
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_38192427/article/details/113804526