Lock显式锁是在JDK1.5引入的,在JDK1.5之前处理多线程并发使用的是synchronized和volatile关键字。在JDK1.5之后增加了一种新的机制Lock,虽然与synchronized类似都是提供加锁机制,但是Lock锁并不是提供了一种替代内置锁synchronized的方式,而是当内置锁机制不适用时,提供了一种可选择的更高级的功能。
synchronized的局限性
synchronized是Java关键字,是JVM的内置属性,当一个线程获取到内置synchronized锁后,是不可以手动释放锁的,其它线程只能一直等待占有锁的线程释放锁。如果使用内置锁synchronized添加锁,那么释放锁一般以如下三种情况之一:
占有锁的线程执行完了该代码块,然后释放对锁的占有;
占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。
原因之一,使用synchronized时无法中断一个正在获取锁的线程,因此在synchronized修饰的线程中如果产生死锁,我们无法使用interrupt()方法中断线程,示例代码文章后面会提供。但是如果使用显示锁Lock的lockInterruptibly()方法线程则会响应中断。
原因之二,在某些场景下效率会降低。由于互斥性是一种强硬的加锁策略,因此synchronized在加锁时,因此也就不必要的限制了并发性。互斥是一种保守的加锁策略,虽然可避免“写/写”冲突和“写/读”冲突,但是也同样不可避免的设置了“读/读”冲突,在某些开发场景下,如果“读/读”场景使用过多则会很大程度降低程序运行的效率。
原因之三,使用内置锁synchronized无法判断线程有没有成功获取到锁。
上面三种大致说明了一下synchronized锁的局限性,而且这三种局限性都可以使用显示锁Lock解决。这里也就验证了文章开始所说的当内置锁机制不适用时,显示锁Lock提供了一种可选择的更高级的功能,关于显示锁Lock相关的类位于java.util.concurrent.locks包下。
显示锁Lock相关方法及类介绍
Lock接口定义了一组抽象的加锁操作,Lock所有的加锁以及解锁都是显示的,Lock接口提供的方法如下:
方法 | 描述 |
---|---|
void lock() | 获取锁。 |
void lockInterruptibly() | 如果当前线程未被中断,则获取锁。 |
Condition newCondition() | 返回绑定到此 Lock 实例的新 Condition 实例。 |
boolean tryLock() | 仅在调用时锁为空闲状态才获取该锁。 |
boolean tryLock(long time, TimeUnit unit) | 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 |
void unlock() | 释放锁。 |
lock方法
下面是使用Lock锁的标准形式,使用显示锁Lock比内置锁synchronized稍微复杂一些,必须在finally中释放锁。否则在被保护的代码块中发生异常时,将永远无法释放锁。
1 2 3 4 5 6 7 8 | Lock lock = ...; lock.lock(); try{ //处理任务 //捕获异常,并在必要时恢复不变形条件 }finally{ lock.unlock(); //释放锁 } |
在显示锁使用过程中一定不可以忘记释放锁,否则一旦发生异常将很难定位到发生异常的地方,因为没有记录应该释放锁的位置和时间。这也是显示锁Lock不能完全替代synchronized内置锁的原因,因为当程序执行完成离开代码块时不会自动释放锁,虽然在finally中释放锁很容易,可是也容易忘记。
tryLock方法
tryLock()方法是有返回值的,如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。
tryLock()还有一个带参数的方法tryLock(long time, TimeUnit unit)如果锁可用,则此方法将立即返回值true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:
锁由当前线程获得;
其他某个线程中断当前线程,并且支持对锁获取的中断;
已超过指定的等待时间。
如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。
1 2 3 4 5 6 7 8 9 10 11 | Lock lock = ...; if (lock.tryLock()) { try { //处理任务 //捕获异常,并在必要时恢复不变形条件 } finally { lock.unlock(); } } else { // 如果不能获取锁,则直接做其他事情 } |
lockInterruptibly方法
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。显示锁可以中断等待获取锁就是通过该方法,而内置锁synchronized没有办法中断一个正在获取锁的线程的。下面是一个简单的demo验证lockInterruptibly()方法与synchronized锁不同,让两个线程产生死锁,然后分别调用线程的interrupt()方法中断线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | public class Thread01 extends Thread{
private Object resource01; private Object resource02; public Thread01(Object resource01, Object resource02) { this.resource01 = resource01; this.resource02 = resource02; }
@Override public void run() { synchronized(resource01){ System.out.println("Thread01 locked resource01"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource02) { System.out.println("Thread01 locked resource02"); } } } } //Thread02代码直接将两个所交换一下即可 public class MainTest { public static void main(String[] args) { final Object resource01="resource01"; final Object resource02="resource02"; Thread01 thread01=new Thread01(resource01, resource02); Thread02 thread02=new Thread02(resource01, resource02); thread01.start(); thread02.start(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } thread01.interrupt(); thread02.interrupt(); }
} |
运行上面代码可以发现,使用synchronized内置锁产生死锁后再次调用interrupt()方法并没有中断对应的线程,下面看一下使用Lock的lockInterruptibly()方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | public class Thread01 extends Thread{
private Lock lock01; private Lock lock02; public Thread01(Lock lock01, Lock lock02) { this.lock01 = lock01; this.lock02 = lock02; }
public void run() { try { lock02.lockInterruptibly(); System.out.println("Thread01 locked02"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } lock01.lockInterruptibly(); System.out.println("Thread01 locked01"); } catch (Exception e) { e.printStackTrace(); } finally { lock01.unlock(); lock02.unlock(); } } } //Thread02代码直接将两个所交换一下即可 public class MainTest { public static void main(String[] args) { final Lock lock01=new ReentrantLock(); final Lock lock02=new ReentrantLock(); Thread01 thread01=new Thread01(lock01, lock02); Thread02 thread02=new Thread02(lock01, lock02); thread01.start(); thread02.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread01.interrupt(); thread02.interrupt(); }
} |
这里运行之后发现死锁已经解除,因为对应的线程都响应了中断。
ReentrantLock
ReentrantLock类实现了Lock接口,该类可以说是显示锁最常使用的一个类。它一个可重入的互斥锁 Lock,它具有与使用 synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。该类的构造方法接受一个可选的boolean类型fair参数,默认值为false。当设置为 true时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,也就是有序的按照FIFO。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。
ReadWriteLock与ReentrantReadWriteLock
1 2 3 4 5 6 7 | public interface ReadWriteLock {
Lock readLock(); Lock writeLock(); } |
读写锁ReadWriteLock有两个锁,一个用于读操作一个用于写操作。ReadWriteLock读写锁的出现是为了解决文章开始所说的“读/读”操作synchronized效率低的问题,读写锁允许在多CPU中真正的并行操作,因此效率上比synchronized高一些。但是这种效率的高也不是一定的,由于ReadWriteLock也是使用的Lock实现的读写部分,因此发现使用读写锁没有提高性能,那么则可以替换为独占锁实现。
ReentrantReadWriteLock实现了ReadWriteLock接口,也为读和写提供了可重入的语义。ReentrantReadWriteLock类也提供了类似ReentrantLock构造方法,该类构造方法接受一个可选的boolean类型fair参数,默认值为false。读写锁在读的时候虽然允许有多个持有者,但是写入锁只能有一个持有者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public class MainTest { private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static long start;
public static void main(String[] args) { final MainTest test = new MainTest(); start = System.currentTimeMillis(); new Thread() { public void run() { test.get(Thread.currentThread()); }; }.start();
new Thread() { public void run() { test.get(Thread.currentThread()); }; }.start();
}
public void get(Thread thread) { rwl.readLock().lock(); try { for (int i = 0; i < 1000; i++) { System.out.println(thread.getName() + "正在进行读操作"); } System.out.println(thread.getName() + "读操作完毕 " + (System.currentTimeMillis() - start)); } finally { rwl.readLock().unlock(); } } } |
显示锁Lock与内置锁synchronized选择
显示锁Lock在加锁和内存上提供了与内置锁synchronized相同的语义。
显示锁Lock是一个接口,是JDK层面上提供了,而synchronized是Java关键字,是在JVM层面实现的。
显示锁Lock必须手动释放锁,无论执行过程中是否发生异常,否则就如同一个定时炸弹,容易发生死锁且不容易定位,而内置锁synchronized在使用后会自动释放锁,这一点上面不容易发生死锁。
显示锁Lock可以知道是否已经成功获取锁,并且可以响应中断,则是内置锁synchronized无法办到的,同样显示锁Lock还提供了读写锁ReentrantReadWriteLock,在多核CPU上面可以真正的实现并行执行。
内置锁synchronized简洁紧凑,为多数开发人员熟悉,且在开发中是被最常用的一种加锁方式,而且许多现有的程序都是内置锁实现,除非在开发过程中已经明确验证显示锁Lock确实可以提供代码执行效率,否则还是直接使用内置锁synchronized。
与内置锁synchronized相比,显示锁Lock提供了一些功能扩展,在处理锁上面具有更高的灵活性,但是显示Lock不能完全代替内置锁synchronized,只有在内置锁synchronized无法满足时才可以使用显示锁。