Java 并发编程系列——锁

用户头像
孙苏勇
关注
发布于: 2020 年 05 月 02 日
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 lock
Thread 21 will get lock
Thread 20 will get lock
Thread 19 will get lock
Thread 18 will get lock
Thread 17 will get lock
Thread 16 will get lock
Thread 15 will get lock
Thread 14 will get lock
Thread 13 will get lock
Thread 14 got the lock
Thread 15 got the lock
Thread 16 got the lock
Thread 17 got the lock
Thread 18 got the lock
Thread 19 got the lock
Thread 20 got the lock
Thread 21 got the lock
Thread 12 got the lock
Thread 13 got the lock
Thread 14 released the lock
Thread 13 released the lock
Thread 21 released the lock
Thread 19 released the lock
Thread 20 released the lock
Thread 15 released the lock
Thread 17 released the lock
Thread 18 released the lock
Thread 16 released the lock
Thread 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 ms
the total is 0 and get total in 53 ms
the total is 0 and get total in 51 ms
the total is 0 and get total in 52 ms
the total is 0 and get total in 53 ms
the total is 0 and get total in 52 ms
the total is 0 and get total in 51 ms
the total is 0 and get total in 51 ms
the total is 0 and get total in 51 ms
the total is 0 and get total in 52 ms
the total is 0 and get total in 53 ms
set total 75 in 66 ms
the total is 75 and get total in 76 ms
the total is 75 and get total in 77 ms
the total is 75 and get total in 76 ms
the total is 75 and get total in 77 ms
the total is 75 and get total in 77 ms
the total is 75 and get total in 77 ms
the total is 75 and get total in 76 ms
the total is 75 and get total in 76 ms
the total is 75 and get total in 77 ms
the total is 75 and get total in 76 ms
set total 59 in 89 ms
the total is 59 and get total in 99 ms
the total is 59 and get total in 98 ms
the total is 59 and get total in 98 ms
the total is 59 and get total in 98 ms
the total is 59 and get total in 99 ms
the total is 59 and get total in 99 ms
the total is 59 and get total in 99 ms
the total is 59 and get total in 99 ms
the total is 59 and get total in 99 ms
the total is 59 and get total in 102 ms



如果不使用读写锁将读写分离,我们可以看下执行的时间。

set total 85 in 10 ms
the total is 85 and get total in 54 ms
the total is 85 and get total in 63 ms
the total is 85 and get total in 76 ms
the total is 85 and get total in 87 ms
the total is 85 and get total in 98 ms
the total is 85 and get total in 109 ms
the total is 85 and get total in 120 ms
the total is 85 and get total in 130 ms
the total is 85 and get total in 141 ms
the total is 85 and get total in 154 ms
set total 70 in 166 ms
the total is 70 and get total in 175 ms
the total is 70 and get total in 189 ms
the total is 70 and get total in 201 ms
the total is 70 and get total in 213 ms
the total is 70 and get total in 224 ms
the total is 70 and get total in 238 ms
the total is 70 and get total in 250 ms
the total is 70 and get total in 261 ms
the total is 70 and get total in 273 ms
the total is 70 and get total in 285 ms
set total 73 in 295 ms
the total is 73 and get total in 305 ms
the total is 73 and get total in 316 ms
the total is 73 and get total in 329 ms
the total is 73 and get total in 343 ms
the total is 73 and get total in 357 ms
the total is 73 and get total in 369 ms
the total is 73 and get total in 380 ms
the total is 73 and get total in 394 ms
the total is 73 and get total in 406 ms
the total is 73 and get total in 417 ms



可以看到,比用读写锁时间长了不少。



如果在某场景下仅有一个线程写,多个线程读,那么使用volatile就可以了。



在实例化锁的时候会注意到构造函数中可以指定是否为公平锁。所谓公平,就是先申请的线程先拿到锁,但锁默认是使用非公平锁的,这主要是出于性能的考虑,为保证公平必定有额外的开销要进行线程的调度,如果某线程迟迟不结束,其他线程也必须等待,如无必要,通常并不使用公平锁。



本系列其他文章:

Java并发编程系列——常用并发工具类

Java并发编程系列——Fork-Join

Java并发编程系列——线程的等待与唤醒

Java并发编程系列插曲——对象的内存结构

Java并发编程系列——线程

发布于: 2020 年 05 月 02 日 阅读数: 111
用户头像

孙苏勇

关注

不读书,思想就会停止。 2018.04.05 加入

公众号“像什么",记录想记录的。

评论

发布
暂无评论
Java并发编程系列——锁