本章内容涵盖Lock的使用讲解,可重入锁、读写锁。Lock和Synchronized的对比等。 多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!
沉下去,再浮上来
,我想我们会变的不一样的。
阳光正好,家给人的感觉真的很舒服
JUC系列
- JUC系列(一) 什么是JUC?
- JUC系列(二) 回顾Synchronized关键字
- JUC系列(三)Lock 锁机制详解 代码理论相结合
- JUC系列(四)集合的线程安全问题
- JUC系列(五)| Synchonized关键字的进一步讲解
- JUC系列(六) | Callable和Future接口详解&使用、FutureTask应用
- JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、Semaphore
- JUC系列(八)| 读写锁-ReadWriteLock
正在持续更新中...
一、什么是 Lock
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。
二、锁类型
可重入锁:在执行对象中所有同步方法不用再次获得锁
可中断锁:在等待获取锁过程中可中断
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
三、Lock接口
public interface Lock { void lock(); //获得锁。 /** 除非当前线程被中断,否则获取锁。 如果可用,则获取锁并立即返回。 如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下两种情况之一: 锁被当前线程获取; 要么其他一些线程中断当前线程,支持中断获取锁。 如果当前线程: 在进入此方法时设置其中断状态; 要么获取锁时中断,支持中断获取锁, */ void lockInterruptibly() throws InterruptedException; /** 仅在调用时空闲时才获取锁。 如果可用,则获取锁并立即返回值为true 。 如果锁不可用,则此方法将立即返回false值。 */ boolean tryLock(); //比上面多一个等待时间 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 解锁 void unlock(); //返回绑定到此Lock实例的新Condition实例。 Condition newCondition(); 。 }
下面讲几个常用方法的使用。
3.1、lock()、unlock()
lock()是最常用的方法之一,作用就是获取锁,如果锁已经被其他线程获得,则当前线程将被禁用以进行线程调度,并处于休眠状态,等待,直到获取锁。
如果使用到了lock的话,那么必须去主动释放锁,就算发生了异常,也需要我们主动释放锁,因为lock并不会像synchronized一样被自动释放。所以使用lock的话,必须是在try{}catch(){}
中进行,并将释放锁的代码放在finally{}
中,以确保锁一定会被释放,以防止死锁现象的发生。
unlock()的作用就是主动释放锁。
lock接口的类型有好几个实现类,这里是随便找了个哈。
Lock lock = new ReentrantLock(); try { lock.lock(); System.out.println("上锁了"); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); System.out.println("解锁了"); }
3.2、newCondition
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。 用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:
- await():会使当前线程等待,同时会释放锁,当等到其他线程调用
signal()
方法时,此时这个沉睡线程会重新获得锁并继续执行代码(在哪里沉睡就在哪里唤醒)。
- signal():用于唤醒一个等待的线程。
注意
:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在调用singal()方法后会从当前 Condition对象的等待队列中,唤醒一个线程,后被唤醒的线程开始尝试去获得锁, 一旦成功获得锁就继续往下执行。
在这个地方我们举个例子来用代码写一下哈:
这里就不举例synchronized 实现了,道理都差不多。
例子:我们有两个线程,实现对一个初始值是0的number变量,一个线程当number = =0时 对number值+1,另外一个线程当number = = 1时对number-1。
class Share { private Integer number = 0; private ReentrantLock lock = new ReentrantLock(); private Condition newCondition = lock.newCondition(); // +1 的方法 public void incr() { try { lock.lock(); // 加锁 while (number != 0) { newCondition.await();//沉睡 } number++; System.out.println(Thread.currentThread().getName() + "::" + number); newCondition.signal(); //唤醒另一个沉睡的线程 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } // -1 的方法 public void decr() { try { lock.lock(); while (number != 1) { newCondition.await(); } number--; System.out.println(Thread.currentThread().getName() + "::" + number); newCondition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class LockDemo2 { public static void main(String[] args) { Share share = new Share(); new Thread(()->{ for (int i=0;i<=10;i++){ share.incr(); } },"AA").start(); new Thread(()->{ for (int i=0;i<=10;i++){ share.decr(); } },"BB").start(); /** * AA::1 * BB::0 * AA::1 * BB::0 * ..... */ } }
四、ReentrantLock (可重入锁)
ReentrantLock
,意思是“可重入锁”。ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。
可重入锁
:什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
package com.crush.juc02; import java.util.Random; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); new Thread(new Runnable() { @Override public void run() { try { lock.lock(); System.out.println("第1次获取锁,这个锁是:" + lock); for (int i = 2;i<=11;i++){ try { lock.lock(); System.out.println("第" + i + "次获取锁,这个锁是:" + lock); try { Thread.sleep(new Random().nextInt(200)); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock();// 如果把这里注释掉的话,那么程序就会陷入死锁当中。 } } } finally { lock.unlock(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { lock.lock(); System.out.println("这里是为了测试死锁而多写一个的线程"); } finally { lock.unlock(); } } }).start(); } } /** * 第1次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0] * 第2次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0] * 第3次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0] * ... */
死锁的话,程序就无法停止,直到资源耗尽或主动终止。
代码中也稍微提了一下死锁的概念,在使用Lock中必须手动解锁
,不然就会可能造成死锁的现象。
五、ReadWriteLock (读写锁)
ReadWriteLock 也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock { // 获取读锁 Lock readLock(); // 获取写锁 Lock writeLock(); }
分为一个读锁一个写锁,将读写进行了分离,使可以多个线程进行读操作,从而提高了效率。
ReentrantReadWriteLock 实现了 ReadWriteLock 接口。里面提供了更丰富的方法,当然最主要的还是获取写锁(writeLock)和读锁(readLock)。
5.1、案例
假如多个线程要进行读的操作,我们用Synchronized 来实现的话。
public class SynchronizedDemo2 { public static void main(String[] args) { final SynchronizedDemo2 test = new SynchronizedDemo2(); new Thread(()->{ test.get(Thread.currentThread()); }).start(); new Thread(()->{ test.get(Thread.currentThread()); }).start(); } public synchronized void get(Thread thread) { long start = System.currentTimeMillis(); while(System.currentTimeMillis() - start <= 1) { System.out.println(thread.getName()+"正在进行读操作"); } System.out.println(thread.getName()+"读操作完毕"); } } /** * 输出 * Thread-0正在进行读操作 * Thread-0读操作完毕 * Thread-1正在进行读操作 * Thread-1正在进行读操作 * Thread-1正在进行读操作 * .... * Thread-1读操作完毕 */
改成读写锁之后
public class SynchronizedDemo2 { private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public static void main(String[] args) { final SynchronizedDemo2 test = new SynchronizedDemo2(); new Thread(()->{ test.get2(Thread.currentThread()); }).start(); new Thread(()->{ test.get2(Thread.currentThread()); }).start(); } public void get2(Thread thread) { rwl.readLock().lock(); try { long start = System.currentTimeMillis(); while(System.currentTimeMillis() - start <= 1) { System.out.println(thread.getName()+"正在进行读操作"); } System.out.println(thread.getName()+"读操作完毕"); } finally { rwl.readLock().unlock(); } } } /** * 输出 * Thread-0正在进行读操作 * Thread-0读操作完毕 * Thread-1正在进行读操作 * Thread-1读操作完毕 */
结论:改用读写锁后 线程1和线程2 同时在读,可以感受到效率的明显提升。
注意
:
- 若此时已经有一个线程占用了读锁,此时其他线程申请读锁是可以的,但是若此时其他线程申请写锁,则只有等待读锁释放,才能成功获得。
- 若此时已经有一个线程占用了写锁,那么此时其他线程申请写锁或读锁,都只有持有写锁的线程释放写锁,才能成功获得。
六、Lock 与的 Synchronized 区别
类别 | synchronized | Lock |
存在层次 | Java的关键字,在jvm层面上 | 是一个接口 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁的释放 | 1、当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用 (不需要手动释放锁)2、若线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁现象 (需要手动释放锁) |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 锁类型 | 可重入 可判断 可公平(两者皆可) |
性能 | 前提:大量线程情况下 同步效率较低 | 前提:大量线程情况下 同步效率比synchronized高的多 |
Lock可以提高多个线程进行读操作的效率。
七、自言自语
最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。
正在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。
你好,我是博主宁在春
,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。
希望与君共勉
😁
待我们,别时相见时,都已有所成。