Java中的锁--Java并发编程的艺术读书笔记


参考书籍:《Java并发编程的艺术》

1 Lock接口

  锁是用来控制多个线程访问共享资源的方式,一般来说一个锁能防止多个线程同时访问共享资源,而读写锁允许多个线程并发访问共享资源。在Java SE 5之前,Java程序靠synchronized关键字实现锁功能。Java SE 5之后,并发包中新增了Lock接口及相关实现类,Lock接口提供了与synchronized类似的同步功能,但使用时需要显式地获取锁和释放锁。虽然失去了隐式获取释放的便捷,但是拥有了锁获取释放的可操作性,可中断性以及超时获取等特性。Lock使用方式如下:

Lock lock = new ReentrantLock();
lock.lock();
try {
}
finally {
    lock.unlock();
}

  Lock接口提供的,但synchronized关键字不具备的主要特性有:
在这里插入图片描述

2 队列同步器AQS

  队列同步器 AbstractQueuedSynchronizer 是用来构建锁或者其他同步组件的基础框架,它使用了int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
  同步器的主要使用方式是继承,抽象方法有getState(),setState(int newState),compareAndSetState(int expect, int update)。同步器的锁的关系为:锁是面向使用者的,同步器是面向锁的实现者的。

2.1 AQS的接口与示例

  AQS的设计是基于模板方法模式。同步器可重写的方法有:
在这里插入图片描述
在这里插入图片描述
  实现自定义同步组件时,将会调用同步器提供的模板方法,如下所示:
在这里插入图片描述
  同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况。
  下面以一个独占锁Mutex来了解同步器的工作原理。独占锁,即同一时刻只能由一个线程获取到了锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能获取锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占是获取和释放状态。在tryAcquire中,如果CAS设置成功,则代表获取了同步状态,而在tryRelease中只是将同步状态重置为0。在Mutex的实现中,以获取锁的lock()为例,只需要在方法实现中调用同步器的模板方法acquire即可。

2.2 AQS的实现分析

2.2.1 同步队列

  同步队列的作用是协助AQS完成同步状态的管理,当一个线程获取同步状态失败后,会产生一个节点包含该线程引用等消息,节点进入同步队列,同时线程进入阻塞态,当同步状态释放后,首个线程会被唤醒,尝试获取同步状态。
  节点属性如下,同步队列是一个双向链表:
在这里插入图片描述
在这里插入图片描述
  同步队列设置尾节点时,会使用CAS比较最后一个尾节点是否和要加入的节点相同,不同才会加入。同步队列的头节点是获取同步状态成功的节点,当头节点释放同步状态后,下一个节点会被唤醒并成为头节点。头节点的插入不需要CAS比较,因为只有一个线程能够成功获取同步状态。

2.2.2 独占式同步状态获取与释放

  调用同步器的acquire方法获得同步状态,如果获取失败进入同步队列后,后续对线程终端操作,线程也不会从同步队列中移出。

2.2.3 共享式同步状态获取与释放

  共享式与独占式最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。例如,写操作要求是独占式的,但是读操作只要求是共享式的。
  通过调用同步器的acquireShared能够共享式的获取同步状态。

2.2.4 独占式超时获取同步状态

  通过调用同步器的doAcquireNanos可以超时获取同步状态,即在指定时间段内获取同步状态,如果获取到了返回true,如果获取失败返回false。这是传统synchronized关键字不具备的特性。

2.2.5 自定义同步组件——TwinsLock

  通过编写自定义同步组件来加深对同步器的理解。
  设计目标:设计一个同步工具,该工具在同一时刻,只允许至多两个线程同时访问,超过两个线程访问将被阻塞,将这个同步工具命名为TwinsLock。
详情见链接:TwinsLock——自定义同步组件.

3 重入锁

  重入锁ReentrantLock就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,除此之外还支持获取锁时的公平和非公平选择。synchronized关键字隐式地支持重进入。但是Mutex锁不支持重进入,也就是如果一个线程获得了锁,再次尝试获得锁时,会被阻塞,因为tryAcquire方法返回了false。
  锁的公平性问题,是指在绝对时间上,先对锁进行获取请求的线程一定先被满足时这个锁时公共的,反之是不公平的。公平的获取锁,也就是等待时间最长的线程优先获取锁,因为使用了同步队列。ReentrantLock提供了一个构造函数能够控制锁的获取是否公平。事实上,公平的锁机制往往没有非公平的效率高,但是并不是任何场景都是以系统吞吐量(Throughput-per-second,TPS)为唯一指标,公平的锁能减少饥饿发生的概率。

