Java并发编程(五)锁的使用(下)

显式锁

上篇讲了使用synchronized关键字来定义锁,其实Java除了使用这个关键字外还可以使用Lock接口及其实现的子类来定义锁,ReentrantLock类是Lock接口的一个实现,Reentrant是“重入”的意思,因此这个锁也是支持重入的,这里就不再测试它的重入性了,感兴趣的同学可以自行测试。这种方式是Java在jdk1.5才引入进来的功能,它的功能比synchronized关键字更为强大,但也有一个缺点:使用起来比synchronized关键字更麻烦。使用Lock来实现value++,代码如下:

class Entity {
public static int value = 0;
}
class IncreaseThread implements Runnable {
private static Lock lock = new ReentrantLock();
public void run() {
for(int i=0; i <100000; i++){
lock.lock();
try {
Entity.value++;
}
finally {
lock.unlock();
}
}
}
}
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new IncreaseThread());
exec.execute(new IncreaseThread());
exec.shutdown();
Thread.sleep(5000);
System.out.println("Value = " + Entity.value);
}
}

  

运行5秒后输出如下结果:

Value = 200000

在定义lock时将其声明为static的,因此两个IncreaseThread对象可以共用一个lock对象;如果使用的不是同一个对象,尝试获取的就不是同一个锁了,也就不会互斥。代码执行到lock()方法时会检查锁是否被占用,如果没有被占用则直接获取锁并执行下面的代码,如果已经被占用了则阻塞当前线程,等待锁被其他线程释放。使用lock设置的临界区从调用lock()方法开始,到调用unlock()方法结束,本例在try块中定义要执行的代码,在finally块中调用unlock()方法是一种常用的方式,这样即使要执行的代码中抛出异常,也可以保证锁会被正常释放。

试图获取锁

tryLock方法给我们提供了两种尝试获取锁的方式,即根据是否获取到了锁来执行不同的策略,我们先介绍两个方法的功能。

tryLock():试图获取锁,如果锁没有被占用则得到锁、返回true、继续执行下面的代码;如果锁被占用了则立即返回false(不会被阻塞)、继续执行下面的代码。

tryLock(long time, TimeUnit unit):与tryLock()类似,这个方法可以指定等待锁的最长时间,在这段时间内当前线程会被阻塞。如果时间内获得了锁则返回true并提前结束阻塞,反之返回false。

具体代码如下:

class TryToGetLockThread implements Runnable {
public void run() {
boolean alreadyGetLock = TryLockTest.lock.tryLock();
if(alreadyGetLock) {
try {
System.out.println("新线程:我拿到锁了");
Thread.sleep(3000);//持有锁3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
System.out.println("新线程:我要释放锁了 ");
TryLockTest.lock.unlock();
}
}
}
}
public class TryLockTest {
public static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new TryToGetLockThread());
exec.shutdown();
boolean alreadyGetLock = lock.tryLock(2, TimeUnit.SECONDS);
if(alreadyGetLock) {
try {
System.out.println("主线程:我拿到锁了");
}
finally {
TryLockTest.lock.unlock();
}
}
else {
System.out.println("主线程:没拿到锁,可以做点别的事情");
}
}
}

运行程序后,输出结果如下,其中第一条是立即输出的,第二句是两秒之后输出的,第三句是三秒后输出的:

新线程:我拿到锁了

主线程:没拿到锁,可以做点别的事情

新线程:我要释放锁了

本例中我们创建了一个新线程,新线程使用tryLock()方法先获得了锁,持有这个锁3秒钟。主线程使用tryLock(long time, TimeUnit unit)方法来试图获取锁,由于新线程持有锁3秒,因此主线程执行lock.tryLock(2, TimeUnit.SECONDS)等待两秒后就放弃了获取锁,返回false;如果主线程等待4秒,那么它就可以得到锁,从而得到不同的结果,感兴趣的同学可以自行测试。

锁的对象是谁

有的同学可能觉得这个锁对应的是这个锁的对象(lock),实际上不是的,这个锁没有对应的锁对象,因此synchronized关键字和显式锁之间不能产生互斥效果。

让我们做一个测试:

class LockTest {
private static Lock lock = new ReentrantLock();
public static void neverStopMethod() {
lock.lock();
try {
while(true){}//Never stop
}
finally{
lock.unlock();
}
}
public static void getLockMethod() {
synchronized(lock) {
System.out.println("我得到了class锁");
}
}
}
class NeverStopThread implements Runnable {
public void run() {
LockTest.neverStopMethod();
}
}
public class TryToGetSameLock {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new NeverStopThread());
exec.shutdown();
Thread.sleep(100);//等待0.1秒,确保新建的线程已经获得了锁
LockTest.getLockMethod();
}
}

程序运行后,立即输出以下结果,并且程序始终没有退出

我得到了class锁

neverStopMethod()方法使用显式锁,getLockMethod()使用内置锁来获取lock对象的锁,我们通过线程池创建了一个调用neverStopMethod()方法的线程,主线程等待0.1秒后再通过getLockMethod()方法来获取锁。如果这两个锁是互斥的,getLockMethod()会一直等待新线程释放锁,但是很遗憾,它没有等待而是直接获取了锁。

锁的公平性

公平锁:按照访问锁的先后顺序排队,先到先得,这个很好理解。

非公平锁:当一个线程想获取锁时,先试图插队,如果刚好占用锁的线程释放了锁,下一个线程还没来得及拿锁,那么当前线程就可以直接获得锁;如果锁正在被其它线程占用,则排队,排队的时候就不能再试图获得锁了,只能等到前面所有线程都执行完才能获得锁。

synchronized关键字和默认情况下的ReentrantLock都是非公平锁。非公平锁比公平锁的性能更好,假设线程一是队列中的第一个,线程二是想要插队的线程,当占用锁的线程释放了锁时JVM有两种选择:

1.阻塞线程二,启动线程一。

2.线程一的状态维持不变继续阻塞,线程二的状态也维持不变继续运行。线程状态的切换是耗费时间的,因此方案二的性能更好。

ReentrantLock类默认的构造方法是非公平锁,如果需要设置为非公平锁,只需要调用ReentrantLock(boolean fair)方法指定即可。

总结

最后我们来盘点一下synchronized关键字和ReentrantLock类各自的优势:

synchronized关键字的优势:

使用简单,不用显示释放锁,编写代码更简洁;ReentrantLock类需要显示释放锁,unlock()方法要写在finally块中,否则会有死锁的风险。

ReentrantLock类的优势:

提供了更多的特性,比如设置锁的公平性,检查锁是否被占用等功能;synchronized关键字只能是公平锁,并且没有额外的特性。

一般来讲我们不需要额外功能的时候才会使用ReentrantLock类,否则都是使用synchronized关键字。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

上一篇:c# – 如何检查值是否可以转换为泛型类型?


下一篇:c# – 我是否过度使用Nullable类型?