1. 简单代码示例
1.1 Future和CompletableFuture的简单用法
FutureTask简单用法
package completablefuture;
import java.util.concurrent.*;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/11 23:00
**/
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/** 一个简单的异步任务有返回值的demo **/
FutureTask<String> futureTask = new FutureTask<>(new MyTask());
Thread thread = new Thread(futureTask);
thread.start();
// 获取线程任务执行得到的返回值
System.err.println(futureTask.get());
/*************************************************************************************************************************/
/** 一个简单的线程池提交任务的demo */
// 顺便温习一下线程池的七大核心参数。
/**
* 核心线程数(线程池刚初始化,便会新建好的线程数量,并且只要线程池不销毁,这些线程就不会销毁)
* 最大线程数(当工作队列放不下任务时,会将多余的任务新起一个线程去处理)
* 线程空闲存活时间(非核心线程在多长时间不处理任务,将被销毁)
* 线程空闲存活时间单位
* 工作队列(任务放入线程池,会先入工作队列)
* 拒绝策略(当工作队列满了,并且线程数量已达到最大,再来任务,就会触发拒绝策略)
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(12,20,60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(4096),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
FutureTask<String> futureTask2 = new FutureTask<>(() -> {
System.err.println("使用线程池执行异步任务");
return "线程池线程异步任务执行完成";
});
threadPoolExecutor.submit(futureTask2);
// 获取线程任务执行得到的返回值
System.err.println(futureTask2.get());
threadPoolExecutor.shutdown();
}
}
class MyTask implements Callable<String> {
@Override
public String call() throws Exception {
System.err.println("线程任务正在执行中...");
return "线程任务执行完成";
}
}
CompletableFuture的简单用法
package completablefuture;
import java.util.Random;
import java.util.concurrent.*;
import java.util.function.Supplier;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/11 22:02
**/
public class CompletableFutureDemo {
public static void main(String[] args) {
// new一个固定线程数量线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
try{
/** 无返回值的completableFuture的demo */
CompletableFuture.runAsync(() -> {
System.err.println(Thread.currentThread().getName());
},executorService);
/** 有返回值的completablefuture简单demo(这里可以直接使用get,功能和Future一样) */
CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(new MyTask1(), executorService);
System.out.println(stringCompletableFuture.get());
/** 使用链式调用,下一步的任务要使用到上一步任务的结果 */
CompletableFuture.supplyAsync(() -> {
System.err.println("whenCompleteAsync");
return new Random().nextInt();
}, executorService).whenCompleteAsync((result, exception) -> {
System.err.println("上一步执行的结果为:"+result);
},executorService).exceptionally(e -> {
System.err.println("执行报错了");
e.printStackTrace();
return null;
});
}catch (Throwable e){
e.printStackTrace();
} finally{
executorService.shutdown();
}
}
}
// 一般来说,我们都会使用lamda表达式,因为Supplier和Runnable都是函数式接口
class MyTask1 implements Supplier<String> {
@Override
public String get() {
System.err.println("the thead is running. thead name is 【"+Thread.currentThread().getName()+"】");
return "线程任务执行完成";
}
}
以下是CompletableFuture比较常用的几个方法:
常用方法基本都分两种,一种是带Async,一种不带。带Async的是把任务提交到ForkJoinPool(公共线程池)或者自定义的线程池。不带Async的是直接使用上一个任务的线程,线程可能是主线程或 ForkJoinPool 线程
runAsync(Runnable runnable):无返回的异步方法,和Runnable功能类似
supplyAsync(Supplier<U> supplier,Executor executor):Supplier是一个函数式接口,U是返回类型(泛型)。他返回的是一个CompletableFuture对象,通过get或者join可以获得返回值
thenApplyAsync(Function<? super T,? extends U> fn, Executor executor):拿到上一步的返回结果,然后执行目前的任务,示例代码如下图
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn):和thenApplyAsync方法的差不多,只不过这个方法的接收参数是CompletableFuture,另一个则是普通值
thenAcceptAsync(Consumer<? super T> action):消费型方法,可以接收之前方法的返回值,进行处理,此方法没有返回值,不会对返回结果有什么影响
CompletableFuture<Void> allOf(CompletableFuture<?>... cfs):可以放入多个CompletableFuture任务,等待所有任务完成
1.2 synchronized简单案例
synchronized锁的可以是对象,方法,或者实例。只有两个线程都竞争同一把锁时(这也就意味着,两个线程访问的是同一个东西),锁才能生效
1.2.1 作用于普通方法上,锁的是当前对象(当前对象实例this)
如图所示,这里要是没加锁,显然会先打印sendMsg里面的内容
结论:这种情况,你只要用的是同一个实例,并且调用的方法,都是普通的方法+锁,都用的是同一把锁
普通方法上锁,等价于
synchronized (this) {
System.err.println("同步代码块");
}
1.2.2 作用于静态方法上,锁的是字节码
如图所示
结论:这种情况,类的所有静态synchronized方法都是用的同一把锁(只有这种情况才会竞争。其他情况不会,比如,你一个调用静态synchronized方法,一个调用普通synchronized方法,二者用的不是同一把锁,互不影响)
静态方法上锁,等价于
synchronized (Phone.class) {
System.err.println("同步代码块");
}
1.2.3 synchronized特性
可重入锁(每重入一次,计数器会+1,同步方法执行完,计数器-1)
会自动释放锁(方法执行完自动释放,无论正常执行还是出现异常)
非公平锁
synchronized和Lock的区别
1、synchronized是java内置的关键字,在jvm层面,Lock是java类
2、synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
3、synchronized会自动释放锁(代码执行完或者出现异常都会自动释放),Lock需要手动释放锁(一般是在finally中调用Lock.unlock())
4、synchronized无法主动中断正在等待获取锁的线程,Lock锁可以主动中断,让正在等待获取锁的线程停止等待,抛出异常
5、synchronized可重入、不可中断、非公平。Lock可重入、可中断、可公平,可非公平。
ReentrantLock中断正在等待获取锁线程的案例
package lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/22 17:07
**/
public class LockDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 尝试获取锁...");
lock.lockInterruptibly(); // 可中断获取锁
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁,执行任务...");
Thread.sleep(5000); // 模拟长时间任务
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 等待锁的过程中被中断!");
}
}, "线程A");
Thread t2 = new Thread(() -> {
lock.lock(); // 先获取锁,阻塞线程A
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁,执行任务...");
Thread.sleep(10000); // 持有锁一段时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}, "线程B");
t2.start();
try { Thread.sleep(100); } catch (InterruptedException e) {} // 确保 t2 先获取锁
t1.start();
try {
Thread.sleep(2000); // 等待一会儿,确保 t1 进入等待状态
System.out.println("尝试中断线程A...");
t1.interrupt(); // 中断线程A
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1.2.4 死锁案例
jstack(配合jps)和jconsole可以检查死锁
package sync;
public class SynchronizedDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
System.err.println(Thread.currentThread().getName());
Phone.senEmail(phone);
},"a线程").start();
new Thread(() -> {
System.err.println(Thread.currentThread().getName());
phone.sendMsg(phone);
},"b线程").start();
}
}
class Phone{
public synchronized void sendMsg(Phone phone){
System.err.println("发送了一条消息");
Phone.senEmail(phone);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static synchronized void senEmail(Phone phone){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
phone.sendMsg(phone);
System.err.println("发送了一封邮件");
}
}
1.3 中断线程的方法(简单案例)
1.3.1 使用volatile中断线程
volatile可以使变量在不同的线程之间具有可见性。还能禁止指令重排(这一点特性在后面再用案例说明)。但是它不能保证原子性,举个例子,一个用volatile修饰的int变量,如果多个线程对这个变量进行++操作,最后得到的结果可能是不正确的
package interrupt;
import java.util.concurrent.TimeUnit;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/23 13:29
**/
public class VolatileDemo {
static volatile Boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// t1线程正在执行任务
new Thread(() -> {
while(true){
if (flag){
System.err.println("线程被中断," + Thread.currentThread().getName() + "线程将中止运行");
break;
}
System.out.println(Thread.currentThread().getName() + "线程正在运行中...");
}
},"t1").start();
// main线程sleep一小段时间,保证t1线程先启动
TimeUnit.NANOSECONDS.sleep(1);
// t2线程改变flag的值,中断t1线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程正在运行中...");
flag = true;
System.out.println("中断t1线程");
},"t2").start();
}
}
1.3.2 使用原子类(AtomicBoolean)中断线程
这里的效果和volatile一样,就不展示运行结果图了。
AtomicBoolean拥有CAS机制(自旋,无锁机制),更适合高并发需要原子性操作的场景
package interrupt;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/23 14:29
**/
public class AtomicBooleanDemo {
static AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) throws InterruptedException {
// t1线程正在执行任务
new Thread(() -> {
while(true){
if (flag.get()){
System.err.println("线程被中断," + Thread.currentThread().getName() + "线程将中止运行");
break;
}
System.out.println(Thread.currentThread().getName() + "线程正在运行中...");
}
},"t1").start();
// main线程sleep一小段时间,保证t1线程先启动
TimeUnit.NANOSECONDS.sleep(1);
// t2线程改变flag的值,中断t1线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程正在运行中...");
flag.compareAndSet(false,true);
System.out.println("中断t1线程");
},"t2").start();
}
}
1.3.3 使用interrupt()方法中断线程
interrupt方法只是将线程的中断标识改为true,只是线程会不会中断,取决于你的线程中,有没有相关的代码进行中断操作
interrupt:将线程的中断标识改为true(若线程阻塞的调用了wait、join、sleep等方法,则中断标识会被清除,并抛出一个InterruptedException的异常)
isInterrupted:获取线程的中断标识
static interrupted:获取当前线程的中断标识并返回,然后将中断标识设置为false(清除中断状态)
package interrupt;
import java.util.concurrent.TimeUnit;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/23 14:40
**/
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
// t1线程正在执行任务
Thread t1 = new Thread(() -> {
while(true){
if (Thread.currentThread().isInterrupted()){
System.err.println("线程被中断," + Thread.currentThread().getName() + "线程将中止运行");
break;
}
System.out.println(Thread.currentThread().getName() + "线程正在运行中...");
try {
TimeUnit.NANOSECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
},"t1");
t1.start();
// main线程sleep一小段时间,保证t1线程先启动
TimeUnit.NANOSECONDS.sleep(10);
// t2线程改变flag的值,中断t1线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程正在运行中...");
t1.interrupt();
System.out.println("中断t1线程(仅仅打上中断标识)");
},"t2").start();
}
}
1.4 等待和唤醒线程的三种方法
1.4.1 synchronized、wait、notify
使用synchronized结合wait和notify完成唤醒线程,必须先等待(wait),后唤醒(notify),并且等待和唤醒的代码必须在synchronized的同步代码里面(不在里面会报MonitorStateException)
package locksupport;
import java.util.concurrent.TimeUnit;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/23 22:59
**/
public class NotifyDemo {
static Object object = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized(object){
System.out.println(Thread.currentThread().getName() + "线程正在执行同步任务......");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.err.println(Thread.currentThread().getName() + "线程被唤醒......");
}
},"t1").start();
// 确保t1线程先获取到锁并执行代码
try {
TimeUnit.NANOSECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
synchronized(object){
System.out.println(Thread.currentThread().getName() + "线程正准备发送通知");
try {
object.notify();
System.err.println("通知已发送");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
},"t2").start();
}
}
1.4.2 ReentrantLock、await、signal
这种实现方式也有几个条件,和上面基本相同,await和signal必须是在lock和unlock方法之内,并且需要先等待(await),再唤醒(signal)
package locksupport;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/23 23:20
**/
public class SignalDemo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 两个线程必须
Condition condition = lock.newCondition();
new Thread(() -> {
try{
lock.lock();
System.out.println(Thread.currentThread().getName() + "线程正在执行同步任务......");
condition.await();
System.err.println(Thread.currentThread().getName() + "线程被唤醒......");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
},"t1").start();
// 确保t1线程先获取到锁并执行代码
try {
TimeUnit.NANOSECONDS.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "线程正准备发送通知");
condition.signal();
System.err.println("通知已发送");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
},"t2").start();
}
}
1.4.3 LockSupport、park、unpart
使用LockSupport,无需先等待(park),再唤醒(unpark)。它是基于令牌去实现等待和唤醒的。当线程没有令牌时,线程会处于等待状态,有令牌则会直接通过。不过一个线程只能拥有一块令牌,无论调用多少次unpark,都只会是1
package locksupport;
import java.util.concurrent.locks.LockSupport;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/23 23:42
**/
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程正在执行异步任务...");
LockSupport.park();
System.err.println(Thread.currentThread().getName() + "线程被唤醒(其实是拥有了令牌,就能直接通过)...");
}, "t1");
t1.start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程正准备给t1线程令牌");
LockSupport.unpark(t1);
System.err.println("令牌已经交给了t1线程");
},"t2").start();
}
}
1.4.4 总结
synchronized
+wait/notify
:适合简单线程通信,代码直观但控制较弱。Lock
+Condition
:适合复杂线程协作,支持多个条件变量,控制更精细。LockSupport
+park/unpark
:适用于底层工具、精准线程控制,不需要锁。
2. JMM
2.1 概念和作用
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则:
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
1、通过JMM来实现线程和主内存之间的抽象关系
2、屏蔽各个硬件平台和操作系统的内存访问差异、以实现让Java程序在各种平台下都能达到一致的内存访问效果
2.2 JMM示意图
可见性:
当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中
JMM规定线程不能直接访问主内存数据,要读取主内存数据,必须先从主内存把数据拷贝一份到CPU缓存中,然后线程再从CPU缓存中读取数据(数据副本)。并且线程之间也不能直接传递数据,也只能通过修改主内存数据,再拷贝主内存数据,实现线程之间的数据传递
happens-before
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
2.3 happens-before
happens-before之8条
1、次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作
2、锁定规则
一个unLock操作先行发生于后面((这里的 “后面” 是指时间上的先后))对同一个锁的lock操作
3、volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的 “后面” 同样是指时间上的先后
4、传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5、线程启动规则(Thread Start Rule)
Thread对象的start()方法先行发生于此线程的每一个动作
6、线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
7、线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行
8、对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
总结
在Java语言里面,Happens-Before的语义本质上是一种可见性
A Happens-Before B意味着A发生过的事情对B来说是可见的,无论A事件和B事件是否发生在同一个线程里
JMM的设计分为两部分
一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了
另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定。
3. volatile
3.1 概念和作用
volatile修饰的变量有两大特性,可见性和有序性
volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
可见
写完后立即刷新回主内存并及时发出通知,其它线程可以去主内存拿最新的数据,前面的修改对后面所有线程可见
有序(禁重排)
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序
存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑
3.2 指令重排案例
下面是指令重排的案例
代码如下,如果按照正常的顺序执行,代码是永远不可能退出的,但是程序却退出了。
package volatiles;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/29 10:45
**/
public class VolatileReordering {
static int a,b;
static int x,y;
public static void main(String[] args) throws InterruptedException {
int i = 1;
while(true){
a = b = x = y = 0;
Thread t1 = new Thread(() -> {
a = 1; // 第一步
x = b; // 第二步
});
Thread t2 = new Thread(() -> {
b = 1; // 第三步
y = a; // 第四步
});
t1.start();
t2.start();
t1.join();
t2.join();
System.err.println("第" + (i++) + "次执行x:" + x + ",y:" + y);
if (x == 0 && y == 0){
System.err.println("程序退出!!!");
break;
}
}
}
}
这里程序能退出,说明是先执行的第二步和第四步,然后再执行的第一步和第三步。指令重排了
3.3 内存屏障
内存屏障
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性
内存屏障之前的所有写操作都要回写到主内存
内存屏障之后的所有读操作都能获取内存屏障之前的所有写操作的最新结果(实现了可见性)
写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行
读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据
因此重排序时,不允许把内存屏障之后的指令重排到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读
四大屏障
读屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
写屏障
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
3.4 可见性案例
可见性案例
此时main线程已经将flag修改为false,但是线程还是处于死循环中(加上volatile便可以实现可见,死循环将会结束)
package volatiles;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/29 14:29
**/
public class VolatileVisibility {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.err.println("线程执行中");
while(flag){
}
System.err.println("flag被改为false,线程执行结束");
}).start();
Thread.sleep(1);
flag = false;
System.err.println("main线程执行结束");
}
}
3.5 原子性案例(volatile无法保证原子性)
不能保证原子性的案例
这里如果能保证原子性,则得到的结果应该是100000。使用synchronized可以保证原子性
package volatiles;
import java.util.ArrayList;
import java.util.List;
/**
* @author lmq
* @version 1.0
* @datetime 2025/3/29 14:33
**/
public class VolattileAtom {
static volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
num++;
}
});
thread.start();
threadList.add(thread);
}
for (int i = 0; i < threadList.size(); i++) {
threadList.get(i).join();
}
System.err.println("所有线程执行完毕,num的值为:" + num);
}
}
4. 原子类和CAS
4.1 CAS
大部分的Atomic类都是通过CAS的思想实现的
如图所示,volatile保证了数据的可见性和有序性,然后使用unsafe类的方法保证原子性
调用unsafe类的getAndAddInt方法
这个方法会先获取volatile变量,然后再比较它的预期值(就是直接从内存地址中取值),若volatile值和预期值不一样,则会重新取值,再比较,直到两个值一样,则进行修改(这个过程是通过native方法实现的,能保证原子性)
CAS
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起synchronized重量级锁,这里的排它时间要短很多,所以在多线程情况下性能会比较好。
cmpxchg指令作用,参考下面的文章:
https://blog.csdn.net/Candy___i/article/details/136184880
简单解读一个Atomic类的原子操作实现方式
1、Unsafe类
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中(JDK8及以前,后面的版本在jdk.internal.misc包下新增了一个同名类,为了解决规范问题,原包下的方法猜测会陆陆续续迁移到新类中),其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法
Unsafe类中的大部分方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行响应任务
2、变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
3、变量value用volatile修饰,保证了多线程之间的内存可见性
CAS的全称为Compare-And-Swap,它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的
AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升
CAS并发原语体现在Java语言中就是Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
使用原子类实现自旋锁(不使用synchronized和Lock)
代码如下,效果如下图
package atomic;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author lmq
* @version 1.0
* @descript 使用原子类实现锁的功能(不使用synchronized和Lock类)
* @datetime 2025/3/30 23:14
**/
public class AtomicLockDemo {
// 自定义锁
AtomicReference<Thread> customLock = new AtomicReference<>();
// 获取锁方法
public void lock(){
// 当前线程
Thread current = Thread.currentThread();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + current.getName() + "正在尝试获取锁...");
// 当锁的值是null时,说明锁没有被其它线程占用,则将锁的拥有者设置为当前线程
while(!customLock.compareAndSet(null,current)){
}
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + current.getName() + "线程获取锁成功!!!");
}
// 释放锁方法
public void unlock(){
// 当前线程
Thread current = Thread.currentThread();
if (!customLock.compareAndSet(current,null)){
throw new RuntimeException(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + "当前线程未获取到锁,释放失败!!!");
}
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + current.getName() + "线程释放锁成功!!!");
}
public static void main(String[] args) throws InterruptedException {
AtomicLockDemo atomicLockDemo = new AtomicLockDemo();
Thread t1 = new Thread(() -> {
try {
// 获取锁
atomicLockDemo.lock();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + Thread.currentThread().getName() + "线程正在执行业务逻辑代码...");
// 模拟正在执行业务逻辑代码
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + Thread.currentThread().getName() + "线程代码执行结束,释放锁");
atomicLockDemo.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
// 获取锁
atomicLockDemo.lock();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + Thread.currentThread().getName() + "线程正在执行业务逻辑代码...");
// 模拟正在执行业务逻辑代码
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + ":" + Thread.currentThread().getName() + "线程代码执行结束,释放锁");
atomicLockDemo.unlock();
}
}, "t2");
t1.start();
Thread.sleep(10);
t2.start();
}
}
CAS会有一个问题,那就是ABA问题
5. ThreadLocal
5.1 简单使用案例
每个线程都对这个numLocal进行操作,但是每个线程得到的值都是不一样的,他们都有一份单独的值在用完之后,记得remove掉,因为不移出容易造成内存泄漏(尤其是在线程池环境下,线程会被复用,ThreadLocal占用得不到释放)
package threadlocal;
import java.security.SecureRandom;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author lmq
* @version 1.0
* @datetime 2025/4/5 16:35
**/
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
User user = new User();
AtomicInteger nums = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(() -> {
int total = 0;
try {
total = new SecureRandom().nextInt(20);
for (int j = 0; j < total; j++) {
user.numLocalAdd();
}
nums.addAndGet(user.numLocal.get());
System.err.println(Thread.currentThread().getName() + ":" +user.numLocal.get());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
user.numLocal.remove();
}
},"t" + (i + 1)).start();
}
countDownLatch.await();
System.out.println(user.numLocal.get());
System.out.println(nums.get());
}
}
class User{
public Integer num;
ThreadLocal<Integer> numLocal = ThreadLocal.withInitial(() -> 0);
public void numLocalAdd(){
numLocal.set(numLocal.get() + 1);
}
}
关于ThreadLocal的分析,参考
6. 对象内存布局
对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)
如下图
Mark Word:对象标记。存储了哈希码、GC标记、GC次数、同步锁标记、偏向锁持有者等等
Class Pointer:类元信息。标识这是哪个类
instance data:类中的实例数据。一般指类中的属性值
padding:对齐填充。使类所占用的字节大小是8的倍数
HotSpot虚拟机对象头Mark Word
下图是通过jol打印出来的对象的内存结构布局
对象分代年龄最大值为15(每次GC之后,未被回收的对象,分代年龄会+1)。因为表示分代年龄的位置只有4bit,用二进制表示的最大值为15(1111)
7. synchronized锁升级
7.1 发展历程
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器(monitor)是依赖于底层操作系统的Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长。时间成本相对较高,这也是为什么早期的synchronized效率低的原因,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
JVM中的同步就是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建、销毁。
Monitor是由ObjectMonitor实现,而ObjectMonitor是由C++的ObjectMonitor.hpp文件实现
Monitor与Java对象以及线程是如何关联?
1、如果一个Java对象被某个线程锁住,则该Java对象的Mark Word字段中LockWord指向monitor的起始地址
2、Monitor的Owner字段会存放拥有相关联对象锁的线程id
7.2 锁升级流程
7.2.1 表格
偏向锁:Mark Word存储的是偏向的线程ID
轻量锁:Mark Word存储的是指向线程栈中Lock Record的指针
重量锁:Mark Word存储的是指向堆中的monitor对象的指针
7.2.2 偏向锁
偏向锁:单线程竞争
当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。
如果不存在其它线程竞争,那么持有偏向锁的线程将永远不需要进行同步
偏向锁的操作,不涉及用户态到内核态转换,不必要直接升级为最高级。线程第一次获取到锁,代码执行完之后,不会主动释放偏向锁。在第二次到达同步代码块时,会先判断持有锁的线程是否还是自己(通过对象的Mark Word判断)。如果是,则直接进入代码块,无需重新加锁
偏向锁,默认有4s的延时,可通过JVM启动参数修改。
在锁竞争不再是一个线程时(两个或多个),偏向锁会撤销。而撤销偏向锁是需要发生stw的时候执行的(stop-the-world)。撤销完偏向锁之后,会判断偏向线程是否执行完同步代码块的代码,若执行完了,则会重新进入偏向锁状态,线程id会改为新线程id。若偏向线程未执行完同步代码块,则会升级成轻量级锁,持有锁的线程继续执行同步代码。竞争锁的线程,通过CAS竞争这把锁。
Java15偏向锁被标记为废除(这个版本还可以通过JVM参数强行启用)。Java16及以后的版本,偏向锁被移除
7.2.3 轻量级锁
当有一个线程来竞争锁时。偏向锁将升级为轻量级锁(本质就是自旋,等待获取锁)
轻量级锁的加锁
JVM会为每个线程在当前线程的栈帧中创建用于存储记录的空间,官方称为Displaced Mark Word。若一个线程获得锁时发现时轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其它线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
轻量级锁的释放
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其它线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
自适应自旋锁的大致原理
自适应意味着自旋的次数不是固定不变的
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功
如果很少自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转
7.2.4 重量级锁
重量级锁原理
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程id,这样它将处于锁定状态,除非退出同步块,否则其它线程无法获取到这个Monitor
7.2.5 锁升级之hashcode
在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashcode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的话,那Mark Word中的identity hash code必然会被偏向线程id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁会将这些信息写回对象头
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头
7.2.6 锁消除和锁粗化
锁消除
下面的这钟情况会发生锁消除。
JIT编译器(Just In Time Compiler),会判断这个锁不会有其它线程来竞争,所以会优化代码,从而消除锁
package com.study.juc.sync;
/**
* @author lmq
* @version 1.0
* @datetime 2025/5/22 22:59
**/
public class SyncEliminate {
public static void syncMethod(){
Object o = new Object();
synchronized (o){
System.out.println("同步代码块内容执行中......");
}
}
public static void main(String[] args) {
new Thread(SyncEliminate::syncMethod).start();
new Thread(SyncEliminate::syncMethod).start();
new Thread(SyncEliminate::syncMethod).start();
new Thread(SyncEliminate::syncMethod).start();
}
}
锁粗化
以下情况会发生锁粗化,因为JIT编译器会认为,这样使用多把锁浪费资源,没有意义。将他们转换成一把锁,然后里面的代码放在同一把锁的范围内,也能得到一样的效果
package com.study.juc.sync;
/**
* @author lmq
* @version 1.0
* @datetime 2025/5/22 22:59
**/
public class SyncEliminate {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj){
System.out.println("锁粗化......");
}
synchronized (obj){
System.out.println("锁粗化......");
}
synchronized (obj){
System.out.println("锁粗化......");
}
synchronized (obj){
System.out.println("锁粗化......");
}
}).start();
}
}
评论区