1. 线程基本概念
1.1 基本概念
1.1.1 线程、进程
-
进程:指计算机中正在执行的一个程序实例。它包括了程序的代码、数据和运行时的状态。每个进程都有自己独立的内存空间,它们之间不能直接访问对方的内存。进程之间是相互独立的,它们在操作系统中被视为独立的个体,可以独立调度和管理。每个进程都有一个唯一的进程标识符(Process Identifier,PID)用于标识和管理。
-
线程:是进程的执行单元。一个进程可以包含多个线程,它们共享相同的内存空间和其他资源。线程是进程内的一个独立执行流,它可以看作是进程的一个子任务。不同的线程可以并发地执行,它们之间可以共享数据和资源,相比于创建多个独立的进程,使用线程可以更有效地利用系统资源。线程之间的切换开销相对较小,因为它们共享了相同的上下文环境。
-
线程和进程有以下几个主要区别:
-
资源占用:每个进程都有自己独立的内存空间、文件描述符、打开的文件等系统资源,因此进程间的资源是相互独立的。而线程是在同一个进程内共享进程的资源,包括内存空间、文件和其他系统资源。因此,创建一个新进程的开销通常比创建一个新线程的开销更大。
-
切换开销:线程切换的开销比进程切换的开销小。由于线程共享进程的内存空间,线程切换只需要切换线程的上下文,而不需要切换内存空间,开销较小。而进程切换需要切换整个进程的上下文和内存空间,开销相对较大。
-
通信和同步:在同一个进程内的线程之间,由于共享相同的内存空间,它们可以直接进行通信和数据共享,不需要额外的机制。而不同进程之间的通信和数据共享需要使用进程间通信(Inter-Process Communication,IPC)机制,如管道、消息队列、共享内存等。此外,线程之间的同步相对简单,可以使用线程同步机制,如锁、条件变量等,而进程之间的同步需要更复杂的机制。
-
安全性:由于线程共享进程的资源,线程之间对共享数据的访问需要进行同步控制,否则可能导致数据竞争和不一致。进程之间的资源相互独立,各自拥有独立的内存空间,因此进程间的数据访问不会直接相互影响。
-
扩展性:线程的创建和销毁开销较小,可以更快速地实现并发处理。因此,在需要高并发和快速响应的场景下,使用多线程可以更好地发挥系统的性能。而进程的创建和销毁开销较大,适合于需要独立管理和隔离的任务。
1.1.2 多线程
-
什么是多线程:多线程是指单个进程中同时运行多个线程,多线程的目的是为了提高CPU的利用率,可以通过避免一些网络IO、磁盘IO等需要等待的操作,让CPU去运行其它线程,这样可以大幅度提升程序效率,提高用户体验。
-
多线程编程的局限性:
-
竞态条件:当多个线程同时访问和修改共享数据时,如果没有适当的同步机制,可能会导致竞态条件(Race Condition),造成数据不一致和程序错误。解决竞态条件需要使用锁、原子操作等同步机制,但这也会引入额外的开销和复杂性。
-
死锁:当多个线程相互等待对方释放资源时,可能会发生死锁(Deadlock)。这种情况下,线程无法继续执行,并且无法解除死锁状态。死锁的发生需要谨慎设计和管理线程之间的资源依赖关系,避免出现循环等待的情况。
-
资源消耗:多线程编程需要共享内存和其他资源,如果资源管理不当,可能会导致资源的过度消耗和浪费。例如,如果创建大量线程而没有合理的线程池管理,可能会占用过多的内存和处理器资源。
-
上下文切换开销:线程的切换需要保存和恢复线程的上下文环境,这涉及一定的开销。当线程数量过多或频繁切换时,上下文切换的开销可能会影响程序的性能。
-
调试和测试困难:多线程程序的调试和测试相对复杂,由于线程的并发执行和异步操作,可能出现难以复现和追踪的问题。并发错误和线程间的交互问题可能很难调试和定位。
-
并发性控制:多线程编程需要合理控制线程的数量和调度,避免线程过多导致资源争夺和性能下降。同时,线程的执行顺序和并发度的控制也需要考虑,以确保程序的正确性和性能。
为了克服这些局限性,多线程编程需要使用适当的同步机制、合理的资源管理、良好的设计和测试实践。此外,其他的并发编程模型,如异步编程、事件驱动编程等,也可以作为替代选择,以避免多线程编程带来的一些问题。
-
1.1.3 串行、并行、并发
-
串行:指任务按照顺序一个接一个地执行,每个任务必须等待前一个任务完成后才能开始执行。在串行执行中,任务之间是相互依赖的,后续任务的执行必须等待前面任务的结果。
-
并行:指多个任务同时执行,每个任务都有自己的执行环境和资源。在并行执行中,不同任务之间相互独立,它们可以同时进行,从而提高整体的处理能力和效率。并行执行通常需要具备多个执行单元,如多核处理器或分布式系统。
-
并发:指多个任务在时间上交替执行,看起来好像是同时进行。在并发执行中,任务之间可能存在一定的重叠和交叉执行,但并不一定要求同时执行。并发执行通常是通过任务的切换和调度来实现的,使得多个任务可以在同一个时间段内进行,以提高系统的效率和资源利用率。
1.1.4 同步、异步、阻塞、非阻塞
同步(Synchronous)和异步(Asynchronous),阻塞(Blocking)和非阻塞(Non-blocking)是用于描述任务执行方式和调用方式的概念。
-
同步和异步关注的是任务的执行方式:
-
同步执行:指任务按照顺序依次执行,每个任务需要等待前一个任务完成后才能开始执行。任务之间存在依赖关系,后续任务的执行需要等待前面任务的结果。
-
异步执行:指任务提交后不需要等待结果,可以继续执行其他任务。任务的执行结果可能在将来的某个时间点返回,或通过回调函数、事件通知等方式通知。
-
-
阻塞和非阻塞关注的是任务调用方式:
-
阻塞调用:指任务发起调用后,调用者会一直等待任务完成才能继续执行后面的操作。在阻塞调用期间,调用者无法进行其他任务。
-
非阻塞调用:指任务发起调用后,调用者不需要等待任务完成,可以立即继续执行后面的操作。在非阻塞调用中,任务的执行可以是同步的或异步的。
-
-
可以将这些概念进行组合:
-
同步阻塞:任务按顺序依次执行,并且调用者需要等待任务完成才能继续执行后续操作。
-
同步非阻塞:任务按顺序依次执行,但调用者不需要等待任务完成,可以继续执行后续操作。在这种情况下,调用者需要主动轮询任务的状态或结果。
-
异步阻塞:任务提交后不需要等待结果,但调用者需要阻塞等待任务完成才能继续执行后续操作。在这种情况下,任务的执行通常由其他线程或进程完成。
-
异步非阻塞:任务提交后不需要等待结果,同时调用者也不需要阻塞等待任务完成,可以立即继续执行后续操作。任务的执行结果可能在将来的某个时间点返回,或通过回调函数、事件通知等方式通知。
-
1.2 线程的创建
1.2.1 继承Thread类,重写Run方法
class MyThread extends Thread {
public void run() {
// 线程执行逻辑
}
}
// 创建线程并启动
MyThread thread = new MyThread();
thread.start();
1.2.2 实现Runnable接口,重写run方法
class MyRunnable implements Runnable {
public void run() {
// 线程执行逻辑
}
}
// 创建线程并启动
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
1.2.3 是实现Callable重写call方法,配合FutureTask
在Java中,除了使用Runnable接口来创建线程,还可以使用Callable接口来创建线程,一般用于有返回结果的同步非阻塞方法。与Runnable不同的是,Callable可以返回一个结果并抛出一个异常。要使用Callable接口创建线程,需要遵循以下步骤:
- 创建一个实现Callable接口的类,并实现其call()方法。call()方法是Callable接口的唯一方法,它定义了线程的执行逻辑,并可以返回一个结果。
import java.util.concurrent.Callable;
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
// 线程执行逻辑
// 返回一个结果
return 42;
}
}
- 创建一个ExecutorService线程池来管理线程的执行。可以通过Executors类的静态方法来创建线程池。
// 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
- 提交Callable任务给线程池进行执行,并获取Future对象。Future对象用于表示异步计算的结果。
// 提交Callable任务并获取Future对象
Future<Integer> future = executor.submit(new MyCallable());
- 可以通过Future对象来获取Callable任务的执行结果。调用Future的get()方法会阻塞当前线程,直到Callable任务完成并返回结果。
try {
// 获取任务执行结果
Integer result = future.get();
System.out.println("结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
- 在使用完Callable任务后,需要关闭线程池以释放资源。
// 关闭线程池
executor.shutdown();
使用Callable接口创建线程可以获取线程执行的结果,并可以捕获异常。它适用于需要返回结果的计算密集型任务或需要处理异常的任务场景。通过线程池管理Callable任务的执行,可以提高线程的效率和资源利用率。注意,Callable接口和Runnable接口的区别在于Callable可以返回结果和抛出异常,但是Callable任务的执行必须通过ExecutorService线程池来进行调度。
1.3 线程的使用
1.3.1 线程的状态
在Java中,线程可以处于不同的状态,这些状态反映了线程在不同阶段的行为和状态变化。Java中的线程状态可以分为以下几种:
-
新建(New)状态:当线程对象被创建但还没有调用start()方法时,线程处于新建状态。此时,线程已经被创建但还没有启动执行。
-
可运行(Runnable)状态:当线程对象调用了start()方法后,线程进入可运行状态。在可运行状态下,线程已经具备了运行的条件,但并不一定正在执行,可能正在等待CPU的调度。
-
运行(Running)状态:在可运行状态下,线程被CPU调度执行时,线程进入运行状态。在运行状态下,线程正在执行其任务代码。
-
阻塞(Blocked)状态:在某些情况下,线程可能会被阻塞。例如,线程在等待获取锁、等待输入输出完成、等待其他线程的通知等情况下,线程会进入阻塞状态。在阻塞状态下,线程暂时停止执行,直到满足了阻塞条件才能继续执行。
-
等待(Waiting)状态:线程可以通过调用Object类的wait()、Thread类的join()、LockSupport类的park()等方法进入等待状态。在等待状态下,线程暂时停止执行,并释放占有的锁,直到其他线程发出通知或等待时间到达。
-
超时等待(Timed Waiting)状态:类似于等待状态,线程通过调用具有超时参数的wait()、join()、sleep()等方法,进入超时等待状态。在超时等待状态下,线程暂时停止执行,直到其他线程发出通知、等待时间到达或超时时间到达。
-
终止(Terminated)状态:线程执行完其任务代码或出现了未捕获的异常时,线程进入终止状态。在终止状态下,线程的执行已经结束,不再具备执行的条件。
1.3.2 线程的常用方法
Java中的线程类(Thread类)提供了一些常用的方法来管理和控制线程的行为。以下是Java线程的一些常用方法:
-
start():启动线程,使线程进入可运行状态,并由系统调度执行run()方法。
-
run():线程的执行逻辑,可以在该方法中定义线程的任务。
-
sleep(long millis):使线程暂停执行指定的毫秒数,让出CPU的执行时间,但不会释放持有的锁。
-
join():线程强占,等待该线程执行完毕,将其合并到当前线程中,直到该线程执行完毕后再继续执行当前线程。
-
interrupt():中断线程,给线程发送中断信号,可以通过isInterrupted()方法来检查线程的中断状态。
-
isInterrupted():检查线程是否被中断。
-
yield():让出CPU的执行时间,暂停当前线程的执行,让其他具有相同优先级的线程有机会执行。
-
setPriority(int priority):设置线程的优先级,优先级范围为1(最低)到10(最高)。
-
getName()和setName(String name):获取和设置线程的名称。
-
isAlive():检查线程是否处于活动状态(可运行、运行、阻塞等)。
-
currentThread():获取当前运行的Thread。
-
setDaemon(True):设置为守护线程,默认情况下线程都是非守护线程,JVM会在程序中没有非守护线程的时候结束掉当前的JVM,主线程默认是非守护线程。
-
wait()/notify():可以让获取synchronzied锁资源的线程通过wait方法进入到锁的等待池,并且会释放锁资源。可以让获取synchronized锁资源的线程,通过notify和notifyAll()方法,将等待池中的线程唤醒,添加到锁池中。
- notify:随机的唤醒等待池中的一个线程到锁池。
- notifyAll:将等待池中的全部线程都唤醒,并且添加到锁池中。
- 在调用wait、notify和notifyAll方法时,必须在synchronized修饰的代码块或者方法内部才可以,因为要操作基于某个对象的锁信息和维护。
这些方法提供了对线程的管理和控制的功能,可以控制线程的执行顺序、中断线程、调整线程的优先级等。需要注意的是,在使用这些方法时需要考虑线程的同步和互斥,以避免线程安全问题和竞态条件。
此外,Java还提供了其他类和接口来支持并发编程,如Lock、Condition、Semaphore、CountDownLatch等,它们提供了更丰富的线程管理和同步机制。合理使用这些方法和类可以更好地实现并发编程,提高程序的性能和可靠性。
1.3.3 线程的结束方式
在Java中,线程可以通过以下几种方式来结束执行:
-
正常结束:线程的run()方法执行完毕,即线程的任务代码执行完毕,线程会自动结束执行。
-
return语句:在线程的run()方法中使用return语句可以提前结束线程的执行。
-
stop()方法(已过时):在过去的版本中,可以通过调用Thread类的stop()方法来强制结束线程的执行。不推荐使用,因为它可能导致线程资源不正确地释放,从而引发线程 安全和资源泄漏问题。
-
interrupt()方法:可以通过调用Thread类的interrupt()方法给线程发送中断信号,以请求线程中断。在线程的任务代码中可以通过检查isInterrupted()方法来判断线程是否被中断,并根据需要决定如何结束线程的执行。线程在收到中断信号后,可以自行决定如何处理中断请求,可以通过捕获InterruptedException异常、清理资源或直接结束执行等方式来结束线程的执行。
-
使用标志位控制线程的执行:可以通过设置一个标志位来控制线程的执行。线程在执行任务代码时,定期检查该标志位,如果标志位指示线程应该结束执行,则线程可以自行结束执行。
2. 并发编程的三大特性
2.1 原子性
2.1.1 什么是原子性?
原子性(Atomicity):原子性是指操作不可被中断,要么全部执行成功,要么全部不执行。在并发编程中,如果一个操作是原子的,那么它可以被看作是一个不可分割的单元。原子操作可以确保多个线程并发执行时,不会产生竞态条件或数据不一致的问题。Java中的原子操作可以使用synchronized关键字、Lock、Atomic类等机制来实现。
2.1.2 Java中如何保证原子性
2.1.2.1 synchronized
以count++操作为例,在Java中count++操作不是原子性操作,可以通过添加synchronized锁来保证原子性,能够保证同时只有一个线程操作同步代码块。
- 下面是count++的字节码操作指令,一个自增操作最后被JVM拆解成为了6条指令,在并发场景下这4条指令的执行可能会被其它线程打断导致最终结果不符合预期,因此不具备原子性。下面是加synchronzied关键字后的代码和字节码。
- 代码: public static void increment(){ count++; }
- 字节码: 0 getstatic #7 <thread/PlusPlusTest.count : J> //从主内存加载到寄存器 3 lconst_1 4 ladd 5 putstatic #7 <thread/PlusPlusTest.count : J> //从寄存器写回主内存 8 return
- synchronized可以修饰方法和同步代码块上来保证原子性
-
修饰方法
- 代码:
public synchronized static void increment(){ count++; }- 字节码:
0x0029 [public static synchronized] //increment方法的访问标志 -
修饰同步代码块
- 代码:
public void increment(){ synchronized (this) { count++; } }- 字节码:
3 monitorenter //进入同步块之前获取对象的监视器(monitor) 4 getstatic #7 <thread/PlusPlusTest.count : J> 7 lconst_1 8 ladd 9 putstatic #7 <thread/PlusPlusTest.count : J> 12 aload_1 13 monitorexit //退出同步块并释放对象的监视器(monitor)具体来说,synchronized关键字在原有的代码块前后添加了monitorenter和monitorexit操作来保证代码的原子性。
- monitorenter 指令在执行时会尝试获取对象的监视器,如果成功获取到监视器,则线程可以进入同步块执行相应的代码。如果无法获取到监视器,线程将被阻塞,等待获取监视器的机会。
- monitorexit 指令在执行时会释放当前线程持有的对象的监视器,并退出同步块,允许其他线程获取该监视器并进入同步块执行相应的代码。
-
2.1.2.2 CAS
CAS(Compare and Swap)是一种并发编程中的原子操作,用于实现多线程环境下的数据同步和线程安全,它是CPU级别的并发原语。CAS操作包含三个参数:内存地址(或称为变量的引用)、预期值和新值。它的工作原理是先比较内存地址处的值是否等于预期值,如果相等,则将新值更新到该内存地址处;如果不相等,则说明其他线程已经修改了该值,CAS操作失败,不会更新值
CAS操作是一种乐观锁机制,它允许多个线程同时访问共享资源,并通过比较预期值和实际值来判断是否发生了冲突。相比于传统的互斥锁,CAS操作避免了线程的阻塞和唤醒,从而减少了上下文切换的开销,提高了并发性能。
尽管CAS操作可以提高并发性能,但它并不适用于所有场景。在高并发情况下,如果竞争非常激烈,CAS操作可能会频繁失败,从而导致性能下降。
在Java中,可以使用java.util.concurrent.atomic包下的原子类来实现CAS(比较并交换)操作。这些原子类提供了一系列的方法,如compareAndSet()、getAndSet()等,用于实现CAS操作。
下面是一个简单的示例,演示了如何使用AtomicInteger类来实现CAS操作:
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
// 比较并交换操作
int expectedValue = 0;
int newValue = 1;
boolean success = counter.compareAndSet(expectedValue, newValue);
System.out.println("CAS operation result: " + success);
// 获取当前值
int currentValue = counter.get();
System.out.println("Current value: " + currentValue);
}
}
在上述示例中,我们使用AtomicInteger类创建了一个原子整型变量counter。首先,我们使用compareAndSet()方法进行CAS操作,将expectedValue(预期值)和newValue(新值)传递给该方法。如果当前的值等于预期值,CAS操作成功,返回true;否则,CAS操作失败,返回false。通过调用get()方法,我们可以获取当前的值。
除了AtomicInteger,Java的原子类库还提供了其他原子类型,如AtomicLong、AtomicBoolean等,以及支持数组的原子类,如AtomicIntegerArray、AtomicLongArray等。
- CAS存在的问题
-
自旋等待:CAS操作是通过不断尝试更新变量的值来实现的。如果多个线程同时进行CAS操作,并且存在竞争,那么某些线程可能会进入自旋等待的状态,不断尝试CAS操作,直到成功或达到一定次数。这可能会导致额外的CPU资源消耗和延迟。
-
ABA问题:CAS操作只关注变量的当前值是否与预期值相等,而不关注变量在CAS操作期间是否发生了其他变化。这可能导致ABA问题的出现。例如,线程A读取变量值为A,然后线程B将变量值修改为B,最后线程B将变量又修改回A。此时,线程A使用CAS操作比较变量值为A,发现相等,执行更新操作。尽管CAS操作成功,但线程A可能无法察觉到变量在期间被修改过。(可以通过版本来解决)
-
只能保证一个变量的原子性:CAS操作只能针对一个变量进行原子性的比较和交换,无法直接支持多个变量之间的原子操作。要实现多个变量之间的原子操作,需要使用其他机制,如锁或原子类的组合。
-
难以解决复杂的并发问题:CAS操作通常适用于简单的原子操作,如自增、自减等。对于涉及复杂的并发控制问题,CAS操作可能难以解决,需要使用更高级的并发机制,如锁、信号量、并发集合等。
- ABA问题的解决
在Java中,可以使用java.util.concurrent.atomic包下的AtomicStampedReference或AtomicMarkableReference类来解决ABA问题。这两个类在AtomicReference的基础上提供了一种方式,通过引入标记(stamp)或标记(mark)来区分变量是否发生了修改。
AtomicStampedReference:它使用一个整型的标记(stamp)来表示变量的版本号,通过比较引用和版本号来判断变量是否发生了修改。当变量发生修改时,版本号会递增。这样,即使变量的值与之前的值相同,但版本号的变化仍会导致CAS操作失败,从而避免了ABA问题的出现。
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
private static AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(0, 0);
public static void main(String[] args) {
// 线程A执行变更操作
Thread threadA = new Thread(() -> {
int stamp = atomicRef.getStamp();
Integer reference = atomicRef.getReference();
// 修改变量的值
atomicRef.compareAndSet(reference, reference + 1, stamp, stamp + 1);
});
// 线程B执行ABA操作
Thread threadB = new Thread(() -> {
int stamp = atomicRef.getStamp();
Integer reference = atomicRef.getReference();
// 修改变量的值
atomicRef.compareAndSet(reference, reference + 1, stamp, stamp + 1);
stamp = atomicRef.getStamp();
reference = atomicRef.getReference();
// 恢复变量的值
atomicRef.compareAndSet(reference, reference - 1, stamp, stamp + 1);
});
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Current value: " + atomicRef.getReference());
}
}
在上述示例中,我们使用AtomicStampedReference类来维护一个整型变量的引用和版本号。线程A执行变更操作时,版本号会递增,而线程B执行ABA操作时,版本号的变化会导致CAS操作失败,从而避免了ABA问题的出现。
AtomicMarkableReference:它使用一个布尔型的标记(mark)来表示变量是否发生了修改,类似于AtomicStampedReference的用法。当变量发生修改时,标记会被设置为true。通过比较引用和标记来判断变量是否发生了修改。
需要注意的是,虽然AtomicStampedReference和AtomicMarkableReference可以解决ABA问题,但它们也有自身的限制和开销。在选择使用时,需要根据具体需求综合考虑,确保选择适合的解决方案。
- 解决自旋时间过长
CAS(比较并交换)操作在并发编程中可以用于实现无锁的同步机制,但它可能会导致自旋时间过长的问题,即多个线程不断尝试CAS操作,导致CPU资源浪费和延迟增加。下面介绍几种可以解决自旋时间过长的方法:
-
自旋次数限制:可以通过设置自旋的最大次数限制来避免自旋时间过长。在达到最大自旋次数后,可以尝试使用其他同步机制,如锁或阻塞等待。
-
自适应自旋:有些JVM实现提供了自适应自旋的机制。根据之前CAS操作的成功率和自旋时间等指标,JVM可以动态地调整自旋次数或自旋的时间间隔,以尽量减少自旋时间过长的情况。
-
线程挂起/阻塞:当自旋时间过长时,可以考虑将线程挂起或阻塞,让出CPU资源给其他线程使用。这可以通过使用线程的
yield()方法、park()和unpark()方法,或者使用阻塞机制如LockSupport等来实现。 -
退避策略:当自旋时间过长时,可以采用退避策略,即暂时退出自旋状态,让出CPU资源给其他线程使用,并在一段时间后重新进入自旋状态。这可以通过使用
Thread.sleep()方法或指数退避算法等来实现。
2.1.2.3 Lock
在Java中,Lock接口提供了一种比传统的synchronized关键字更灵活和强大的锁定机制。它是Java并发包(java.util.concurrent)中的一部分,用于控制多个线程对共享资源的访问。Lock锁在JDK1.5时期比synchronized性能好很多,但是在JDK1.6对synchronized优化之后性能就相差不大了。但是如果涉及并发量较大的情况下,使用ReetrantLock,性能更优秀。
Lock接口定义了以下常用方法:
lock():尝试获取锁,如果锁不可用,则当前线程会被阻塞,直到获取到锁。tryLock():尝试非阻塞地获取锁,如果锁可用,则获取锁并立即返回true,否则返回false。unlock():释放锁,允许其他线程获取该锁。newCondition():创建与锁相关的条件变量(Condition),用于实现更复杂的线程间通信和协作。
下面是一个简单的示例,演示了如何使用Lock接口来实现线程同步:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private static Lock lock = new ReentrantLock();
private static int counter = 0;
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
increment();
});
Thread thread2 = new Thread(() -> {
increment();
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter: " + counter);
}
public static void increment() {
lock.lock(); // 获取锁
try {
// 临界区代码
for (int i = 0; i < 100000; i++) {
counter++;
}
} finally {
lock.unlock(); // 释放锁
}
}
}
在上述示例中,我们使用ReentrantLock类实现了Lock接口,并使用lock()方法获取锁,unlock()方法释放锁。在increment()方法中,我们使用锁来保护counter变量的增加操作,确保线程安全。与synchronized关键字相比,Lock接口提供了更多的灵活性,如可重入性、可中断性、超时等待和公平性控制。它适用于更复杂的并发场景,并且可以更精确地控制线程的同步和互斥访问。
2.1.2.4 ThreadLocal
ThreadLocal并不直接保证原子性,它主要用于在多线程环境下为每个线程提供独立的变量副本,从而实现线程间的数据隔离。
ThreadLocal通过为每个线程维护一个独立的变量副本,使得每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的副本。这是通过在ThreadLocal对象内部使用一个Map来管理线程和变量副本之间的映射关系实现的。
由于每个线程都拥有自己独立的变量副本,因此在不同线程之间对于同一个ThreadLocal变量的操作不会相互干扰,从而实现了线程间的数据隔离。然而,需要注意的是,ThreadLocal并不解决多线程并发访问同一个变量的原子性问题。如果多个线程同时访问同一个共享变量,即使使用了ThreadLocal,仍然需要考虑对共享变量的同步机制,例如使用锁或volatile关键字等来确保线程安全。
- ThreadLocal实现原理:
- 每个Thread中都存在着一个成员变量,ThreadLocalMap。
- ThreadLocal不存储数据,只会基于ThreadLocal去操作ThreadLocalMap。
- ThreadLocalMap本身就是Entry[]实现的,一个线程可以绑定多个ThreadLocal,可能需要存储多个数据,所以采用Entry[]形式。
- 每个线程有自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取。
- ThreadLocalMap的key是一个弱引用,将在下一次被GC的时候被回收。这是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收。
- ThreadLocal内存泄漏问题:
- 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果此时线程还没被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
- 只需要在使用完毕ThreadLocal对象后,及时调用remove方法,移除Entry即可。
2.2 可见性
2.2.1 什么是可见性?
可见性(Visibility):可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到修改后的值。在多线程环境下,线程之间存在着各自的工作内存,线程对共享变量的修改可能先保存在自己的工作内存中,而不立即写回主内存,导致其他线程无法看到最新的值而产生数据不一致。为了保证可见性,需要使用volatile关键字、synchronized关键字、Lock、Atomic类等机制来同步线程之间的内存访问。
2.2.2 解决可见性问题?
2.2.2.1 volatile
volatile 是Java中的关键字,用于修饰变量。它的主要作用是保证可见性和禁止指令重排序,用于解决多线程并发访问共享变量的一致性问题。
- 可见性: 在多线程环境下,当一个线程修改了被
volatile修饰的变量的值时,该变量的新值会立即被写入主内存,并通知其他线程在读取该变量时重新从主内存中加载最新的值,保证了多个线程之间对该变量的可见性。并且被Volatile修饰的变量,会直接从主内存中读值,而不是从CPU缓存中获取值。
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须从主内存中重新读取共享变量。
- 禁止指令重排序: 编译器和处理器在优化代码执行过程中可能会对指令进行重排序,但是对于被
volatile修饰的变量,编译器和处理器会遵守特定的规则,保证对volatile变量的写操作不会被重排序到其后的读操作之前,从而保证了多线程环境下的顺序性。
需要注意的是,volatile 关键字不能保证原子性。如果需要保证多线程环境下对变量的原子操作,可以考虑使用 synchronized 关键字或者 java.util.concurrent.atomic 包中的原子类。
- 字节码层面: 字节码层面只是添加了ACC_VOLATILE访问标志
- JVM层面:
- 写操作 StoreStoreBarrir volatile 写操作 StoreLoadBarrir
- 读操作 LoadLoadBarrir volatile读操作 LoadStoreBarrir
- OS和硬件层面: window上使用lock指令,volatile关键字修饰的变量会在转为汇编之后追加一个lock前缀,CPU执行这个指令的时候,如果带有lock前缀会做2个事情:
- 将当前处理器缓存行的数据写会到主内存。
- 写回的数据在其它CPU内核的缓存中无效,必须从主内存中读取。
2.2.2.2 synchronized
- synchronzied内存语义: CPU在执行到synchronized同步代码块和同步方法时,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去从主内存中重新拿数据;在释放锁资源之后,会立即将CPU缓存中的数据同步到主内存。
2.2.2.3 Lock
Lock锁保证可见性的方式和synchronized完全不同,synchronized基于其内存语义,在获取锁和释放锁的时候对CPU缓存左一个同步到主内存的操作。
Lock锁是基于volatile实现的可见性,Lock锁内部在加锁和释放锁的时候,会对volatile修饰的state变量进行加减操作。如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将缓存修改的数据从缓存立即同步到主内存,同时也会将其它属性也立即同步到主内存,还会将其它CPU缓存行中的这个数据设置为无效,重新从主内存中获取。
2.2.2.4 final
final修饰的属性在运行期间是不允许修改的,这样就间接的保证了可见性,所以多线程读取的值肯定是一样的。
final和volatile是不允许同时修饰一个属性的,final修饰的属性不允许再次被写了,而volatile是保证每次读写数据去主内存读写,并且volatile会影响一定的性能。
2.3 有序性
2.3.1 什么是有序性
-
指令重排: 在Java中,.java中的文件内容会被变异,在执行前需要再次编译成为CPU可执行的指令,CPU在执行这些指令时,为了提升效率,在不影响最终结果的前提下,会对指令进行重排序。
-
有序性: 保证程序执行的结果按照一定的顺序,不会出现随机的、无序的情况。在多线程环境下,由于线程的交替执行和指令重排等优化技术,可能导致代码执行的顺序发生变化,从而产生不符合预期的结果。为了保证有序性,需要使用volatile关键字、synchronized关键字、Lock、并发集合类(如ConcurrentHashMap、ConcurrentLinkedQueue)等机制来同步线程之间的操作顺序。
2.3.2 as-if-serial
"as-if-serial"语义是Java内存模型(Java Memory Model,简称JMM)中的一个重要概念,它描述了在多线程环境下程序的执行结果应当具备与在单线程环境下按照程序顺序执行所得到的结果相同的特性。
根据"as-if-serial"语义,虚拟机(JVM)可以对程序进行各种优化,包括指令重排序、线程本地存储、缓存优化等,只要这些优化不会改变单线程环境下程序的执行结果。换句话说,即使在多线程环境下,程序的执行结果也必须和在单线程环境下的顺序执行结果一致。
具体来说,根据"as-if-serial"语义,Java编译器、JVM和硬件可以对指令进行以下优化:
-
重排序(Reordering):编译器和JVM可以对指令进行优化重排,以提高性能。但是,这些重排不能改变程序的单线程语义和单线程执行的结果。
-
合并读写(Read-Write Fusion):编译器和JVM可以将多个读和写操作合并为一个操作,以减少内存访问。
-
线程本地存储(Thread-local Storage):JVM可以将变量存储在线程的本地存储中,以避免对共享内存的访问,提高效率。
-
缓存优化:硬件和JVM可以对内存访问进行优化,如缓存命中、预取等,以提高访问速度。
总的来说,"as-if-serial"语义提供了对多线程程序正确性的保证,它确保了程序在多线程环境下的执行结果与在单线程环境下按照程序顺序执行所得到的结果一致。这种语义为编译器、JVM和硬件的优化提供了一定的自由度,但同时也要求它们不能破坏程序的单线程语义和单线程执行结果的一致性。
2.3.3 happens-before
"Happens-before"是Java内存模型(Java Memory Model,简称JMM)中的一个重要概念,用于定义多线程程序中操作的执行顺序和可见性规则。它提供了一种保证,确保在多线程环境下的操作按照预期顺序执行,避免出现意外的结果。
根据"happens-before"规则,如果操作A在操作B之前发生(即A happens-before B),则在程序执行的任何时刻,操作B的结果对于操作A来说是可见的。这意味着在满足"happens-before"关系的条件下,程序可以对操作进行重排、优化和并发执行,但必须保证所有线程对于操作的执行顺序是一致的。
Java内存模型中定义了几种"happens-before"关系:
-
程序顺序规则(Program Order Rule):在单个线程中,按照程序代码的顺序执行的操作具有"happens-before"关系。
-
监视器锁规则(Monitor Lock Rule):一个解锁操作happens-before于后续对同一监视器锁的加锁操作。
-
volatile变量规则(Volatile Variable Rule):对volatile变量的写操作happens-before于后续对该变量的读操作。
-
线程启动规则(Thread Start Rule):线程的启动操作happens-before于启动线程中的任何操作。
-
线程终止规则(Thread Termination Rule):线程中的任何操作happens-before于其他线程检测到该线程已终止的操作。
-
线程中断规则(Thread Interruption Rule):对线程的中断操作happens-before于被中断线程检测到中断操作的发生。
-
对象终结规则(Finalizer Rule):对象的初始化操作happens-before于后续对该对象的finalize()方法的调用。
"happens-before"关系提供了在多线程环境下操作执行顺序的一致性保证,它为程序员提供了一套规则来编写正确且线程安全的多线程代码。理解"happens-before"关系对于正确地理解和设计多线程程序非常重要。
2.3.4 volatile
volatile通过内存屏障来保证有序性。
2.3.5 单例模式由于指令重排序出现的问题
由于指令重排序,在第二个if(test==null)的判断中,线程可能会拿到未初始化的对象,导致单例模式的生成产生问题,解决办法则是添加volatile,代码如下:
private static volatile MiTest test;
public static MiTest getInstance(){
if(test == null){
synchronzied(MiTest.class){
if(test == null){
test = new MiTest();
}
}
}
}
3. 锁
3.1 锁的分类
3.1.1 可重入锁、不可重入锁
-
可重入锁:
Java中提供的synchronized、ReentrantLock、ReentrantReadWriteLock都是可重入锁。
可重入锁是指同一个线程在持有锁的情况下,可以重复地获取同一个锁,而不会造成死锁或其他异常情况。当一个线程多次获取同一个可重入锁时,必须相应地释放相同数量的锁,才能完全释放该锁。
可重入锁的主要特点是:
- 同一个线程可以多次获取同一个锁。
- 线程在释放锁之前必须相应地释放相同数量的锁。
- 可重入锁可以避免死锁和其他异常情况。
-
不可重入锁:
不可重入锁是指同一个线程在持有锁的情况下,再次尝试获取该锁时会导致线程被阻塞或产生死锁。不可重入锁不允许同一个线程重复获取同一个锁,即使是在同一个线程的内部。 不可重入锁的主要特点是:
- 同一个线程无法多次获取同一个锁,会导致线程被阻塞或死锁。
- 不可重入锁可能会导致死锁和其他异常情况。
3.1.2 乐观锁、悲观锁
-
悲观锁(Pessimistic Locking): 悲观锁假设在整个操作过程中会发生并发冲突,因此在访问共享资源之前会将其加锁,阻止其他线程对该资源的访问,直到当前线程完成操作并释放锁。悲观锁常见的实现方式是使用互斥锁(如synchronized关键字或ReentrantLock类)来保证对共享资源的独占访问,确保数据的一致性。Java中提供的synchronized、ReentrantLock、ReentrantReadWriteLock都是悲观锁,获取不到锁资源的时候,会将当前线程挂起(进入BLOCKED,WAITING),线程挂起会涉及到用户态和内核态的切换,这种切换是非常消耗资源的。
-
乐观锁(Optimistic Locking): 乐观锁假设在整个操作过程中不会发生并发冲突,因此不会使用锁来保护共享资源的访问。相反,它通过使用一种乐观的策略来进行操作,通常是使用无锁的并发控制机制(如CAS操作)。乐观锁的思想是,在读取共享资源时不进行加锁,而是先读取资源的版本号或标记,然后在更新资源时比较版本号,如果发现其他线程已经修改了资源,则放弃当前操作,重新尝试或执行相应的冲突解决策略。Java中提供的CAS操作就是一种乐观锁,当获取不到锁资源的时候,可以再次让CPU调度,重新尝试获取锁资源。
乐观锁和悲观锁的选择取决于具体的场景和需求:
-
悲观锁适用于对共享资源的并发访问较为频繁,且冲突较多的情况。它能够保证数据的一致性,但可能会导致线程的阻塞和上下文切换,对系统的并发性能有一定的影响。
-
乐观锁适用于对共享资源的并发访问冲突较少的情况。它通过避免加锁操作来提高并发性能,但需要实现冲突检测和冲突解决策略,确保数据的一致性。
在Java中,乐观锁的常见实现方式是使用CAS(Compare and Swap)操作,例如使用Atomic类或StampedLock类。而悲观锁则可以通过synchronized关键字或ReentrantLock类来实现。选择合适的锁策略取决于对数据一致性和并发性能的权衡。
3.1.3 公平锁、非公平锁
在Java中,公平锁(Fair Lock)和非公平锁(Nonfair Lock)是锁机制的两种实现方式,用于控制多个线程对共享资源的访问顺序。
Java中synchronized只能是非公平锁,而ReentrantLock、ReentrantReadWriteLock可以实现公平锁和非公平锁。
- 公平锁: 公平锁是指多个线程按照请求的顺序依次获取锁。当一个线程释放锁后,等待时间最长的线程将有机会获取锁。公平锁可以保证线程获取锁的顺序符合其请求的顺序,避免线程饥饿的情况发生。在公平锁中,线程会排队等待获取锁。
在Java中,通过ReentrantLock类的构造函数可以创建一个公平锁,如下所示:
ReentrantLock lock = new ReentrantLock(true); // 公平锁
- 非公平锁: 非公平锁是指多个线程获取锁的顺序不受其请求顺序的限制。当一个线程释放锁后,任何一个等待获取锁的线程都有机会立即获取到锁,不考虑等待时间的长短。非公平锁可能导致某些线程长时间等待,造成线程饥饿的情况。
在Java中,默认情况下,使用ReentrantLock类创建的锁是非公平锁,如下所示:
ReentrantLock lock = new ReentrantLock(); // 非公平锁
公平锁和非公平锁的选择取决于具体的应用场景和需求:
-
公平锁能够保证线程获取锁的顺序按照其请求的顺序,避免线程饥饿,但可能会牺牲一些并发性能。
-
非公平锁能够提高并发性能,因为等待时间较短的线程有机会更快地获取到锁,但可能会导致某些线程长时间等待,出现线程饥饿的情况。
需要注意的是,公平锁和非公平锁的选择并不一定会影响锁的功能和线程安全性,只是决定了线程获取锁的顺序。根据具体的场景和性能要求,选择合适的锁类型以优化并发性能或保证公平性。
3.1.4 互斥锁、共享锁
互斥锁(Mutex Lock)和共享锁(Shared Lock)是两种并发编程中的锁机制,用于控制对共享资源的访问。
- 互斥锁: 互斥锁是一种独占锁,它确保在任何给定的时刻只有一个线程可以持有锁并访问共享资源。当一个线程获取了互斥锁后,其他线程必须等待,直到持有锁的线程释放锁才能获取该锁。互斥锁用于保护临界区,防止多个线程同时访问和修改共享资源,以确保数据的一致性和线程安全。
在Java中,synchronized关键字和ReentrantLock类都可以用于实现互斥锁。
- 共享锁: 共享锁是一种允许多个线程同时持有锁并访问共享资源的锁。与互斥锁不同,共享锁可以实现多线程对共享资源的并发读取,提高并发性能。多个线程可以同时持有共享锁,只要它们不试图修改共享资源。只有当一个线程持有共享锁时,其他线程才能获取该锁,但它们只能以共享模式读取共享资源。
在Java中,ReentrantReadWriteLock类提供了对共享锁的支持,其中的读锁是共享锁,写锁是互斥锁。读锁可以被多个线程同时持有,写锁是独占的,只能被一个线程持有。
互斥锁和共享锁的选择取决于对共享资源的访问模式和需求:
- 互斥锁适用于对共享资源的修改和独占访问,保证数据的一致性和线程安全。
- 共享锁适用于对共享资源的并发读取,允许多个线程同时持有锁并读取共享资源,提高并发性能。
需要根据具体的场景和需求选择适当的锁类型,以实现对共享资源的正确访问和控制。
3.2 深入synchronized
3.2.1 类锁、对象锁
synchronized锁是基于对象实现的,一般的使用方式就是修饰同步方法和修饰同步代码块。
-
修饰方法
-
static方法:当前.class作为锁(类锁)
-
非static方法:当前对象作为锁(对象锁)
-
-
修饰同步代码块: 以代码块中括号里面设置的对象为锁。
3.2.2 synchronized优化
JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中对synchronzied做了大量优化。
-
**锁消除:**锁消除(Lock Elimination)是一种优化技术,在某些情况下,编译器或运行时环境可以自动识别出不会发生并发问题的代码段,并消除对锁的使用,以提高程序的执行性能。 public void synchronzied method(){ //没有操作临界区资源,此时synchronzied操作无效 }
-
锁膨胀: 锁膨胀(Lock Escalation)是一种优化技术,在某些情况下,当多个线程竞争同一个锁时,锁膨胀可以将细粒度的锁升级为粗粒度的锁,以减少锁的数量和开销。通常情况下,细粒度的锁可以提供更好的并发性能,因为它允许多个线程同时访问不同的共享资源。然而,当多个线程频繁地竞争同一个锁时,细粒度的锁可能导致过多的锁竞争和上下文切换,从而降低系统的性能。此时,锁膨胀可以将多个细粒度的锁合并为一个粗粒度的锁,减少锁竞争的频率和开销。 public void method(){ for(int i=0;i<9999;i++){ synchronized(this){
} } //上面的代码会触发锁膨胀,会在循环体外面加锁 } -
锁升级: ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源才会挂起线程。synchronized在JDK1.6之前完全就是获取不到锁立即挂起当前线程,所以synchronized性能比较差。synchronzied就在JDK1.6做了锁升级的优化,锁升级(Lock Upgrade)是一种锁优化技术,在某些情况下,当线程持有了较低级别的锁时,可以将其升级为更高级别的锁,以提供更好的并发性能。
在锁升级中,锁的级别通常是按照粒度从低到高进行划分的,例如:
-
**无锁状态:**表示没有锁竞争,线程可以自由访问共享资源。
-
**偏向锁(Biased Lock):**在最初访问共享资源的线程之后,将共享资源加上偏向标记,后续访问将直接获取锁,无需竞争。如果当前锁只有一个线程在频繁获取、释放,只需要判断当前指向得是否该线程:
-
如果是,则该线程直接获取锁。
-
如果不是,则基于CAS得方式尝试将偏向锁指向当前线程,获取不到则升级成轻量锁(即偏向锁出现了锁竞争)。
-
-
**轻量级锁(Lightweight Lock):**当多个线程竞争同一个锁时,将使用轻量级锁进行锁竞争,通过自旋等待其他线程释放锁。会采用自旋得方式去频繁以CAS得方式获取锁资源(采用得是自适应自旋锁):
- 如果成功则获取锁资源
- 如果自旋一段时间未成功获取锁,则再次升级为重量级锁
-
**重量级锁(Heavyweight Lock):**当轻量级锁竞争失败时,升级为重量级锁,将阻塞等待其他线程释放锁(用户态、内核态切换)。
锁升级的目的是通过减少锁的开销和竞争,提高并发性能。通过将低级别的锁升级为高级别的锁,可以减少锁竞争的频率和开销,但可能增加锁的持有时间和阻塞等待的时间。因此,锁升级需要在减少锁竞争和降低开销之间进行权衡。
3.2.3 synchronized实现原理
synchronized是基于对象实现的,理解synchronized必须先理解Java对象在内存中的存储。Java对象一般包含三部分内容:
-
对象头:
- markword(64位):
MarkWord中标记着4种锁的信息,无锁(001)、偏向锁(101)、轻量级锁(00)和重量级锁(10)
- classpointer(64位):
-
实例数据:对象实例数据
-
对象填充:不满8字节的填充满8字节
3.2.4 synchronized锁升级
为了在Java中能够看到MarkWord信息需要先导入依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
- 代码:
public class ObjectHeaderTest {
public static void main(String[] args) {
Object object = new Object();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
- 结果:
//无锁
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 11 04 00 (00000000 00010001 00000100 00000000) (266496)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
//轻量级锁(因为偏向锁延迟开启,所以直接升级为轻量级锁)
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c8 3a b8 0a (11001000 00111010 10111000 00001010) (179845832)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 00 11 04 00 (00000000 00010001 00000100 00000000) (266496)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
偏向锁延迟: synchronized在默认情况下开启了偏向锁延迟,偏向锁升级为升级为轻量级锁的时候会涉及到偏向锁撤销,需要等到一个安全点(STW)才可以做偏向锁撤销。JVM在知道存在并发情况下,就可以选择不开启偏向锁或是设置偏向锁延迟开启。
因为JVM在启动时需要加载大量的.class文件到内存,这个操作会涉及到大量的synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期有一个延迟5s开启偏向锁操作。
如果正常开启了偏向锁,那么就不会出现无锁状态,对象会直接变为匿名偏向锁的状态。
-
锁升级的状态转换:
- Lock Record和ObjectMonitor存储的内容
3.2.5 重量锁底层ObjectMonitor
- ObjectMonitor.hpp中的代码
ObjectMonitor() {
_header = NULL; //header存储着markword
_count = 0; //竞争锁的线程个数
_waiters = 0, //wait的线程个数
_recursions = 0; //标识当前sychronized锁重入的次数
_object = NULL;
_owner = NULL; //持有锁的线程
_WaitSet = NULL; //保存wait的线程信息,双向链表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //获取锁资源失败后,线程要放到当前的单向链表中
FreeNext = NULL ;
_EntryList = NULL ; //_cxq以及为唤醒的WaitSet中的线程,在一定机制下会放到EntryList中
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
- synchronized加锁的hotspot代码:
int ObjectMonitor::TryLock (Thread * Self) {
for (;;) {
void * own = _owner ; //拿到持有锁的线程
if (own != NULL) return 0 ; //如果有线程持有锁,告辞
//没有线程持有锁,owner为null。cmpxchg指令就是底层的CAS实现
if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
return 1 ; //成功获取锁资源
}
//这里重试操作没什么意义,直接返回-1
if (true) return -1 ;
}
}
- tryEnter
bool ObjectMonitor::try_enter(Thread* THREAD) {
//当前线程不是持有锁线程
if (THREAD != _owner) {
//判断当前持有锁的线程是否是当前线程,说明这是从轻量级锁刚刚升级过来
if (THREAD->is_lock_owned ((address)_owner)) {
_owner = THREAD ;
_recursions = 1 ;
OwnerIsThread = 1 ;
return true;
}
//CAS操作,尝试获取锁
if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
return false; //没拿到锁,告辞
}
return true; //拿到锁资源
} else {
_recursions++; //将_recursions + 1,代表锁重入操作
return true;
}
}
- enter:想方设法拿到锁资源,如果没拿到就扔到cxq单向链表中
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ; //拿到当前线程
void * cur ;
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; //CAS拿锁
if (cur == NULL) { //拿锁成功
return ;
}
if (cur == Self) {//锁重入操作
_recursions ++ ;
return ;
}
//轻量级锁
if (Self->is_lock_owned ((address)cur)) {
_recursions = 1 ;
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
Self->_Stalled = intptr_t(this) ;
if (Knob_SpinEarly && TrySpin (Self) > 0) {
Self->_Stalled = 0 ;
return ;
}
JavaThread * jt = (JavaThread *) Self ;
//没拿到锁资源,count++增加竞争锁资源的线程数目
Atomic::inc_ptr(&_count);
EventJavaMonitorEnter event;
{
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_enter()) {
JvmtiExport::post_monitor_contended_enter(jt, this);
}
OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);
Self->set_current_pending_monitor(this);
for (;;) {
jt->set_suspend_equivalent();
//入队操作,进入到cxq中
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
//count--,当前线程获取到锁资源
Atomic::dec_ptr(&_count);
Self->_Stalled = 0 ;
DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_entered()) {
JvmtiExport::post_monitor_contended_entered(jt, this);
}
if (event.should_commit()) {
event.set_klass(((oop)this->object())->klass());
event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
event.commit();
}
if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
}
}
- EnterI
for (;;) {
node._next = nxt = _cxq ; //入队
//CAS方式入队
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
//重新尝试获取锁资源
if (TryLock (Self) > 0) {
return ;
}
}
3.3 深入ReentrantLock
3.3.1 ReentrantLock和synchronized的区别
ReentrantLock和synchronized是Java中用于实现线程同步的两种机制,它们在功能和使用上有一些区别,如下所示:
-
可重入性:ReentrantLock是可重入锁,也就是说同一个线程可以多次获得同一个锁,而synchronized也具有可重入性。当线程已经获得了某个锁时,再次请求该锁时会自动获取,而不会被阻塞。这种机制可以避免死锁情况的发生。
-
锁的获取方式:ReentrantLock提供了两种获取锁的方式,即公平锁和非公平锁,而synchronized是非公平锁。公平锁会按照线程的请求顺序获取锁,而非公平锁则不保证请求顺序。在某些情况下,公平锁可能会导致性能下降,而非公平锁可能会出现线程饥饿的情况。
-
等待可中断:ReentrantLock可以支持线程等待锁的过程中被中断,即可以对等待的线程发出中断信号,而synchronized无法响应中断。
-
条件变量:ReentrantLock可以使用Condition对象来实现更灵活的线程等待和唤醒机制,可以分组唤醒线程,而synchronized没有内置的条件变量机制。
-
性能:在低竞争情况下,synchronized的性能通常比ReentrantLock好,因为synchronized是JVM内置的机制,无需额外的方法调用和线程状态切换。然而,在高竞争情况下,ReentrantLock可能更具优势,这是因为synchronized存在锁升级的概念,如果升级到重量级锁,synchronized的效率较低。同时ReentrantLock在公平锁模式下,可以避免线程饥饿问题。
-
实现原理:ReentrantLock基于AQS实现的,而synchronized是基于ObjectMonitor实现的。
3.3.2 AQS概述
AQS(AbstractQueuedSynchronizer)是Java并发包中的一个抽象类,它提供了实现同步器(synchronizer)的基础框架,用于构建自定义的同步器和锁。如ReentrantLock、ThreadPoolExecutor、阻塞队列、CountDownLatch、Semaphore、CyclicBarrier等都是基于AQS实现的。
AQS通过使用一个基于双向链表的 等待队列 和一个状态变量来管理线程的同步和调度。它提供了一组底层的原子操作,可以实现各种同步器的常用功能,如互斥锁、共享锁、信号量等。
AQS的核心思想是使用一个 volatile int类型的状态变量state 来表示同步状态,通过CAS(Compare and Swap)操作来实现对状态的原子更新。在AQS中,状态的值通常表示锁的占用情况或资源的可用数量。
AQS提供了两种方式来实现具体的同步器:
- 独占模式(Exclusive Mode):独占模式下,同一时刻只能有一个线程持有锁或访问共享资源,其他线程需要等待。ReentrantLock就是基于AQS实现的独占锁。
- 共享模式(Shared Mode):共享模式下,多个线程可以同时持有锁或访问共享资源。CountDownLatch、Semaphore等就是基于AQS实现的共享锁。
AQS的设计使得它可以被广泛应用于构建各种同步器,包括锁、信号量、倒计时门栓等。它为并发编程提供了强大的基础支持,并且在Java并发包的实现中被广泛使用,如ReentrantLock、Semaphore、CountDownLatch等都是基于AQS的实现。
3.3.3 加锁流程源码剖析
3.3.3.1 加锁流程概述
- 非公平锁流程:
3.3.3.2 三种加锁源码分析
- lock方法: 当一个线程调用 lock() 方法尝试获取锁时,如果锁已经被其他线程持有,那么当前线程将会进入阻塞状态,直到获取到锁为止。但是,如果在阻塞期间发生了线程中断,那么线程将被唤醒,但是并不会抛出 InterruptedException 异常。
//jdk1.8
final void lock() {
if (compareAndSetState(0, 1)) //尝试设置state变量,设置成功则获取锁
setExclusiveOwnerThread(Thread.currentThread()); //当前线程获取锁
else
acquire(1);
}
- tryLock方法: tryLock() 是 Lock 接口中定义的一个方法,用于尝试获取锁,但不会阻塞当前线程。tryLock() 方法尝试立即获取锁,如果锁当前没有被其他线程持有,则当前线程会获取到锁,并返回 true。如果锁当前被其他线程持有,则当前线程无法获取锁,并立即返回 false。
//tryLock方法无论是公平锁还是非公平锁,都会走非公平锁的抢占锁的操作。
public boolean tryLock() {
return sync.nonfairTryAcquire(1); //非公平锁的加锁方式
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); //获取当前线程
int c = getState(); //获取state
if (c == 0) { //当前锁未被获取
if (compareAndSetState(0, acquires)) { //更改state成功,则获取锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //如果当前线程已经获取锁,则重入
int nextc = c + acquires;
if (nextc < 0) // 超过了int的最大的取值范围,超过了锁重入的次数
throw new Error("Maximum lock count exceeded");
setState(nextc); //锁重入成功
return true;
}
return false; //否则返回获取锁失败
}
- tryLock定时
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) || //立马去tryLock一把,获取到了就返回成功
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout; //设置超时时间
final Node node = addWaiter(Node.EXCLUSIVE); //将当前线程加入等待队列
boolean failed = true;
try {
for (;;) { //不断轮询去获取锁
final Node p = node.predecessor(); //当前节点的前一个节点
if (p == head && tryAcquire(arg)) { //当前节点为等待队列中的第一个节点,则尝试去获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) //如果超时,则返回失败
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
//避免剩余时间太少就不用挂起
nanosTimeout > spinForTimeoutThreshold)
//剩余时间足够,则挂起剩余时间,线程状态为TIME_WAITING
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- lockInterruptibly方法: lockInterruptibly() 方法在尝试获取锁的过程中能够响应中断。如果当前线程在调用 lockInterruptibly() 时被中断,它将尽快中断自己并抛出 InterruptedException 异常。这样,使用 lockInterruptibly() 方法可以在锁被其他线程持有的情况下,响应中断并进行相应的处理,而不是无限期地等待锁的释放。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //如果线程中断,则直接抛出异常
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) { //自旋,直到线程抛出异常
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //线程中断,抛出异常
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) && //首先直接去获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //获取不到再将当前线程入队
selfInterrupt();
}
//当前线程没有拿到锁,并且到AQS排队之后触发的方法。中断操作这里无需考虑。
//判断当前线程是否还能再次尝试获取锁资源,如果不能再次获取锁资源,或者还是没获取到锁资源,则尝试将当前线程挂起,线程属于WAITTING状态。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //自旋获取锁
final Node p = node.predecessor(); //获取前面一个节点
if (p == head && tryAcquire(arg)) { //当前节点是第一个节点的时候,尝试获取锁
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//没拿到锁资源
if (shouldParkAfterFailedAcquire(p, node) && //基于上一个节点状态判断当前节点是否可以挂起线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) //除非JVM出问题,一般不会执行
cancelAcquire(node);
}
}
//获取锁资源成功之后,先执行setHead
private void setHead(Node node) {
head = node; //当前节点作为头节点,作为新的伪节点
//头节点不需要线程信息
node.thread = null;
node.prev = null;
}
//当前线程没有拿到锁资源或者没有资格竞争锁资源,来看能否挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//-1 SINGNAL状态:代表当前节点的后继节点可以挂起线程,后续我会唤醒后继节点
//1 CANCELLED状态:代表当前节点已经取消了
if (ws == Node.SIGNAL) //如果前继节点状态为SIGNAL,当前节点才可以安心挂起线程
return true;
if (ws > 0) { //如果当前节点的上一个节点为取消状态,需要往前找到一个不为1的Node,作为当前节点的前继节点,被取消的节点将会被GC回收。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果上一个节点的状态不是SIGNAL或者CANCELLED,代表上一个节点状态正常,但是要将上一个节点状态改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//挂起线程,将线程从Running状态变为Wait状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
//基于Unsafe类挂起线程
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker); //将当前线程的状态设置为阻塞状态,同样也是hotsopt
UNSAFE.park(false, 0L); //hotspot 中去挂起线程
setBlocker(t, null);
}
- addWaiter: 将当前没有拿到锁资源的线程扔到AQS线程中去排队。
private Node addWaiter(Node mode) { //默认互斥锁
Node node = new Node(Thread.currentThread(), mode); //获取当前线程,封装为Node
Node pred = tail; //拿到尾节点
if (pred != null) { //如果尾节点不为null,说明队列中有节点
node.prev = pred; //当前节点的prev指向尾节点
if (compareAndSetTail(pred, node)) { //以CAS的方式,将当前线程设置为tail节点
pred.next = node;
return node;
}
}
enq(node); //如果CAS设置tail失败,则自旋保证当前的node一定可以保证AQS队列的末尾
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail; //拿到尾节点
if (t == null) { // 尾节点为空,AQS中一个节点都没有
if (compareAndSetHead(new Node())) //构建一个伪节点作为head和tail
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //还是以CAS方式设置tail
t.next = node;
return t;
}
}
}
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
//hotspot的JNI代码 unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
UnsafeWrapper("Unsafe_CompareAndSwapObject");
oop x = JNIHandles::resolve(x_h);
oop e = JNIHandles::resolve(e_h);
oop p = JNIHandles::resolve(obj);
HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);
oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);
jboolean success = (res == e);
if (success)
update_barrier_set((void*)addr, x);
return success;
UNSAFE_END
inline void update_barrier_set(void* p, oop v, bool release = false) {
oopDesc::bs()->write_ref_field(p, v, release);
}
- cancelAcquire:
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null; //线程设置为null
Node pred = node.prev;
while (pred.waitStatus > 0) //向前找到有效节点作为当前节点的prev
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED; //节点设置为取消状态
//将当前节点从AQS中移除
if (node == tail && compareAndSetTail(node, pred)) { //当前node是tail,将tail替换为当前节点
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//不是head的后继节点
if (pred != head &&
//拿到上一个节点的状态并判断是否为-1,如果不是-1就改为-1
((ws = pred.waitStatus) == Node.SIGNAL ||
//
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//当前节点是head的后继节点
unparkSuccessor(node); //移除当前节点
}
node.next = node; // help GC
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
3.3.4 释放锁流程源码剖析
3.3.4.1 释放流程概述
以一个简单的例子来阐述锁资源的释放流程,假设线程A持有当前锁且重入了一次(state=2),线程B和线程C获取锁资源失败,在AQS中排队,具体释放锁资源的大体流程如下:
-
- 线程A调用释放锁资源的unlock方法,首先会判断是不是线程A持有锁,如果不是就抛异常,如果是则state-1.
-
- 接着判断state-1之后state是否为0,不为0则说明锁资源未完全释放,直接结束;如果为0则继续执行3.
-
- 判断头节点的状态是否为SIGNAL,如果为SIGNAL则表示链表中有挂起的线程需要唤醒;如果为0则表示后面没有需要被唤醒的线程。
-
- 唤醒线程时,需要将SIGNAL改为0,并找到有效节点然后唤醒。
3.3.4.2 释放锁源码分析
- tryRelease方法
public final boolean release(int arg) {
if (tryRelease(arg)) { //尝试释放锁
signalNext(head); //释放成功则唤醒后续排队线程
return true;
}
return false;
}
//ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
//拿到state - 1
int c = getState() - releases;
//判断当前持有锁的线程是否当前线程,如果不是,直接抛出异常
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException();
//锁是否完全释放
boolean free = (c == 0);
//锁完全释放
if (free)
setExclusiveOwnerThread(null); //释放锁,将持有锁的线程置为null
setState(c); //设置state
return free;
}
3.4 ConditionObject
3.4.1 Condition的介绍
类似于synchronized提供了wait和notify的方法实现线程在持有锁时可以实现挂起,唤醒等操作,ReentrantLock也拥有类似功能,提供了await和signal方法去实现类似wait和notify的功能,下面的方法是一个实例。
public class ConditionTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程获取锁资源并await挂起线程");
try{
condition.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("子线程挂起后被唤醒!");
}).start();
Thread.sleep(1000);
lock.lock();
System.out.println("主线程等待5s拿到锁资源,子线程执行了await方法");
condition.signal();
System.out.println("主线程唤醒了await挂起的子线程");
lock.unlock();
}
3.4.2 Condition的构建方式&核心属性
通过lock锁对象执行newCondition方法时,本质就是直接new AQS提供的ConditionObject对象。lock锁中可以有多个Condition对象,在对Condition1进行操作时,不会影响到Condition2.
在ConditionObject中只有2个核心属性,firstWaiter和lastWaiter,虽然Node对象有prev和next,但是在ConditionObject是不会使用这两个属性的。在ConditionObject只会使用nextWaiter属性实现单向链表效果。
private transient ConditionNode firstWaiter; //头节点
private transient ConditionNode lastWaiter; //尾节点
final ConditionObject newCondition() {
return new ConditionObject();
}
3.4.3 Condition的await方法分析
-
await方法 持有锁的线程在执行await之后会做几个操作:
-
判断线程是否中断,如果中断了则抛出异常。
-
未中断,则将当前线程封装为Node添加到Condition单向链表中。
-
一次性释放所有锁资源
-
如果线程没在AQS队列中,则正常挂起线程
-
public final void await() throws InterruptedException {
//如果线程中断,则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//创建Node对象并加入Condition的单向链表
Node node = addConditionWaiter();
//全量释放锁资源,并保留重入次数
int savedState = fullyRelease(node);
//中断模式
int interruptMode = 0;
//当前Node是否在AQS队列中
//执行fullyRelease方法后,线程就释放了锁资源,如果线程刚刚释放锁资源,其它线程就立刻执
//行了signal方法,此时当前线程就被放到了AQS的队列中,这样一来线程就不需要执行LockSupport.park(this)去挂起线程
while (!isOnSyncQueue(node)) {
LockSupport.park(this); //挂起线程
//如果线程执行到这里,说明现在被唤醒。
//1. 线程可以被signal唤醒(如果被signal唤醒,说明线程已经在AQS队列中)
//2. 线程可以被中断唤醒,线程被唤醒后可能没在AQS队列中
//3. 如果先被signal唤醒,然后线程中断了
//checkInterruptWhileWaiting可以确认当前Node一定在AQS中,返回的值有3种
// 0: 正常signal唤醒(不确定是否在AQS队列)
// -1(THROW_IE):中断唤醒(可以确保在AQS队列)
// 1 (REINTERRUPT):signal唤醒,但是线程被中断,可以确保在AQS队列中
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//Node一定在AQS队列
//执行acquireQueued,尝试在lock中获取锁资源,返回true代表线程在AQS队列中挂起时被中断过
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
//如果线程在AQS中排队时被中断过
interruptMode = REINTERRUPT;
//如果当前Node还在condition的单向链表中,则脱离单向链表
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//为0,线程在signal以及持有锁的过程中没被中断过则什么也不做
//不为0,如果是中断唤醒的线程则抛出异常
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter; //先拿到尾节点
//如果尾节点不为null,且尾节点的状态不正常,则移除链表中状态不为CONDITION的线程
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//构建当前线程的Node,状态设置为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//将node插入链表尾
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//全量释放锁资源
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); //拿到当前的state值
if (release(savedState)) { //一次性释放所有锁资源
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED; //如果释放锁资源失败,则将节点状态设置为取消
}
}
3.4.4 Condition的signal方法分析
signal方法主要完成以下操作:
-
确保执行signal方法的是持有锁的线程
-
脱离Condition队列
-
将Node状态从CONDITION改成0
-
将Node添加到AQS队列中
-
为了避免当前Node无法在AQS中被正常唤醒而做了一些判断和操作
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter; //拿到单链表头节点
if (first != null) //头节点不为空,说明有线程挂起
doSignal(first); //唤醒挂起线程
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) //找到链表中的第一个节点去唤醒
&& (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//采用CAS的方式更改当前node的状态,从CONDITION改为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将当前Node加入AQS的等待队列(双向链表)
Node p = enq(node);
int ws = p.waitStatus;
//将Node的waitStatus设置为SIGNAL
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//使用Unsafe类方法唤醒线程
LockSupport.unpark(node.thread);
return true;
}
3.5 深入ReentrantReadWriteLock
3.5.1 为什么会出现读写锁?
读写锁(Read-Write Lock)是一种特殊的锁机制,它允许多个线程同时读取共享资源,但在写操作时需要独占访问。读写锁的出现主要是为了提高对共享资源的并发访问性能和效率。
-
读多写少的场景:在某些应用场景中,对共享资源的读操作远远多于写操作,如果采用传统的互斥锁来保护共享资源,会导致读操作的并发性能受到限制。读写锁允许多个线程同时获取读锁,提高了读操作的并发性能。
-
读写冲突:在读写并发的场景中,如果没有适当的同步机制,可能会出现读写冲突的问题。当一个线程正在写共享资源时,其他线程如果同时读取共享资源可能会导致数据不一致或错误的结果。读写锁通过允许多个线程同时获取读锁,但在写锁被获取时阻塞读锁的获取,有效地解决了读写冲突的问题。
-
提高并发性能:读写锁的设计可以提高对共享资源的并发访问性能。多个读操作可以并发地进行,不会互斥地阻塞,提高了并发性能。而写操作需要独占访问,确保数据的一致性和完整性。
package thread;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) {
new Thread(() -> {
readLock.lock();
System.out.println("子线程!");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
readLock.unlock();
}
}).start();
readLock.lock();
try {
Thread.sleep(1000);
System.out.println("主线程");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
readLock.unlock();
}
}
}
3.5.2 读写锁实现原理
ReentrantReadWriteLock也是基于AQS实现的,还是对state进行操作,如果没拿到锁依然也会去AQS中排队。
-
读锁操作: 基于state的高16位进行操作
-
写锁操作: 基于state的低16位进行操作
ReentrantReadWriteLock也是可重入锁
-
写锁重入: 中的写锁的重入方式基本和ReentrantLock一致,也是对state+1,只要确认持有锁资源的线程是当前写锁线程即可。区别在于ReentrantLock的重入次数是state的正数取值范围,但是写锁的取值范围变小了。
-
读锁重入: 因为读锁是共享锁,读锁在获取锁资源操作时,要对state的高16为进行+1操作,所以同一时间会有多个线程持有读锁资源。这样的话多个读操作在持有读锁的时候无法确认每个线程的读锁重入的次数。为了记录读锁的重入次数,每个读锁的操作线程都会有一个ThreadLocl变量记录读锁重入次数。
-
写锁饥饿问题: 写锁饥饿问题指的是在读写锁机制中,由于读操作的频繁发生,可能导致写操作一直无法获取写锁,从而导致写线程长时间被阻塞,无法执行写操作的情况。
-
读优先策略:在读写锁的设计中,通常会采用读优先的策略,即允许多个线程同时获取读锁,但在有写锁等待时,读锁的获取会被阻塞。这样可能导致写线程长时间等待,特别是在读操作非常频繁的情况下,写线程可能一直无法获取写锁。
-
读锁占用长时间:如果读操作的执行时间非常长,那么写线程在等待写锁时就会长时间被阻塞,从而无法及时执行写操作。
解决方法就是当读锁在拿到锁资源后,如果再有线程需要获取读锁资源则需要去AQS队列中排队。如果队列的前面有需要写锁资源的线程,那么后续读线程是无法拿到锁资源的,持有读锁线程只会让写锁线程之前的读线程拿到锁资源。
-
3.5.3 写锁分析
3.5.3.1 写锁加锁流程
3.5.3.2 写锁加锁源码分析
写锁的加锁和释放锁流程跟ReentrantLock相同,也是排他锁,区别在于只判断state的低16位,这里不再赘述,详细参考上面。
- lock方法:
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- unlock方法:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.5.4 读锁分析
3.5.4.1 读锁加锁流程
3.5.4.2 读锁加锁源码分析
- lock方法
public void lock() {
sync.acquireShared(1); //获取共享锁
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //尝试先获取一次锁,返回-1表示拿锁成功
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread(); //获取当前线程
int c = getState(); //获取state
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) //有写锁存在,且写锁不为当前线程返回-1
return -1;
int r = sharedCount(c); //根据高16位获取读锁数量
//如果当前读线程不应当被阻塞
//公平锁:查看队列是否有排队的,有排队的直接执行后面代码
//非公平锁:没有排队的直接抢。有排队的,但是其实读锁不需要排队,如果出现这个情况大部分是写锁资源刚刚释放,后续Node还没来得及拿到读锁资源,当前竞争的读线程可以直接获取。
if (!readerShouldBlock() &&
r < MAX_COUNT && //剩余锁数量小于MAX_COUNT
compareAndSetState(c, c + SHARED_UNIT)) { //以CAS方式修改state
if (r == 0) { //读锁为0
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++; //读锁重入操作
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- unlock方法
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
3.6 死锁
4. 阻塞队列
4.1 基础概念
4.1.1 阻塞队列基本概念
生产者和消费者是设计模式的一种,让生产者和消费者基于一个容器来解决强耦合问题,他们之前不会直接通讯,而是通过一个容器(队列)进行通讯。
阻塞队列是一种特殊的队列,它提供了阻塞操作的特性。在并发编程中,阻塞队列常用于实现生产者-消费者模式或多线程间的数据传递与同步。Java 并发包中的 java.util.concurrent 包提供了多种阻塞队列的实现。
下面列举了几种常见的阻塞队列实现:
-
ArrayBlockingQueue:一个有界的阻塞队列,底层基于数组实现。需要指定队列的容量。 -
LinkedBlockingQueue:一个可选有界的或无界的阻塞队列,底层基于链表实现。如果指定了容量,则为有界队列。 -
PriorityBlockingQueue:一个无界阻塞优先级队列,底层基于堆实现。元素按照优先级进行排序。 -
DelayQueue:一个无界阻塞延迟队列,底层基于优先级队列实现。其中的元素必须实现Delayed接口,根据指定的延迟时间进行排序。 -
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待相应的删除操作。
4.1.2 JUC阻塞队列常用的存取方法
JUC(Java Util Concurrent)包中提供了多种阻塞队列实现,常见的存取方法包括以下几种:
-
put(E element):向队列末尾插入元素,如果队列已满则阻塞等待直到有空间可用。 -
offer(E element):向队列末尾插入元素,如果队列已满则立即返回false,不阻塞。 -
take():从队列头部获取并移除元素,如果队列为空则阻塞等待直到有元素可用。 -
poll():从队列头部获取并移除元素,如果队列为空则立即返回null,不阻塞。 -
offer(E element, long timeout, TimeUnit unit):向队列末尾插入元素,如果队列已满则阻塞等待一段时间,超过指定时间后返回false。 -
poll(long timeout, TimeUnit unit):从队列头部获取并移除元素,如果队列为空则阻塞等待一段时间,超过指定时间后返回null。
这些方法是阻塞队列中常用的存取方法,根据具体的需求和场景选择合适的方法。阻塞队列的实现类如 ArrayBlockingQueue、LinkedBlockingQueue 和 SynchronousQueue 等都提供了这些方法,可以根据需要选择适合的实现类来使用。
4.2 ArrayBlockingQueue
4.2.1 ArrayBlockingQueue基本使用
ArrayBlockingQueue 是Java并发包中提供的一个有界阻塞队列实现,下面是ArrayBlockingQueue 的基本使用示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ArrayBlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个容量为 10 的 ArrayBlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 向队列中添加元素
queue.put(1);
queue.put(2);
queue.put(3);
// 从队列中获取并移除元素
int element = queue.take();
System.out.println("Removed element: " + element);
// 查看队列的大小
int size = queue.size();
System.out.println("Queue size: " + size);
// 判断队列是否为空
boolean isEmpty = queue.isEmpty();
System.out.println("Is queue empty? " + isEmpty);
// 查看队列头部元素,不移除
int peekElement = queue.peek();
System.out.println("Peeked element: " + peekElement);
}
}
上述示例中,我们创建了一个容量为 10 的 ArrayBlockingQueue,并使用 put() 方法向队列中添加元素。然后,我们使用 take() 方法从队列中获取并移除一个元素,并打印出被移除的元素。我们还使用 size() 方法查看队列的大小,使用 isEmpty() 方法判断队列是否为空,以及使用 peek() 方法查看队列的头部元素(不移除)。
执行上述代码后,你将得到类似以下的输出:
Removed element: 1
Queue size: 2
Is queue empty? false
Peeked element: 2
4.2.2 生产者方法实现原理
- ArrayBlockingQueue的一些成员变量
lock: 一个ReentrantLock
count: 当前数组中元素个数
items: 数组本身
//使用数组实现了队列的先进先出效果
putIndex: 存储数据下标
takeIndex: 取数据的下标
notEmpty: 消费者挂起线程和唤醒线程用到的Condition
notFull: 生产者挂起线程用到的Condition
- add方法:
public boolean add(E e) {
if (offer(e)) //走offer方法
return true;
else
throw new IllegalStateException("Queue full"); //失败抛异常
}
- offer方法:
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock; //获取当前阻塞队列的锁
lock.lock(); //加锁
try {
if (count == items.length) //队列已满,返回false
return false;
else {
enqueue(e); //入队
return true;
}
} finally {
lock.unlock(); //释放锁
}
}
private void enqueue(E x) {
final Object[] items = this.items; //拿到数组的引用
items[putIndex] = x; //放入数组
if (++putIndex == items.length) //判断阻塞队列是否已满
putIndex = 0;
count++;
notEmpty.signal(); //notEmpty为一个Condition,在阻塞队列初始化时赋值,这里通知队列非空唤醒线程
}
- offer(E e, long timeout, TimeUnit unit):
生产者在添加数据的时候,如果队列已满则会阻塞一段时间。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout); //将时间单位转换成纳秒
final ReentrantLock lock = this.lock; //加锁,可中断并抛出异常
lock.lockInterruptibly();
try {
while (count == items.length) { //虚假唤醒,如果队列已满
if (nanos <= 0) //等待时间是否超时
return false;
nanos = notFull.awaitNanos(nanos); //挂起等待,会释放锁资源,返回剩余阻塞时间
}
enqueue(e); //入队
return true;
} finally {
lock.unlock();
}
}
- put方法:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //加锁,可被中断
try {
while (count == items.length) //队列已满
notFull.await(); //阻塞
enqueue(e); //入队
} finally {
lock.unlock(); //解锁
}
}
4.2.3 消费者方法实现原理
- remove方法
public E remove() {
E x = poll(); //直接调用poll方法
if (x != null)
return x;
else
throw new NoSuchElementException(); //抛出异常
}
- take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) //队列为空
notEmpty.await(); //阻塞
return dequeue(); //出队
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //通知队列非满
return x;
}
- poll方法
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue(); //队列非空则出队,否则返回null
} finally {
lock.unlock();
}
}
- ** poll(long timeout, TimeUnit unit)**
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0) //超时返回null
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
4.3 LinkedBlockingQueue
4.3.1 实现原理
LinkedBlockingQueue 是 Java 并发包中的一个阻塞队列实现,底层基于链表结构。它支持可选的有界容量,当容量被限制时,它可以作为有界队列,否则可以作为无界队列。
LinkedBlockingQueue 的主要实现原理如下:
-
链表结构:
LinkedBlockingQueue内部使用一个链表来存储元素。链表中的每个节点都包含一个元素和对下一个节点的引用。 -
head 和 tail 指针:
LinkedBlockingQueue维护了两个指针,即 head 和 tail。head 指向队列头部的节点,tail 指向队列尾部的节点。这两个指针用于支持并发的入队和出队操作。 -
入队操作:当元素被插入到队列时,一个新的节点会被创建并添加到链表的尾部。入队操作会修改 tail 指针的引用,将其指向新节点。
-
出队操作:当元素被取出队列时,head 指针指向的节点会被移除,并返回其包含的元素。出队操作会修改 head 指针的引用,将其指向下一个节点。
-
阻塞等待:当队列为空时,执行出队操作的线程将被阻塞,直到队列中有新的元素插入。同样,当队列已满时,执行入队操作的线程将被阻塞,直到队列中有元素被取出。
LinkedBlockingQueue 内部使用 ReentrantLock 和 Condition 来实现线程之间的同步和阻塞等待。通过这种方式,它能够在并发环境下提供线程安全的入队和出队操作。
需要注意的是,LinkedBlockingQueue 的容量可以根据需要进行配置,可以创建有界或无界的队列。在无界模式下,队列可以一直增长,直到内存耗尽。
LinkedBlockingQueue 的实现原理使其适用于生产者-消费者模式和其他多线程场景,提供了高效的并发操作和线程间的数据传递。
4.3.2 生产者方法实现原理
- add
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
- put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
- offer
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock; //获取生产者锁
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
private void enqueue(Node<E> node) {
last = last.next = node;
}
- boolean offer(E e, long timeout, TimeUnit unit)
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
4.3.3 消费者方法实现原理
- take
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- poll
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock; //获取消费者锁
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- E poll(long timeout, TimeUnit unit)
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
- remove
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
4.4 PriorityBlockingQueue
4.4.1 PriorityBlockingQueue基本原理
PriorityBlockingQueue 是 Java 并发包中的一个阻塞优先级队列实现,它继承自 AbstractQueue 类,并实现了 BlockingQueue 接口。
PriorityBlockingQueue 的基本原理如下:
-
基于堆的数据结构:
PriorityBlockingQueue内部使用一个平衡二叉堆(binary heap)作为数据结构来存储元素。这个堆是一个完全二叉树,具有以下特性:- 任意节点的值不大于(或不小于)其子节点的值,这取决于是否为最小堆或最大堆。
- 左子节点的索引为
2i + 1,右子节点的索引为2i + 2,父节点的索引为(i - 1) / 2。
-
优先级排序:
PriorityBlockingQueue中的元素按照优先级进行排序。排序的方式取决于元素的比较器(Comparator)或元素自身的自然排序(如果元素实现了Comparable接口)。 -
入队操作:当元素被插入到队列时,它会根据优先级被插入到堆的适当位置。元素的插入操作需要维护堆的结构和优先级的顺序。
-
出队操作:当需要取出队列中的元素时,会从堆的根节点取出具有最高优先级的元素。然后,需要调整堆的结构,以保持堆的性质。
-
线程安全性:
PriorityBlockingQueue是线程安全的,支持多线程环境下的并发操作。它使用了内部锁来保证并发访问的安全性。
由于基于堆的实现,PriorityBlockingQueue 具有以下特点:
- 每次取出的元素都是优先级最高(或最低)的元素。
- 队列中的元素并不按照插入顺序排列。
需要注意的是,PriorityBlockingQueue 是一个无界队列,即队列的容量可以无限增长。这意味着可以不受限制地添加元素,但也需要注意内存使用和处理速度。
PriorityBlockingQueue 提供了一种方便的方式来管理具有不同优先级的元素,并按照优先级顺序进行处理。它在并发环境下是线程安全的,可用于多线程任务调度、事件处理等场景。
4.4.2 PriorityBlockingQueue的写入原理
- offer
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
newCap = MAX_ARRAY_SIZE;
}
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
if (newArray == null) // back off if another thread is allocating
Thread.yield();
lock.lock();
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
4.4.3 PriorityBlockingQueue的读取原理
- poll
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
4.5 DelayQueue
4.5.1 DelayQueue基本原理
DelayQueue 是 Java 并发包中的一个延迟队列实现,它实现了 BlockingQueue 接口。DelayQueue 中的元素只有在指定的延迟时间过去后才能被获取,这个延迟时间是根据元素的时间单位进行计算的。
DelayQueue 的基本原理如下:
-
内部存储结构:
DelayQueue内部使用优先级队列(PriorityQueue)来存储元素。这个优先级队列是一个无界队列,其中的元素按照其剩余延迟时间进行排序。 -
元素的实现:
DelayQueue中的元素必须实现Delayed接口,它包含两个方法:getDelay(TimeUnit unit):返回元素的剩余延迟时间,以指定的时间单位表示。compareTo(Delayed other):用于比较元素之间的延迟时间。
-
入队操作:当一个元素被插入到
DelayQueue中时,它会根据元素的延迟时间被放入优先级队列的适当位置。插入操作需要维护优先级队列的结构和元素的延迟时间顺序。 -
出队操作:只有在元素的延迟时间过去后,才能从
DelayQueue中获取该元素。如果没有元素的延迟时间过去,获取操作将被阻塞,直到有元素变为可用状态。 -
线程安全性:
DelayQueue是线程安全的,支持多线程环境下的并发操作。它使用了内部锁来保证并发访问的安全性。
需要注意的是,元素的延迟时间是相对于元素被插入到队列中的时间点计算的。延迟时间过去后,元素可以从队列中被获取并进行处理。可以使用不同的时间单位来表示延迟时间,例如秒、毫秒、纳秒等。
DelayQueue 提供了一种方便的方式来管理具有延迟需求的元素,并在延迟时间过去后按照优先级顺序进行处理。它在并发环境下是线程安全的,可用于任务调度、定时任务等场景。
4.5.2 DelayQueue示例
下面是一个简单的示例,演示如何使用 DelayQueue:
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueExample {
public static void main(String[] args) throws InterruptedException {
// 创建 DelayQueue
DelayQueue<DelayedElement> delayQueue = new DelayQueue<>();
// 创建延迟元素并添加到队列中
DelayedElement element1 = new DelayedElement("Element 1", 2000); // 延迟 2 秒
DelayedElement element2 = new DelayedElement("Element 2", 5000); // 延迟 5 秒
DelayedElement element3 = new DelayedElement("Element 3", 3000); // 延迟 3 秒
delayQueue.put(element1);
delayQueue.put(element2);
delayQueue.put(element3);
// 从队列中获取并处理延迟元素
while (!delayQueue.isEmpty()) {
DelayedElement element = delayQueue.take();
System.out.println("Processing element: " + element.getName());
}
}
static class DelayedElement implements Delayed {
private String name;
private long delayTime; // 延迟时间(毫秒)
private long startTime; // 元素插入队列的时间点
public DelayedElement(String name, long delayTime) {
this.name = name;
this.delayTime = delayTime;
this.startTime = System.currentTimeMillis();
}
public String getName() {
return name;
}
@Override
public long getDelay(TimeUnit unit) {
long elapsedTime = System.currentTimeMillis() - startTime;
return unit.convert(delayTime - elapsedTime, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed other) {
long diff = this.getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
return Long.compare(diff, 0);
}
}
}
在上述示例中,我们创建了一个 DelayQueue 并向队列中添加了三个延迟元素。每个延迟元素都具有一个名称和延迟时间,在指定的延迟时间过去后才能从队列中获取并处理。
延迟元素的延迟时间通过实现 Delayed 接口的 getDelay() 方法进行计算。在每次调用 getDelay() 方法时,会计算当前时间与元素插入队列的时间之间的差值,并将其转换为指定的时间单位。
compareTo() 方法用于比较延迟元素之间的优先级。在本例中,我们根据延迟时间的大小进行比较,优先级高的元素将排在队列前面。
最后,我们使用 take() 方法从队列中取出延迟元素,并处理它们。在示例中,我们只是简单地打印出元素的名称,你可以根据实际需求进行相应的处理。
执行上述代码后,你将看到延迟元素按照延迟时间从小到大依次被处理。注意:示例中的延迟时间以毫秒为单位,你可以根据需要进行调整。
4.5.2 DelayQueue常见成员
private Thread leader = null;
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private final Condition available = lock.newCondition();
4.5.2 DelayQueue写入原理分析
- offer
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
4.5.3 DelayQueue读取原理分析
- poll
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
4.6 SynchronousQueue(阻塞队列)
4.6.1 SynchronousQueue介绍&原理
SynchronousQueue 是 Java 并发包中的一个特殊的阻塞队列实现,它是一个没有存储元素的队列,用于在线程之间进行直接的交付。
SynchronousQueue 的主要特点和原理如下:
-
没有存储元素:
SynchronousQueue不像其他队列一样存储元素,它只是用于线程之间的元素交付。每个插入操作都必须等待对应的移除操作,反之亦然。 -
一对一交付:
SynchronousQueue实现了一种一对一的交付机制。当一个线程尝试向队列中插入元素时,它会被阻塞,直到另一个线程从队列中获取该元素。类似地,当一个线程尝试从队列中获取元素时,它也会被阻塞,直到另一个线程向队列中插入元素。 -
同步阻塞:
SynchronousQueue使用了同步阻塞的方式来实现线程之间的等待和通信。它使用了Lock和Condition等机制来保证线程的安全性和正确的交付行为。 -
可选公平性:
SynchronousQueue提供了公平和非公平两种模式。在公平模式下,插入操作和移除操作按照线程的到达顺序进行处理。在非公平模式下,插入操作和移除操作不保证按照顺序处理。
SynchronousQueue 的特性使它非常适用于一些特定的线程交互场景,例如生产者-消费者模式、线程池的任务提交和处理等。它可以有效地实现线程间的数据传递和同步,并提供了高效的并发操作。
需要注意的是,由于 SynchronousQueue 是一个没有容量限制的队列,它的插入和移除操作是互相等待的。这意味着如果没有另一个线程正好在等待相反的操作,插入操作或移除操作可能会一直被阻塞,从而导致死锁的发生。
因此,在使用 SynchronousQueue 时,确保有恰当的线程协调和同步机制,以避免潜在的死锁情况。
4.6.2 简单示例
下面是一个简单的示例,演示如何使用 SynchronousQueue 进行线程之间的直接交付:
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueExample {
public static void main(String[] args) {
SynchronousQueue<Integer> queue = new SynchronousQueue<>();
// 生产者线程
Thread producer = new Thread(() -> {
try {
System.out.println("Producer is putting element...");
queue.put(1); // 阻塞等待消费者线程获取元素
System.out.println("Producer put element successfully");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
System.out.println("Consumer is taking element...");
int element = queue.take(); // 阻塞等待生产者线程放入元素
System.out.println("Consumer took element: " + element);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动生产者和消费者线程
producer.start();
consumer.start();
}
}
在上述示例中,我们创建了一个 SynchronousQueue 对象,并创建了一个生产者线程和一个消费者线程。
生产者线程使用 put() 方法向队列中插入一个元素,然后被阻塞等待消费者线程获取该元素。
消费者线程使用 take() 方法从队列中获取元素,然后打印出被获取的元素。
当运行该示例时,你会看到生产者线程被阻塞,直到消费者线程开始运行并从队列中获取元素。一旦消费者线程获取了元素,生产者线程才能继续执行,并输出相应的信息。
SynchronousQueue 的特性使得生产者和消费者线程必须同步等待对方的操作,实现了一种线程之间的直接交付机制。
4.6.3 SynchronousQueue核心属性
占坑
4.6.4 SynchronousQueue的TransferQueue源码
占坑