并发编程学习笔记之 Lock 与 synchronized
一什么是可重入锁
Lcok 在 Java 中是一个接口, 一般在面试问题中问到的可能是 ReentrantLock 与 synchronized 的区别 ReentrantLock 是 Lock 的一个实现类, 字面意思的话就是可重入锁, 那么什么是可重入锁呢
可重入锁是锁的一个相关概念, 并不是特指我们的 ReentrantLock, 而是如果一个锁具备可重入性, 那我们就说这是一个可重入锁 ReentrantLock 和 synchronized 都是可重入锁至于什么是可重入性, 这里举个简单的例子, 现在在一个类里我们有两个方法 (代码如下), 一个叫做去北京, 一个叫做买票, 那我们在去北京的方法里可以直接调用买票方法, 假如两个方法全都用 synchronized 修饰的话, 在执行去北京的方法, 线程获取了对象的锁, 接着执行买票方法, 如果 synchronized 不具备可重入性, 那么线程已经有这个对象的锁了, 现在又来申请, 就会导致线程永远等待无法获取到锁而 synchronized 和 ReentrantLock 都是可重入锁, 就不会出现上述的问题
- class Trip {
- public synchronized void goToBeiJing() {
- // 去北京
- buyATicket();
- }
- public synchronized void buyATicket() {
- // 买票
- }
- }
二 Lock 与 synchronized 的不同
二者都是可重入锁, 那么为什么要有两个呢? 既然存在, 那么就一定是有意义的 synchronized 是 Java 中的一个关键字, 而 Lock 是 Java1.5 后在 java.util.concurrent.locks 包下提供的一种实现同步的方法, 那么显然的, synchronized 一定是有什么做不到的或者缺陷, 才导致了 Lock 的诞生
1.synchronized 的缺点
1) 当一个代码块被 synchronized 修饰的时候, 一个线程获取到了锁, 并且执行代码块, 那么其他的线程需要等待正在使用的线程释放掉这个锁, 那么释放锁的方法只有两种, 一种是代码执行完毕自动释放, 一种是发生异常以后 jvm 会让线程去释放锁那么如果这个正在执行的线程遇到什么问题, 比如等待 IO 或者调用 sleep 方法等等被阻塞了, 无法释放锁, 而这时候其他线程只能一直等待, 将会特别影响效率那么有没有一种办法让其他线程不必一直傻乎乎的等在这里吗?
2) 当一个文件, 同时被多个线程操作时, 读操作和写操作会发生冲突, 写操作和写操作会发生冲突, 而读操作和读操作并不会冲突, 但是如果我们用 synchronized 的话, 会导致一个线程在读的时候, 其他线程想要读的话只能等待, 那么有什么办法能不锁读操作吗?
3) 在使用 synchronized 时, 我们无法得知线程是否成功获取到锁, 那么有什么办法能知道是否获取到锁吗?
2.java.util.concurrent.locks 包
1)Lock
Lock 是一个接口, 源码如下
- public interface Lock {
- void lock();
- void lockInterruptibly() throws InterruptedException;
- boolean tryLock();
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
- void unlock();
- Condition newCondition();
- }
ReentrantLock 是 Lock 的一个实现类, 也是唯一一个实现类, 它的意思是可重入锁, 可重入锁前面已经讲过了 ReentrantLock 中提供了更多的一些方法不过常用的就是 Lock 中的这些
来看一下 Lock 接口这些方法的使用, lock()tryLock()tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 是用来获取锁的 unLock() 方法是用来释放锁的
这里有四个方法来获取锁, 那么区别在哪里呢?
lock() 使我们平时用的最多的, 最用是用来获取锁, 如果锁已经被其他线程获取, 那么就等待但是采用 Lock 必须要主动释放锁, 所以我们一般在 try{}catch{} 块中处理然后在 finally 中释放锁, 举个例子:
- lock.lock();
- try{
- // 处理
- }catch(Exception ex){
- // 捕获异常
- }finally{
- // 释放锁
- lock.unlock();
- }
tryLock() 是一个 boolean 类型的方法, 当调用这个方法的时候, 线程会去尝试获取锁, 如果获取到的话会返回 true, 如果获取不到返回 false, 也就是说这个方法会立马返回一个结果, 线程不会等待
tryLock(long time, TimeUnit unit) 是上面 tryLock() 方法的一个重载方法, 加了两个参数, 给定了等待的时间, 如果在规定时间拿到锁返回 true, 如果拿不到返回 false 这两个方法的一般用法和 Lock 类似
- if (lock.tryLock()) {
- try{
- // 处理
- }catch(Exception ex){
- // 捕获异常
- }finally{
- // 释放锁
- lock.unlock();
- }
- }
lockInterruptibly() 就比较特殊了, 它表示可被中断的, 意思就是, 当尝试获取锁的时候, 如果获取不到的话就会等待, 但是, 在等待的过程中它是可以响应中断的, 也就是中断线程的等待过程使用形式的话一样用 try catch 处理, 就不贴代码了
2)ReadWriteLock
ReadWriteLock 也是一个接口, 这个接口中只有两个方法, 源码如下:
- public interface ReadWriteLock {
- Lock readLock();
- Lock writeLock();
- }
这个接口的从字面就能看出来他的用途, 读锁和写锁, 这时候是不是想起了前面我写到的 synchronized 的第二条
ReentrantReadWriteLock 是 ReadWriteLock 的一个实现类, 最常用到的也是获取读锁和获取写锁下面看例子:
首先是使用 synchronized 的:
- public class Main {
- public static void main(String[] args) {
- final Main m = new Main();
- new Thread(){
- public void run() {
- m.read(Thread.currentThread());
- };
- }.start();
- new Thread(){
- public void run() {
- m.read(Thread.currentThread());
- };
- }.start();
- }
- public synchronized void read(Thread thread) {
- long startTime = System.currentTimeMillis();
- while(System.currentTimeMillis() - startTime <= 1) {
- System.out.println(thread.getName()+"线程在进行读操作");
- }
- System.out.println(thread.getName()+"线程完成读操作");
- }
- }
这段代码的执行结果是在一个线程打印出完成读操作后, 另一条线程才会开始进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程完成读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程完成读操作
下面看如果用 ReadWriteLock:
- public class Main {
- public static void main(String[] args) {
- private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- final Main m = new Main();
- new Thread(){
- public void run() {
- m.read(Thread.currentThread());
- };
- }.start();
- new Thread(){
- public void run() {
- m.read(Thread.currentThread());
- };
- }.start();
- }
- public void read(Thread thread) {
- readWriteLock.readLock().lock();
- try {
- long startTime = System.currentTimeMillis();
- while(System.currentTimeMillis() - startTime <= 1) {
- System.out.println(thread.getName()+"线程在进行读操作");
- }
- System.out.println(thread.getName()+"线程完成读操作");
- } finally {
- readWriteLock.unlock();
- }
- }
- }
只是把之前的 synchronized 换成了 ReadWriteLock, 但是输出结果却是两个线程在一起进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程在进行读操作
Thread-1 线程在进行读操作
Thread-0 线程完成读操作
Thread-1 线程完成读操作
这个结果可以看到两个线程同时进行读操作, 效率大大的提升了但是要注意的是, 如果一个线程获取了读锁, 那么另外的线程想要获取写锁则需要等待释放; 而如果一个线程已经获取了写锁, 则另外的线程想获取读锁或写锁都需要等待写锁被释放
三总结
总结一下二者:
1.synchronized 是 Java 的关键字, 是内置特性, 而 Lock 是一个接口, 可以用它来实现同步
2.synchronized 同步的时候, 其中一条线程用完会自动释放锁, 而 Lock 需要手动释放, 如果不手动释放, 可能会造成死锁
3. 使用 synchronized 如果其中一个线程不释放锁, 那么其他需要获取锁的线程会一直等待下去, 知道使用完释放或者出现异常, 而 Lock 可以使用可以响应中断的锁或者使用规定等待时间的锁
4.synchronized 无法得知是否获取到锁, 而 Lcok 可以做到
5. 用 ReadWriteLock 可以提高多个线程进行读操作的效率
所以综上所述, 在两种锁的选择上, 当线程对于资源的竞争不激烈的时候, 效率差不太多, 但是当大量线程同时竞争的时候, Lock 的性能会远高于 synchronized
来源: https://www.cnblogs.com/huangbw/p/8516024.html