Java 并发
此文章已收录至项目 Developer-Knowledge-Base
信息来源
https://www.cnblogs.com/snow-flower/p/6114765.html
java 中的 Lock 锁
https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html
https://www.cnblogs.com/lifegoeson/p/13683785.html
线程和进程
程序是由指令和数据有序组成的静态概念,程序本身没有运行的含义,只有在处理器赋予其生命(操作系统执行)时,它才能成为一个活动的实体,即进程。进程是程序在执行过程中分配和管理资源的基本单位,它是一个动态概念,每个进程都拥有独立的地址空间和系统资源,如内存、文件、网络连接等。不同的进程之间相互独立,并且需要通过 IPC(进程间通信)机制进行数据交换和通信。
进程可以包括多个线程,线程是 CPU 调度和分派的基本单位,是进程的一部分,它们共享进程所拥有的全部资源,并且可以共享对象和资源,如有冲突或需要协同处理,线程之间可以进行沟通以解决冲突或保持同步。一个线程只能属于一个进程,但一个进程可以包括多个线程,而必须至少包括一个线程。通过多线程技术,可以在一个进程中创建多个线程,让它们在同一时刻并发地去处理不同的任务。
在操作系统中运行的程序就是进程,一个进程可以有多个线程,如视频中同时听声音,看图像,看弹幕,等等
有了线程技术,我们就可以在一个进程中创建多个线程,让它们在“同一时刻”分别去做不同的工作了。这些线程共享同一块内存,线程之间可以共享对象、资源,如果有冲突或需要协同,还可以随时沟通以解决冲突或保持同步。
注意:很多多线程是模拟出来的,真正的多线程是指有多个 cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个 cpu 的情况下,在同一个时间点,cpu 只能执行一个代码,因为切换的很快,所以就有同时执行的错局。
- 在 Java 程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,GC 线程,而
main()
称之为主线程,为系统的入口,用于执行整个程序 - 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如 CPU 调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
Java 实现多线程
继承 Thread
Thread
类在 Java.lang
包中定义,一个类只继承 Thread
类,此类就称为多线程操作类
在 Thread
子类中必须明确地覆写 Thread
类中的 run()
方法,此方法为线程的主体
定义语法
Thread
类本质上是实现了 Runnable
接口的一个实例,代表一个线程的实例。启动线程的唯一方 法就是通过 Thread
类的 start()
实例方法。start()
方法是一个 native
方法,它将启动一个新线 程,并执行 run()
方法
native 方法称为本地方法。在 java 源程序中以关键字“native”声明,不提供函数体。其实现使用 C/C++语言在另外的文件中编写,编写的规则遵循 Java 本地接口的规范 (简称 JNI)。简而言就是 Java 中声明的可调用的使用 C/C++实现的方法。Object.class 类中的 getClass() 方法、hashCode() 方法、clone() 方法都是 native 方法
class 类名称 extends Thread类{ //继承 Thread 类
属性...; //类中定义方法
方法...; //类中定义方法
public void run(){ //覆写 Thread 类中的 run() 方法,此方法是线程的主体
线程主体;
}
}
启动多线程需要使用从 Thread
类中继承而来的 start()
方法
若通过 Thread
类实现多线程,那么只能调用一次 start()
方法,若调用多次,则会抛出 IllegalThreadStateException
异常
通过在 run()
方法前加 synchronized
关键字,使多个线程在执行 run()
方法时,以排队的方式进行处理
synchronized public void run()
通过 this
关键字可以指定当前线程的变量
可以直接调用 Thread 类的 run 方法吗?
调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
实现 Runnable 接口
class 类名称 implements Runnable{ //实现 Runnable 接口
属性...; //类中定义属性
方法...; //类中定义方法
public void run(){ //覆写 Runnable 接口中的 run() 方法
线程主体;
}
}
若要启动实现了 Runnable
接口的多线程,必须靠 Thread
类完成启动
Thread
类提供了两个构造方法:public Thread(Runnable target)
和 public Thread(Runnable target,String name)
两个构造方法
具体实现方法
class A implements Runnable {
private String name;
public A(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + "线程+i=" + i);
}
}
}
public class test {
public static void main(String[] args) {
A a = new A("A");
A b = new A("B");
Thread c = new Thread(a);
Thread d = new Thread(b);
c.start();
d.start();
}
}
Thread 类和 Runnable 接口
参考: https://www.cnblogs.com/dolphin0520/p/3949310.html
Thread
类也是 Runable
接口的子类
如果一个类继承 Thread
类,则不适合于多个线程共享资源,而实现了 Ruanable
接口,就可以方便地实现资源共享
实现 Runnable
接口相当于继承 Thread
类来说,有以下显著优势:
- 适合多个相同程序代码的线程去处理同一资源的情况
- 可以避免由于 Java 的单继承特性带来的局限
- 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
建议在开发中使用 Runnable
接口实现多线程
通过 Callable 和 Future 创建线程
创建线程的 2 种方式,一种是直接继承 Thread,另外一种就是实现 Runnable 接口。
这 2 种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从 java 1.5 开始,就提供了 Callable
和 Future
,通过它们可以在任务执行完毕之后得到任务执行结果
Callable
接口代表一段可以调用并返回结果的代码;Future
接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable
用于产生结果,Future
用于获取结果。
-
Callable
类似于Runnable
,但是它有返回值,Runnable
没有。 -
new Thread(futureTask);
的方式来创建FuntureTask
任务,FuntureTask
是一个实现了Runnable
和Future
接口的类 -
call()
方法可以抛出异常,run()
方法不可以。 -
Callable
规定 (重写) 的方法是call()
,Runnable
规定(重写)的方法是run()
。 - 运行
Callable
任务可以拿到一个Future
对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future
对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 提交 Callable 任务
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "hhhh";
}
});
// 获取结果
String result = future.get();
System.out.println(result);
// 关闭线程池
executorService.shutdown();
}
Callable
Callable
位于 java.util.concurrent
包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做 call()
:
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
一般情况下是配合 ExecutorService
来使用的,ExecutorService
的 submit
方法可以提交一个 Callable
的参数
Future
Future
就是对于具体的 Runnable
或者 Callable
任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get
方法获取执行结果,该方法会阻塞直到任务返回结果。
Future 类位于 java.util.concurrent
包下,它是一个接口,在 Future 接口中声明了 5 个方法
-
boolean cancel(boolean mayInterruptIfRunning)
:用来取消任务,如果取消任务成功则返回 true,如果取消任务失败则返回 false。参数 mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务,如果设置 true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论 mayInterruptIfRunning 为 true 还是 false,此方法肯定返回 false,即如果取消已经完成的任务会返回 false;如果任务正在执行,若 mayInterruptIfRunning 设置为 true,则返回 true,若 mayInterruptIfRunning 设置为 false,则返回 false;如果任务还没有执行,则无论 mayInterruptIfRunning 为 true 还是 false,肯定返回 true。 -
isCancelled()
表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。 -
isDone()
表示任务是否已经完成,若任务完成,则返回 true; -
get()
用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回; -
get(long timeout, TimeUnit unit)
用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null
。
FutureTask
FutureTask
是 Future
接口的一个唯一实现类,FutureTask
实现了 RunnableFuture
接口,而 RunnableFuture
继承了 Runnable
接口和 Future
接口所以它既可以作为 Runnable
被线程执行,又可以作为 Future
得到 Callable
的返回值。
因为实现了 Runnable
所以可以直接使用 Thread
提交线程
// 创建 FutureTask
FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "aa";
}
});
//启动线程
new Thread(futureTask).start();
// 获取执行结果
String result = futureTask.get();
System.out.println(result);
它的两个构造器如下
FutureTask(Callable<V> callable);
FutureTask(Runnable runnable, V result);
传入 Runnable 和 result 的构造函数就是用来封装 Callable 任务的。它要求我们传入一个 Runnable 对象和一个泛型 V,表示 Callable 任务的返回类型。在内部,FutureTask 会将 Runnable 对象转换为一个 Callable 对象,并在执行时返回 result 的值。
通过线程池
参考: https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
使用线程池的好处:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while (true) {
threadPool.execute(new Runnable() {
// 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
mport java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Date date1 = new Date();
int taskSize = 5;
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
//执行任务并获取Future对象
Future f = (Future) pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
@Override
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}
线程的生命周期
部分来源: JavaGuide 面试指南
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。 - RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 - BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行)状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行)状态。
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态。
为什么 JVM 没有区分这两种状态呢? (摘自:Java 线程运行怎么有第六种状态? - Dawell 的回答 )现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
初始状态
在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用 Thread
类的构造方法来实现,例如 Thread thread = new Thread();
注意:不能对已经启动的线程再次调用
start()
方法,否则会出现Java.lang.IllegalThreadStateException
异常。
就绪状态
新建线程对象后,调用该线程的 start()
方法就可以启动线程。当线程启动时,线程进入就绪状态。
此时,线程将进入线程队列排队(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为 CPU 的调度不一定是按照先进先出的顺序来调度的)等待系统为其分配 CPU。
等待状态并不是执行状态,当系统选定一个等待执行的 Thread
对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“CPU 调度”。一旦获得 CPU,线程就进入运行状态并自动调用自己的 run()
方法。
如果希望子线程调用
start()
方法后立即执行,可以使用Thread.sleep()
方式使主线程睡眠一儿,转去执行子线程。
运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了 cpu 的调度,就会从就绪状态变为运行状态,执行 run()
方法中的任务。如果该线程失去了 cpu 资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用 yield()
方法,它就会让出 cpu 资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
- 线程调用
sleep
方法主动放弃所占用的系统资源 - 线程调用一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
- 线程在等待某个通知 (
notify
) - 程序调用了线程的
suspend
方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的 run() 方法执行完,或者被强制性地终止,例如出现异常,或者调用了 stop()、desyory() 方法等等,就会从运行状态转变为死亡状态。
堵塞状态
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作时,将让出 CPU 并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用 sleep()
、suspend()
、wait()
等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。如睡眠时间已到,或等待的 I/O 设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
阻塞情况分三种:
等待阻塞 (o.wait->等待对列): 运行 (running) 的线程执行 o.wait()
方法,JVM 会把该线程放入等待队列 (waitting queue) 中。
同步阻塞 (lock->锁池) 运行 (running) 的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线 程放入锁池 (lock pool) 中。
其他阻塞 (sleep/join) 运行 (running) 的线程执行 Thread.sleep(long ms)
或 t.join()
方法,或者发出了 I/O
请求时,JVM 会把该线程置为阻塞状态。当 sleep()
状态超时、join()
等待线程终止或者超时、或者 I/O
处理完毕时,线程重新转入可运行 (runnable
) 状态。
死亡状态
线程调用 stop()
方法时或 run()
方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用 start() 方法,会抛出java.lang.IllegalThreadStateException
异常。
线程会以下面三种方式结束,结束后就是死亡状态。
-
正常结束 :
run()
或call()
方法执行完成,线程正常结束。 -
异常结束 : 线程抛出一个未捕获的
Exception
或Error
。 -
调用 stop : 直接调用该线程的
stop()
方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
一旦线程进入可执行状态,它会在就绪状态与运行状态下辗转,同时也可能进入等待状态、休眠状态、阻塞状态或死亡状态
终止线程的四种方式
- 等待
run()
或者是call()
方法执行完毕 - 设置共享变量,如 boolean flag。flag 作为线程是否继续执行的标志
- 利用
Thread
类提供的interrupt()
和InterruptedException
。 - 利用
Thread
类提供的interrupt()
和isInterrupted()
自然结束
等待 run()
或者是 call()
方法执行完毕,线程自然就结束了
使用退出标志退出线程
一般 run()
方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的 运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean
类型的标志,并通过设置这个标志为 true
或 false
来控制 while
循环是否退出
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
return;
}
}
}
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit 时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只 能由一个线程来修改 exit 的值
Interrupt 方法结束线程
当一个线程处于阻塞状态时,例如在 sleep()、wait()、join() 等方法内部时,如果此时另一个线程调用了该线程的 interrupt() 方法,那么该线程就会被中断,也就是抛出 InterruptedException 异常(如果线程没有处于阻塞状态,则不会有任何影响)。这个 InterruptedException 异常可以被捕获并进行相应的处理,例如跳出循环、释放资源等。因为在 InterruptedException 异常被抛出之前,线程所持有的锁不会被释放,因此需要在 finally 块中处理相关资源的释放。
需要注意的是,interrupt()
方法只是设置了一个中断标志位,并不能强制结束线程的执行。如果我们希望结束线程的执行,需要在相应的业务逻辑中做出响应的处理,比如检查中断标志位,然后主动退出线程的执行。如果不进行相应的处理,线程就会继续执行下去,直到完成所有任务,这可能不是我们期望的结果。
public static void main(String[] args) {
Thread thread = new Thread(()->{
try {
Thread.sleep(5000);
System.out.println("线程执行完毕");
} catch (InterruptedException e) {
System.out.println("线程被中断了");
return;
}
System.out.println("线程正常结束");
});
thread.start();
thread.interrupt();
}
如果不捕获异常,不想使线程进入阻塞状态的话,可以使用 isInterrupted()
来作为线程是否继续执行的标志
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
// 模拟任务的执行过程
System.out.println("正在执行任务...");
}
} finally {
// 释放相应的资源
System.out.println("线程执行完毕,释放资源");
}
});
thread.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.interrupt();
}
线程操作相关方法
方法名称 | 返回值类型 | 描述 |
---|---|---|
Thread(Runnable target) | 构造 | 接收 Runnable 接口子类对象,实例化 Thread 对象 |
Thread(Runnable target,String name) | 构造 | 接收 Runnable 接口子类对象,实例化 Thread 对象,并设置线程名称 |
Thread(String name) | 构造 | 实例化 Thread 对象,并设置线程名称 |
currentThread() | Thread | 返回目前正在执行的线程 |
getName() | String | 返回线程的名称 |
getPriority() | int | 返回线程的优先级 |
isInterrupted() | boolean | 判断目前线程是否被中断,如果是,回 true,否则返回 false |
isAlive() | void | 判断线程是否在活动,如果是,回 true,否则返回 false |
join() throws InterruptedException | void | 等待线程死亡 |
join(long millis) throws InterruptedException | void | 等待 millis 毫秒后,线程死亡 |
run() | void | 执行线程 |
setName(String name) | void | 设定线程名称 |
setPriority(int newPriority) | void | 设定线程的优先值 |
sleep(long millis)throws InterruptedException | void | 使目前正在执行的线程休眠 millis 毫秒 |
start() | void | 开始执行线程 |
toString() | String | 返回代表线程的字符串 |
yield() | void | 将正在执行的线程暂停,允许其他线程执行 |
setDaemon(boolean on) | void | 将一个线程设置成后台允许 |
Java 程序每次运行至少启动两个线程
取得和设置线程名称
取得:getName()
设置:setName(String name)
系统自动设置线程名称 new Thread(my).start
系统分配一个名称,格式Thread-Xx
运行时设置 new Thread(my,"name").start
中断线程
通过**interrupt()**方法中断其运行状态
interrupt()
方法可以用来中断一个正处于阻塞状态(如 sleep()
、wait()
、join()
等)的线程,以便让线程尽快结束。具体来说,调用线程的 interrupt()
方法之后,它的中断标志位会被设置为 true(实际上是通过将一个名为中断状态的 volatile
变量设为 true
来实现的),表示该线程已经被中断,但并不会强制终止该线程的执行。
当一个线程处于阻塞状态时,例如在 sleep()
、wait()
、join()
等方法内部时,如果此时另一个线程调用了该线程的 interrupt()
方法,那么该线程就会被中断,也就是抛出 InterruptedException 异常(如果线程没有处于阻塞状态,则不会有任何影响)。这个 InterruptedException 异常可以被捕获并进行相应的处理,例如跳出循环、释放资源等。因为在 InterruptedException
异常被抛出之前,线程所持有的锁不会被释放,因此需要在 finally
块中处理相关资源的释放。
需要注意的是,interrupt()
方法只是设置了一个中断标志位,并不能强制结束线程的执行。如果我们希望结束线程的执行,需要在相应的业务逻辑中做出响应的处理,比如检查中断标志位,然后主动退出线程的执行。如果不进行相应的处理,线程就会继续执行下去,直到完成所有任务,这可能不是我们期望的结果。
线程休眠 (sleep)
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用 Thread 的 sleep 方法。
通过 sleep(long millis) 方法,需要异常处理
try{
Thread.sleep(500);//线程休眠
}catch(Exception e){}//异常处理
sleep 是静态方法,最好不要用 Thread 的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。因为使用 sleep 方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用 Thread.sleep(1000) 使得线程睡眠 1 秒,可能结果会大于 1 秒
线程让步 (yield)
yield()
方法和 sleep()
方法有点相似,它也是 Thread
类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出 cpu 资源给其他的线程。但是和 sleep()
方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()
方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用 yield()
方法之后,线程调度器又将其调度出来重新进入到运行状态执行
实际上,当某个线程调用了 yield()
方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉 cpu 调度线程
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 1; i <= 50; i++) {
System.out.println(Thread.currentThread().getName() + " 执行了第 " + i + " 次");
if (i == 1) {
System.out.println("线程让步");
Thread.yield();
}
}
}, "线程 1");
Thread thread2 = new Thread(() -> {
for (int i = 1; i <= 50; i++) {
System.out.println(Thread.currentThread().getName() + " 执行了第 " + i + " 次");
}
}, "线程 2");
thread.start();
thread2.start();
}
线程合并 (join)
线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread
类提供了 join
方法来完成这个功能,注意,它不是静态方法。
从上面的方法的列表可以看到,它有 3 个重载的方法:
方法 | 描述 |
---|---|
void join() | 当前线程等该加入该线程后面,等待该线程终止。 |
void join(long millis) | 当前线程等待该线程终止的时间最长为 millis 毫秒。如果在 millis 时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待 cpu 调度 |
void join(long millis,int nanos) | 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在 millis 时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待 cpu 调度 |
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "线程 1");
thread.start();
System.out.println("执行 join");
thread.join();
System.out.println("结束 main");
}
示例中,将线程 1 加入了主线程,阻塞了主线程,当线程 1 执行完毕后才会继续执行主线程
线程等待(wait)
wait()
方法是 Object 类提供的一种等待方法,主要用于在多线程编程中进行线程间的协作。在调用 wait()
方法时,当前线程会释放锁并进入对象的等待队列,等待其他线程调用该对象的 notify()
或者 notifyAll()
方法唤醒它,并重新获得锁。
wait()
方法有三个重载方法:
// 当前线程一直等待,直到其他线程调用该对象的 notify() 或者 notifyAll() 方法
public final void wait() throws InterruptedException
// 当前线程等待一定时间,在等待时间内如果没有其他线程调用该对象的 notify() 或者 notifyAll() 方法,则该线程自动被唤醒
public final void wait(long timeout) throws InterruptedException
// 当前线程等待一定时间,但是等待时间精度更高,单位为纳秒
public final void wait(long timeout, int nanos) throws InterruptedException
使用 wait()
方法时需要注意以下几点:
-
wait()
方法只能和synchronized
关键字一起使用,即在 synchronized 代码块或方法中才能使用wait()
方法。无论是wait
还是notify
,如果不配合synchronized
一起使用,在程序运行时就会报IllegalMonitorStateException
非法的监视器状态异常,而且notify
也不能实现程序的唤醒功能了。 - 调用
wait()
方法的线程会释放锁并进入对象的等待队列,等待其他线程调用该对象的notify()
或者notifyAll()
方法唤醒它,并重新获得锁。 - 被唤醒的线程不会立即执行,它仍需要竞争锁资源才能继续执行。
- 调用
wait()
方法的线程可以通过interrupt()
方法被打断等待状态,并抛出InterruptedException
异常。 - 如果出现多个线程调用
wait()
方法并进入等待状态,那么唤醒时是不确定的,也就是说唤醒的线程是随机的。
使用 wait()
方法进行线程协作时,通常的流程如下:
- 在 synchronized 代码块或方法中,调用
wait()
方法,当前线程释放锁并进入等待状态。 - 另一个线程(比如生产者线程)执行完毕后,调用该对象的
notify()
或者notifyAll()
方法,唤醒等待中的线程。 - 等待中的线程被唤醒后,需要重新竞争锁资源,如果成功获得锁,则继续执行。
需要注意的是,在使用 wait()
方法时需要慎重考虑,避免死锁、性能问题等。通常情况下需要使用 notify()
或者 notifyAll()
方法来唤醒等待中的线程,从而保证线程能够正常运行。同时,要确保在 synchronized
代码块或方法中正确使用 wait()
和 notify()
方法,以避免出现线程安全问题。
public class WaitNotifyDemo {
private static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("线程 1 开始执行");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程 1 被唤醒");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("线程 2 开始执行");
lock.notify();
}
}
});
t1.start();
t2.start();
}
}
在上面的代码中,我们创建了两个线程 t1 和 t2。线程 t1 在 synchronized
代码块中调用了 lock.wait()
方法,进入等待状态并释放锁资源。线程 t2 同样在 synchronized
代码块中调用了 lock.notify()
方法来唤醒等待中的线程 t1。
线程唤醒(notify notifyAll)
notify()
方法和 notifyAll()
方法都是 Object
类中的方法,用来唤醒在该对象上等待的线程。
notify()
方法用来唤醒在该对象上等待时间最长的一个线程,使其从 wait()
方法返回,并继续执行。如果有多个线程在该对象上等待,那么只能唤醒其中一个线程,其他的线程仍然处于等待状态。
notifyAll()
方法则用来唤醒在该对象上等待的所有线程,使它们从 wait()
方法返回,并重新竞争锁资源。唤醒的顺序并不确定,取决于线程调度器的实现。
需要注意的是,notify()
和 notifyAll()
方法只有在持有该对象的锁时才能被调用,因此需要使用 synchronized
关键字来保证并发访问的线程安全性。
public class M {
private static List<String> list = new ArrayList<>();
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread producer = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() < 10) {
String data = String.valueOf(IdUtil.fastUUID());
System.out.println(Thread.currentThread().getName() + "生产了数据:" + data);
list.add(data);
lock.notifyAll();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "Producer");
Thread consumer = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() > 0) {
String data = list.get(0);
list.remove(0);
System.out.println(Thread.currentThread().getName() + "消费了数据:" + data);
lock.notify();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "Consumer");
producer.start();
consumer.start();
}
}
在该示例中,生产者线程会不断的生产数据,并将数据添加到 list 集合中。如果 list 集合中的数据量达到 10 个,则生产者线程会调用 wait() 方法进入等待状态,等待消费者线程将数据消费掉。当消费者线程消费了数据后,会调用 notify() 方法唤醒生产者线程进行数据生产。消费者线程同样也是不断循环,如果 list 集合中没有数据,则调用 wait() 方法进入等待状态,等待生产者线程生产数据。当生产者线程完成生产后,会调用 notifyAll() 方法唤醒所有等待中的消费者线程返回执行。
通过 notify() 和 notifyAll() 方法的使用,实现了生产者和消费者线程之间的协作,保证了对共享资源的正确访问。
各方法区别
sleep() 和 yield()
-
sleep
方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield
方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。 -
sleep
方法声明抛出了InterruptedException
,所以调用sleep
方法的时候要捕获该异常,或者显示声明抛出该异常。而yield
方法则没有声明抛出任务异常。 -
sleep
方法比yield
方法有更好的可移植性,通常不要依靠yield
方法来控制并发线程的执行。
sleep 与 wait 区别
- 对于
sleep()
方法,我们首先要知道该方法是属于Thread
类中的。而wait()
方法,则是属于Object
类中的。 -
sleep()
方法导致了程序暂停执行指定的时间,让出cpu
该其他线程,但是他的监控状态依然 保持者,当指定的时间到了又会自动恢复运行状态。 - 在调用
sleep()
方法的过程中,线程不会释放对象锁。而当调用wait()
方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此 对象调用notify()
方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
start 与 run 区别
-
start()
方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。 - 通过调用
Thread
类的start()
方法来启动一个线程,这时此线程是处于就绪状态,并没有运 行。方法run()
称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运 行run
函数当中的代码。run
方法运行结束,此线程终止。然后 CPU 再调度其它线程
线程优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取 CPU 资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main 线程具有普通优先级
Thread 类提供了 setPriority(int newPriority) 和 getPriority() 方法来设置和返回一个指定线程的优先级
其中 setPriority 方法的参数是一个整数,范围是 1 到 10,也可以使用下列三个常量
定义 | 描述 | 表示的常量
—|:–😐:–:
public static final int MIN_PRIORITY|最低优先级|1
public static final int NORM_PRIORITY|中等优先级,线程默认优先级|5
public static final int MAX_PRIORITY|最高优先级|10
虽然 Java 提供了 10 个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和 Java 的 10 个优先级别对应。所以我们应该使用 MAX_PRIORITY、MIN_PRIORITY 和 NORM_PRIORITY 三个静态常量来设定优先级,这样才能保证程序最好的可移植性。
后台线程 (守护线程)
守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
守护线程使用的情况较少,但并非无用,举例来说,JVM 的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法 setDaemon(true)
,则可以将其设置为守护线程
守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务
通过 setDaemon(true)
来设置线程为“守护线程”,将一个用户线程设置为守护线程 的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
Thread daemonTread = new Thread();
// 设定 daemonThread 为 守护线程,default false(非守护线程)
daemonThread.setDaemon(true);
// 验证当前线程是否为守护线程,返回 true 则为守护线程
daemonThread.isDaemon();
(1) thread.setDaemon(true) 必须在 thread.start() 之前设置,否则会抛出出一个 IllegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在 Daemon 线程中产生的新线程也是 Daemon 的。
(3) 不要认为所有的应用都可以分配给 Daemon 来进行服务,比如读写操作或者计算逻辑。
线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的 生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程 依旧是活跃的。
线程同步(synchronized)
java 允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
冲突示例
public class SynchronizedTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread).start();
new Thread(myThread).start();
new Thread(myThread).start();
}
}
class MyThread implements Runnable {
private int ticket = 5; //一共有 5 张票
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (ticket>0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票:ticket = "+ticket--);
}
}
}
}
/**
* 运行结果:
* 卖票:ticket = 5
* 卖票:ticket = 4
* 卖票:ticket = 5
* 卖票:ticket = 3
* 卖票:ticket = 3
* 卖票:ticket = 2
* 卖票:ticket = -1
* 卖票:ticket = 0
* 卖票:ticket = 1
*/
从程序的运行结果中可以发现,程序中加入了延迟操作,所以在运行的最后出现了负数的情况,那么为什么现在会产生这样的问题呢?
从上面的操作代码中可以发现对于票数的操作步骤如下:
(1)判断票数是否大于 0,大于 0 则表示还有票可以卖
(2)如果票数大于 0,则将票卖出。
但是,在上面的操作代码中,在步骤(1)和步骤(2)之间加入了延迟操作,那么一个线程就有可能在还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样一来就会出现票数为负的情况
如果想要解决这样的问题,就必须使用线程同步,所谓同步就是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成之后才可以继续执行
即用 synchronized
关键字修饰的方法。由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态
synchronized
关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
synchronized
关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果 再细的分类,synchronized
可作用于 instance
变量、object reference
(对象引用)、static
函数和 class literals
(类名称字面常量) 身上。
无论 synchronized
关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
每个对象只有一个锁(lock)与之相关联。
实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
同步方法
synchronized
方法是 Java 中提供的一种线程同步机制,它可以保证多个线程在并发访问对象时,能够正确地访问共享变量,避免出现数据不一致的情况。
对于实例方法,如果一个方法被声明为 synchronized
,则当一个线程进入该方法时,它会自动地获取当前对象(this)的锁,并阻塞其他线程进入该方法,直到该线程退出该方法并释放锁为止。因此,当多个线程同时访问同一个对象的 synchronized
实例方法时,只有一个线程可以执行方法,其他线程需要等待锁的释放后才能继续执行。
对于静态方法,synchronized
关键字作用于整个类 ( Class 锁),只有一个线程可以访问该类任意一个 synchronized
静态方法。这意味着,在同一时刻,只有一个线程可以执行该类的 synchronized
静态方法,其他线程需要等待锁的释放后才能继续执行。
需要注意的是,synchronized
方法必须是可重入的。可重入意味着,一个线程在持有一个锁后,可以再次获取该锁而不会被阻塞。这种情况往往会在递归调用或者子类调用父类的 synchronized
方法时发生。
public class SynchronizedDemo {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
在该示例中,increment()
、decrement()
和 getCount()
方法都被声明为 synchronized
方法,因此在并发访问时能够保证操作的原子性。如果没有 synchronized
关键字进行修饰,那么多个线程同时访问 count 变量时可能出现数据不一致的情况。
需要注意的是,由于 synchronized
方法会阻塞其他线程的执行,因此尽量避免在 synchronized
方法中进行耗时的计算或者 I/O 操作。为了避免影响程序性能,可以考虑使用 synchronized
块来代替 synchronized
方法,在访问共享变量时只锁定必要的部分,从而减小锁的粒度,提高程序的并发性能。
静态 synchronized 方法 示例
public class SynchronizedStaticDemo {
private static int count = 0;
public static synchronized void increaseCount() {
count++;
}
public static int getCount() {
return count;
}
}
在该示例中,increaseCount() 方法被声明为静态 synchronized 方法,因此在并发访问时能够保证对 count 变量的操作的原子性。如果没有 synchronized 关键字进行修饰,那么多个线程同时访问 count 变量时可能出现数据不一致的情况。
需要注意的是,由于静态 synchronized 方法是作用于整个类的,因此在使用时需要考虑到其影响范围,避免因为过度使用导致锁的粒度过大,从而降低程序的并发性能。
同步代码快
同步代码块是 Java 中提供的一种线程同步机制,它可以保证多个线程在并发访问对象时,能够正确地访问共享变量,避免出现数据不一致的情况。
同步代码块的语法格式如下:
synchronized (object) {
//需要同步的代码块
}
其中,object 可以是任意一个对象(包括 this 对象),该对象称为锁对象。当一个线程进入同步代码块时,它会自动地获取锁对象,并阻塞其他线程进入该代码块,直到该线程退出代码块并释放锁为止。因此,当多个线程同时访问同一个对象的同步代码块时,只有一个线程可以执行代码块,其他线程需要等待锁的释放后才能继续执行。
需要注意的是,同步代码块必须是可重入的。可重入意味着,在持有一个锁的情况下,可以再次获取该锁而不会被阻塞。这种情况往往会在递归调用或者子类调用父类的同步代码块时发生。
下面是一个简单的示例,其中使用同步代码块实现了对共享变量 count 的线程同步:
public class SynchronizedBlockDemo {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public void decrement() {
synchronized (lock) {
count--;
}
}
public synchronized int getCount() {
return count;
}
}
在该示例中,increment()
、decrement()
方法使用同步代码块实现了对共享变量 count
的操作的线程同步。在同步代码块中,使用了 lock
对象作为锁对象,保证了多个线程并发访问时对共享变量 count
的操作的原子性。
与 synchronized
方法相比,同步代码块可以锁定任意一个对象,从而减小锁的粒度,避免因为过度使用导致锁的粒度过大,从而降低程序的并发性能。
synchronized 关键字总结
- 如果修饰的是具体对象:锁的是对象
- 如果修饰的是成员方法:那锁的就是 this
- 如果修饰的是静态方法:锁的就是这个对象.class
- 类锁和对象锁是不会冲突
- 当⽅法 (代码块) 执⾏完毕后会⾃动释放锁,不需要做任何的操作\
- 当⼀个线程执⾏的代码出现异常时,其所持有的锁会⾃动释放,不会由于异常导致出现死锁现象
volatile 关键字
来源:https://www.cnblogs.com/zhengbin/p/5654805.html
Java 内存模型中的可见性、原子性和有序性
可见性:
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的 也就是一个线程修改的结果。另一个线程马上就能看到。比如:用 volatile
修饰的变量,就会具有可见性。volatile
修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile
只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;
之后有一个操作 a++;
这个变量 a
具有可见性,但是 a++
依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
在 Java 中 volatile、synchronized 和 final 实现可见性。
原子性:
原子是世界上的最小单位,具有不可分割性。比如 a=0;
(a 非 long 和 double 类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++;
这个操作实际是 a = a + 1;
是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术 (sychronized)
来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java 的 concurrent
包下提供了一些原子类,我们可以通过阅读 API 来了解这些原子类的用法。比如:AtomicInteger
、AtomicLong
、AtomicReference
等。
在 Java 中 synchronized
和在 lock
、unlock
中操作保证原子性。
有序性:
Java 语言提供了 volatile
和 synchronized
两个关键字来保证线程之间操作的有序性,volatile
是因为其本身包含“禁止指令重排序”的语义,synchronized
是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
当一个变量定义为 volatile
之后,将具备两种特性:
-
保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,
volatile
保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存 (详见:Java 内存模型) 来完成。 -
禁止指令重排序优化。有
volatile
修饰的变量,赋值后多执行了一个load addl $0x0, (%esp)
操作,这个操作相当于一个内存屏障 (指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个 CPU 访问内存时,并不需要内存屏障;
什么是指令重排序:是指 CPU 采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
锁(Lock)
来源: https://www.cnblogs.com/lifegoeson/p/13683785.html
java 的锁分为两类:
第一类是 synchronized 同步关键字,这个关键字属于隐式的锁,是 jvm 层面实现,使用的时候看不见
第二类是在 jdk5 后增加的 Lock 接口以及对应的各种实现类,这属于显式的锁,就是我们能在代码层面看到锁这个对象,而这些个对象的方法实现,大都是直接依赖 CPU 指令的,无关 jvm 的实现。
锁的各种概念
按机制分
偏向锁->自旋锁->轻量级锁->重量级锁。按照这个顺序,锁的重量依次增加。
-
偏向锁:
在 Java 中,每个对象都会具有一个对象头(Object Header),对象头中存储了该对象的元信息,如对象所属类的信息、锁状态、GC 标记等等。在偏向锁的情况下,对象头中会再额外存储一个线程 ID,用来记录拥有该对象锁的线程标识。
当一个线程访问一个同步资源时,它会获得这个同步资源的偏向锁,并将对象头中的线程 ID 设置为自己的线程 ID。之后,当该线程再次访问同步资源时,就可以直接通过对象头中的线程 ID 判断是否持有该锁,而无需进行加锁和解锁操作。
这样,在无竞争情况下,偏向锁的获得操作和释放操作的代价可以忽略不计,从而提高了程序的性能。当有其他线程尝试获取同步资源时,持有偏向锁的线程需要进行锁撤销操作,并且会使得该对象回到无锁状态或者重新进入轻量级锁状态,以便其他线程可以获得该锁。
因此,在高竞争的情况下,使用偏向锁反而可能会降低程序性能。
-
轻量级锁:
与偏向锁类似,轻量级锁也是为了减少无竞争情况下对锁的使用成本。当一个线程访问一个同步资源时,如果该同步资源没有被其他线程占用,该线程会通过 CAS(Compare and Swap)操作尝试将对象头中的 Mark Word 替换为指向自己线程栈中的锁记录(Lock Record)。
如果 CAS 操作成功,该线程就获得了这个同步资源的轻量级锁,之后在释放锁的时候,它只需要通过 CAS 操作恢复对象头的 Mark Word 即可,无需像传统锁那样进行加锁和解锁操作。
当有其他线程尝试获取同步资源时,持有轻量级锁的线程需要撤销锁并切换为重量级锁。这种情况可能出现在两个线程同时竞争同一把锁的情况下,且此时对象头中的 Mark Word 指向的锁记录仍是持有轻量级锁的线程的栈中。在这种情况下,被阻塞的线程就无法直接通过 CAS 操作获得该同步资源的轻量级锁,而需要将该同步资源转换为重量级锁。
因此,在高竞争的情况下,轻量级锁的性能表现可能会不如传统锁,甚至比偏向锁还要差。
-
重量级锁:
与偏向锁和轻量级锁不同,重量级锁不是通过对象头中的 Mark Word 实现的,而是使用操作系统级别的互斥量来保证同步。当多个线程试图竞争同一把锁时,持有该锁的线程会将其转换为重量级锁,此时该同步资源就被加锁保护。
当其他线程尝试获取该同步资源的锁时,它们会被阻塞,并进入等待队列。在重量级锁下,线程的切换和调度是由操作系统负责的,因此处理器会切换到内核模式,这种切换需要消耗大量的系统资源和 CPU 时间片,因此重量级锁的性能较低,应该避免过度使用。
在某些特殊情况下,重量级锁可能会自适应的变成轻量级锁,也就是所谓的自适应自旋锁。这种情况下,如果线程在尝试获取同步资源的锁时发现持有该锁的线程已经释放锁了,或者锁的竞争情况比较少,它可以通过自旋等待方式来获取该锁,从而避免了线程切换和调度的开销。
如果重量级锁在某些特定情况下能够自适应的转化为轻量级锁,那么它的性能可能会得到一定的提升。
-
自旋锁:
自旋锁是一种基于“忙等待”(busy-wait)的锁机制,在多线程环境下用来保护共享资源的原子性访问。当一个线程尝试获取某个同步资源的锁时,如果该锁已经被其他线程持有,那么它会进入自旋状态,不断重复尝试获取锁的操作,直到获得锁为止。
在自旋锁的实现中,线程获取锁的方式通常是通过 CAS 操作(Compare and Swap)来实现的。CAS 操作可以比较并交换某个变量的值,只有在变量的当前值与预期值相同时才会进行交换。如果线程在尝试获取锁的过程中发现其他线程正在持有锁,它就会不断的进行 CAS 操作,直到成功获取锁或者达到一定的重试次数后放弃锁的获取。自旋锁的好处在于它可以减少线程上下文切换的次数,从而提高程序的执行效率。
当线程获取锁的时间非常短暂时,自旋锁的性能通常会比传统的锁机制更加优越。但是,如果锁被持有的时间过长或者竞争情况非常激烈时,自旋锁可能会浪费大量的 CPU 时间,造成系统的性能下降。由于自旋锁一直占用 CPU 资源,如果自旋时间过长,会导致 CPU 利用率过高,造成系统资源的浪费,因此在使用自旋锁的时候需要设置一个合理的自旋次数,在自旋次数达到上限后放弃自旋锁并切换为等待锁的方式等待。
按特性分
-
悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题 (比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。Java 中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
-
乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
乐观锁的使用场景主要是多读少写的情况,例如缓存系统、消息队列等。在这些场景下,由于并发读取的操作较多,因此采用乐观锁可以避免悲观锁的锁竞争和上下文切换的代价,并且能够提高程序的并发性和执行效率。
在 Java 中,乐观锁可以通过 AtomicInteger、AtomicLong、AtomicReference 等原子类来实现,或者手动实现版本号或时间戳等标识来保证数据的一致性。但是,乐观锁也存在 ABA 问题,需要通过引入版本号和时间戳等辅助信息来解决。
理论上来说:
-
悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如 LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
-
乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考
java.util.concurrent.atomic
包下面的原子变量类)。
按照锁的顺序分类
- 公平锁:它可以保证多个线程获取锁的顺序与它们发出请求的顺序一致。即先到先得,等待时间最长的线程优先获取锁资源,避免了线程饥饿现象的产生。
- 非公平锁:它不考虑多个线程获取锁的顺序,而是直接抢占锁资源。即先到不一定先得,有可能在等待队列中的线程可能会饱受饥饿之苦,而某些线程却可以一直占用锁资源。相对于公平锁而言,非公平锁的最大特点就是在并发环境下更加迅速和高效,因为一个线程在请求锁时可以直接抢占锁资源,而不需要排队等待其他线程持有的锁的释放,从而可以减少线程间的上下文切换,优化程序的性能。
按照使用方式分类
-
独占锁,也称为互斥锁,是一种只允许一个线程持有锁资源的锁,它可以保证在同一时刻只有一个线程可以访问共享资源。线程在获得独占锁之后,其他线程就无法再获得该锁,直到该线程释放锁资源。常见的独占锁有 ReentrantLock。
可重入锁:可重入锁是一种特殊的独占锁,它允许同一线程多次获取同一个锁,而不会被自己所阻塞。可重入锁也常被称为重入锁或者递归锁。在使用可重入锁时,若一个线程已经持有了该锁,那么它可以再次获取该锁,而不会被阻塞。当需要释放锁资源时,线程需要多次调用相应的解锁操作。这种机制的优点是能够避免死锁的发生,并提高代码的可靠性和简洁性。可重入锁在 Java 中的实现为
ReentrantLock
类。ReentrantLock
提供了lock()
和unlock()
等方法,线程在获取锁时需要通过lock()
方法获得锁资源,在释放锁资源时则需要调用unlock()
方法来释放锁资源。可重入锁是一种非常实用的锁机制,它允许同一线程多次获取同一个锁,从而能够有效地提高程序的并发性和性能,同时还能够避免死锁等问题的发生。 -
共享锁是一种允许多个线程同时持有锁资源的锁。当一个线程获取了共享锁之后,其他线程也可以继续获得该锁,并且并发地访问共享资源,但是它们只能进行读取操作,不能进行写入操作。共享锁适用于对共享资源进行读操作的场景,例如多个线程同时读取某个文件等。在 Java 中,ReadWriteLock 类提供了共享锁的实现。
是否可被中断
-
可中断锁:也称为可取消锁,是一种允许在等待锁资源期间,线程可以被其他线程打断从而提前结束等待的锁。当一个线程获取了可中断锁之后,若其他线程请求锁资源被阻塞,则该线程可以选择继续持有锁资源,也可以选择释放锁资源从而使得其他线程可以获取锁资源。这种机制能够避免死锁,并且提高程序的可靠性和可控性。在 Java 中,ReentrantLock 类提供了可中断锁的实现方式。
-
不可中断锁:也称为非可取消锁,是一种在等待锁资源期间,线程不能被中断并提前结束等待的锁。当一个线程获取了不可中断锁之后,其他线程请求锁资源被阻塞,只能等待该线程执行完相应操作释放锁资源。这种机制能够保证程序的正确性,并减少线程调度的开销。在 Java 中,synchronized 关键字就是一种不可中断锁。
可中断锁和不可中断锁都是重要的线程同步机制,它们根据线程是否可以被中断来进行区分。应该根据具体的应用场景来选择使用哪一种锁机制,从而能够提高程序的并发性和可靠性。
乐观锁
来源: https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html#如何实现乐观锁
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
版本号机制
版本号机制一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:假设数据库中帐户信息表中有一个 version 字段,当前值为 1;而当前帐户余额字段(balance)为 $100。
- 操作员 A 此时将其读出(version=1),并从其帐户余额中扣除 $50( $100-$50)。
- 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其帐户余额中扣除 $20( $100-$20)。
- 操作员 A 完成了修改工作,将数据版本号(version=1),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2。
- 操作员 B 完成了操作,也将版本号(version=1)试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1,数据库记录当前版本也为 2,不满足“提交版本必须等于当前版本才能执行更新“的乐观锁策略,因此,操作员 B 的提交被驳回。
这样就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能
CAS 算法
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
CAS 涉及到三个操作数:
- V:要更新的变量值 (Var)
- E:预期值 (Expected)
- N:拟写入的新值 (New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
举一个简单的例子:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
- i 与 1 进行比较,如果相等,则说明没被其他线程修改,可以被设置为 6。
- i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此,CAS 的具体实现和操作系统以及 CPU 都有关系。
乐观锁的问题
-
ABA 问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
-
循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
-
只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。
显式锁
来源: java 中的 Lock 锁
在 Java 中,java.util.concurrent.locks
包提供了一组用于线程同步的高级锁机制。这个包中的类和接口提供了比传统的 synchronized
关键字更灵活、可扩展和功能强大的锁实现。
Lock
和 ReadWriteLock
是两大锁的根接口,Lock
代表实现类是 ReentrantLock
(可重入锁),ReadWriteLock
(读写锁)的代表实现类是 ReentrantReadWriteLock
。
synchronized
与这些显式锁的区别
-
synchronized
是关键字,是JVM
层面的,而这些显式锁,是JDK
提供的API
-
synchronized
只支持独占锁,即同一时间只允许一个线程获取锁,其他线程需要等待。而显式锁可以根据需求使用独占锁或共享锁,允许多个线程同时获取读锁,但只有一个线程能够获取写锁(ReentrantReadWriteLock)。 - 可以通过 Lock 得知线程有没有成功获取到锁 (例如:ReentrantLock) ,但这个是 synchronized 无法办到的。
-
synchronized
是不可中断锁和非公平锁,ReentrantLock
可以进行中断操作并别可以控制是否是公平锁。 - synchronized 能锁住方法和代码块,而
Lock
只能锁住代码块。 -
synchronized
无法判断锁的状态,而Lock
可以知道线程有没有拿到锁。 - 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时显式锁的性能要远远优于
synchronized
。
Lock
Lock
接口是 Java 中提供的用于线程同步的锁机制。它定义了一组方法,用于获取和释放锁,并提供了比传统的 synchronized 关键字更灵活和可控的线程同步方式。
以下是 Lock
接口中一些常用的方法:
-
void lock()
: 获取锁。如果锁已经被其他线程持有,则当前线程会被阻塞,直到获取到锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在try catch
块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。private static Lock lock = new ReentrantLock(); public static void main(String[] args) { lock.lock(); try{ System.out.println("获取锁成功!!"); }catch(Exception e){ e.printStackTrace(); }finally{ System.out.println("释放锁成功"); lock.unlock(); } }
-
void unlock()
: 释放锁。 -
boolean tryLock()
: 尝试获取锁。如果锁可用,则立即获取并返回 true;否则立即返回 false,不会阻塞当前线程。也就说这个方法无论如何都会立即返回,在拿不到锁时也不会一直在那等待。private static Lock lock = new ReentrantLock(); public static void main(String[] args) { if(lock.tryLock()) { try{ System.out.println("成功获取锁!!"); }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } }else { System.out.println("未获取锁,先干别的"); } }
-
boolean tryLock(long time, TimeUnit unit)
: 在指定的时间内尝试获取锁。如果在给定的时间内成功获取锁,则返回 true;否则返回 false。private static Lock lock = new ReentrantLock(); public static void main(String[] args) { try{ if(lock.tryLock(5000, TimeUnit.MILLISECONDS)) { System.out.println("成功获取锁!!"); }else { System.out.println("未获取锁,先干别的"); } }catch(Exception e){ e.printStackTrace(); }finally{ lock.unlock(); } }
-
void lockInterruptibly() throws InterruptedException
: 当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就说,当两个线程同时通过lock.lockInterruptibly()
想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等待,那么对线程 B 调用threadB.interrupt()
方法能够中断线程 B 的等待过程。package ReentrantLockTest; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test { private Lock lock = new ReentrantLock(); public static void main(String[] args) { Test test = new Test(); MyThread a = new MyThread(test); MyThread b = new MyThread(test); a.start(); b.start(); b.interrupt(); } public void insert(Thread thread) throws InterruptedException { // 注意:如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将 InterruptedException 抛出 lock.lockInterruptibly(); try { System.out.println(thread.getName() + "得到了锁"); Thread.sleep(3000); } finally { lock.unlock(); System.out.println(thread.getName() + "释放了锁"); } } static class MyThread extends Thread { private Test test; public MyThread(Test test) { this.test = test; } @Override public void run() { try { test.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "被中断"); } } } }
当一个线程获取了锁之后,是不会被
interrupt()
方法中断的。因为单独调用interrupt()
方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()
方法获取某个锁时,如果不能获取到,只有进行等待的情况下,才可以响应中断。而用synchronized
修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只能一直等待下去。 -
Condition newCondition()
: 创建一个与锁关联的条件对象,用于实现等待/通知模式的线程间通信。
Condition
synchronized
关键字与 wait()
和 notify/notifyAll()
方法相结合可以实现等待/通知机制,ReentrantLock
类也可以借助于 Condition
接口与 newCondition()
方法。
synchronized
关键字就相当于整个 Lock
对象中只有一个 Condition
实例,所有的线程都注册在该一个实例上。如果执行 notifyAll()
方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题,而 Condition
可以实现多路通知功能也就是在一个 Lock
对象中可以创建多个 Condition
实例(即对象监视器),线程对象可以注册在指定的 Condition
中,Condition
实例的 signalAll()
方法 只会唤醒注册在该 Condition
实例中的所有等待线程。从而可以有选择性的进行线程通知,在调度线程上更加灵活。
Condition
接口方法
-
await()
:使当前线程进入等待状态,并释放持有的锁。当满足某个条件时,线程将被唤醒,并尝试重新获取锁,然后继续执行。 -
awaitUninterruptibly()
:与await()
方法类似,但是不响应中断。即使当前线程被中断,也会继续等待条件满足。 -
signal()
:唤醒一个等待的线程,该线程可以尝试重新获取锁并继续执行。注意,只有当前线程持有相关的锁时,才能调用此方法。 -
signalAll()
:唤醒所有等待的线程,它们将尝试重新获取锁并继续执行。同样,只有当前线程持有相关锁时才能调用此方法。 -
awaitNanos(long nanosTimeout)
:使当前线程进入等待状态,最多等待指定的纳秒数。在指定时间内,线程可以通过signal()
或signalAll()
方法被唤醒,或者等待超时返回。 -
awaitUntil(Date deadline)
:使当前线程进入等待状态,直到指定的时间。在指定时间前,线程可以通过signal()
或signalAll()
方法被唤醒,或者等待超时返回。
单个 Condition
实例
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("准备调用 condition.await() 方法,将该线程阻塞");
condition.await();
System.out.println("已被 signal() 方法唤醒");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
lock.lock();
try {
System.out.println("三秒后调用 condition.signal() 方法");
Thread.sleep(3000);
condition.signal();
System.out.println("已调用 condition.signal() 方法,去唤醒其他线程");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
当调用 Condition.await()
方法时,当前线程会被阻塞,并且当前线程持有的锁会被释放。这允许其他线程获取该锁并执行与之关联的临界区代码。
需要注意的是:在调用
Condition
对象的signalAll()
方法之前,必须先获取与该Condition
对象关联的锁。例如,使用ReentrantLock
作为锁对象时,需要先调用lock()
方法获取锁,然后才能调用signalAll()
方法。
在 await()
方法被调用后,线程会进入等待状态,直到以下情况之一发生:
- 其他线程调用了与
Condition
对象关联的signal()
或signalAll()
方法,通知等待的线程可以继续执行。 - 其他线程中断了当前线程,即调用了当前线程的
interrupt()
方法,此时InterruptedException
会被抛出,可以通过捕获异常来处理中断情况。 - 虚假唤醒(spurious wake-up):在某些情况下,等待的线程可能会在没有明确唤醒信号的情况下被唤醒。为了避免虚假唤醒,应使用循环检查等待条件。
多个 Condition
实例
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程一阻塞");
conditionA.await();
System.out.println("线程一 已被唤醒");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "线程一").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程二阻塞");
conditionB.await();
System.out.println("线程二 已被唤醒");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "线程二").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程三阻塞");
conditionA.await();
System.out.println("线程三 已被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "线程三").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("三秒后唤醒所有 conditionA 的线程");
Thread.sleep(3000);
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "线程四").start();
}
一个 Lock
对象中可以创建多个 Condition
实例,调用某个实例的 signalAll()
方法 只会唤醒注册在该 Condition
实例中的所有等待线程
ReadWriteLock
ReadWriteLock
接口是 Java
并发包中的一部分,用于实现读写锁。读写锁允许多个线程同时读取共享数据,但只允许一个线程进行写操作。相比于独占锁(如 ReentrantLock
),读写锁可以提供更高的并发性。
读写锁维护了两个锁,一个是读操作相关的锁也称为共享锁,一个是写操作相关的锁也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
主要实现类是 java.util.concurrent.locks.ReentrantReadWriteLock
在 ReentrantReadWriteLock
内有两个静态内部类,读锁(ReadLock
)和写锁(WriteLock
)。读锁可以由多个线程同时获取,只有在没有线程持有写锁时才能获取写锁。
ReadWriteLock
接口主要有以下方法:
-
readLock()
:返回一个ReadLock
对象,用于获取读锁。 -
writeLock()
:返回一个WriteLock
对象,用于获取写锁。
ReadLock
类定义了读锁的操作,有以下方法:
-
lock()
:获取读锁。如果当前有线程持有写锁或有写锁等待,则当前线程将被阻塞。 -
unlock()
:释放读锁。
WriteLock
定义了写锁的操作,包括以下方法:
-
lock()
:获取写锁。如果当前有任何线程持有读锁或写锁,则当前线程将被阻塞。 -
unlock()
:释放写锁。 -
tryLock()
:尝试获取写锁,如果获取成功返回true
,否则返回false
。不会阻塞当前线程。 -
tryLock(long time, TimeUnit unit)
:在指定的时间内尝试获取写锁,如果获取成功返回true
,否则返回false
。若超过指定时间仍未获取到写锁,则返回false
。
ReentrantReadWriteLock 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显示的指定
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
new Thread(() -> {
lock.readLock().lock();
try {
System.out.println("线程一获取读锁");
for (int i = 0; i < 5; i++) {
System.out.println("线程一 正在进行读操作");
Thread.sleep(1000);
}
System.out.println("线程一 读操作完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.readLock().unlock();
}
}, "线程一").start();
new Thread(() -> {
lock.readLock().lock();
try {
System.out.println("线程二获取读锁");
for (int i = 0; i < 5; i++) {
System.out.println("线程二 正在进行读操作");
Thread.sleep(1000);
}
System.out.println("线程二 读操作完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.readLock().unlock();
}
}, "线程二").start();
new Thread(() -> {
lock.writeLock().lock();
try {
System.out.println("线程三获取写锁");
for (int i = 0; i < 5; i++) {
System.out.println("线程三 正在进行写操作");
Thread.sleep(1000);
}
System.out.println("线程三 写操作完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.writeLock().unlock();
}
}, "线程三").start();
}
AQS
参考: https://developer.aliyun.com/article/779674#slide-1
参考: https://javaguide.cn/java/concurrent/aqs.html#aqs-核心思想
AQS
全称 AbstractQueuedSynchronizer
是一个用来构建锁和同步器的框架(抽象队列同步器),也在 java.util.concurrent.locks
包下 使用 AQS
能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
,其他的诸如 ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于 AQS
的。当然,我们自己也能利用 AQS
非常轻松容易地构造出符合我们自己需求的同步器。
同步器(Synchronizer
)是在多线程编程中用于协调和控制线程之间执行顺序的一种机制。它提供了一个可靠的方式来确保线程按照预期的顺序进行操作,以避免并发访问共享资源时可能出现的问题。
同步器能够实现线程之间的互斥(Mutual Exclusion
)和线程间的通信(Inter-Thread Communication
),从而实现并发编程中的线程同步。它主要用于解决以下两个问题:
-
互斥:同步器可以确保在同一时间只有一个线程可以访问共享资源,以避免资源竞争和数据不一致问题。通过同步器提供的锁机制,线程可以按照一定的规则来获取和释放资源,从而实现互斥访问。
-
线程间通信:同步器还可以提供一种线程间通信的机制,使得线程可以按照特定的顺序进行等待、唤醒和通知。这样可以实现线程之间的协调和合作,以完成特定的任务或满足特定的条件。
AQS 通过内部状态变量和等待队列来管理线程的获取和释放资源,并提供了一组核心方法供子类实现。
AQS 的实现主要依赖以下两个关键组件:
-
状态变量(state):AQS 使用一个整型变量来表示同步器的状态。状态变量可以被子类用来表示锁的持有情况或其他自定义的状态。开发者在继承 AQS 时可以根据自己的需求来定义和更新状态变量,从而实现不同类型的同步机制。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改
// 共享变量,使用 volatile 修饰保证线程可见性 private volatile int state;
状态信息通过 procted 类型的 getState,setState,compareAndSetState 进行操作
//返回同步状态的当前值 protected final int getState() { return state; } // 设置同步状态的值 protected final void setState(int newState) { state = newState; } //原子地 (CAS 操作) 将同步状态值设置为给定值 update 如果当前同步状态的值等于 expect(期望值) protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
-
state
使用volatile
修饰,保证多线程间的可见性。 -
getState()
、setState()
、compareAndSetState()
使用final
修饰,限制子类不能对其重写。 -
compareAndSetState()
采用乐观锁思想的 CAS 算法,保证原子性操作。
以
ReentrantLock
为例,state
初始值为 0,表示未锁定状态。A 线程lock()
时,会调用tryAcquire()
独占该锁并将state+1
。此后,其他线程再tryAcquire()
时就会失败,直到 A 线程unlock()
到state=0
(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证state
是能回到零态的。再以
CountDownLatch
以例,任务分为 N 个子线程去执行,state
也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后countDown()
一次,state
会CAS
减 1。等到所有子线程都执行完后 (即state=0
),会unpark()
主调用线程,然后主调用线程就会从await()
函数返回,继续后余动作。 -
-
等待队列:AQS 使用一个 FIFO(先进先出)虚拟的双向队列 (虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系) 作为等待队列,用于存储等待获取同步资源的线程。当一个线程无法获取到资源时,它会先判断当前线程是否需要被阻塞,之后会被加入等待队列并阻塞。等待队列的管理和线程的调度是实现同步的关键。其内部由
head
和tail
分别记录头结点和尾结点,队列的元素类型是Node
//头结点 private transient volatile Node head; //尾节点 private transient volatile Node tail; //内部类,构建链表的 Node 节点 static final class Node { //共享模式下的等待标记 static final Node SHARED = new Node(); //独占模式下的等待标记 static final Node EXCLUSIVE = null; //表示当前节点的线程因为超时或者中断被取消 static final int CANCELLED = 1; //表示当前节点的后续节点的线程需要运行,也就是通过 unpark 操作 static final int SIGNAL = -1; //表示当前节点在 condition 队列中 static final int CONDITION = -2; //共享模式下起作用,表示后续的节点会传播唤醒的操作 static final int PROPAGATE = -3; //状态,包括上面的四种状态值,初始值为 0,一般是节点的初始状态 volatile int waitStatus; //上一个节点的引用 volatile Node prev; //下一个节点的引用 volatile Node next; //保存在当前节点的线程引用 volatile Thread thread; //condition 队列的后续节点 Node nextWaiter; }
在 AQS 的实现中,子类可以实现以下几个钩子方法:
-
boolean tryAcquire(int)
: 独占方式。尝试获取资源,成功则返回 true,失败则返回 false。如果该方法返回 false,则会将当前线程加入等待队列,并使其进入阻塞状态 -
boolean tryRelease(int)
:独占方式。尝试释放资源,成功则返回 true,失败则返回 false。当资源被释放时,AQS 会根据等待队列中的线程情况,选择唤醒一个或多个线程,让它们继续尝试获取资源。 -
int tryAcquireShared(int)
:共享方式。尝试获取资源。负数表示失败,表示当前线程需要进入等待队列。;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -
boolean tryReleaseShared(int)
:共享方式。尝试释放资源,成功则返回 true,失败则返回 false。当资源被释放时,AQS 会根据等待队列中的线程情况,选择唤醒一个或多个线程,让它们继续尝试获取资源。 -
boolean isHeldExclusively()
:该线程是否正在独占资源。只有用到 condition 才需要去实现它。
什么是钩子方法呢?钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。
AQS
定义两种资源共享方式:Exclusive
(独占,只有一个线程能执行,如 ReentrantLock
)和 Share
(共享,多个线程可同时执行,如 Semaphore/CountDownLatch
)。一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现 tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但 AQS
也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock
。
AQS
中线程的阻塞通常使用LockSupport.park()
来实现
LockSupport.park()
方法可以将当前线程挂起(阻塞)等待某个条件的发生。当调用 LockSupport.park()
方法时,线程会被阻塞,停止执行,并进入一个特定的等待状态。
待线程满足特定条件后,其他线程可以通过调用 LockSupport.unpark(thread)
方法来唤醒被挂起的线程。unpark(thread)
方法会解除线程的阻塞状态,使其可以继续执行。
相比于传统的 wait()
和 notify()
方法,LockSupport.park()
提供了更灵活和可控的线程挂起和唤醒机制。它没有依赖于特定对象的监视器,而是作用于线程本身,使得线程可以在任何时候被挂起和唤醒。
LockSupport.park()
方法 方法内部使用了 Unsafe.park()
方法来阻塞当前线程。Unsafe.park()
方法是专门用于线程阻塞和唤醒的底层方法。
UnSafe
转载: https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。
先来看下这张图,对 UnSafe 类总体功能:
如上图所示,Unsafe 提供的 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类
Unsafe 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例,当且仅当调用 getUnsafe 方法的类为引导类加载器所加载时才合法,否则抛出 SecurityException 异常。
那如若想使用这个类,该如何获取其实例?有如下两个可行方案。
其一,从 getUnsafe 方法的使用限制条件出发,通过 Java 命令行命令-Xbootclasspath/a 把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。
java -Xbootclasspath/a: ${path} // 其中 path 为调用 Unsafe 相关方法的类所在 jar 包路径
其二,通过反射获取单例对象 theUnsafe。
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
内存操作
这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。
//分配内存,相当于 C++的 malloc 函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有:getInt,getDouble,getLong,getChar 等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有:putInt,putDouble,putLong,putChar 等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的 byte 类型的值(当且仅当该内存地址为 allocateMemory 分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置 byte 类型的值(当且仅当该内存地址为 allocateMemory 分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);
通常,我们在 Java 中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法。
使用堆外内存的原因
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
- 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
CAS 相关
如下源代码释义所示,这部分主要为 CAS 相关操作的方法。
/**
* CAS
* @param o 包含要修改 field 的对象
* @param offset 对象中某 field 的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什么是 CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。
典型应用
CAS 在 java.util.concurrent.atomic
相关类、Java AQS
、CurrentHashMap
等实现上有非常广泛的应用。如下图所示,AtomicInteger
的实现中,静态字段 valueOffset
即为字段 value
的内存偏移地址,valueOffset
的值在 AtomicInteger
初始化时,在静态代码块中通过 Unsafe
的 objectFieldOffset
方法获取。在 AtomicInteger 中提供的线程安全方法中,通过字段 valueOffset
的值可以定位到 AtomicInteger
对象中 value
的内存地址,从而可以根据 CAS 实现对 value
字段的原子操作。
线程调度
这部分,包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
如上源码说明中,方法 park、unpark 即可实现线程的挂起与恢复,将一个线程进行挂起是通过 park 方法实现的,调用 park 方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark 可以终止一个挂起的线程,使其恢复正常。
典型应用
Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer
,就是通过调用 LockSupport.park()
和 LockSupport.unpark()
实现线程的阻塞和唤醒的,而 LockSupport
的 park
、unpark
方法实际是调用 Unsafe
的 park
、unpark
方式来实现。
Class 相关
此部分主要提供 Class 和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。
//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
//获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。当且仅当 ensureClassInitialized 方法不生效时返回 false。
public native boolean shouldBeInitialized(Class<?> c);
//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定义一个类,此方法会跳过 JVM 的所有安全检查,默认情况下,ClassLoader(类加载器)和 ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
典型应用
从 Java 8 开始,JDK 使用 invokedynamic 及 VM Anonymous Class 结合来实现 Java 语言层面上的 Lambda 表达式。
invokedynamic:invokedynamic 是 Java 7 为了实现在 JVM 上运行动态语言而引入的一条新的虚拟机指令,它可以实现在运行期动态解析出调用点限定符所引用的方法,然后再执行该方法,invokedynamic 指令的分派逻辑是由用户设定的引导方法决定。
VM Anonymous Class:可以看做是一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类,而后通过 Unsafe.defineAnonymousClass 方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何 ClassLoader 下面,只要当该类没有存在的实例对象、且没有强引用来引用该类的 Class 对象时,该类就会被 GC 回收。故而 VM Anonymous Class 相比于 Java 语言层面的匿名内部类无需通过 ClassClassLoader 进行类加载且更易回收。
在 Lambda 表达式实现中,通过 invokedynamic 指令调用引导方法生成调用点,在此过程中,会通过 ASM 动态生成字节码,而后利用 Unsafe 的 defineAnonymousClass 方法定义实现相应的函数式接口的匿名类,然后再实例化此匿名类,并返回与此匿名类中函数式方法的方法句柄关联的调用点;而后可以通过此调用点实现调用相应 Lambda 表达式定义逻辑的功能。
对象操作
此部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。
//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
public native long objectFieldOffset(Field f);
//获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar 等
public native Object getObject(Object o, long offset);
//给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar 等
public native void putObject(Object o, long offset, Object x);
//从对象的指定偏移量处获取变量的引用,使用 volatile 的加载语义
public native Object getObjectVolatile(Object o, long offset);
//存储变量的引用到对象的指定的偏移量处,使用 volatile 的存储语义
public native void putObjectVolatile(Object o, long offset, Object x);
//有序、延迟版本的 putObjectVolatile 方法,不保证值的改变被其他线程立即看到。只有在 field 被 volatile 修饰符修饰时有效
public native void putOrderedObject(Object o, long offset, Object x);
//绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
典型应用
常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
非常规的实例化方式:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
数组相关
这部分主要介绍与数据操作相关的 arrayBaseOffset 与 arrayIndexScale 这两个方法,两者配合起来使用,即可定位数组中每个元素在内存中的位置。
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
典型应用
这两个与数据操作相关的方法,在 java.util.concurrent.atomic
包下的 AtomicIntegerArray
(可以实现对 Integer
数组中每个元素的原子性操作)中有典型的应用,如下图 AtomicIntegerArray
源码所示,通过 Unsafe
的 arrayBaseOffset
、arrayIndexScale
分别获取数组首元素的偏移地址 base
及单个元素大小因子 scale
。
内存屏障
在 Java 8 中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
//内存屏障,禁止 load 操作重排序。屏障前的 load 操作不能被重排序到屏障后,屏障后的 load 操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止 store 操作重排序。屏障前的 store 操作不能被重排序到屏障后,屏障后的 store 操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止 load、store 操作重排序
public native void fullFence();
系统相关
这部分包含两个获取系统相关信息的方法。
//返回系统指针的大小。返回值为 4(32 位系统)或 8(64 位系统)。
public native int addressSize();
//内存页的大小,此值为 2 的幂次方。
public native int pageSize();
Varhandle
参考:https://www.xiehai.zone/jdk-features/jdk9/11-var-handle.html
参考:https://zhuanlan.zhihu.com/p/144741342
VarHandle
是新的原子访问属性规范,JDK8 以前都是通过 Unsafe
实现原子属性访问,从 JDK9 开始,会尽可能使用 VarHandle
代替 Unsafe
,除了 atomic
包下一些依赖问题没解决,很多 API 都使用 VarHandle
代替了。
VarHandle
提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的 API。VarHandle
可以与任何字段、数组元素或静态变量关联,支持在不同访问模型下对这些类型变量的访问,包括简单的 read/write
访问,volatile
类型的 read/write
访问,和 CAS(compare-and-swap)
等。
获取 VarHandle
-
MethodHandles.privateLookupIn(Class<?> class, Lookup lookup)
:用于在指定类的私有访问上下文中创建一个 Lookup 对象,使得可以访问私有的方法或字段。import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.lang.invoke.MethodHandles.Lookup; public class PrivateVarHandleExample { private String privateField = "Private Field"; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { PrivateVarHandleExample example = new PrivateVarHandleExample(); Lookup lookup = MethodHandles.privateLookupIn(PrivateVarHandleExample.class, MethodHandles.lookup()); VarHandle varHandle = lookup.findVarHandle(PrivateVarHandleExample.class, "privateField", String.class); String value = (String) varHandle.get(example); System.out.println("Private Field value: " + value); } }
-
MethodHandles.lookup()
:返回一个新的 Lookup 对象,通过该对象可以访问当前调用者的类中的方法或字段。import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; public class PublicVarHandleExample { public int publicField = 42; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { PublicVarHandleExample example = new PublicVarHandleExample(); VarHandle varHandle = MethodHandles.lookup().findVarHandle(PublicVarHandleExample.class, "publicField", int.class); int value = (int) varHandle.get(example); System.out.println("Public Field value: " + value); } }
-
Lookup.findVarHandle(Class<?> declaringClass, String name, Class<?>... parameterTypes)
:在指定的类中查找和返回一个VarHandle
对象,用于访问实例字段。import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; public class InstanceVarHandleExample { private String instanceField = "Instance Field"; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { InstanceVarHandleExample example = new InstanceVarHandleExample(); VarHandle varHandle = MethodHandles.lookup().findVarHandle(InstanceVarHandleExample.class, "instanceField", String.class); String value = (String) varHandle.get(example); System.out.println("Instance Field value: " + value); } }
-
Lookup.findStaticVarHandle(Class<?> declaringClass, String name, Class<?> fieldtype)
:在指定的类中查找和返回一个VarHandle
对象,用于访问静态字段。import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; public class StaticVarHandleExample { public static int staticField = 42; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { VarHandle varHandle = MethodHandles.lookup().findStaticVarHandle(StaticVarHandleExample.class, "staticField", int.class); int value = (int) varHandle.get(); System.out.println("Static Field value: " + value); } }
-
Lookup.unreflectVarHandle(Field field)
:返回一个 VarHandle 对象,用于访问反射中提供的字段。import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.lang.reflect.Field; public class ReflectVarHandleExample { private int reflectField = 42; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { ReflectVarHandleExample example = new ReflectVarHandleExample(); Field field = ReflectVarHandleExample.class.getDeclaredField("reflectField"); VarHandle varHandle = MethodHandles.lookup().unreflectVarHandle(field); int value = (int) varHandle.get(example); System.out.println("Reflected Field value: " + value); } }
-
MethodHandles.arrayElementVarHandle(Class<?> arrayClass)
:返回一个 VarHandle 对象,用于访问数组元素。import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; public class ArrayVarHandleExample { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { int[] array = {1, 2, 3, 4, 5}; VarHandle varHandle = MethodHandles.arrayElementVarHandle(int[].class); int value = (int) varHandle.get(array, 2); System.out.println("Array Element value: " + value); } }
方法
VarHandle 来使用 plain
、opaque
、release/acquire
和 volatile
四种共享内存的访问模式,根据这四种共享内存的访问模式又分为写入访问模式、读取访问模式、原子更新访问模式、数值更新访问模式、按位原子更新访问模式。
-
Plain(普通访问):Plain 是指最普通的访问方式,没有任何特殊的语义。它不保证内存可见性,也不保证执行顺序。这意味着对一个变量的普通访问可能不会看到其他线程所做的更新,并且对于指令重排,编译器和处理器可以自由地进行优化。
-
Opaque(不保证内存可见性,但保证执行顺序):Opaque 是一种保证执行顺序的访问方式,但不保证内存可见性。这意味着前面的读写操作不会被重排序到后面,但对其他线程所做的更新可能不会立即可见。因此,在使用 Opaque 访问方式时,需要使用其他手段(如锁或其他同步机制)来确保数据正确同步。
-
Acquire(保证执行顺序,但不保证前面的读写不能被重排序到后面):Acquire 是一种保证执行顺序的访问方式。在加载操作(load)上使用 getAcquire 可以保证后面的读写操作不会被重排序到前面。但并不保证前面的读写操作不能被重排序到后面。
-
Release(保证执行顺序,但不保证后面的读写不能被重排序到前面):Release 是一种保证执行顺序的访问方式。在存储操作(store)上使用 setRelease 可以保证前面的读写操作不会被重排序到后面。但并不保证后面的读写操作不能被重排序到前面。
-
Volatile(保证执行顺序,且保证变量不会被重排):Volatile 是一种保证执行顺序和内存可见性的访问方式。对于 volatile 变量的读写操作,保证了所有线程都能看到最新的值,并且禁止编译器和处理器对 volatile 变量的指令重排。
方法有这些
- get
- get
- getVolatile
- getAcquire
- getOpaque
- set
- set
- setVolatile
- setRelease
- setOpaque
- CAS(compare-and-swap)
- compareAndSet
- weakCompareAndSetPlain
- weakCompareAndSet
- weakCompareAndSetAcquire
- weakCompareAndSetRelease
- CAE(compare-and-exchange)
- compareAndExchange
- compareAndExchangeAcquire
- compareAndExchangeRelease
- GAU(get-and-update)
- getAndAdd
- getAndAddAcquire
- getAndAddRelease
- getAndBitwiseOr
- getAndBitwiseOrRelease
- getAndBitwiseOrAcquire
- getAndBitwiseAnd
- getAndBitwiseAndRelease
- getAndBitwiseAndAcquire
- getAndBitwiseXor
- getAndBitwiseXorRelease
- getAndBitwiseXorAcquire
VarHandle 除了支持各种访问模式下访问变量之外,还提供了一套内存屏障方法,目的是为了给内存排序提供更细粒度的控制。主要如下几个静态方法:
-
static void fullFence
:保证方法调用之前的所有读写操作不会被方法后的读写操作重排 -
static void acquireFence
:保证方法调用之前的所有读操作不会被方法后的读写操作重排 -
static void releaseFence
:保证方法调用之前的所有读写操作不会被方法后的写操作重排 -
static void loadLoadFence
:保证方法调用之前的所有读操作不会被方法后的读操作重排 -
static void storeStoreFence
:保证方法调用之前的所有写操不会被方法后的写操作重排
Atomic 原子类
Atomic
的包名为 java.util.concurrent.atomic
。这个包里面提供了一组原子变量的操作类,这些类可以保证在多线程环境下,当某个线程在执行 atomic
的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行
Atomic
成员分为四大块
- 原子方式更新基本类型
- 原子方式更新数组
- 原子方式更新引用
- 原子方式更新字段
基本类型
-
AtomicBoolean
:原子更新布尔类型 -
AtomicInteger
:原子更新整型 -
AtomicLong
:原子更新长整型
来看看 AtomicInteger
提供了哪些方法
构造方法:
-
AtomicInteger()
:创建一个初始值为 0 的 AtomicInteger 对象。 -
AtomicInteger(int initialValue)
:创建一个指定初始值的 AtomicInteger 对象。
值的获取和设置方法:
-
int get()
:获取当前值。 -
void set(int newValue)
:设置新值。
原子更新方法:
-
int getAndSet(int newValue)
:以原子方式设置新值,并返回旧值。 -
int getAndUpdate(IntUnaryOperator updateFunction)
:以原子方式对当前值进行更新操作,并返回更新前的旧值。 -
int updateAndGet(IntUnaryOperator updateFunction)
:以原子方式对当前值进行更新操作,并返回更新后的新值。 -
int getAndIncrement()
:以原子方式将当前值加 1,并返回加 1 前的旧值。 -
int getAndDecrement()
:以原子方式将当前值减 1,并返回减 1 前的旧值。 -
int incrementAndGet()
:以原子方式将当前值加 1,并返回加 1 后的新值。 -
int decrementAndGet()
:以原子方式将当前值减 1,并返回减 1 后的新值。 -
int getAndAdd(int delta)
:以原子方式将指定值添加到当前值,并返回添加前的旧值。 -
int addAndGet(int delta)
:以原子方式将指定值添加到当前值,并返回添加后的新值。
其他方法:
-
boolean compareAndSet(int expect, int update)
:如果当前值等于预期值,则以原子方式将该值设置为新值,返回是否成功设置。(CAS 操作) -
void lazySet(int newValue)
:最终将该值设置为新值,使用懒惰写入方法,也就是仅仅当 get 时才会 set。
使用
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounterExample {
private AtomicInteger counter = new AtomicInteger();
public void increment() {
counter.incrementAndGet();
}
public int getCount() {
return counter.get();
}
public static void main(String[] args) {
AtomicCounterExample example = new AtomicCounterExample();
// 创建多个线程并发地增加计数器值
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount()); // 输出最终的计数器值
}
}
其他几个类都类似,只是数据类型不同
AtomicInteger
底层用的是 volatile
的变量和 Unsafe
的 CAS
来进行更改数据的。
在 JKD9 AtomicInteger
增加了 VarHandle
效果的一些方法,例如 getAcquire
、getOpaque
、setRelease
、setOpaque
,可以选择共享内存的访问模式
数组
-
AtomicIntegerArray
:原子更新整型数组里的元素 -
AtomicLongArray
:原子更新长整型数组里的元素 -
AtomicReferenceArray
:原子更新引用类型数组里的元素
提供了原子性操作整型数组的功能。它可以用于在多线程环境下对整型数组进行原子性的更新和访问操作,从而避免了数据竞争和并发访问的问题。
常用方法
-
AtomicIntegerArray(int length)
:构造一个新的 AtomicIntegerArray 对象,指定数组的长度。 -
AtomicIntegerArray(int[] array)
:构造一个新的AtomicIntegerArray
对象,指定数组,AtomicIntegerArray
内部会拷贝一份数组 -
int length()
:获取数组的长度。 -
int get(int index)
:获取指定索引处的元素的值。 -
void set(int index, int newValue)
:将指定索引处的元素设置为新的值。 -
int getAndSet(int index, int newValue)
:将指定索引处的元素设置为新的值,并返回旧的值。 -
boolean compareAndSet(int index, int expect, int update)
:如果当前索引处的值等于期望值,则将其更新为新的值,并返回是否成功。 -
int getAndIncrement(int index)
:将指定索引处的元素增加 1,并返回旧的值。 -
int getAndDecrement(int index)
:将指定索引处的元素减少 1,并返回旧的值。 -
int getAndAdd(int index, int delta)
:将指定索引处的元素增加指定的增量值,并返回旧的值。 -
int incrementAndGet(int index)
:将指定索引处的元素增加 1,并返回新的值。 -
int decrementAndGet(int index)
:将指定索引处的元素减少 1,并返回新的值。 -
int addAndGet(int index, int delta)
:将指定索引处的元素增加指定的增量值,并返回新的值。
其他几个类都类似,只是数据类型不同
AtomicIntegerArray
底层使用了 volatile
修饰的 int
数组来实现。这个数组被声明为 private final
,确保了它的可见性和线程安全性。
在进行原子性操作时,AtomicIntegerArray
使用 Unsafe
类提供的底层方法来执行 CAS 操作,以确保对数组元素的原子读取、写入和更新。
引用类型
-
AtomicReference
: 原子更新引用类型。 -
AtomicStampedReference
: 原子更新引用类型,AtomicReference
的一个扩展,它内部使用 Pair 来存储元素值及其版本号。它除了具有原子更新引用类型的功能外,还可以解决 ABA 问题。 -
AtomicMarkableReferce
:AtomicReference
的另一个扩展,它用于原子更新带有标记位的引用类型。除了具备原子性操作引用类型的功能外,AtomicMarkableReference 还可以为引用对象附加一个布尔类型的标记位。该标记位可以用来表示某种状态,例如对象是否已被处理或对象是否可用等。
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象 set 进 Atomic 类,然后调用 compareAndSet
等一些方法去进行原子操作,原理都是基于 Unsafe
实现,但 AtomicReferenceFieldUpdater
略有不同,更新的字段必须用 volatile 修饰。
以 AtomicReference
为例,有这些方法
-
get()
:获取当前引用对象的值。 -
set(T newValue)
:设置引用对象的值为指定的 newValue。 -
getAndSet(T newValue)
:获取当前引用对象的值,并设置新值为指定的 newValue。 -
compareAndSet(T expect, T update)
:比较当前引用对象是否等于期望值 expect,如果是,则将引用对象更新为 update。 -
weakCompareAndSet(T expect, T update)
:弱版本的 compareAndSet 方法,提供了更轻量级的原子性操作,但不保证线程安全。 -
lazySet(T newValue)
:最终设置引用对象的值为指定的 newValue,可能会延迟到稍后的一些时间点,但不保证立即可见。
在 JKD9 增加了 VarHandle
效果的一些方法,例如 getAcquire
、getOpaque
、setRelease
、setOpaque
,可以选择共享内存的访问模式
AtomicReference
使用示例
// 会自动转换成包装类型
AtomicReference<Integer> atomicRef = new AtomicReference<>(0);
Integer oldValue = atomicRef.get(); // 0
Integer newValue = 10;
// 如果当前引用值等于期望值 0,则进行替换为新值 10
System.out.println(atomicRef.compareAndSet(oldValue, newValue)); // true
// 由于 整数缓存池,Integer 的 10 与 int 的 10 相等,所以会替换为新值 20
System.out.println(atomicRef.compareAndSet(10, 20)); // true
// new 了新的对象,所以与期望值不同,所以失败
System.out.println(atomicRef.compareAndSet(new Integer(20), 30));// false
System.out.println(atomicRef.get()); // 10
AtomicStampedReference
有版本号机制,所以他的方法都需要传版本号
-
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
:尝试以原子方式将引用和时间戳进行比较和设置。如果当前引用和时间戳与期望值匹配,则更新为新的引用和时间戳,并返回 true;否则,返回 false。 -
weakCompareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
:类似于compareAndSet
方法,但是使用弱比较和设置。 -
getReference()
:获取当前引用的值。 -
getStamp()
:获取当前时间戳的值。 -
set(V newReference, int newStamp)
:以原子方式设置新的引用和时间戳的值。 -
attemptStamp(V expectedReference, int newStamp)
:尝试以原子方式将引用的时间戳增加 1。前提是当前引用与期望值匹配,如果匹配成功,则更新时间戳并返回 true;否则,返回 false。 -
get(int[] stampHolder)
:获取当前引用和时间戳的值,并将时间戳存储在给定的整型数组中。 -
set(V newReference, int newStamp)
:以原子方式设置新的引用和时间戳的值。
使用示例
// 传入初始值与初始版本号,版本号为 int 类型
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(0, 0);
// 如果当前引用值等于期望值 0 且版本号等于旧版本号,则进行替换为新值 10,并更新版本号
atomicStampedRef.compareAndSet(0, 10, 0, 1); // true
atomicStampedRef.compareAndSet(0, 10, 0, 1); // false
AtomicMarkableReference
与 AtomicStampedReference
很相似,但使用的是一个布尔变量标记,这个标记是一个布尔值,可以用来表示某种状态、条件或其他信息。
不同的有这些方法
-
isMarked()
:是否被标记 -
attemptMark(V expectedReference, boolean newMark)
:如果当前引用等于预期值,则原子地尝试设置新的标记。 -
compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)
:如果当前引用和标记与预期值相等,则原子地将引用和标记设置为新的值。 -
weakCompareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)
:与 compareAndSet 方法类似,但是使用弱比较和设置,可能会在竞争条件下失败并返回 false。 -
void set(V newReference, boolean newMark)
:设置新引用与标记 -
getReference()
:获取当前引用。
字段
AtomicIntegerFieldUpdater
:原子更新整型字段的更新器AtomicLongFieldUpdater
:原子更新长整型字段的更新器AtomicStampedReference
:原子更新带有版本号的引用类型
AtomicIntegerFieldUpdater
为例
public class AtomicExample {
private static class MyClass {
// 原子更新整型字段,必须使用 volatile 修饰要更新的字段
private volatile int count;
}
public static void main(String[] args) {
// 创建 AtomicIntegerFieldUpdater 对象,指定要更新的字段类型和所属类
AtomicIntegerFieldUpdater<MyClass> updater = AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "count");
// 创建 MyClass 对象
MyClass obj = new MyClass();
// 使用原子更新器更新字段的值
updater.set(obj, 10); // 设置 count 字段的初始值为 10
// 当前值加 1,返回新值
int newValue = updater.incrementAndGet(obj);
System.out.println("New Value: " + newValue); // 输出:New Value: 11
// 比较并交换操作,如果当前值为 10,则将其更新为 20
boolean success = updater.compareAndSet(obj, 10, 20);
System.out.println("Update success: " + success); // 输出:Update success: true
// 获取更新后的字段值
int updatedValue = updater.get(obj);
System.out.println("Updated Value: " + updatedValue); // 输出:Updated Value: 20
}
}
集合
Java 提供了一些线程安全的集合
- Queue
-
ConcurrentLinkedQueue
:它是一个基于链表实现的线程安全的队列。它采用无锁算法,通过 CAS(Compare and Swap)操作来实现并发操作。ConcurrentLinkedQueue
提供了高效的并发插入(offer)和删除(poll)操作。 -
ArrayBlockingQueue
:ArrayBlockingQueue
继承自AbstractQueue
类,并且实现了BlockingQueue
接口。它使用可重入锁(ReentrantLock)来保证线程安全,并提供了阻塞的添加和移除元素操作,即当队列已满或者为空时,线程将被阻塞。 -
LinkedBlockingQueue
:LinkedBlockingQueue
是一个基于链表的可选有界/无界线程安全队列。如果在创建时不指定容量,则默认为无界队列。与 ConcurrentLinkedQueue 类似,它也采用链表数据结构,并且在并发环境中表现良好。它提供了阻塞的添加和移除元素操作,当队列为空或者已满时,线程将被阻塞。 -
PriorityBlockingQueue
:PriorityBlockingQueue
是一个基于优先级的无界线程安全队列。它可以根据元素的优先级自动排序。元素需要实现Comparable
接口或者在构造时指定一个Comparator
来定义优先级。它同样使用可重入锁来保证线程安全。 -
SynchronousQueue
:SynchronousQueue
是 Java 中的一个特殊的阻塞队列,用于在多个线程之间进行元素的传递。SynchronousQueue
没有存储元素的能力,它只充当了线程之间数据交换的通道。每个插入操作必须等待另一个线程的相应移除操作,反之亦然。换句话说,SynchronousQueue
中的每个插入操作都必须等待另一个线程的对应移除操作,否则插入操作将一直被阻塞。
-
- List
-
Vector
:Vector
是 Java 中最早提供的线程安全的List
实现类。它使用synchronized
关键字来保证线程安全,但这可能会导致性能下降,因为每次只允许一个线程访问该集合。 - Stack:
Stack
是一个基于向量的类,它继承自Vector
并提供了栈(后进先出)的行为。它同样使用synchronized
关键字来保证线程安全。 -
CopyOnWriteArrayList
:CopyOnWriteArrayList
是 Java 5 中引入的一种高效的并发容器。它通过创造一个新的副本来实现线程安全,即当对集合进行修改时,会创建一个底层数组的新副本,并在新副本上执行修改操作,而原始数据则保持不变。这种设计方式使得读操作无锁化,不会阻塞其他线程,适用于读操作频繁、写操作较少的场景。
-
- Set
-
ConcurrentSkipListSet
:ConcurrentSkipListSet
是 Java 中基于跳表数据结构实现的线程安全有序集合。它是一个有序的集合,同时支持高效的并发访问。在多线程环境下,它能够保持良好的性能,并且提供一致的排序结果。 -
CopyOnWriteArraySet
:CopyOnWriteArraySet
是 Java 中基于CopyOnWriteArrayList
实现的线程安全无序集合。它内部维护了一个 CopyOnWriteArrayList,通过创造一个新的副本来实现线程安全。与CopyOnWriteArrayList
类似,它适用于读操作频繁、写操作较少的场景,并且在读取数据时不会阻塞其他线程。
-
- Map
-
ConcurrentHashMap``:ConcurrentHashMap
是 Java 中最常用的线程安全的哈希表实现类。它采用了分段锁(Segment)的机制,将整个数据结构分割成多个部分,每个部分都有一个独立的锁。这使得多个线程可以同时并发地进行读操作,而写操作仍然需要获得对应部分的锁。相较于传统的同步Map
,ConcurrentHashMap
在并发访问时性能更好。 -
Hashtable``:Hashtable
是 Java 中最早提供的线程安全的哈希表实现类。它使用synchronized
关键字来保证线程安全,但这可能会导致性能下降,因为每次只允许一个线程访问该集合。虽然在 Java 1.2 之后引入了ConcurrentHashMap
,但Hashtable
仍然被保留用于向后兼容。文章来源:https://www.toymoban.com/news/detail-696411.html -
ConcurrentSkipListMap
:ConcurrentSkipListMap
是 Java 中基于跳表数据结构实现的线程安全有序映射。它是一个有序的 Map,可以按照键的顺序进行遍历。与ConcurrentHashMap
类似,它也使用分段锁的机制来实现并发访问。文章来源地址https://www.toymoban.com/news/detail-696411.html
-
到了这里,关于Java 并发(多线程)超详细的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!