无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
CAS算法:Java的Atomic包使用的CAS算法来更新数据,而不需要加锁
使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
synchronized关键字锁住的是对象,而不是代码块,具体锁住的是对象的对象头。对象的对象头中有3个bit来表是对象被锁的状态:
JMM关于synchronized的两条规定:
在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量锁和重量锁。
这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级成轻量锁后不能降级成偏向锁。
这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁的初始化:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁们只需简单地测试一下对象头的Mark Word里是否存储这只想当前线程的偏向锁。如果测试成功,表示已经获得了锁;如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1:如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁只想当前线程。
偏向锁的撤销:偏向锁使用了一种等到竞争者出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
关闭偏向锁:偏向锁在Java中是默认启动的,但是但是它在应用程序启动几秒钟之后才激活,如有必要,可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果确定应用程序所有的锁通常情况处于竞争状态,可以通过JVM参数关闭偏向锁:-XX-UseBiasedLocking=false,那么程序默认会进入轻量锁状态。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁的竞争,会带来额外的锁撤销的消耗 | 只有一个线程访问同步块的场景 |
轻量锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行时间较长 |
package com.demo.study.concurrent.synchronize.demo01;
/**
* Descriptions...
*
* @author Johnson
* @date 2019/10/3.
*/
public class Demo {
private int count = 10;
private static int sCount = 10;
private Object object = new Object();
//锁住object对象
public void test1(){
synchronized (object){
count--;
System.out.println(Thread.currentThread().getName());
System.out.println("count:"+count);
}
}
//锁住当前对象,也就是想要执行这个方法,必须先拿到Demo对象的锁
public void test2(){
synchronized (this){
count--;
System.out.println(Thread.currentThread().getName());
System.out.println("count:"+count);
}
}
//test2的变形,特别注意这里锁住的是Demo的对象,而不是test3方法
public synchronized void test3(){
count--;
System.out.println(Thread.currentThread().getName());
System.out.println("count:"+count);
}
//这里的区别是,锁住的不是Demo对象,而是Demo这个类
public synchronized static void test4(){
sCount--;
System.out.println(Thread.currentThread().getName());
System.out.println("count:"+sCount);
}
//test4的变形
public static void test5(){
synchronized (Demo.class){
sCount--;
System.out.println(Thread.currentThread().getName());
System.out.println("count:"+sCount);
}
}
}
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
两条规定:
代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化
无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)
重排序不会给单线程带来内存可见性的问题,多线程中程序交错执行时,重排序可能会造成内存可见性的问题
不可见的原因 | synchronized解决方案 | volatile解决方案 |
---|---|---|
线程的交叉执行 | 原子性 | 不支持 |
重排序结合线程交叉执行 | 原子性 | 不支持 |
共享变量未及时更新 | 可见性 | 可见性 |
现代操作系统在运行一个程序时,会为其创建一个进程。现在操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
更多的处理核心
更快的响应时间
更好的编程模型
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多余优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要涩会较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
在不同的JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略对线程优先级的设定。
状态码 | 状态名称 | 说明 |
---|---|---|
NEW | 初始状态 | 线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态 | Java线程将操作系统中的就绪和隐形两种状态统称为“运行中” |
BLOCKED | 阻塞状态 | 标识线程阻塞于锁 |
WAITING | 等待状态 | 进入该状态表示当前线程需要其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态 | 不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态 | 表示当前线程已经执行完毕 |
线程创建后,调用start()方法开始执行,当线程执行wait()方法之后,线程进入等待状态;
进入等待状态的线程需要依靠其它线程的通知才能够返回到运行状态;
超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态;
当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态;
线程在执行Runnable的run()方法之后会进入到终止状态;
阻塞状态是线程进入synchronized关键字修饰的方法或代码块时的状态,但是阻塞在java.concurrent包中Lock接口中的线程状态确实等待状态,因为java.concurrent包中的接口对于阻塞的实现均使用了LockSupport类中的相关方法
在运行线程之前,首先要构造一个线程,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否为Daemon线程等信息。
通过start()方法启动线程
定义:中断可以理解为线程的一个标志位属性,它表示一个运行中的线程是否被其它线程进行了中断;
实现:其它线程通过调用该线程的interrupt()方法对其进行中断操作;
检查:线程通过方法isInterrupted()来进行判断是否被中断;
复位:静态方法Thread.interrupted()对当前线程的中断标识进行复位;
终结状态的线程,中断状态始终是false;
注意:从Java API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException异常之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。
分别代表 暂停、恢复、终止 线程,但是已经过期了,不建议使用;
原因:以suspend()为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题;stop()方法在终结一个线程时,不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
suspend和resume方法可以用后面提到的等待/通知机制来替代
可以通过interrupt(中断)或cancel(取消)方法来终止线程,这样操作有机会去清理资源,而不是武断的将线程停止,因此这种终止线程的做法显得更加安全和优雅。
https://blog.csdn.net/Crazypokerk_/article/details/87171229
方法 | 调用者 | 运行位置 | 释放锁 | 说明 |
---|---|---|---|---|
sleep | Thread对象 | / | 否 | sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。 |
wait | Object对象 | 同步块或方法 | 是 | wait()方法需要和notify()及notifyAll()两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁;同时使用wait方法后会释放锁。注意,它们都是Object类的方法,而不是Thread类的方法 |
yield | Thread对象 | / | 否 | yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同 |
suspend | Thread对象 | / | 否 | 不会释放资源,需要和resume方法配合使用,已经不建议使用 |
volatile:用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新会共享内存,它能保证所有线程对变量访问的可见性;
synchronized:可以修饰方法或者以同步块的形式来使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程变量访问的可见性和排他性
下面对synchronized详细讲解:
对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
每个对象都有自己的监视器,当这个对象有同步块或者同步方法调用时执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入Blocked状态。
下图描述了对象、对象的监视器、同步队列和执行线程之间的关系
方法 | 描述 |
---|---|
notify() | 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待在该对象上的线程 |
wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待n毫秒,如果没有通知就超时返回 |
wait(long, int) | 对于超时时间更细粒度的控制,可以达到纳秒级 |
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
细节说明:
从上述细节中可以看到:等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改
等待方遵循如下原则:
获取对象的锁;
如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件;
条件满足则执行对应的逻辑。
伪代码:
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的处理逻辑
}
通知方遵循如下原则:
获取对象的锁;
改变条件;
通知所有等待在对象上的线程。
伪代码:
synchronized(对象){
改变条件
对象.notifyAll();
}
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
主要有4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter
对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输出/输出流绑定起来,对于该流的访问将会抛出异常
如果一个线程A执行了thread.join()方法,其语义是:当前线程A等待thread线程终止之后才从thread.join()返回;
当线程终止时,会调用自身的notifyAll()方法,通知所有等待在该线程对象上的线程;
所以,join方法其实使用的是等待/通知机制:synchronized的对象是thread,等待的线程是A,notifyAll()方法是在线程thread终止时自动调的
通过阅读join方法源码也能看出使用的是等待/通知机制:
//加锁当前线程对象,即thread
public final synchronized void join() throws InterrupterException {
//条件不满足,继续等待
while(isAlive()){
wait(0);
}
//条件符合,方法返回
}
https://www.jianshu.com/p/98b68c97df9b
在等待/通知的经典范式,即加锁、条件循环和处理逻辑3个步骤的基础上,增加超时条件,可参考join()方法源码:
//加锁当前线程对象,即thread
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {//当isAlive返回值不满足要求,继续等待
while (isAlive()) {
wait(0);
}
} else {//当超时大于0并且isAlive返回值不满足要求,继续等待
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);//等待时间一直在变化,都是 总等待时间-已经等待过的时间
now = System.currentTimeMillis() - base;
}
}
//条件满足需求,返回
}
开始觉得这个可以直接用wait(long mills)方法代替,但是细看之后发现是有区别的,直接调用wait(long mills)方法会一直超时等待mills时间,超时后继续下一个超时等待,这样一直不会被唤醒,所以用如上范例,在wait(long mills)方法外面增加一个总超时时间的限制,而且wait的时间一直是动态变化的,一直是future-now,也就是一定会在mills时间内返回的,经典的例子可以参考join(long mills)方法
Lock缺少了synchronized提供的隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断性以及超时获取锁等多种synchronized关键字所不具备的同步特性:
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻没有被其它线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 与synchronized不同,获取到锁的线程能响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回、 |
Lock的API:
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回 |
void lockInterruptibly() throws InterruptedException | 可中断地获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超时的获取锁,当前线程在以下3中情况下会返回: 当前线程在超时时间内获得了锁 当前线程在超时时间内被中断 超时时间结束,返回false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将会释放锁 |
队列同步器(AQS)是用来构建锁或者其他同步组件的基础框架
它使用了一个int成员变量表示同步状态
通过内置的FIFO队列来完成资源获取线程的排队工作
同步器主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态
AQS提供了三个方法来操作同步状态
getState():获取当前同步状态
setState(int newState):设置当前状态(获取锁时使用)
compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性(释放锁时使用)
子类推荐被定义为自定义同步组件的静态内部类
AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用
AQS既可以支持独占式获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)
- 同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器的实现锁的语义;
- 锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;
- 同步器面向的是锁的实现这,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作
AQS可重写的方法:
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所 |
AQS提供的模板方法:
方法名称 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法 |
void acquireInterruptibly(int arg) | 与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptException并返回 |
boolean tryAcquireNanos(int arg, long nanos) | void acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true |
void acquireShared(int arg) | 共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 与acquireShared(int arg)相同,该方法响应中断 |
blooean acquireSharedNanos(int arg, long nanos) | 在acquireSharedInterruptibly(int arg)基础上增加了超时限制 |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 |
boolean releaseShared(int arg) | 共享式的释放同步状态 |
Collection getQueuedThreads() | 获取等待在同步队列上的线程集合 |
AQS依赖内部的同步队列(一个FIFO双向队列)来完成同步状态管理,当前线程获取同步状态失败时,同步器会将当前线程以及登台状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下:
属性类型与名称 | 描述 |
---|---|
int waitStatus | 等待状态 包含如下状态: 1.CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化; 2.SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或被取消,将会通知后继节点,使后继节点的线程得以运行; 3.CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中; 4.PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去; 5.INITIAL,值为0,初始状态 |
Node prev | 前驱节点,当节点加入同步队列时被设置(尾部添加) |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段 |
Thread thread | 获取同步状态的线程 |
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;
移出队列的条件是前驱节点为头节点且成功获取了同步状态;
在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
Lock接口 | ReentrantLock实现 |
---|---|
lock() | sync.acquire() |
lockInterruptibly() | sync.acquireInterrunptibly(1) |
tryLock() | sync.nonfairTryAcquire(1) |
tryLock(long time, TimeUnit unit) | sync.tryAcquireNanos(1, unit.toNanos(timeout)) |
unlock() | sync.release(1) |
newCondition() | sync.newCondition() |
ReantrantLock有三个重要特性:
可以根据以上三点来理解ReentrantLock:
ReentrantLock中的同步器重写了AQS的tryAcquire()和tryRelease()方法,没有重写shared相关方法,所以是一个地地道道的独占锁,不支持共享锁机制
重入的意思是一个线程获取了锁,然后再来获取锁可以直接得到锁,而不需要进入同步队列进行排队,实现原理就是tryAcquire()方法中的一段:
//如果尝试获取锁的线程就是当前获取到锁的线程则直接返回true
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); //注意这里设置了增加了state,后续释放锁的时候需要用到
return true;
}
在公平锁和非公平锁中都有这个一段,这里实现了锁重入的机制。
这里不但返回了true,而且state值增加了,这里就是重入锁的关键,也就是state记录了锁被重复获取的次数,在释放锁的时候,需要释放同样多次才完成锁的释放
公平锁是尝试获取锁的时候,如果有同步队列,则自动加到队尾,等待前面的获取完成后再来获取锁,保证按顺序获取到锁,即先到先得;
非公平锁则是在尝试获取锁的时候,先直接获取一下锁,如果获取到了直接返回true,没有获取到再去队尾排队,这样造成的结果就是可能有的线程一直在等待而获取不到锁的情况发生
公平锁尝试获取锁:
//ReentrantLock公平锁尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//这里是和非公平锁唯一区别的地方
//公平锁会先判断一下是否存在同步队列,存在就直接去队尾排队
//而非公平锁不会先判断是否有同步队列,而是直接尝试获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果尝试获取锁的线程就是当前获取到锁的线程则直接返回true
else if (current == getExclusiveOwnerThread()) {
//每重复获取一次 state 值就增加一次
//对应着释放锁的时候,每释放一次就需要减一次
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平锁尝试获取锁:
//ReentrantLock非公平锁尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//直接CAS,不需判断是否有同步队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//每重复获取一次 state 值就增加一次
//对应着释放锁的时候,每释放一次就需要减一次
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看到唯一的区别就是公平锁在尝试获取锁的时候先执行**hasQueuedPredecessors()**这个方法
//ReentrantLock尝试释放锁
protected final boolean tryRelease(int releases) {
//对应之前加锁时 state 值做了加法,这里需要做减法
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//必须要将 state 减到0才算完成锁的释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
由于ReentrantLock是可重入锁,所以在同一个线程重入的时候需要对state做加法,释放的时候需要对state做减法,也就是重入多少次就需要释放多少次
由于ReentrantLock是独占锁,同一时间只会有一个线程来释放锁
所以重入锁需要保证加锁多少次就需要释放多少次,也就是lock()多少次,就需要unlock()多少次
公平锁保证了锁的获取按照FIFO的原则,而代价是进行大量的线程切换,影响性能;
非公平锁虽然可能造成“饥饿”,但极少的线程切换,保证了其更大的吞吐量
读写锁中既有独占锁(写锁),也有共享锁(读锁)
AQS中state的高16位作为读锁标识,低16位作为写锁标识
锁降级是指,同一个线程获取到写锁不释放,获取到读锁,再释放写锁的过程
必须要在写锁释放前获取到读锁,不然不能称为降级锁
//获取写锁
writeLock.lock();
try {
if (!update) {
// 准备数据的流程(略)
update = true;
}
//释放写锁之前获取读锁
readLock.lock();
} finally {
//释放写锁
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
读写锁的获取关系总结如下:
被获取的锁 | 当前线程tryReadLock | 当前线程tryWriteLock | 其它线程tryReadLock | 其它线程tryWriteLock |
---|---|---|---|---|
ReadLock | Y(锁重入) | N(数据可见性) | Y(共享锁) | N(数据可见性) |
WriteLock | Y(锁降级) | Y(锁重入) | N(数据可见性) | N(数据可见性) |
方法名称 | 描述 |
---|---|
void park() | 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回 |
void parkNanos(long nanos) | 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回 |
void parkUnit(long deadline) | 阻塞当前线程,直到deadline时间(从1970年到deadline时间的毫秒数) |
void unpark(Thread thread) | 唤醒处于阻塞状态的线程thread |
除此之外,在Java6中,增加了**park(Object blocker)、park(Object blocker, long nanos)和park(Object blocker, long deadline)**3个方法,其中的参数blocker用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。
比如通过 jstack pid 命令查看线程状态:
"线程2" #12 prio=5 os_prio=0 tid=0x000000001c639000 nid=0xfd24 waiting on condition [0x000000001d0ee000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
//这里比线程1多显示了当前等待的对象,即传入的 blocker
//这里比线程1多显示了当前等待的对象,即传入的 blocker
//这里比线程1多显示了当前等待的对象,即传入的 blocker
- parking to wait for <0x0000000780990348> (a book.chapter05.ProcessData)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at book.chapter05.ProcessData.lambda$main$1(ProcessData.java:48)
at book.chapter05.ProcessData$$Lambda$2/999966131.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"线程1" #11 prio=5 os_prio=0 tid=0x000000001c634800 nid=0x13444 waiting on condition [0x000000001cfee000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:338)
at book.chapter05.ProcessData.lambda$main$0(ProcessData.java:45)
at book.chapter05.ProcessData$$Lambda$1/1149319664.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
线程2用的是park(Object blocker, long nanos),线程1用的是void parkNanos(long nanos)
可以看出来:线程2比线程1多显示了当前等待的对象,即传入的 blocker,有益于问题的排查和系统监控
任意一个Java对象都拥有一组监视器方法(定义在java.lang.Object上),主要包括:wait()、wait(long timeout)、notify()、notifyAll(),这些方法和synchronized关键字结合使用,可以实现等待/通知模式。
Condition接口也提供了类似Object的监视器方法,与Lock接口配合使用实现等待/通知模式。对比Object的监视器方法和Condition接口,可以更加详细的了解Condition 的特性:
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
获取一个Condition必须通过Lock的newCondition()方法
Condition的等待/通知模型:
//等待
lock.lock();
try {
while (条件不成立) {
condition.await();
}
//线程继续执行
} finally {
lock.unlock();
}
//通知
lock.lock();
try {
改变条件
condition.signal();//唤醒等待线程
} finally {
lock.unlock();
}
Condition定义的(部分)方法以及描述:
等待队列是一个FIFO双向队列,在队列的每个节点中都包含了一个线程的引用,该线程就是在Condition对象上等待的线程
如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造节点加入等待队列(类似Object.wait())
在AQS中,等待队列用的也是AQS中的Node,和同步队列一样
一个Condition包含一个等待队列
Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)
当前线程调用Condition.await()方法将会以当前线程构造节点,并将节点从尾部加入等待队列
如上图所示,Condition拥有首尾节点的引用,而新增节点只需将原有的尾节点 nextWaiter 指向它,并且更新尾节点(lastWaiter)即可。
上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证
Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切的说应该是同步器AQS)拥有一个同步队列和多个等待队列,其对应关系如图所示:
Condition的实现是同步器的内部类,隐刺每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。
调用Condition的await()方法(或者await开头的方法),会使当前线程进入等待队列,**并释放锁**,同时线程变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁
- 注意等待节点不是将head节点移动到nextWaiter,而是通过addConditionWaiter()方法,把当前线程构造成一个新的节点并将其加入到等待队列中
- 释放锁时head的移除在acquireQueue(Node node)中,通过下一个获取到锁的线程实现
源码:
/**
*Condition.await()
*/
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入等待队列,需要构造新的节点
Node node = addConditionWaiter();
//释放同步队列,也就是释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//这里就是等待唤醒的条件(节点是否进入同步队列)
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
/**
* 构造新的节点加入等待队列尾部
* @return 构造的新节点
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
/**
* 完全释放锁
* @param 根据当前线程构造的等待节点
* @return 前驱节点的 state
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
调用Condition的signal()方法,降火唤醒在等待队列中等待时间最长的节点(即首节点),在节点在被唤醒之前,会被移动到同步队列中:
- 调用Signal()方法的前置条件是当前线程必须获取了锁,在signal()方法中进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程
- 接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程
- 通过调用同步器的enq(Node node)方法,等待队列的投节点线程安全的移动到同步队列
- 当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程
- 被唤醒的线程,将从await()方法中的while循环中退出(isOnSyncQueued())方法加入到获取同步状态的竞争中
源码:
public final void signal() {
//这里判断调用signal的线程必须是获取了锁的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//唤醒操作
doSignal(first);
}
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程