我们都知道在java中,当多个线程需要并发访问共享资源时需要使用同步,我们经常使用的同步方式就是synchronized关键字,事实上,在jdk1.5之前,只有synchronized一种同步方式。而在jdk1.5中提供了一种新的同步方式–显示锁(Lock)。显示锁是随java.util.concurrent包一起发布的,java.util.concurrent包是并发大神Doug Lea写的一个并发工具包,里面除了显示锁,还有许多其他的实用并发工具类。
什么是显示锁
什么是显示锁?用一段代码来说明:
package com.gome; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockPractice { private static int a=0; private static Lock lock=new ReentrantLock(); public static void increateBySynchronized(){ a=0; for (int i = 0; i < 1000; i++) { Thread t=new Thread(new Runnable() { public void run() { for (int j = 0; j < 100; j++) { synchronized (LockPractice.class) { a++; } } } }); t.start(); } while (Thread.activeCount()>1) { Thread.yield(); } System.out.println(a); } public static void increateByLock(){ a=0; for (int i = 0; i < 1000; i++) { Thread t=new Thread(new Runnable() { public void run() { for (int j = 0; j < 100; j++) { lock.lock(); try { a++; } finally { lock.unlock(); } } } }); t.start(); } while (Thread.activeCount()>1) { Thread.yield(); } System.out.println(a); } public static void main(String[] args) { increateBySynchronized(); increateByLock(); } }
执行结果:
解释:
类LockPractice 中有两个方法increateBySynchronized()和increateByLock(),这两个方法都成功地用多线程并发将a累加到100000而没有出现竞态条件(race condition)问题。
其中increateBySynchronized()的同步是我们熟悉的synchronized关键字实现的:
synchronized (LockPractice.class) { a++; }
这句代码的含义是:当有一个线程A进入synchronized代码块后,阻塞其他要进入该代码块的线程直到A执行完代码块。synchronized关键字会关联一个锁对象,这里是LockPractice.class。synchronized关键字底层是由jvm来实现的,当一个线程进入synchronized块时,会在关联的锁对象的对象头(MarkWord)中记录下线程信息(可以简单的理解为线程id),这样这个锁对象就被当前线程独占了,其他试图获取这个锁对象的线程将被阻塞。
因此一个线程进入、退出synchronized代码块的本质就是这个线程对锁对象的获取、释放。
而increateByLock()的同步代码如下,其中 lock是全局变量 private static Lock lock=new ReentrantLock();
lock.lock(); try { a++; } finally { lock.unlock(); }
从代码上可以看出,显示锁Lock的使用和synchronized的本质很像,也是定义了一个锁对象(new ReentrantLock()),然后在进入同步代码前加锁,执行同步代码后释放锁。
但是显示锁的底层却和synchronized完全不同,并没有使用到对象头(MarkWord)这样底层的东西,显示锁只是表现出了和synchronized一样的行为(第一个访问同步代码的线程获得锁,阻塞后来的线程)。
显示锁的优点
从上面的描述来看,显示锁实现了和synchronized一样的功能,但是写起来更复杂(需要手动加锁解锁,还需要写finally防止发生异常后锁不能释放),那为什么还要加入显示锁呢?
我们可以从Lock接口提供的方法看出端倪:
方法名称 | 描述 |
void lock() | 获取锁 |
void lockInterruptibly() throws InterruptedException | 可中断地获取锁,在线程获取锁的过程中可以响应中断 |
boolean tryLock() | 尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断 |
void unlock(); | 释放锁 |
Condition newCondition(); | 获取等待组件,等待组件实现类似于Object.wait()方法的功能 |
从Lock提供的接口可以看出来,显示锁至少比synchronized多了以下功能:
- 可中断获取锁:使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断(interrupt)的,而使用Lock.lockInterruptibly()获取锁时被中断,线程将抛出中断异常。
- 可非阻塞获取锁:使用sync关键字获取锁时,如果没有成功获取,只有被阻塞,而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。
- 可限定获取锁的超时时间:使用Lock.tryLock(long time, TimeUnit unit)。
- 其实显示锁还有其他的优势,比如同一锁对象上可以有多个等待队列(相当于Object.wait()),我们后面会讲。
其实除了更多的功能,显示锁还有一个很大的优势:synchronized的同步是jvm底层实现的,对一般程序员来说程序遇到出乎意料的行为的时候,除了查官方文档几乎没有别的办法;而显示锁除了个别操作用了底层的Unsafe类之外,几乎都是用java语言实现的,我们可以通过学习显示锁的源码,来更加得心应手的使用显示锁。
显示锁的缺点
当然显示锁也不是完美的,否则java就不会保留着synchronized关键字了,显示锁的缺点主要有两个:
- 使用比较复杂,这点之前提到了,需要手动加锁,解锁,而且还必须保证在异常状态下也要能够解锁。而synchronized的使用就简单多了。
- 效率较低,synchronized关键字毕竟是jvm底层实现的,因此用了很多优化措施来优化速度(偏向锁、轻量锁等),而显示锁的效率相对低一些。
因此当需要进行同步时,优先考虑使用synchronized关键字,只有synchronized关键字不能满足需求时,才考虑使用显示锁。
总结
这篇文章介绍了显示锁是什么,显示锁的优点与缺点,在什么情况下会用到显示锁。
后文将重点学习显示锁的底层实现:队列同步器(AbstractQueuedSynchronizer)的实现、重入锁(ReentrantLock)的实现、读写锁(ReadWriteLock)的实现、等待/通知(Condition)的实现。