🍤 并发操作合集系列 目录
🍕 并发操作合集系列 源代码
并发API支持多种显式的锁,它们由Lock
接口规定,用于代替synchronized
的隐式锁。锁对细粒度的控制支持多种方法,因此它们比隐式的监视器具有更大的开销。
Lock接口和synchronized关键字的差别
两者的区别如下:
类别 |
synchronized |
Lock |
存在层次 |
Java的关键字,在jvm层面上 |
是一个类 |
锁的释放 |
1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 |
在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 |
假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 |
分情况而定,Lock有多个锁获锁的方式,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 |
无法判断 |
可以判断 |
锁类型 |
可重入 不可中断 非公平 |
可重入 可中断 可公平(两者皆可) |
性能 |
少量同步 |
大量同步 |
表中提到了所类型,先了解一下锁类型都有哪些:
- 可重入锁:在执行对象中所有同步方法不用再次获得锁
- 可中断锁:在等待获取锁过程中可中断
- 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
- 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
Lock接口定义的方法和使用
lock():获取锁,如果锁被暂用则一直等待
unlock():释放锁
tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
lockInterruptibly():锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
通过 以上的解释,大致可以解释在上个部分中“锁类型(lockInterruptibly())”,“锁状态(tryLock())”等问题,还有就是前面子所获取的过程我所写的“大致就是可以尝试获得锁,线程可以不会一直等待”用了“可以”的原因。
简单看一下比较常用的lock()和tryLock()方法的使用方式,启动两个线程来调用这些方法:
lock()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| private void testLock(Thread thread) { lock.lock(); try { System.out.println("线程名" + thread.getName() + "获得了锁"); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("线程名" + thread.getName() + "释放了锁"); lock.unlock(); } } --------------------------- 输出 线程名Thread-1获得了锁 线程名Thread-1释放了锁 线程名Thread-0获得了锁 线程名Thread-0释放了锁
|
tryLock()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private void testTryLock(Thread thread) { if (lock.tryLock()) { try { System.out.println("线程名" + thread.getName() + "获得了锁"); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("线程名" + thread.getName() + "释放了锁"); lock.unlock(); } } else { System.out.println("我是" + Thread.currentThread().getName() + "有人占着锁,我就不要啦"); } } --------------------------- 输出 线程名Thread-0获得了锁 我是Thread-1有人占着锁,我就不要啦 线程名Thread-0释放了锁
|
Lock接口的实现
锁的多个实现在标准JDK中提供,它们会在下面的章节中展示
ReentrantLock
ReentrantLock
类是互斥锁,与通过synchronized
访问的隐式监视器具有相同行为,但是具有扩展功能。就像它的名称一样,这个锁实现了重入特性,就像隐式监视器一样。
使用ReentrantLock
写一个例子。
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
| public class ReentrantLockDemo { private static ReentrantLock lock = new ReentrantLock();
private static int count = 0;
private static void increment() { lock.lock(); try { count++; System.out.println(Thread.currentThread().getName()); } finally { lock.unlock(); } }
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> IntStream.range(0, 3)
.forEach(i -> increment());
Thread t1 = new Thread(runnable); Thread t2 = new Thread(runnable); t1.start(); t2.start();
TimeUnit.SECONDS.sleep(1); System.out.println(count); } } ------------------------------------ 输出 Thread-0 Thread-0 Thread-0 Thread-1 Thread-1 Thread-1 6
|
上面的代码中我分让每个线程分别执行3次 increment()。而同一个线程连续执行了三次才换下一个线程,很显然是可重入锁。我们让increment多执行几次,把IntStream.range(0, 3)
改为IntStream.range(0, 10000)
,结果得到20000,也说明不存在竞争。
另外,锁对细粒度的控制支持多种方法,如下:
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 ReentrantLock2Demo { private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { lock.lock(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } });
Thread t2 = new Thread(() -> { System.out.println("锁是否被占用: " + lock.isLocked()); System.out.println("占用所的是否是我: " + lock.isHeldByCurrentThread()); boolean locked = lock.tryLock(); System.out.println("是否获取锁 : " + locked); });
t1.start(); t2.start();
} } ------------------------------------ 输出 锁是否被占用: true 占用所的是否是我: false 是否获取锁 : false
|
ReadWriteLock
ReadWriteLock
接口规定了锁的另一种类型,包含用于读写访问的一对锁。读写锁的理念是,只要没有任何线程写入变量,并发读取可变变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更加频繁。
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
| public class ReadWriteLockDemo { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); ReadWriteLock lock = new ReentrantReadWriteLock();
Runnable write = () -> { lock.writeLock().lock(); try { TimeUnit.SECONDS.sleep(1); map.put("foo", "bar"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } }; Runnable read = () -> { lock.readLock().lock(); try { System.out.println(map.get("foo")); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } };
new Thread(write).start(); new Thread(read).start(); new Thread(read).start(); } }
|
当你执行这一代码示例时,你会注意到两个读任务需要等待写任务完成。在释放了写锁之后,两个读任务会同时执行,并同时打印结果。它们不需要相互等待完成,因为读锁可以安全同步获取,只要没有其它线程获取了写锁。
StampedLock
Java 8 自带了一种新的锁,叫做StampedLock
,它同样支持读写锁,就像上面的例子那样。与ReadWriteLock
不同的是,StampedLock
的锁方法会返回表示为long
的标记。你可以使用这些标记来释放锁,或者检查锁是否有效。此外,StampedLock
支持另一种叫做乐观锁(optimistic locking)的模式。
让我们使用StampedLock
代替ReadWriteLock
重写上面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Runnable write = () -> { long stamp = lock.writeLock(); try { TimeUnit.SECONDS.sleep(1); map.put("foo", "bar"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlockWrite(stamp); } };
Runnable read = () -> { long stamp = lock.readLock(); try { System.out.println(map.get("foo")); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlockRead(stamp); } };
|
输出结果和上例相同。通过readLock()
或 writeLock()
来获取读锁或写锁会返回一个标记,它可以在稍后用于在finally
块中解锁。要记住StampedLock
并没有实现重入特性。每次调用加锁都会返回一个新的标记,并且在没有可用的锁时阻塞,即使相同线程已经拿锁了。所以你需要额外注意不要出现死锁。
就像前面的ReadWriteLock
例子那样,两个读任务都需要等待写锁释放。之后两个读任务同时向控制台打印信息,因为多个读操作不会相互阻塞,只要没有线程拿到写锁。
下面的例子展示了乐观锁:
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
| public class StampedLockDemo2 { public static void main(String[] args) throws InterruptedException { StampedLock lock = new StampedLock();
Runnable optimisticRead = () ->{ long stamp = lock.tryOptimisticRead(); try { System.out.println("乐观锁是否有效 : "+ lock.validate(stamp)); TimeUnit.SECONDS.sleep(1); System.out.println("乐观锁是否有效 : "+ lock.validate(stamp)); TimeUnit.SECONDS.sleep(2); System.out.println("乐观锁是否有效 : "+ lock.validate(stamp)); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.tryUnlockRead(); } };
Runnable write = () -> { long stamp = lock.writeLock(); try { System.out.println("获取写锁"); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(stamp); System.out.println("写完了"); } }; new Thread(optimisticRead).start(); new Thread(write).start(); } }
|
乐观的读锁通过调用tryOptimisticRead()
获取,它总是返回一个标记而不阻塞当前线程,无论锁是否真正可用。如果已经有写锁被拿到,返回的标记等于0。你需要总是通过lock.validate(stamp)
检查标记是否有效。
执行上面的代码会产生以下输出:
1 2 3 4 5
| 乐观锁是否有效 : true 获取写锁 乐观锁是否有效 : false 写完了 乐观锁是否有效 : false
|
乐观锁在刚刚拿到锁之后是有效的。和普通的读锁不同的是,乐观锁不阻止其他线程同时获取写锁。在第一个线程暂停一秒之后,第二个线程拿到写锁而无需等待乐观的读锁被释放。此时,乐观的读锁就不再有效了。甚至当写锁释放时,乐观的读锁还处于无效状态。
所以在使用乐观锁时,你需要每次在访问任何共享可变变量之后都要检查锁,来确保读锁仍然有效。
有时,将读锁转换为写锁而不用再次解锁和加锁十分实用。StampedLock
为这种目的提供了tryConvertToWriteLock()
方法,就像下面那样:
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
| public class StampedLockDemo3 {
private static int count = 0;
public static void main(String[] args) { StampedLock lock = new StampedLock();
Runnable runnable = () -> { long stamp = lock.readLock(); try { if (count == 0) { stamp = lock.tryConvertToWriteLock(stamp); if (stamp == 0L) { System.out.println("转换写锁失败"); stamp = lock.writeLock(); } count = 23; } System.out.println(count);
} finally { lock.unlock(stamp); } };
new Thread(runnable).start(); } }
|
第一个任务获取读锁,并向控制台打印count
字段的当前值。但是如果当前值是零,我们希望将其赋值为23
。我们首先需要将读锁转换为写锁,来避免打破其它线程潜在的并发访问。tryConvertToWriteLock()
的调用不会阻塞,但是可能会返回为零的标记,表示当前没有可用的写锁。这种情况下,我们调用writeLock()
来阻塞当前线程,直到有可用的写锁。
Condition
任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait()与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。
参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:
- 与Object.wait方法对应的Condition.await方法
- 与Object.notify/notifyAll方法对应的Condition.signal/signalAll方法
与Lock一起使用
通过一个的示例,来理解如何和配合Lock使用Condition。
我在 2.线程状态转换方法 中写过通过wait/notify实现的发送者 - 接收者同步问题示例,如果你没看过找一下我之前的文章。我把上次的wait/notify改成Condition的方法。
修改后的代码如下:
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
| public class Data { private String packet; private final Lock lock = new ReentrantLock(); private boolean transfer = true;
private final Condition receiveCondition = lock.newCondition(); private final Condition senderCondition = lock.newCondition();
public String receive() { lock.lock(); try { while (transfer) { receiveCondition.await(); } transfer = true; senderCondition.signalAll();
} catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); return packet; } }
public void send(String packet) { lock.lock(); try { while (!transfer) { senderCondition.await(); } transfer = false; this.packet = packet; receiveCondition.signalAll();
} catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
|
Reference
详解synchronized与Lock的区别与使用
Java 8 并发教程:同步和锁
线程高级篇-Lock锁和Condition条件