您当前的位置: 首页 > 

止步前行

暂无认证

  • 1浏览

    0关注

    247博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

并发中的Lock框架

止步前行 发布时间:2018-11-20 21:12:52 ,浏览量:1

一、引言

在上一篇文章中,对 synchronized 进行了介绍。 我们知道它是 Java 的关键字,属于 Java 的内置特性,是在 JVM 层面实现了对临界资源的同步互斥访问,但 synchronized 的粒度有些大,在处理实际问题时存在诸多局限性。比如:响应中断等。而下面要介绍的 Lock 提供了比 synchronized 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。

下面以 synchronized 与 Lock 的对比作为切入点,对 Java 中的 Lock 框架进行了学习介绍,最后给出了锁的一些相关概念。

二、 synchronized 与 Lock 的对比

假设一个代码块被 synchronized 关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,直到占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

a.	占有锁的线程执行完了该代码块,然后释放对锁的占有;(正常释放锁)

b.	占有锁线程执行发生异常,此时JVM会让线程自动释放锁;(异常释放锁)

c.	占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。(主动释放锁)

我们知道,synchronized 是 Java 语言的内置特性,它可以轻松实现对临界资源的同步互斥访问。那么,为什么还会出现 Lock 呢?下面我们试着考虑以下三种情况:

(1)、情况一:

在使用 synchronized 关键字的情形下,假设占有锁的线程由于要等待IO或者其他原因被阻塞了(比如调用sleep()方法),但是又没有释放锁,那么其他线程就只能一直等待,这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。

(2)、情况二:

当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用 synchronized 关键字实现同步读的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

(3)、情况三:

我们可以通过 Lock 得知线程有没有成功获取到锁 (解决方案:ReentrantLock),但这个是 synchronized 无法办到的。

上面三种情形,我们都可以通过 Lock 来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口。 实现提供了比 synchronized 关键字 更灵活、更广泛、粒度更细 的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock 提供了比 synchronized 更多的功能。但是要注意以下几点:

1)、 synchronized 是 Java 的关键字,因此是 Java 的内置特性,是基于JVM层面实现的,其经过编译之后,会在同步块的前后分别
形成 monitorenter 和 monitorexit 两个字节码指令;而 Lock 是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现
同步访问;

2)、 采用 synchronized 方式不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动
让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁 (发生异常时,不会自动释放锁),如果没有主动释放锁,就有可能
导致死锁现象。

这是很好理解的。synchronized 方式是 Java 原生支持的,开发人员在使用它来解决并发问题时,一定会方便很多,在这里,开发人员就不需要手动获取锁和释放锁,这些操作均有 Java 自身自动完成;而Lock方式是JDK层面的提供给开发人员的接口,因此开发人员在使用它来解决并发问题时,需要手动获取锁和释放锁。

三、 java.util.concurrent.locks包下常用的类与接口

以下是 java.util.concurrent.locks包下主要常用的类与接口的关系: 在这里插入图片描述

1、Lock

通过查看 Lock 的源码可知,Lock 是一个接口:

	public interface Lock {
	    void lock();
	    void lockInterruptibly() throws InterruptedException;  // 可以响应中断
	    boolean tryLock();
	    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  // 可以响应中断
	    void unlock();
	    Condition newCondition();
	}

下面来逐个分析 Lock 接口中每个方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作。

(1)、 lock()

在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?首先,lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

	Lock lock = ...;
	lock.lock();
	try{
	    //处理任务
	}catch(Exception ex){
	
	}finally{
	    lock.unlock();   //释放锁
	}
(2)、 tryLock() & tryLock(long time, TimeUnit unit)

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断(即不用等待需要访问的临界资源,立刻执行下面的代码)。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

一般情况下,通过tryLock()来获取锁时是这样使用的:

	Lock lock = ...;
	if(lock.tryLock()) {
	     try{
	         //处理任务
	     }catch(Exception ex){
	
	     }finally{
	         lock.unlock();   //释放锁
	     } 
	}else {
	    //如果不能获取锁,则直接做其他事情
	}
(3)、 lockInterruptibly()

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者,原因稍后阐述。因此,lockInterruptibly()一般的使用形式如下:

	public void method() throws InterruptedException {
	    lock.lockInterruptibly();
	    try {  
	     //.....
	    }
	    finally {
	        lock.unlock();
	    }  
	}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

总结:在使用Lock时,无论以哪种方式获取锁,习惯上最好一律将获取锁的代码放到 try…catch…,因为我们一般将锁的unlock操作放到finally子句中,如果线程没有获取到锁,在执行finally子句时,就会执行unlock操作,从而抛出 IllegalMonitorStateException,因为该线程并未获得到锁却执行了解锁操作。

2、ReentrantLock

ReentrantLock,即可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些例子学习如何使用 ReentrantLock。

例 1 : Lock 的正确使用
	public class Test {

	    private ArrayList arrayList = new ArrayList();
	
	    public static void main(String[] args) {
	        final Test test = new Test();
	
	        new Thread("A") {
	            public void run() {
	                test.insert(Thread.currentThread());
	            };
	        }.start();
	
	        new Thread("B") {
	            public void run() {
	                test.insert(Thread.currentThread());
	            };
	        }.start();
	    }
	
	    public void insert(Thread thread) {
	        Lock lock = new ReentrantLock();  // 注意这个地方:lock被声明为局部变量
	        lock.lock();
	        try {
	            System.out.println("线程" + thread.getName() + "得到了锁...");
	            for (int i = 0; i             
关注
打赏
1657848381
查看更多评论
0.0539s