Java 并发编程系列——锁
在并发编程中通常我们使用synchronized来加锁,这篇回顾几个其他的加锁方式。
先介绍ReentrantLock。通常情况下,使用synchronized就够用了,但是Java内部不少地方采用了ReentranLock,还是有必要了解下其基本的用法。先提供一个使用锁的一般范式,如下
lock.lock();try { doSomething();} finally { lock.unlock();}
这里需要特别注意的是一定要保证锁的释放,所以一般将unlock的操作放在finally中。接下来就用一个示例来说明,如下:
public class ShowLock { public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new LockRunnable()).start(); } } private static class LockRunnable implements Runnable { private static final Lock lock = new ReentrantLock(); @Override public void run() { System.out.printf("Thread %d will get lock\n", Thread.currentThread().getId()); lock.lock(); System.out.printf("Thread %d got the lock\n", Thread.currentThread().getId()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.printf("Thread %d released the lock\n", Thread.currentThread().getId()); } } }}
相应的结果:
Thread 12 will get lockThread 21 will get lockThread 20 will get lockThread 19 will get lockThread 18 will get lockThread 17 will get lockThread 16 will get lockThread 15 will get lockThread 14 will get lockThread 13 will get lockThread 14 got the lockThread 15 got the lockThread 16 got the lockThread 17 got the lockThread 18 got the lockThread 19 got the lockThread 20 got the lockThread 21 got the lockThread 12 got the lockThread 13 got the lockThread 14 released the lockThread 13 released the lockThread 21 released the lockThread 19 released the lockThread 20 released the lockThread 15 released the lockThread 17 released the lockThread 18 released the lockThread 16 released the lockThread 12 released the lock
可以看出其效果与使用synchronized一样。接下来,我们用Lock中Condition来改造一下之前的wait/notify,之前的例子notifyAll会唤醒相关的所有线程,即使线程针对的是不同条件,用Condition可以根据不同的条件来唤醒不同的线程,控制更为精细。用Condition改造后的wait/notify如下:
public class ShowCondition { public static void main(String[] args) throws InterruptedException { Show show = new Show(); for (int i = 0; i < 3; i++) { new Thread(() -> show.checkCondition2()).start(); new Thread(() -> show.checkCondition1()).start(); } Thread.sleep(2000); System.out.println(); show.changeCondition1(8); Thread.sleep(1000); System.out.println(); show.changeCondition1(11); Thread.sleep(1000); System.out.println(); show.changeCondition2(true); Thread.sleep(1000); } private static class Show { private final Lock lock = new ReentrantLock(); private final Condition lockCondition1 = lock.newCondition(); private final Condition lockCondition2 = lock.newCondition(); private int condition1 = 0; private boolean condition2 = false; public void checkCondition1() { lock.lock(); try { while (this.condition1 <= 10) { try { System.out.printf("thread %d start check conditions1 now\n", Thread.currentThread().getId()); lockCondition1.await(); System.out.printf("condition1 is %d, waiting in thread %d\n", this.condition1, Thread.currentThread().getId()); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("condition1 is %d now, bigger than 10, thread %d over\n", this.condition1, Thread.currentThread().getId()); } finally { lock.unlock(); } } public void checkCondition2() { lock.lock(); try { while (!condition2) { try { System.out.printf("thread %d start check conditions2 now\n", Thread.currentThread().getId()); lockCondition2.await(); System.out.printf("condition2 is false, waiting in thread %d\n", Thread.currentThread().getId()); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("condition2 is true now, thread %d over\n", Thread.currentThread().getId()); } finally { lock.unlock(); } } public void changeCondition1(int condition1) { lock.lock(); try { this.condition1 = condition1; lockCondition1.signalAll(); } finally { lock.unlock(); } } public void changeCondition2(boolean condition2) { lock.lock(); try { this.condition2 = condition2; lockCondition2.signalAll(); } finally { lock.unlock(); } } }}
如代码中所示,我们用两个Condition分别作为等待与唤醒的条件,这样就可以实现精确的唤醒操作,这里仍然用notifyAll()来唤醒与该条件相关的线程,如果仅希望唤醒其中的一个则使用notify()。
接下来介绍另一种常用锁——读写锁。在很多业务场景下,我们是大量的线程需要读取,而仅有少量线程会执行写入,这种情况下读写锁会大幅提高并发能力。读锁控制部分允许多个线程同时读取,进入写锁时所有读写线程均被阻塞,只能有一个线程写入,这样保证了正确性同样提高了整个并发能力。仍然用一个示例来说明,如下:
public class ShowReadWriteLock { private static ReadWrite readWrite = new ReadWrite(); public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(new WriteRunnable()).start(); for (int j = 0; j < 10; j++) { new Thread(new ReadRunnable()).start(); } } } private static class ReadRunnable implements Runnable { @Override public void run() { int total = readWrite.getTotal(); } } private static class WriteRunnable implements Runnable { @Override public void run() { int random = new Random().nextInt(100); readWrite.setTotal(random); } } private static class ReadWrite { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); private int total; public int getTotal() { long start = System.currentTimeMillis(); readLock.lock(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.printf("the total is %d and get total in %d ms\n", total, System.currentTimeMillis() - start); readLock.unlock(); } return this.total; } public void setTotal(int total) { long start = System.currentTimeMillis(); writeLock.lock(); try { Thread.sleep(10); this.total = total; System.out.printf("set total %d in %d ms\n", total, System.currentTimeMillis() - start); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); } } }}
执行结果如下,可以看出在读多写少的情况下,阻塞的时间相对短很多。
set total 0 in 12 msthe total is 0 and get total in 53 msthe total is 0 and get total in 51 msthe total is 0 and get total in 52 msthe total is 0 and get total in 53 msthe total is 0 and get total in 52 msthe total is 0 and get total in 51 msthe total is 0 and get total in 51 msthe total is 0 and get total in 51 msthe total is 0 and get total in 52 msthe total is 0 and get total in 53 msset total 75 in 66 msthe total is 75 and get total in 76 msthe total is 75 and get total in 77 msthe total is 75 and get total in 76 msthe total is 75 and get total in 77 msthe total is 75 and get total in 77 msthe total is 75 and get total in 77 msthe total is 75 and get total in 76 msthe total is 75 and get total in 76 msthe total is 75 and get total in 77 msthe total is 75 and get total in 76 msset total 59 in 89 msthe total is 59 and get total in 99 msthe total is 59 and get total in 98 msthe total is 59 and get total in 98 msthe total is 59 and get total in 98 msthe total is 59 and get total in 99 msthe total is 59 and get total in 99 msthe total is 59 and get total in 99 msthe total is 59 and get total in 99 msthe total is 59 and get total in 99 msthe total is 59 and get total in 102 ms
如果不使用读写锁将读写分离,我们可以看下执行的时间。
set total 85 in 10 msthe total is 85 and get total in 54 msthe total is 85 and get total in 63 msthe total is 85 and get total in 76 msthe total is 85 and get total in 87 msthe total is 85 and get total in 98 msthe total is 85 and get total in 109 msthe total is 85 and get total in 120 msthe total is 85 and get total in 130 msthe total is 85 and get total in 141 msthe total is 85 and get total in 154 msset total 70 in 166 msthe total is 70 and get total in 175 msthe total is 70 and get total in 189 msthe total is 70 and get total in 201 msthe total is 70 and get total in 213 msthe total is 70 and get total in 224 msthe total is 70 and get total in 238 msthe total is 70 and get total in 250 msthe total is 70 and get total in 261 msthe total is 70 and get total in 273 msthe total is 70 and get total in 285 msset total 73 in 295 msthe total is 73 and get total in 305 msthe total is 73 and get total in 316 msthe total is 73 and get total in 329 msthe total is 73 and get total in 343 msthe total is 73 and get total in 357 msthe total is 73 and get total in 369 msthe total is 73 and get total in 380 msthe total is 73 and get total in 394 msthe total is 73 and get total in 406 msthe total is 73 and get total in 417 ms
可以看到,比用读写锁时间长了不少。
如果在某场景下仅有一个线程写,多个线程读,那么使用volatile就可以了。
在实例化锁的时候会注意到构造函数中可以指定是否为公平锁。所谓公平,就是先申请的线程先拿到锁,但锁默认是使用非公平锁的,这主要是出于性能的考虑,为保证公平必定有额外的开销要进行线程的调度,如果某线程迟迟不结束,其他线程也必须等待,如无必要,通常并不使用公平锁。
本系列其他文章:
版权声明: 本文为 InfoQ 作者【孙苏勇】的原创文章。
原文链接:【http://xie.infoq.cn/article/9b34fc0675a371ab84ab4dfbd】。文章转载请联系作者。
孙苏勇
不读书,思想就会停止。 2018.04.05 加入
公众号“像什么",记录想记录的。
评论