JUC有哪些知识点?
什么是Juc
Lock接口
线程间通信
集合的线程安全问题
多线程锁
Callable接口
JUC三大辅助类 CountDownLatch CyclicBarrier Semaphore
读写锁 ReetrantReadWriteLOck
阻塞队列
ThreadPool线程池
Fork/join
CompletableFuture
中断机制
LockSupport
JMM
volatile
CAS
原子类
ThreadLocal
synchronized之锁升级
AQS
StampedLock
是 java.util .concurrent 工具包的简称,这是一个处理线程的工具包,JDK 1.5 开始出现的
public enum State {
/**
* Thread state for a thread which has not yet started.
新建态,线程尚未执行
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
可运行态,可以理解为操作系统本地线程的就绪态
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
阻塞态,一个等待获得同步监视器,准备进入同步带模块的线程
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
*
* - {@link Object#wait() Object.wait} with no timeout
* - {@link #join() Thread.join} with no timeout
* - {@link LockSupport#park() LockSupport.park}
*
*
* A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called Object.wait()
* on an object is waiting for another thread to call
* Object.notify() or Object.notifyAll() on
* that object. A thread that has called Thread.join()
* is waiting for a specified thread to terminate.
等待态,必须满足上述方法调用逻辑的情况下才能成为此状态
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
*
* - {@link #sleep Thread.sleep}
* - {@link Object#wait(long) Object.wait} with timeout
* - {@link #join(long) Thread.join} with timeout
* - {@link LockSupport#parkNanos LockSupport.parkNanos}
* - {@link LockSupport#parkUntil LockSupport.parkUntil}
*
超时等待态,必须满足上述方法调用逻辑的情况下才能成为此状态
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
终结态,线程完成执行任务。
*/
TERMINATED;
}
和操作系统里对进程、线程状态的描述还是有区别的
操作系统中 进程五态和线程的状态是类似的主要关注的就是 就绪 阻塞 执行。
(1)sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都 能调用。
(2)sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提 是当前线程占有锁(即代码要在 synchronized 中)。
(3)它们都可以被 interrupted 方法中断。
请注意,我问题中的【异步性】 是操作系统的特性,它指的是
操作系统对于进程/作业的调度顺序在业务层面来看是不确定的(并不是从具体调度算法的角度看),在并发执行任务的场景下,就可能出现这些现象:
一个任务多次被执行,但是其他任务始终没用被执行
理想希望任务按照一定的顺序执行,但实际执行中任务是乱序的。
在操作系统中,解决上述问题的办法,就是同步与互斥机制。
同步互斥机制在实现原理上基本就是硬件层面和软件层面,具体实现有原语、互斥锁,信号量,管程等机制。
同步是一种直接制约关系,它强调的是对进程/线程的工作顺序、传递信息进行控制,协调。
互斥是间接制约关系 它表现的是多个进程/线程对临界资源的访问关系,一者访问,其余等待。
在java中,对于并发执行产生的异步性问题,也有它自己的具体实现方案。除此之外,Java所说的异步处理任务,和操作系统异步性的概念是不一样的。
java的异步处理任务,是通过Java的某些API或外部中间件,使得的多个任务的执行互相不再发生干涉,简而言之,就是:
你干你的事,我干我的事,你干完给我一个结果通知,而不是让我干等你的执行结果,再开始工作。
但是请注意一点:不管是开发上的异步处理,同步、互斥,他们并不是在任何场景下都能用的,要根据具体业务场景来具体设计。
比如生产者-消费者模型下,缓冲区(容量是1)内必须要写满数据,消费者才能执行,缓冲区内为空,生产者才能生产。二者不可以同时访问缓冲区。
这个模型就是典型的同步+互斥。并不适合用异步处理。
那怎么用异步来改善这个模型呢?
假设当前的生产者执行的速率很快,消费者的执行速率很慢,此时就存在一个速度差。
为了缓解这个问题,我们可与模拟计算机网络的流量控制的思想,为消费者创建接收缓存队列(容量大于5),
假设生产速度是消费速度的5倍,缓冲区内很快塞满了,消费者一个一个的去消费,就可能让生产者一直等待。但是有了接收缓存队列后,生产者可以保持运行,也不必等待消费者必须把缓冲区的内容消费了,生产者可以生产到缓存队列满了再停等,减少了系统的停等时间。
进一步的,可以设计一个滑动窗口算法,让二者的发送和接收的速率达到一个平衡点。
通过一个缓存队列,把生产者和消费者的直接制约关系破坏,打破同步,形成异步关系,可以有效提高CPU的利用率,减少空等时间,提高了系统吞吐量。
java关于异步的一个API CompletableFuture,在后面再详细介绍。
Q:管程
管程实际上操作系统的一种进程同步工具。管程可以保证进程互斥,降低由于信号量pv操作不当引发的死锁问题
同一时间内,只有一个进程可以在管程内活动。
管程有自己的共享数据结构和具体过程
共享数据结构抽象的标识系统的共享资源,而对该数据结构执行的一组操作,可以定义为一组过程。进程对于共享资源的申请、释放等操作,都是通过这些过程来实现的。这组过程可以根据资源共享的情况,或接收或阻塞进程的访问,确保每次仅仅有一个进程使用共享资源,这样就实现对共享资源的统一管理,实现互斥。
简单来说,管程的定义结构如下:
管程的名称
局部于管程内的共享数据结构
对该数据结构进行操作的一组过程
对局部于管程内的数据结构的初始值语句
除了上面对管程的定义外,管程还具有条件变量
当一个进程进入管程后,其他所有尝试获取管程的进程会被阻塞,被加入该管程的阻塞队列。
直到该进程用完管程,释放资源,并且唤醒处于阻塞队列中的进程。
monitor Demo{
共享数据结构 S;
condition x;
init_code(){...}
take_away(){
if(S<0) x.wait();
//若资源足够,则不用阻塞,直接进行分配
}
give_back(){
归还资源 进一步处理;
if(阻塞队列里有进程等待) x.signal() //唤醒一个进程
}
}
现在我们回到java的管程——synchronized 同步监视器
JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程 (monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁
执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方 法在执行时候会持有管程,其他线程无法再获取同一个管程.
我们可以看到,JVM中的管程是线程粒度的,毕竟,一个JVM只对应一个工作进程。
用户线程:平时用到的普通线程,自定义线程
守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
当主线程结束后,用户线程还在运行,JVM 存活
如果没有用户线程,都是守护线程,JVM 结束
先说一下Synchronized:
上文中已经介绍了管程的具体概念,这里说一下Synchronized的常规用法:
synchronized 是 Java 中的关键字,是一种同步锁。它修饰的目标有以下几种:
1). 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象;
2). 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用 的对象是调用这个方法的对象;
注意同步监视器的类型 :
可以是普通对象,也可以是Class对象
前者在普通方法,或者代码块中使用,锁住的粒度是一个被实例化的对像。(要搞清楚一个概念,所谓的锁,加锁的底层实质是通过一些PV原语,来锁某些资源,控制线程对临界资源访问的行为,之所以线程只能串行访问,是因为底层有一个阻塞队列和唤醒机制)。
java每个对象都有一个监视器,生命周期等同于对象。这就使得线程在进行同步时,可以将锁进行对象化处理。可以理解是把锁进行了具象化。(就像令牌网,谁拿到令牌,谁就可以说话)
一个线程A 获得了管程对象(锁),去访问临界资源(这个资源可以在锁里,也可以与锁无关),当他操作的过程中,其他线程B请求访问临界资源,结果被放进了锁对象的阻塞队列等待,当A操作完成,唤醒阻塞中的线程B。
但是它锁的仅仅是一个具体的被实例化的对象 。
后者锁住的是整个类的对象。它的作用范围是这个类的所有对象。(在静态方法,代码块中使用)
class Ticket{
//临界资源
int number=10;
//每个对象内都有一个monitor,随着对象而生死,在普通方法中监视器对象默认为this
public synchronized void sale(){
if(num>0){
num--;
}
}
}
如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执 行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里 获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;(线程的正常退出)
2)线程执行发生异常,此时 JVM 会让线程自动释放锁。(线程的异常退出)
那么如果这个获取锁的线程由于要等待 IO 或者其他原因被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一 下,这多么影响程序执行效率。
//注意,这里不是说synchronized 在线程异常的时候释放不掉锁,而是强同步性导致的持锁线程如果是长作业或必须要等待IO的时候,没用一个主动释放(或条件释放)的机制,即使是wait()和notify()机制 这个也是Object的,并不是synchronized 自带的。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等 待一定的时间或者能够响应中断),通过 Lock 就可以办到。
public interface Lock {
//Acquires the lock
void lock();
//Acquires the lock unless the current thread is interrupted.
void lockInterruptibly() throws InterruptedException;
//Acquires the lock only if it is free at the time of invocation
boolean tryLock();
//Acquires the lock if it is free within the given waiting time and the current thread has not been interrupted.
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//Releases the lock.
void unlock();
//Returns a new Condition instance that is bound to this Lock instance.
Condition newCondition();
}
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允 许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对 象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内 置特性。Lock 是一个类,通过这个类可以实现同步访问;
Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户 去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如 果没有主动释放锁,就有可能导致出现死锁现象。
下面逐个讲解Lock接口的每个方法:
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他 线程获取,则进行等待。 采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一 般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock 来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock(); //上锁
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通 知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:
• await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重 新获得锁并继续执行。
• signal()用于唤醒一个等待的线程。
==注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦 获得锁成功就继续执行。==
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。
ReadWriteLock 也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分 成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。
ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个 方法:readLock()和 writeLock()用来获取读锁和写锁。
下面通过几个例子来看一下 ReentrantReadWriteLock 具体用法。 假如有多个线程要同时进行读操作的话,先看一下 synchronized 达到的效果:
synchronized 是全部都阻塞
但是读写锁是 只读时,不阻塞
==注意:== • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写 锁的线程会一直等待释放读锁。
• 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则 申请的线程会一直等待释放写锁。
可以同时读,但是不能同时写,也不可以一边读一边写。只要有线程在工作,新的写请求就必须阻塞。(后面详细讲解)
Lock 和 synchronized 有以下几点不同:
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内 置的语言实现;
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现 象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很 可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源 非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。
线程间通信的模型有两种:共享内存和消息传递
记得IPC(进程间通信)么,一回事,共享内存,消息机制,管道
场景---两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求 用线程间通信
package com.alex.java.lockDemo.InterThread_communication;
/**
* @Author: Alex
* @Email: [email protected]
* @Description:
* 基于synchronized
* @Date: 2023/8/10 14:55
*/
public class Demo1 {
public static void main(String[] args) {
EntryBody entryBody = new EntryBody();
new Thread(()->{
for(int i=0;i<5;i++){entryBody.increment();}
},"线程1").start();
new Thread(()->{
for(int i=0;i<5;i++){entryBody.decrement();}
},"线程2").start();
}
}
class EntryBody{
private int number =0;
public synchronized void increment(){
try{
while(number!=0){
this.wait();
System.out.println("尝试加一,被阻塞");
}
number++;
System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------,值为:" + number);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void decrement(){
try {
while (number == 0){
this.wait();
System.out.println("尝试减一,被阻塞");
}
number--;
System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------,值为:" + number);
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
}
class EntryBody2{
//加减对象
private int number = 0;
//声明锁
private Lock lock = new ReentrantLock();
//声明钥匙
private Condition condition = lock.newCondition();
/**
* 加 1
*/
public void increment(){
try {
lock.lock();
while(number!=0){
condition.await();
System.out.println("尝试加一,被阻塞");
}
number++;
System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------,值为:" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* 减一
*/
public void decrement(){
try {
lock.lock();
while (number == 0){
condition.await();
System.out.println("尝试减一,被阻塞");
}
number--;
System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------,值为:" + number);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
==问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照 此顺序循环 10 轮==
package com.alex.java.lockDemo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: Alex
* @Email: [email protected]
* @Description: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照
* 此顺序循环 10 轮
* @Date: 2023/8/10 15:24
*/
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
ExcuteMan excuteMan = new ExcuteMan();
while(true) {
new Thread(() -> {
excuteMan.printA();
}, "A").start();
new Thread(() -> {
excuteMan.printB();
}, "B").start();
new Thread(() -> {
excuteMan.printC();
}, "C").start();
TimeUnit.SECONDS.sleep(3);
}
}
public void foo() {
}
}
class ExcuteMan {
Lock lock = new ReentrantLock();
//顺序标识 这个思想在 读者 - 写者模型里也用到了 读者计数器
int num =0;
//三个钥匙
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
public void printA() {
try {
//抢到就上锁
lock.lock();
while(num!=0){
System.out.println("A抢到了");
conditionA.await();//await *有释放锁的过程,否则会死锁
}
for (int i = 0; i < 5; i++) {
System.out.println("我是A");
}
num=1;
conditionB.signalAll(); //*唤醒阻塞队列的线程,根据不同条件形成不同阻塞队列
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void printB() {
try {
//抢到就上锁
lock.lock();
while(num!=1){
conditionB.await();
}
for (int i = 0; i < 10; i++) {
System.out.println("我是B");
}
num=2;
conditionC.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void printC() {
try {
//抢到就上锁
lock.lock();
while(num!=2){
conditionC.await();
}
for (int i = 0; i < 15; i++) {
System.out.println("我是C");
}
num=0;
conditionA.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
星号 * 在AQS 篇进行论证。
package com.atguigu.test;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 集合线程安全案例
*/
public class NotSafeDemo {
/**
* 多个线程同时对集合进行修改
* @param args
*/
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 0; i < 100; i++) {
new Thread(() ->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}, "线程" + i).start();
}
}
}
异常内容 java.util.ConcurrentModificationException
看一下描述:
这个异常可能被检测到并发的方法抛出在不允许的情况下对对象进行修改。
上面例子中,多个线程在并发的执行,就有可能会同时的去修改list。
从机器指令角度来说,list在 add 方法执行过程中,被拆分成多个指令,一个线程
在list的位置x出写入某一数据,由于并发性缘故,其他线程在这一时刻打断了该线程的执行,覆盖了它修改的内容。
可以看见这个方法没用线程安全修饰
==那么我们如何去解决 List 类型的线程安全问题?==
Vector
Collections
CopyOnWriteArrayList
Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现 了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList, 实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功 能。 Vector 实现了 RandmoAccess 接口,即提供了随机访问功能。 RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在 Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访 问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。
*注:RandmoAccess 是一个mark型的接口,具体如何工作 后续补充
==和 ArrayList 不同,Vector 中的操作是线程安全的。==
class SafeCollection{
public void foo(){
Vector vector = new Vector();
for(int i = 0; i < 100; i++){
new Thread(() ->{
vector.add(UUID.randomUUID().toString());
System.out.println(vector);
}, "线程" + i).start();
}
}
}
Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的
Collections的整体结构设计很有意思。通过静态内部类来实现对普通集合进行安全封装
比如:
具体在使用时候如下:
class SafeCollection2{
public void foo(){
List list = Collections.synchronizedList(new ArrayList());
for(int i = 0; i < 100; i++){
new Thread(() ->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}, "线程" + i).start();
}
}
}
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时,它具有以下特性:
1.它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多 于可变操作,需要在遍历期间防止线程间的冲突。
2.它是线程安全的。
3.因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
4.迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
5.使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代 器时,迭代器依赖于不变的数组快照
特定:
独占锁效率低:采用读写分离思想解决
写线程获取到锁,其他写线程阻塞
复制思想
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容 器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素 之后,再将原容器的引用指向新的容器。
原因分析(重点):
==动态数组与线程安全== 下面从“动态数组”和“线程安全”两个方面进一步对 CopyOnWriteArrayList 的原理进行说明。
• “动态数组”机制
o 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据 时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该 数组赋值给“volatile 数组”, 这就是它叫做 CopyOnWriteArrayList 的原因
o 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的 操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话, 效率比较高。
• “线程安全”机制
o 通过 volatile 和互斥锁来实现的。
o 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看 到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读 取到的数据总是最新的”这个机制的保证。
o 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”, 再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥 锁”,就达到了保护数据的目的。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的 Class 对象。
对于同步方法块,锁是 Synchonized 括号里配置的对象
//TODO
Thread 创建线程 或Runable创建线程,run都是无返回值的。为了支持此功能, Java 中提供了 Callable 接口。
• 为了实现 Runnable,需要实现不返回任何内容的 run()方法,而对于 Callable,需要实现在完成时返回结果的 call()方法。
• call()方法可以引发异常,而 run()则不能。
• 为实现 Callable 而必须重写 call 方法
• 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread2 implements Callable{
//带返回值
@Override
public String call() throws Exception {
return "hello I am callable !!";
}
}
当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可 以知道该线程返回的结果。为此,可以使用 Future 对象
API:描述
Future表示异步计算的结果。提供了检查计算是否完成、等待计算完成和检索计算结果的方法。只有在计算完成后使用方法get才能检索结果,必要时阻塞,直到准备好为止。取消由cancel方法执行。提供了其他方法来确定任务是否正常完成或被取消。一旦计算完成,计算就不能被取消。如果为了可取消性而使用Future,但不提供可用的结果,则可以声明Future>并返回null作为底层任务的结果。
怎么样把Future接口和Runable接口结合起来,在线程中运用?
public interface RunnableFuture extends Runnable, Future {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
public class FutureTask implements RunnableFuture
最终供开发人员使用的是FutureTask
通过构造器 就可以把callable这个可以获得返回值的 线程任务接口,与异步性的Future结合在了一起
public FutureTask(Callable callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
这里不得不先说说FutureTask到底是个啥:
/**
* A cancellable asynchronous computation. This class provides a base
* implementation of {@link Future}, with methods to start and cancel
* a computation, query to see if the computation is complete, and
* retrieve the result of the computation. The result can only be
* retrieved when the computation has completed; the {@code get}
* methods will block if the computation has not yet completed. Once
* the computation has completed, the computation cannot be restarted
* or cancelled (unless the computation is invoked using
* {@link #runAndReset}).
*
* A {@code FutureTask} can be used to wrap a {@link Callable} or
* {@link Runnable} object. Because {@code FutureTask} implements
* {@code Runnable}, a {@code FutureTask} can be submitted to an
* {@link Executor} for execution.
*
*
In addition to serving as a standalone class, this class provides
* {@code protected} functionality that may be useful when creating
* customized task classes.
*
* @since 1.5
* @author Doug Lea
* @param The result type returned by this FutureTask's {@code get} methods
*/
首先,我们可以看出,它可以执行一次异步计算任务,而且在执行时可以被取消的(有状态机制)
在Thread类中的一些构造函数中,是可以接收Futuretask的参数的,因为Futuretask也间接的实现了Runable接口。
所以,关于callable FutureTask 我们可以写这个例子:
//TODO
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些 作业交给 Future 对象在后台完成
• 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执 行状态FutureTask 有个构造函数:
• 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去 获取结果。
• 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法 • 一旦计算完成,就不能再重新开始或取消计算
• get 方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完 成状态,然后会返回结果或者抛出异常
• get 只计算一次,因此 get 方法放到最后
• 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些 作业交给 Future 对象在后台完成, 当主线程将来需要时,就可以通过 Future 对象获得后台作业的计算结果或者执行状态
• 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去 获取结果
• 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计 算完成,就不能再重新开始或取消计算。get 方法而获取结果只有在计算完成 时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异 常。
• 只计算一次
CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞, 可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可 以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future 接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程 的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的 CompletableFuture 类。
在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提 交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方 法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获 取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或 者不断轮询才能知道任务是否完成。
Future 的主要缺点如下:
(1)不支持手动完成 我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果, 现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直 等待它执行完成
(2)不支持进一步的非阻塞调用 通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行 额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能 (3)不支持链式调用 对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成 一个链式的 pipline 调用,这在 Future 中是没法实现的。
4)不支持多个 Future 合并 比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后, 执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理 Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题 是不好定位的。
场景:主线程里面创建一个 CompletableFuture,然后主线程调用 get 方法会 阻塞,最后我们在一个子线程中使其终止。
public class CompletableFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture
可以看到,子线程被阻塞
没保存。给我整没了????
JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过 多时 Lock 锁的频繁操作。这三种辅助类为:
• CountDownLatch: 减少计数
• CyclicBarrier: 循环栅栏
• Semaphore: 信号灯
• CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这 些线程会阻塞
• 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程 不会阻塞)
• 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
场景: 6 个同学陆续离开教室后值班同学才可以关门。
概括模型: 事件x 要先等其他几个事件完成,才能发生
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch count = new CountDownLatch(6);
for(int i =0;i<6;i++){
final String nu=String.valueOf(i);
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 同学 --- ");
try {
if(Thread.currentThread().getName().equals("5")){
TimeUnit.SECONDS.sleep(2);
}
System.out.println(Thread.currentThread().getName() + "离开了");
count.countDown(); //计数器减一
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},nu).start();
}
System.out.println("班长在等着 (主线程)");
count.await();//班长(主线程)被阻塞
System.out.println("同学全部离开,班长关灯走人");
}
}
CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一 次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作
场景: 集齐 7 颗龙珠就可以召唤神龙
package com.alex.java.jucDemo.auxiliary;
import java.util.concurrent.CyclicBarrier;
/**
* @Author: Alex
* @Email: [email protected]
* @Description:
* @Date: 2023/8/12 13:55
*/
public class CyclicBarrierDemo {
//定义神龙召唤需要的龙珠总数
private final static int NUMBER = 7;
/**
* 集齐 7 颗龙珠就可以召唤神龙
* @param args
*/
public static void main(String[] args) {
//定义循环栅栏
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () ->{
System.out.println("集齐" + NUMBER + "颗龙珠,现在召唤神龙!!!!!!!!!");
});
//定义 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() + "收集到 了!!!!");
}
cyclicBarrier.await(); //阻塞点在这
{ //await()之后的语句,在达成阻塞了Number次之后,才会被执行
System.out.println("释放阻塞点!!!" + Thread.currentThread().getName());
}
}catch (Exception e){
e.printStackTrace();
}
}, "龙珠" + i + "号").start();
}
}
}
龙珠2号收集到 了!!!!
龙珠5号收集到 了!!!!
龙珠4号收集到 了!!!!
龙珠6号收集到 了!!!!
龙珠7号收集到 了!!!!
龙珠 3 号抢夺战结束,孙悟空打赢了,拿到了龙珠 3号!
集齐7颗龙珠,现在召唤神龙!!!!!!!!!
释放阻塞点!!!龙珠3号
释放阻塞点!!!龙珠1号
释放阻塞点!!!龙珠2号
释放阻塞点!!!龙珠7号
释放阻塞点!!!龙珠5号
释放阻塞点!!!龙珠6号
释放阻塞点!!!龙珠4号
Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方 法获得许可证,release 方法释放许可
场景: 抢车位, 6 部汽车 3 个停车位
package com.alex.java.jucDemo.auxiliary;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @Author: Alex
* @Email: [email protected]
* @Description:
* @Date: 2023/8/12 14:11
*/
public class SemaphoreDemo {
private final static int Number =6;
public static void main(String[] args) throws InterruptedException {
Semaphore avaliable = new Semaphore(3); //只有3个空闲车位 perimts许可数
for(int i=0;i<6;i++){
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"准备停车 ,寻找车位........ ");
avaliable.acquire(); //请求许可,请求不到的等待其他线程释放
System.out.println(Thread.currentThread().getName()+"找到车位,停车成功");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
System.out.println(Thread.currentThread().getName()+ "准备撤》》》》");
avaliable.release(); //释放许可
}
},"car--"+i).start();
}
}
}
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那 么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以 应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称 为排他锁
• 没有其他线程的写锁
• 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入 锁)。
• 没有其他线程的读锁
• 没有其他线程的写锁
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公 平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为 读锁
案例:
场景: 使用 ReentrantReadWriteLock 对一个 hashmap 进行读和写操作
public class ReetrantRWLockDemo2 {
//创建map集合
private volatile Map map =new HashMap<>();
//创建读写锁对象
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据:写操作
public void put(String key,Object value){
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName()+" 正在读操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 读完了 ="+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return result;
}
public static void main(String[] args) {
ReetrantRWLockDemo2 demo = new ReetrantRWLockDemo2();
for(int i=0;i<5;i++){
final int num= i;
new Thread(()->{
String key = String.valueOf(num);//UUID.randomUUID().toString().substring(0,8);
String value =new String(Thread.currentThread().getName());
demo.put(key,value);
},"写者 "+i).start();
}
for(int i=0;i<5;i++){
final int num=i%10;
new Thread(()->{
Object o = demo.get(String.valueOf(num));
//System.out.println("读到 -- key =" +num +" value = "+o.toString());
},"读者 "+i).start();
}
}
}
写者 0 正在写操作0
写者 0 写完了0
写者 2 正在写操作2
写者 2 写完了2
写者 1 正在写操作1
写者 1 写完了1
写者 3 正在写操作3
写者 3 写完了3
写者 4 正在写操作4
写者 4 写完了4
读者 0 正在读操作0
读者 1 正在读操作1
读者 2 正在读操作2
读者 3 正在读操作3
读者 4 正在读操作4
读者 0 读完了 =0
读者 3 读完了 =3
读者 2 读完了 =2
读者 1 读完了 =1
读者 4 读完了 =4
从输出结果,可以看到,写锁是互斥的,写的线程之间互斥的访问map。
但是读锁是共享锁。
再看看读写锁的几个特性:
This lock allows both readers and writers to reacquire read or write locks in the style of a ReentrantLock
. Non-reentrant readers are not allowed until all write locks held by the writing thread have been released.
Additionally, a writer can acquire the read lock, but not vice-versa. Among other applications, reentrancy can be useful when write locks are held during calls or callbacks to methods that perform reads under read locks. If a reader tries to acquire the write lock it will never succeed.
重入性:
它是允许所有的读线程和写线程以可重入的方式,再次获取读锁或写锁。在写线程持有的所有写锁被释放之前,不允许使用不可重入的读取器。
另外,写锁可以获得读锁,反之则不行。
public void put(String key,Object value){
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
System.out.println(Thread.currentThread().getName()+"尝试重入写锁");
{ //本线程重入写锁
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "写锁被重入了");
map.put(key, "重入时被修改");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
{
System.out.println(Thread.currentThread().getName()+"尝试获取写锁");
put(key,"读锁能否获取写锁?"); //put操作中有获取写锁的步骤。 发现获取不了,被阻塞
}
System.out.println(Thread.currentThread().getName()+" 正在读操作"+key);
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 读完了 ="+result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return result;
}
写者 0 正在写操作0
写者 0 写完了0
写者 1 正在写操作1
写者 1 写完了1
读者 0尝试获取写锁
读者 1尝试获取写锁
读者 2尝试获取写锁
读者 3尝试获取写锁
读者 4尝试获取写锁
结果被阻塞
public void put(String key,Object value){
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
{ //写锁内 获取读锁
System.out.println(Thread.currentThread().getName()+"尝试获取读锁");
Object o = get(key); //get中有获取读锁的行为
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
输出结果:
------------------
写者 0 正在写操作0
写者 0 写完了0
写者 0尝试获取读锁
写者 0 正在读操作0
写者 0 读完了 =写者 0
-------------------
写者 1 正在写操作1
写者 1 写完了1
写者 1尝试获取读锁
写者 1 正在读操作1
写者 1 读完了 =写者 1
-------------------
读者 0 正在读操作0
读者 1 正在读操作1
读者 2 正在读操作0
读者 4 正在读操作0
读者 3 正在读操作1
读者 2 读完了 =写者 0
读者 1 读完了 =写者 1
读者 3 读完了 =写者 1
读者 0 读完了 =写者 0
读者 4 读完了 =写者 0
程序正常输出,并没有被阻塞
Reentrancy also allows downgrading from the write lock to a read lock, by acquiring the write lock, then the read lock and then releasing the write lock. However, upgrading from a read lock to the write lock is not possible.
写锁的降级:方法是先获得写锁,再获得读锁,然后释放写锁
案例:
public void put(String key,Object value){
//添加写锁
try {
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
//写锁内 获取读锁
rwLock.readLock().lock();
rwLock.writeLock().unlock();//将写锁进行降级
System.out.println(Thread.currentThread().getName() + "写锁已经被降级!!!!!");
System.out.println(Thread.currentThread().getName() + " 正在读操作" + key);
TimeUnit.MICROSECONDS.sleep(300);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读完了 =" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
}
public static void main(String[] args) {
ReetrantRWLockDemo3 demo = new ReetrantRWLockDemo3();
for(int i=0;i<3;i++){
final int num= i;
new Thread(()->{
String key = String.valueOf(num);//UUID.randomUUID().toString().substring(0,8);
String value =new String(Thread.currentThread().getName());
demo.put(key,value);
},"写者 "+i).start();
new Thread(()->{
Object o = demo.get(String.valueOf(num));
},"读者 "+i).start();
}
}
输出结果:
写者 0 正在写操作0
写者 0 写完了0
-----------------------
写者 0写锁已经被降级!!!!!
写者 0 正在读操作0
读者 0 正在读操作0
写者 0 读完了 =写者 0
读者 0 读完了 =写者 0
------------------------
写者 1 正在写操作1
写者 1 写完了1
写者 1写锁已经被降级!!!!!
读者 1 正在读操作1
写者 1 正在读操作1
写者 1 读完了 =写者 1
读者 1 读完了 =写者 1
写者 2 正在写操作2
写者 2 写完了2
写者 2写锁已经被降级!!!!!
读者 2 正在读操作2
写者 2 正在读操作2
写者 2 读完了 =写者 2
读者 2 读完了 =写者 2
可以看到降级后,读者和写者之间出现交替执行现象
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发 现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
读时谁都不能写
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写 锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
写时只能自己读
原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把 获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写 锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释 放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
注意:ReentrantReadWriteLock 的实例化对象,在多线程环境下,只有1把,归根结底,这还是个同步模型。
ReetrantReadWriteLock有三个比较重要的内部类:
ReadLock
WriteLock
和Sync (通过AQS来实现)
写锁的lock
/**
* Acquires the write lock.
* 获取一个写锁
* Acquires the write lock if neither the read nor write lock
* are held by another thread
* and returns immediately, setting the write lock hold count to
* one.
*
*
If the current thread already holds the write lock then the
* hold count is incremented by one and the method returns
* immediately.
如果读锁和写锁都没有被另一个线程持有,则获取写锁并立即返回,将写锁持有计数设置为1。如果当前线程已经持有写锁,那么持有计数将增加1,并且该方法立即返回。(写锁的可重入)
*
*
If the lock is held by another thread then the current
* thread becomes disabled for thread scheduling purposes and
* lies dormant until the write lock has been acquired, at which
* time the write lock hold count is set to one.
如果该锁由另一个线程持有,那么当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到获得写锁,此时写锁持有计数被设置为1。
补充一句。当前线程如果已经获得读锁,获取写锁也是失败的。
*/
public void lock() {
sync.acquire(1);
}
其中的
sync.acquire(1)
以独占模式获取,忽略中断。通过调用至少一次tryAcquire实现,成功时返回。否则,线程将被排队,可能会反复阻塞和解除阻塞,调用tryAcquire直到成功。这个方法可以用来实现方法Lock.lock。
读锁的lock
/**
* Acquires the read lock.
* 获取读锁
* Acquires the read lock if the write lock is not held by
* another thread and returns immediately.
如果写锁未被其他线程持有,则获取读锁并立即返回。(潜台词,只能本线程持有写锁,才有可能继续获得读锁)
*
*
If the write lock is held by another thread then
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until the read lock has been acquired.
如果写锁由另一个线程持有,那么当前线程就会因为线程调度的目的而被禁用,并且在获得读锁之前处于休眠状态。
*/
public void lock() {
sync.acquireShared(1);
}