3.1 实现重进入

  nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:判断当前线程是否为获取锁的线程,如果是则同步状态指增加并返回true,表示获取同步状态成功。tryRelease释放同步状态,如果锁被获取了n次,那么前(n-1)次tryRelease方法将返回false,只有同步状态完全释放(为0),才能返回true。

3.2 公平与非公平获取锁的区别

  公平获取锁为tryAcquire方法,相比nonfairTryAcquire方法多了判断当前节点是否有前驱节点,如果有则需要等待前驱线程获取并释放锁之后才能获取锁。下面是公平锁和非公平锁的测试结果:
在这里插入图片描述
  可以看到,非公平锁出现了同一线程连续获取锁的情况,这是因为一个线程刚释放锁时再次获取锁的几率非常大。实际上,非公平锁是默认实现,这是因为非公平锁开销更小,观察图中可以看到公平锁进行了10次切换,而非公平锁进行了5次切换。下面是书中10个线程,每个线程获取100000次锁统计得出的,可以看到公平锁比非公平锁耗时94.3倍。
  总结:公平锁可以保证公平,防止发生饥饿,代价是进行大量的线程切换。非公平锁减少了线程切换次数,运行效率高,但是有可能发生饥饿。
在这里插入图片描述

4 读写锁

  上面提到的Mutex和ReentrantLock都是排他锁。读写送维护了一对锁,一个读锁一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。并且读写锁也能很好地解决读多写少的场景。Java并发包提供的读写锁是ReentrantReadWriteLock,特性如下:
在这里插入图片描述

4.1 读写锁的接口与示例

  ReadWriteLock除了定了readLock和writeLock之外,还提供了一些控制内部工作状态的方法:
在这里插入图片描述

4.2 读写锁的实现分析

4.2.1 读写状态的设计

  读写状态时同步器的同步状态。读写锁将32位的整形变量进行”按位切割使用“,高16位表示读,低16位表示写。
在这里插入图片描述
  由于读写锁可以重进入,那么如何迅速确定读和写各自状态呢?位运算!假设当前同步状态为S,写状态等于S&0x0000FFFF(位运算将高16为全部抹去),读状态等于S>>16(无符号补0右移16位)。当写状态加1,等于S+1,当读状态加1,等于S+(1<<16),也就是S+0x00010000。

4.2.2 写锁的获取与释放

  写锁是支持重进入排他锁。如果一个线程获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,那么线程进入阻塞态。在tryAcquire方法中,除了加了重进入的实现,还加了判断读锁是否存在。

4.2.3 读锁的获取与释放

  读锁是支持重进入共享锁。在trySharedAcquire方法中,如果其他线程获取了写锁,当前线程获取读锁失败,如果当前线程获取了写锁或者写锁为被获取,则当前线程增加读状态。

4.2.4 锁降级

  锁降级是指写锁降级为读锁。并且需要一个线程已经获取了写锁且没有释放时,又获取了读锁,随后释放写锁和读锁的过程。这样主要是为了保证数据可见性,获取写锁后再获取读锁,使得当释放写锁时仍然能使用数据,最后释放完读锁后,其他线程才能获取写锁进行数据更新。ReentrantReadWriteLock不支持锁升级。

5 LockSupport工具

  当需要阻塞或者唤醒一个线程时,使用LockSupport工具类来完成相应功能。
在这里插入图片描述

6 Condition接口

  任意一个Java对象都有一组监视器方法,主要包括wait(),notify(),notifyAll()等,这些方法与synchronized关键字配合可以实现等待/通知模式。
  Lock和Condition(有类似监视器的方法)配合也可以实现等待/通知模式。
在这里插入图片描述

6.1 Condition接口与示例

  Condition对象是由Lock对象创建的(调用Lock对象的newCondition()方法)。Condition的方法如下:
在这里插入图片描述
在这里插入图片描述

6.2 Condition的实现分析

  每个Condition对象都包含一个等待队列,它时实现等待/通知的关键。

6.2.1 等待队列

  如果一个线程调用了await()方法,那么该线程会释放锁,构造成节点并加入等待队列,事实上这里的节点和同步队列里的节点是一样的(AbstractQueuedSynchronizer.Node)。await方法并没有使用CAS保证,因为调用await放大必定是获取了锁的进程。
在这里插入图片描述

6.2.2 等待

  调用await()方法,线程会进入等待队列并释放锁,同时线程进入等待状态。换个角度,其实调用await方法后,同步队列的首节点就移动到等待队列尾节点。
在这里插入图片描述

6.2.3 通知

  调用signal方法,将会移动等待队列中等待时间最长的节点(也就是首节点),至同步队列,然后唤醒该节点的线程。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、、同步等。学习Java并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少竞争、避免死等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值