并发编程(十)Lock锁
文章更新时间:2021/09/03
一、引言
线程并发的过程中,肯定会涉及到一个变量共享的概念,那么我们在多线程运行过程中,怎么保证每个先拿获取的变量信息都是最新且有序的呢?这一篇我们来专门学习一下Lock锁。
我们先来了解几个概念:
乐观锁与悲观锁
悲观锁:
假定会发生并发冲突,即共享资源会被某个线程更改。所以当某个线程获取共享资源时,会阻止别的线程获取共享资源。也称独占锁或者互斥锁,例如java中的synchronized同步锁。
乐观锁:
假设不会发生并发冲突,只有在最后更新共享资源的时候会判断一下在此期间有没有别的线程修改了这个共享资源。如果发生冲突就重试,直到没有冲突,更新成功。CAS就是一种乐观锁实现方式。
公平锁与非公平锁
- 公平锁的实现就是谁等待时间最长,谁就先获取锁
- 非公平锁就是随机获取的过程,谁运气好,cpu时间片轮询到哪个线程,哪个线程就能获取锁
可重入锁与不可重入锁
不可重入锁
若当前线程执行中已经获取了锁,如果再次获取该锁时,就会获取不到被阻塞。
可重入锁
每一个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。
二、Condition
在使用Lock之前,我们使用的最多的同步方式应该是synchronized关键字来实现同步方式了。配合Object的wait()、notify()系列方法可以实现线程的等待/通知模式。
PS:Condition的实质是通过控制线程的等待和唤醒来达到控制指定线程的功能。
特点:
- 依赖于Lock对象,调用Lock对象的newCondition()对象创建而来
- 可以实现等待/通知形式的线程交互模式
- 可以有选择性的进行线程通知,唤醒指定线程
基本方法:
public interface Condition { void await() throws InterruptedException; boolean await(long var1, TimeUnit var3) throws InterruptedException; long awaitNanos(long var1) throws InterruptedException; void awaitUninterruptibly(); boolean awaitUntil(Date var1) throws InterruptedException; void signal(); void signalAll(); }
- await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
- await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
- awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
- awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
- awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
- signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
- signalAll() :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
使用举例:
/** * Condition使用范例 * * @author 有梦想的肥宅 */ public class ConditionTest { //1、创建一个Lock对象,Condition的使用需要依赖Lock对象 public Lock lock = new ReentrantLock(); //2、使用Lock对象的newCondition()方法来生成Condition对象 public Condition condition = lock.newCondition(); //3、main方法测试Condition的作用 public static void main(String[] args) { ConditionTest conditionTest = new ConditionTest(); //3.1 创建线程1 new Thread(new Runnable() { @Override public void run() { conditionTest.conditionWait();//等待 } }, "线程1【有梦想的肥宅】").start(); //3.2 创建线程2 new Thread(new Runnable() { @Override public void run() { conditionTest.conditionSignal();//唤醒 } }, "线程2【有梦想的肥宅】").start(); } /** * 线程等待 */ public void conditionWait() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "拿到锁了"); System.out.println(Thread.currentThread().getName() + "等待信号【等待】"); condition.await();//线程进入等待状态,不进入finally语句块进行锁的释放,要等待被唤醒 System.out.println(Thread.currentThread().getName() + "拿到信号"); } catch (Exception e) { } finally { lock.unlock(); } } /** * 线程唤醒 */ public void conditionSignal() { lock.lock(); try { Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + "拿到锁了"); condition.signal();//唤醒线程 System.out.println(Thread.currentThread().getName() + "发出信号【唤醒】"); } catch (Exception e) { } finally { lock.unlock(); } } }
三、ReentrantLock可重入锁
定义:是一个可重入锁,且它可以设置自身为非公平锁或者是公平锁。
常用方法:
- ReentrantLock() : 创建一个ReentrantLock实例【默认非公平锁】
- lock() : 获得锁
- unlock() : 释放锁
/** * ReentrantLock测试类 * * @author 有梦想的肥宅 */ public class ReentrantLockTest { //全局对象lock【构造参数设置为true表示为公平锁,false或为空则默认是非公平锁】 private static Lock lock = new ReentrantLock(true); //线程方法 public static void test() { for (int i = 0; i < 2; i++) { try { lock.lock(); System.out.println(Thread.currentThread().getName() + "获取了锁"); TimeUnit.MILLISECONDS.sleep(1000);//等待1秒,为了更直观地观察公平锁的机制 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } //运行方法 public static void main(String[] args) { System.out.println("=====公平锁实例====="); //启动一个名叫“线程A”的线程 new Thread("线程A") { @Override public void run() { test(); } }.start(); //启动一个名叫“线程B”的线程 new Thread("线程B") { @Override public void run() { test(); } }.start(); } }
ReentrantLock与synchronized的比较
相似点
它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,等到释放掉锁或者唤醒后才能继续获得锁。
区别
1️⃣对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
2️⃣便利性:Synchronized的使用比较方便简洁,并且由编译器去自动保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
3️⃣锁的细粒度和灵活度:ReenTrantLock优于Synchronized【可以指定在哪加锁和解锁】
4️⃣sychronized锁的是对象,锁信息保存在对象头中。ReentrantLock通过代码中int类型的state标识来标识锁的状态。
5️⃣sychronized底层有⼀个锁升级的过程。
四、ReentrantReadWriteLock可重入读写锁
定义:ReentrantReadWriteLock是一种可重入读写锁,内部有两把锁来实现读和写的锁功能,在ReentrantLock的基础上优化了性能,但是使用起来需要更加谨慎。
性质:
可重入
如果你了解过synchronized关键字,一定知道他的可重入性,可重入就是同一个线程可以重复加锁,每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取。
读写分离
我们知道,对于一个数据,不管是几个线程同时读都不会出现任何问题,但是写就不一样了,几个线程对同一个数据进行更改就可能会出现数据不一致的问题,因此想出了一个方法就是对数据加锁,这时候出现了一个问题:线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。
锁可以降级【写锁->读锁】
线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
锁不可升级【读锁!->写锁】
线程获取读锁是不能直接升级为写入锁的。需要释放所有读取锁,才可获取写锁。
使用示例:
/** * ReentrantReadWriteLock测试类【可重入读写锁】 * * @author 有梦想的肥宅 */ public class ReentrantReadWriteLockTest { private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);//全局可重入读写锁对象 private static Lock readLock = reentrantReadWriteLock.readLock();//读锁 private static Lock writeLock = reentrantReadWriteLock.writeLock();//写锁 private static List<String> data = new ArrayList<>();//模拟被操作的数据 public static void main(String[] args) { //创建线程1【写】 new Thread(new Runnable() { @Override public void run() { write(); } }, "线程1【有梦想的肥宅】").start(); //创建线程2【读】 new Thread(new Runnable() { @Override public void run() { read(); } }, "线程2【有梦想的肥宅】").start(); } /** * 写数据的方法 * * @Description 使用writeLock获取一把写锁,然后内部List写入数据,最后在finally中释放写锁。 */ public static void write() { try { //1、加上写锁 writeLock.lock(); //2、操作公共数据 data.add("写数据"); System.out.println(Thread.currentThread().getName() + "正在写数据"); //3、线程等待3秒 Thread.sleep(5000); } catch (Exception e) { System.out.println(e.getMessage()); } finally { //4、释放写锁 System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } /** * 读数据的方法 * * @Description 使用readLock获取一把读锁,然后内部List读取数据,最后再finally中释放读锁。 */ public static void read() { try { //1、加上读锁 readLock.lock(); //2、读取公共数据 for (String str : data) { System.out.println(Thread.currentThread().getName() + "正在读数据"); } //3、线程等待3秒 Thread.sleep(5000); } catch (Exception e) { System.out.println(e.getMessage()); } finally { //4、释放读锁 System.out.println(Thread.currentThread().getName() + "释放读锁"); readLock.unlock(); } } }
参考资料:
- JAVA Condition
- Condition
- Java并发之Condition
- 可重入锁详解(什么是可重入)
- ReentrantLock详解
- 可重入读写锁ReentrantReadWriteLock的使用详解
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2019-07-21 dubbo学习(七)dubbo项目搭建--生产者(服务提供者)
2019-07-21 创建型设计模式(五)单例模式
2019-07-21 创建型设计模式(四)原型模式
2019-07-21 dubbo学习(五)注册中心zookeeper
2019-07-21 dubbo学习(六)dubbo管理控制台
2019-07-21 dubbo学习(二)配置dubbo XML方式配置
2019-07-21 dubbo学习(四)配置dubbo 注解方式配置