volitile关键字
主要是为了保证可见性和有序性。
synchoronized优点缺点和适用场景。
栈桢的存储结构:https://blog.csdn.net/u014296316/article/details/82668670
如何保证线程的有序性。
JMM内存模型 happens-before原则
加了static和没加对synchronize的影响。
https://www.cnblogs.com/eternityz/p/12238802.html
对象的调用过程:https://www.cnblogs.com/gjmhome/p/11401397.html
通过实例引用调用实例方法的时候,先从方法区中对象的实际类型信息找,找不到的话再去父类类型信息中找。
堆区:
1.存储的全部是对象,每个对象都du包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
方法区:
1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
synchronized 在发生异常时会自动释放线程占用的锁资源,Lock 需要在异常时主动释放,synchronized 在锁等待状态下无法响应中断而 Lock 可以。
1、两者所处层面不同
synchronized是Java中的一个关键字,当我们调用它时会从在虚拟机指令层面加锁,关键字为monitorenter和monitorexit
Lock是Java中的一个接口,它有许多的实现类来为它提供各种功能,加锁的关键代码为大体为Lock和unLock;
2、获锁方式
synchronized可对实例方法、静态方法和代码块加锁,相对应的,加锁前需要获得实例对象的锁或类对象的锁或指定对象的锁。说到底就是要先获得对象的监视器(即对象的锁)然后才能够进行相关操作。
Lock的使用离不开它的实现类AQS,而它的加锁并不是针对对象的,而是针对当前线程的,并且AQS中有一个原子类state来进行加锁次数的计数
3、获锁失败
使用关键字synchronized加锁的程序中,获锁失败的对象会被加入到一个虚拟的等待队列中被阻塞,直到锁被释放;1.6以后加入了自旋操作
使用Lock加锁的程序中,获锁失败的线程会被自动加入到AQS的等待队列中进行自旋,自旋的同时再尝试去获取锁,等到自旋到一定次数并且获锁操作未成功,线程就会被阻塞
4、偏向或重入
synchronized中叫做偏向锁
当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。
Lock中叫做重入锁
AQS的实现类ReentrantLock实现了重入的机制,即若线程a已经获得了锁,a再次请求锁时则会判断a是否持正有锁,然后会将原子值state+1来实现重入的计数操作
5、Lock独有的队列
condition队列是AQS中的一个Lock的子接口的内部现类,它一般会和ReentrantLock一起使用来满足除了加锁和解锁以外的一些附加条件,比如对线程的分组和临界数量的判断(阻塞队列)
6、解锁操作
synchronized:不能指定解锁操作,执行完代码块的对象会自动释放锁
Lock:可调用unlock方法去释放锁比synchronized更灵活
7:可中断与不可中断:
Lock是可中断锁,而synchronized不是可中断锁。现假设线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,如果使用synchronized,如果A不释放,B将一直等下去,不能被中断;如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。获取锁超时机制还是属于不可中断,属于超时被动放弃去竞争锁,而lockInterruptibly是可主动放弃竞争锁行为的一种方式。
ReentrantLock的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。
从字节码中可知同步语句块实现是使用monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取objectref(对象锁)所对应的monitor的持有权。直到线程执行完毕,monitorexit指令执行,释放monitor。
synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。
释放锁的情况单一,不能绑定多个条件,不能实现读写锁,不可中断,不能获取锁的状态。
思考:为什么jdk源码中保留了大量的synchoronized
synchoronized使用监视器实现。 Java中每一个对象都和一个监视器关联,线程可以锁或则解锁监视器, 同一时间只有一个线程持有监视器的锁,其他任何想获取该监视器锁的线程都会被阻塞知道可以获得该锁(ps: 拥有锁的线程释放过后)。 一个线程可能对一个监视器锁多次(ps: 可重入),每一个解锁恢复一次锁操作(ps: 内部维护一个监视器锁的次数,每退出一个减少1直到为0就释放该监视器的锁)
synchoronized块计算一个对象的引用,然后开始在对象的监视器上执行锁操作并且不继续向下执行直到锁操作成功后。然后,synchoronized块的内容开始执行。 如果块中的内容执行完成(不管是正常还是突然(ps: 被外部关闭之类)),在该监视器上就会自动执行解锁操作。
synchoronized方法在调用它的时候自动执行锁操作。它的方法内容在成功获取到锁之前不会执行,如果是实例方法,它锁住了调用它的实例的监视器(方法中的this),如果是静态方法,它锁住了定义这个方法的类的Class对象的监视器, 如果块中的内容执行完成(不管是正常还是突然(ps: 被外部关闭之类)),在该监视器上就会自动执行解锁操作。
Java语言既不预防也不检查死锁(ps:这是程序员的事)
其他机制,比如volatile的读和写或则java.util.concurrent包提供了其他的可替代的同步方式。
synchronized块:
一个synchronized块请求一个互斥锁,当拥有锁的线程在执行时,其他线程要获取这个锁必须等待。 它的语法如下:
SynchronizedStatement: synchronized (Expression) Block
表达式的类型必须为引用类型,否则编译期报错。该方法块 首先计算表达式的值,然后执行其中的代码。** 如果计算表达式突然结束,那么代码块已同样的理由突然结束。 如果是null,就会抛出空指针异常。** 否则,就获取到表达式值锁代表的对象的监视器的锁,然后开始执行同步代码块。 如果代码块正常退出,监视器就会被解锁然后synchronized块也正常退出。 如果是已其他任何理由突然中断的话,监视器会被解锁并且同步代码块会已同样的方式结束。
synchronized方法:
一个synchronized方法在运行之前会先请求一个监视器(的锁)。对于一个静态方法,该类的Class对象关联的监视器将被获取。 对于一个实例方法, this所代表的实例的监视器将被获取。
同样,在JLS中也写清楚了每一个对象关联的监视器都有一个Wait Sets,显而易见的就是用来保存当前等待获取当前监视器锁的线程集合。该集合仅仅可以被Object.wait , Object.notify ,Object.notifyAll操纵。
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
线程总共有5大状态,通过上面第二个知识点的介绍,理解起来就简单了。
新建状态:新建线程对象,并没有调用start()方法之前
就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。
运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态
阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态
死亡状态:线程执行结束
重入锁:可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
可重入锁的意义之一在于防止死锁。
所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。例子:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。
自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。
自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此操作系统的实现在很多地方往往用自旋锁。Windows操作系统提供的轻型读写锁(SRW Lock)内部就用了自旋锁。显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。
互斥锁有一个缺点,他的执行流程是这样的 托管代码 - 用户态代码 - 内核态代码、上下文切换开销与损耗,假如获取到资源锁的线程A立马处理完逻辑释放掉资源锁,如果是采取互斥的方式,那么线程B从没有获取锁到获取锁这个过程中,就要用户态和内核态调度、上下文切换的开销和损耗。所以就有了自旋锁的模式,让线程B就在用户态循环等着,减少消耗。
自旋锁比较适用于锁使用者保持锁时间比较短的情况,因为如果锁保持时间较长,那么其他cpu空转等待的时间就会变长,这种情况下自旋锁的效率要远高于互斥锁。
自旋锁可能潜在的问题
过多占用CPU的资源,如果锁持有者线程A一直长时间的持有锁处理自己的逻辑,那么这个线程B就会一直循环等待过度占用cpu资源
java对于自旋锁的应用:cas,java.util.concurrent.atomic包
1、lock是可中断锁,而synchronized 不是可中断锁
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
2、synchronized是在JVM层面上实现的,lock是通过代码实现的,JVM会自动释放锁定(代码执行完成或者出现异常),但是使用Lock则不行,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
相同点: 都会让线程进入阻塞的状态. 都会响应中断.
不同点:
wait/notify 必须在同步方法中去执行(线程更加安全, 防止死锁和永久等待), 而sleep不需要
wait/notify 会释放锁, sleep不会释放锁
sleep方法必须传递参数,设置休眠时间. 而wait方法可以不传递时间参数, 如果不传递,直到被唤醒
wait/notify 所属的Object类(Java对象中,每一个类都是一把锁), sleep属于Thread.
主要是为了线程间通信的可靠, 防止死锁或者线程永久等待唤醒的发生 .
如果不把wait放入同步代码块中的话, 在wait执行之前, CPU的调度可能会把线程切换到另外一个notify的线程了,但是此时其实是没有wait中的线程的, 如果执行了notify线程后, 再执行wait, 那么会造成线程的永久等待唤醒. 而sleep是本身线程的休眠, 与其他线程的关系不大, 因此不需要放入同步代码块中.
wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。对语言设计者而言, 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用, 那么 Object 类则是的合理的声明位置。记住同步和等待通知是两个不同的领域,不要把它们看成是相同的或相关的。同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制。
每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因。
在 Java 中,为了进入代码的临界区,线程需要锁定并等待锁,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且需要等待以取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁。
Java 是基于 Hoare 的监视器的思想(http://en.wikipedia.org/wiki/…)。在Java中,所有对象都有一个监视器。
线程在监视器上等待,为执行等待,我们需要2个参数:
一个线程
一个监视器(任何对象)
在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,影响线程执行顺序,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中造成入侵的操作都被弃用了(例如 Thread.stop 方法)。
主要是因为 wait notify notifyAll 是一个锁级别的操作, 每一个对象, 在对象头中,都会有数据保存当前锁的状态的. 因此锁是绑定在某个对象中, 而不是线程中的. 反之 ,如果把wait notify notifyAll 定义在Thread中, 那么就会造成一定的局限性, 每一个线程虽然可以休眠, 每一个线程也可以持有多个锁, 并且这些锁是相互配合的 ,
如果把wait定义在Thread类中, 就无法实现灵活的逻辑.
由于Java中所有的类,都是继承于Obejct类的, 因此Thread.wait 也能使得线程进入等待状态 . 但是Thread类比较特殊的是, 线程退出的时候, 会自动的去执行notify , 这样会使得代码的流程受到干扰. 因此创建锁对象和调用wait方法的时候, 不要用Thread类.
之前的文章中, 已经对此有提到是选择使用notify 还是notifyAll. 如果是唤醒一个线程 ,就使用notify ,如果是多个线程, 那么就是notifyAll
https://javaweixin6.blog.csdn.net/article/details/108295773
notifyAll之后所有的线程都会再次的抢夺锁 , 只是回到了最初使的状态. 如果没有抢到锁, 就只会进入等待的状态, 等待其他的线程,释放锁, 再去抢锁, 或者再去接受线程调度器的调度. 直到去拿到这把锁.
suspend 和resume方法 已经由于安全问题, 被弃用了 , 推荐使用wait 和notify来实现 休眠和唤醒的功能.
可以实现
1:有序性
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
那么禁止指令重排序又是如何实现的呢?答案是加内存屏障。JMM为volatile加内存屏障有以下4种情况:
在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
上述内存屏障的插入策略是非常保守的,比如一个volatile的写操作后面需要加上StoreStore和StoreLoad屏障,但这个写volatile后面可能并没有读操作,因此理论上只加上StoreStore屏障就可以,的确,有的处理器就是这么做的。但JMM这种保守的内存屏障插入策略能够保证在任意的处理器平台,volatile变量都是有序的。
2:可见性
当写instance这个volatile变量时,发现add前面加个一个lock指令,我在截图中框了出来,如何不加volatile修饰,是没有lock的。
lock指令在多核处理器下会引发下面的事件:
将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。
为了提高处理速度,处理器一般不直接和内存通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完成后并不知道处理器何时将缓存数据写回到内存。但如果对加了volatile修饰的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量在缓存行的数据写回到系统内存。这时只是写回到系统内存,但其他处理器的缓存行中的数据还是旧的,要使其他处理器缓存行的数据也是新写回的系统内存的数据,就需要实现缓存一致性协议。即在一个处理器将自己缓存行的数据写回到系统内存后,其他的每个处理器就会通过嗅探在总线上传播的数据来检查自己缓存的数据是否已过期,当处理器发现自己缓存行对应的内存地址的数据被修改后,就会将自己缓存行缓存的数据设置为无效,当处理器要对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到自己的缓存行,重新缓存。
总结下:volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
写volatile时处理器会将缓存写回到主内存。
一个处理器的缓存写回到内存会导致其他处理器的缓存失效。
3:受限原子性
https://blog.csdn.net/haovin/article/details/93483240
所谓死锁,就是指两个或两个以上的线程/进程在执行的过程中,因争夺资源而造成的一种相互等到的现象,如果没有外力作用,他们将无法进行下去。
1、导致死锁的原因
产生死锁的可能原因有:
1、系统资源不足
2、资源分配不当
3、进程/线程运行推进的顺序不合适
产生死锁的四个必要条件:
1、互斥条件,指分配的资源进行排他性使用,即在一定的时间内该资源只能被一个进程/线程占用,如果此时还有其他进程/线程请求该资源,则只能等待,直到该资源占用着自己使用完成后释放。
2、请求与保持条件,指进程/线程已经获得了资源,又提出了新的资源请求,而这个资源已经被 其他进程/线程占有,此时请求进程/线程阻塞,但自己之前已经获得的资源继续保持占有。
3、不可剥夺条件,指进程/线程已经获得资源,在没有使用完成之前,不能被抢占剥夺,只能使用完成后自己释放。
4、循环等待条件,指发生死锁时,必然存在一个资源占用链,即P1等待P2正在占用的资源,P2等待P3正在占用的资源…Pn等待P1正在占用的资源.
2、死锁的处理
https://blog.csdn.net/shaowei6969/article/details/108032403
措施,有
在JDK1.6之后JVM对synchronized底层进行了优化,提高了并发访问性能得到提升:
第一点:锁的膨胀升级过程是最大的优化
第二点:锁的粗化
第三点:锁的消除
锁的膨胀升级过程:
无锁–》偏向锁–》轻量级锁–》重量级锁(互斥锁)
锁优化后一共有四种状态,无锁、偏向锁、轻量级锁、重量级锁。一开始从new出来的对象没有线程使用和加锁是无锁状态,然后随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重 量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级
偏向锁:
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞 争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心 思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争 的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就 失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的 是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁
偏向锁是针对在单个同一线程执行的情况下,对锁的一种优化,就是同一线程多次获取锁,不需要再次申请锁,提高性能。
轻量级锁:
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时 Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞 争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场 合,就会导致轻量级锁膨胀为重量级锁
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情 况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线 程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是 自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了
JVM在开启逃逸分析之后,对代码上下文的分析,然后把一些不合理的代码进行优,比如在同一个方法里,多次调用同一个同步的方法,会在编译的时候进行优化,将多次加锁和释放,优化成一次加锁和释放。
当加锁对象是在方法里的局部变量,属于线程栈独享的锁,不存在其他线程来竞争的情况,逃逸分析会对同步块的锁消除掉。
应用层面
是一种互斥锁,怎么应用,加在方法、方法体同步代码块、静态方法,不同的位置加锁的粒度不一样。
原理层面:
入锁,从字节码层面代码怎么做的,然后锁的优化(粗化、消除、逃逸分析),锁的膨胀升级过程
实现类似synchronied互斥锁的功能,由java实现的工具包,在juc包下的lock包有dog li 写的aqs抽象队列同步器也能实现类似的功能,比如该框架的实现类reetrantlock,是一种互斥锁,实现了aqs的特性,例如非公平锁与公平锁 可重入。
与synchronied的区别,synchronied是由jvm自己实现加锁和释放的过程,是一种内置隐式锁。而我们的
AbstractQueuedSynchronizer实现的锁是一种显示锁,需要手动加锁和释放,