JUC-day02
- 集合的线程安全
- callable和future
- JUC三个工具类(练习)
- 读写锁: 共享锁 独占锁(练习)
- AQS: 实现原理(核心方法)
- CAS: 原理–>可见性关键字
1 集合的线程安全(重点)
1.1 集合操作Demo
NotSafeDemo
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 10; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}, "线程" + i).start();
}
}
异常内容
java.util.ConcurrentModificationException
问题: 为什么会出现并发修改异常?
查看ArrayList的add方法源码
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
那么我们如何去解决List类型的线程安全问题?
1.2 Vector
Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。
Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。
Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
和ArrayList不同,Vector中的操作是线程安全的。
NotSafeDemo代码修改
// Vector没有线程安全问题,因为方法中用synchronized同步关键字修饰 所有没有并发异常
public static void main(String[] args) {
Vector vector = new Vector();
for (int i = 0;i < 100;i++){
new Thread(() -> {
vector.add(UUID.randomUUID().toString());
System.out.println(vector);
}, "线程" + i).start();
}
}
现在没有运行出现并发异常,为什么?
查看Vector的add方法
/**
* Appends the specified element to the end of this Vector.
*
* @param e element to be appended to this Vector
* @return {@code true} (as specified by {@link Collection#add})
* @since 1.2
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
add方法被synchronized同步修辞,线程安全!因此没有并发异常
1.3 Collections
Collections提供了方法synchronizedList保证list是同步线程安全的
NotSafeDemo代码修改
// Collections提供了方法synchronizedList保证list是同步线程安全的
public static void main(String[] args) {
List list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 100;i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString());
System.out.println(list);
}).start();
}
}
没有并发修改异常
查看方法源码
/**
* Returns a synchronized (thread-safe) list backed by the specified
* list. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing list is accomplished
* through the returned list.<p>
*
* It is imperative that the user manually synchronize on the returned
* list when iterating over it:
* <pre>
* List list = Collections.synchronizedList(new ArrayList());
* ...
* synchronized (list) {
* Iterator i = list.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned list will be serializable if the specified list is
* serializable.
*
* @param <T> the class of the objects in the list
* @param list the list to be "wrapped" in a synchronized list.
* @return a synchronized view of the specified list.
*/
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
1.4 CopyOnWriteArrayList(重点)
首先我们对CopyOnWriteArrayList进行学习,其特点如下:
它相当于线程安全的ArrayList。和ArrayList一样,它是个可变数组;但是和ArrayList不同的时,它具有以下特性:
- 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
- 它是线程安全的。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
- 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
1. 独占锁效率低:采用读写分离思想解决
2. 写线程获取到锁,其他写线程阻塞
3. 复制思想:
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。
这就是CopyOnWriteArrayList 的思想和原理。就是拷贝一份。
NotSafeDemo代码修改
// CopyOnWriteArrayList线程安全,写时先复制一个副本,在副本中操作完,使用副本替换原数组 不影响其他线程读取原数组的数组
public static void main(String[] args) {
List list = new CopyOnWriteArrayList();
for (int i = 0;i < 100;i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString());
System.out.println(list);
}).start();
}
}
没有线程安全问题
方法源码
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
原因分析(重点):动态数组与线程安全
下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。
-
“动态数组”机制
- 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”, 这就是它叫做CopyOnWriteArrayList的原因
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。
-
“线程安全”机制
- 通过volatile和互斥锁来实现的。
- 通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。
- 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。
1.5 小结(重点)
1.线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:
ArrayList ----- Vector
HashMap -----HashTable
但是以上都是通过synchronized关键字实现,效率较低
2.Collections构建的线程安全集合
3.java.util.concurrent并发包下
CopyOnWriteArrayList CopyOnWriteArraySet类型,通过动态数组与线程安全个方面保证线程安全
2 Callable&Future接口
2.1 Callable接口
目前我们学习了有两种创建线程的方法-一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。
Callable接口的特点如下(重点)
- 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。
- call()方法可以引发异常,而run()则不能。
- 为实现Callable而必须重写call方法
- 不能直接替换runnable,因为Thread类的构造方法根本没有Callable
创建新类MyThread实现runnable接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
2.2 Future接口
当call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。
将Future视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable返回)。Future基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写5种方法,这里列出了重要的方法,如下:
-
**public boolean cancel(boolean mayInterrupt):**用于停止任务。
如果尚未启动,它将停止任务。如果已启动,则仅在mayInterrupt为true时才会中断任务。
-
**public Object get()抛出InterruptedException,ExecutionException:**用于获取任务的结果。
如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
-
**public boolean isDone():**如果任务完成,则返回true,否则返回false
可以看到Callable和Future做两件事-Callable与Runnable类似,因为它封装了要在另一个线程上运行的任务,而Future用于存储从另一个线程获得的结果。实际上,future也可以与Runnable一起使用。
要创建线程,需要Runnable。为了获得结果,需要future。
2.4 FutureTask
Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。
可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。
核心原理:(重点)
- 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成
- 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
- 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
- 一旦计算完成,就不能再重新开始或取消计算
- get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
- get只计算一次,因此get方法放到最后
2.5 使用Callable和Future
CallableDemo案例
public static void main(String[] args) throws Exception{
System.out.println(Thread.currentThread().getName() + "开始干活");
FutureTask<String > futureTask1 = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "执行了runable");
}, "abc");
// FutureTask<String> futureTask = new FutureTask<String>(() -> {
// System.out.println(Thread.currentThread().getName() + ",子任务开始干活");
// Thread.sleep(5000);
// return "123";
// });
// 子线程是由主线程创建的
new Thread(futureTask1).start();;
// 阻塞调用线程 必须等获取到返回值才能往下执行
System.out.println(futureTask1.get());
System.out.println(Thread.currentThread().getName() + "干活结束");
}
2.6 小结(重点)
-
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成, 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
-
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
-
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
-
只计算一次
3 JUC三大辅助类
JUC中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时Lock锁的频繁操作。这三种辅助类为:
- CountDownLatch: 减少计数
- CyclicBarrier: 循环栅栏
- Semaphore: 信号灯
下面我们分别进行详细的介绍和学习
3.1 减少计数CountDownLatch
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句。
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
场景: 6个同学陆续离开教室后值班同学才可以关门。
CountDownLatchDemo
/**
* CountDownLatch:减少计数
* 6个同学陆续离开教室后值班同学才可以关门
* @param args
*/
public static void main(String[] args) throws InterruptedException {
// 定义一个数值为6的计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
// 创建6个同学
for (int i = 1;i <= 6;i++){
new Thread(() -> {
try {
if (Thread.currentThread().getName().equals("同学6")){
Thread.sleep(2000);
}
System.out.println(Thread.currentThread().getName() + "离开了");
// 计数器减1 不会阻塞
countDownLatch.countDown();
}catch (Exception e){
e.printStackTrace();
}
}, "同学" + i).start();
}
// 主线程await休息
System.out.println("主线程睡觉");
// 等全部同学离开关门
countDownLatch.await();
// 全部离开后自动唤醒主线程
System.out.println("全部离开了,现在的计数器为:" +countDownLatch.getCount());
}
3.2 循环栅栏CyclicBarrier
CyclicBarrier看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加1操作
场景: 集齐7颗龙珠就可以召唤神龙
CyclicBarrierDemo
/**
* CyclicBarrier:循环栅栏
* 场景:机器7颗龙珠召唤神龙
* @param args
*/
public static void main(String[] args) {
// 定义神龙召唤需要的龙珠总数
CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
// 定义7个线程分别去收集龙珠
for (int i = 1;i <= 7;i++){
new Thread(() -> {
try {
if (Thread.currentThread().getName().equals("龙珠3号")){
System.out.println("龙珠3号抢夺战开始,孙悟空开启超级赛亚人模式!!!");
Thread.sleep(5000);
System.out.println("龙珠3号抢夺战结束,孙悟空打赢了,拿到了龙珠3号");
}else{
System.out.println(Thread.currentThread().getName() + "收集到了!!!");
}
// 每收集到1个 栅栏+1
cyclicBarrier.await();
}catch (Exception e){
e.printStackTrace();
}
}, "龙珠" + i + "号").start();
}
}
3.3 信号灯Semaphore
Semaphore的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用acquire方法获得许可证,release方法释放许可
场景: 抢车位, 10部汽车3个停车位
SemaphoreDemo
/**
* 信号灯:Semaphore
* 场景:抢车位:6部汽车3个停车位
* @param args
*/
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
// 模拟10辆汽车停车
for (int i = 1;i <= 10;i++){
Thread.sleep(100);
// 停车
new Thread( () -> {
try {
System.out.println(Thread.currentThread().getName() + "找车位ing");
// 抢车位
semaphore.acquire();
// 所有抢到车位的都停3秒
System.out.println(Thread.currentThread().getName() + "停车成功!!");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName() + "溜了溜了");
// 释放车位
semaphore.release();
}
}, "汽车" + i).start();
}
}
4 读写锁(重点重点重点)
4.1 读写锁介绍
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
- 线程进入读锁的前提条件:
-
没有其他线程的写锁
-
没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。
- 线程进入写锁的前提条件:
-
没有其他线程的读锁
-
没有其他线程的写锁
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
4.2 ReentrantReadWriteLock
ReentrantReadWriteLock 类的整体结构
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}
}
可以看到,ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。
ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mW9iXf2x-1677056773217)(assets/image-20230217204330949.png)]
- Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类
- ReadLock实现了Lock接口、WriteLock也实现了Lock接口
4.3 入门案例
场景: 使用ReentrantReadWriteLock 对一个hashmap进行读和写操作
思考:
- 场景一: 多个线程同时获取读锁,结果如何?
- 场景二: 多线程同时获取写锁, 结果如何?
- 场景三:同一个线程先获取读锁后再去获取写锁,结果如何
- 场景四:同一个线程先获取写锁后再去获取读锁,结果如何
- 场景五: 一个线程先获取读锁后其他线程获取写锁,结果如何?
- 场景六: 一个线程获取写锁后其他线程获取读锁,结果如何?
- 场景七: 同一个线程获取读锁后再去获取写锁,结果如何?
- 场景八: 同一个线程获取写锁后再去获取读锁,结果如何?
接下来我们一一进行实战验证
4.3.1 场景一 多个线程之间同时加读锁—> 成功—> 共享锁!
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
// 定义一把读写锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
// 加锁
readLock.lock();
// 删除
System.out.println(Thread.currentThread().getName() + "加读锁成功!");
Thread.sleep(3000);
}catch (Exception e){
}finally {
System.out.println("读锁释放");
// 释放锁
readLock.unlock();
}
}
/**
* 场景一:多个线程之间同时加读锁---> 成功! ---> 共享锁!
*/
public static void main(String[] args) {
// 初始化对象
Test test = new Test();
new Thread(() ->{
test.read();
}, "读线程1").start();
new Thread(() ->{
test.read();
}, "读线程2").start();
}
}
结论: 多个线程可以同时获取读锁
4.3.2 场景二 多个线程之间同时加写锁—>排他—>独占锁
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test1 {
// 定义一把读写锁
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void write(){
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
// 加锁
writeLock.lock();
//输出
System.out.println(Thread.currentThread().getName() + "加写锁成功");
Thread.sleep(3000);
}catch (Exception e){
}finally {
System.out.println(Thread.currentThread().getName() + "释放");
// 释放锁
writeLock.unlock();
}
}
/**
* 场景二:多个线程之间同时加写锁--->排他--->独占锁
* @param args
*/
public static void main(String[] args) {
// 初始化对象
Test1 test1 = new Test1();
new Thread(() -> {
test1.write();
}, "写线程1").start();
new Thread(() -> {
test1.write();
}, "写线程2").start();
}
}
结论: 多个线程同时获取写锁,同一时间只有一个线程能获取到写锁
4.3.3 场景三 同一个线程先读后写—>失败–>加了读锁不饿能加写锁 进程会卡住
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test2 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 先读后写
*/
public void readAndWrite(){
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
// 加读锁
readLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加读锁成功");
// 加写锁
writeLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加写锁成功");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放锁
readLock.unlock();
System.out.println("读锁释放");
writeLock.unlock();
System.out.println("写锁释放");
}
}
/**
* 场景三:同一个线程先读后写--->失败-->加了读锁不饿能加写锁 进程会卡住
* @param args
*/
public static void main(String[] args) {
// 创建对象
Test2 test2 = new Test2();
new Thread(() -> {
test2.readAndWrite();
}).start();
}
}
结论: 当对象具有读锁时,不能再获取写锁,进程会卡住
4.3.4 场景四 同一个线程先写后读---->成功—>加了写锁可以再加读锁
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test3 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 先写后读
*/
public void WriteAndRead(){
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
// 加写锁
writeLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加写锁成功");
// 加读锁
readLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加读锁成功");
Thread.sleep(3000);
}catch (Exception e){
}finally {
// 释放锁
writeLock.unlock();
System.out.println("写锁释放");
readLock.unlock();
System.out.println("读锁释放");
}
}
/**
* 场景四:同一个线程先写后读---->成功--->加了写锁可以再加读锁
* @param args
*/
public static void main(String[] args) {
// 初始化对象
Test3 test3 = new Test3();
new Thread(() -> {
test3.WriteAndRead();
},"线程A").start();
}
}
4.3.5 场景五:不同线程,先读后写—>失败—>不同的线程之间读锁和写锁是互斥的
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test4 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void readAndWrite(){
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
// 加读锁
readLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加读锁成功");
// 加写锁
writeLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加写锁成功");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}finally {
// 释放读锁
readLock.unlock();
System.out.println("释放读锁");
// 释放写锁
writeLock.unlock();
System.out.println("释放写锁");
}
}
/**
* 场景五:不同线程,先读后写--->失败--->不同的线程之间读锁和写锁是互斥的
* @param args
*/
public static void main(String[] args) {
// 初始化对象
Test4 test4 = new Test4();
new Thread(() -> {
test4.readAndWrite();
}, "线程A").start();
new Thread(() -> {
test4.readAndWrite();
}, "线程B").start();
}
}
4.3.6 场景六:不同线程,先写后读—>失败—>不同的线程之间读锁和写锁是互斥的
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test5 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void writeAndRead(){
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try{
// 加写锁
writeLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加写锁");
// 加读锁
readLock.lock();
// 输出
System.out.println(Thread.currentThread().getName() + "加读锁");
Thread.sleep(3000);
}catch (Exception e){
}finally {
// 释放写锁
writeLock.unlock();
System.out.println("释放写锁");
// 释放读锁
readLock.unlock();
System.out.println("释放读锁");
}
}
/**
* 场景六:不同线程,先写后读--->失败--->不同的线程之间读锁和写锁是互斥的
* @param args
*/
public static void main(String[] args) {
// 初始化对象
Test5 test5 = new Test5();
new Thread(() -> {
test5.writeAndRead();
}, "线程A").start();
new Thread(() -> {
test5.writeAndRead();
}, "线程B").start();
}
}
4.3.7 场景七:同一个线程先读再读—>成功—>读锁可重入
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test6 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 先读后读
*/
public void readAndRead(){
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
try {
// 加锁一次
readLock.lock();
System.out.println("第一次成功!");
// 加锁两次
readLock.lock();
System.out.println("第二次成功!");
// 输出
System.out.println(Thread.currentThread().getName() + "加读锁成功!");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}finally {
readLock.unlock();
System.out.println("读锁释放一次");
readLock.unlock();
System.out.println("读锁释放两次");
}
}
/**
* 场景七:同一个线程先读再读--->成功--->读锁可重入
* @param args
*/
public static void main(String[] args) {
// 初始化对象
Test6 test6 = new Test6();
new Thread(() -> {
test6.readAndRead();
}, "读线程").start();
}
}
场景八:同一个线程先写再写—>成功—>写锁可重入
package com.atguigu;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test7 {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 先写后写
*/
public void writeAndWrite(){
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
try {
// 加锁一次
writeLock.lock();
// 输出
System.out.println("加写锁一次");
// 加锁两次
writeLock.lock();
// 输出
System.out.println("加写锁两次");
System.out.println(Thread.currentThread().getName() + "加锁成功");
Thread.sleep(3000);
}catch (Exception e){
}finally {
// 释放锁
writeLock.unlock();
System.out.println("释放写锁一次");
writeLock.unlock();
System.out.println("释放写锁两次");
}
}
/**
* 场景八:同一个线程先写再写--->成功--->写锁可重入
* @param args
*/
public static void main(String[] args) {
// 初始化对象
Test7 test7 = new Test7();
new Thread(() -> {
test7.writeAndWrite();
}, "写线程").start();
}
}
4.4 AQS(重点)
4.4.1 AQS简介
AbstractQueuedSynchronizer抽象的队列式的同步器, AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
4.4.2 AQS核心方法介绍
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
4.4.3 基于AQS的实际案列
以ReentrantLock为例:
- state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。
- 其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。
- 释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例:
- 任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。
- 这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。
- 等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
4.5 CAS
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
1.在内存地址V当中,存储着值为10的变量
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的
7.线程1进行SWAP,把地址V的值替换为B,也就是12
缺点:
1.CPU开销较大
2.不能保证代码块的原子性
应用场景
-
ConcurrentHashMap的put中key或者头节点不存在时用到
- AQS的实现类ReentrantReadWriteLock 中加锁释放锁中用到
- AQS的实现类ReentrantReadWriteLock 中加锁释放锁中用到
-
Unsafe中的getAndAddInt方法中有用到
4.6 小结(重要)
-
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
-
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。