1. 易忘知识点
用于记录已遗忘的知识点
1.1 基础语法
-
一个JAVA文件中的不同类中均可以有public static 方法, 并且可用
java 类名的方式运行public static方法 -
javadoc 用于生成java文件的文档,使用方式
javadoc xxx.java. -
补码 = 反码 + 1, 计算机运行使用补码
- 正数:原码,反码,补码都一样:第一位是符号位,0代表正数,数值位为二进制数
- 负数:
- 原码:第一位是符号位,1代表负数,数值位为2进制数
- 反码:第一位是符号位,1代表负数,数值位为对应原码的数值位取反
- 补码:反码数值位加1
-
方法重载(Overload)
-
方法名必须相同
-
形参列表必须不同:参数顺序,类型,个数,至少有一样不同
-
返回类型:无要求
注意:仅仅是形参名不同无法实现方法重载
-
-
可变参数
java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法
public void varPara(int... parameters){//parameters 是一个数组 }- 可变参数的实参可以为0或任意多个
- 可变参数的实参可以是数组
- 可变参数可以和普通类型的参数放在一个形参列表里,但需要保证可变参数在最后
- 一个形参列表中只能出现一个可变参数
-
构造器
- 构造器没有返回值
- 方法名和类名必须相同
- 参数列表和成员方法一样的规则
- 构造器的调用由系统完成
- 构造器,即构造方法可以重载
class Constructer{public Constructer(){} }作用:对新对象的初始化
-
反编译
javap [-c -v] 类名-c 对代码反汇编
-v 输出附加信息
1.2 面向对象
-
代码块
属于类中的成员,将逻辑语句封装在方法体中,通过“{}”包围起来。
和方法不同,没有方法名,没有返回值,没有参数,只有方法体,不通过类或对象显式调用。在加载类或者创建对象时隐式调用。
[修饰符]{code... }; // 此分号可有可无修饰符 可选,使用修饰符时只能用static
代码块使用场景:e.g. 当多个构造器中有相同的代码语句时,可以将其放入代码块中
创建对象时,系统先调用代码块,后调用构造器
-
类加载顺序
- 父类的静态代码块和静态属性
- 子类的静态代买块和静态属性
- 父类的普通代码块和普通属性
- 父类的构造方法
- 子类的普通代码块和普通属性
- 子类的构造方法
-
final关键字使用情况:
(1) 当一个类不希望被继承时, 可使用final关键字修饰
(2) 当不希望类中某个方法被子类重写时, 可用final关键字修饰
(3) 当不希望类中某个属性的值被修改时, 使用final关键字修饰
(4) 当不希望某个局部变量被修改时, 使用final关键字修饰
-
abstract类当不打算在父类中定义某些方法,但又想提前声明这些方法时, 使用
abstract修饰这个父类以及这些方法class abstract Test{public abstract void m(); // 没有方法体 }-
interface
JDK8.0后接口可以有静态方法, 默认方法
在接口中抽象方法可以省略
abstract关键字public interface Test{public int n1 = 10;default public void ok(){ // 默认方法, 注意要使用default关键字修饰System.out.println("ok..");}public static void test(){ // 静态方法System.out.println("test.."); } }一个类可以同时实现多个接口
class Test implements IA, IB, ...{... }接口中的属性是
final static修饰的, 例如在接口中写int a = 1;, 实际上是public static final int a = 1;
-
-
内部类
一个类的内部又完整地嵌套了另一个类结构. 被嵌套的类称为内部类(inner class), 嵌套的其他类称为外部类(outer class).
// 基本语法 class Outer{// 外部类class Inner{// 内部类} }内部类的分类
//1. 定义在外部类的局部位置(如方法中, 代码块中) //1.1 局部内部类 class Outer01{private int n1 = 1;public void m1(){class Inner01{ // 可使用final修饰private int n1 = 22;public void m10(){System.out.println(A.this.n1); // 可以直接访问所在外部内的所有成员, 使用 外部类名.this精确定位}}}//局部内部类的作用域只在对应的方法体内或代码块中 }//1.2 匿名内部类 class Outer02{private int n2 = 1;public void m2(){IA ia = new IA(){ // class xxx implements IA{...}public void test(){System.out.println(n2);}}} } interface IA{}//2. 定义在外部类的成员变量位置 //2.1 成员内部类class Outer03{private int n3 = 1;class Inner{public void m3(){System.out.println(n3);}} }//2.2 静态内部类class Outer04{private int n4 = 1;static class Inner{public void m4(){System.out.println(n4);}} } -
枚举
枚举: 枚举属于一种特定的类, 里面只包含一组有限的特定的对象
//1. 自定义枚举class Season{private String name;private String desc;// public Season season = new Season("ok", "001");public static final Season SPRING = new Season("Spring", "Warm");public static final Season SUMMER = new Season("Summer", "Hot");public static final Season AUTUMN = new Season("Autumn", "Cozy");public static final Season WINTER = new Season("Winter", "Cold");private Season(String name, String desc){this.name = name;this.desc = desc;}public String getName() {return name;}public String getDesc() {return desc;}
}//2. 使用enum关键字enum Season_{//1. 多个常量时, 用','间隔//2. 常量对象需要定义在类的最前面SPRING("Spring", "Warm"), SUMMER("Summer", "Hot"), AUTUMN("Autumn","Cozy"), WINTER("Winter","Cold");private String name ;private String desc;private Season_(String name, String desc){this.name = name;this.desc = desc;}public String getName() {return name;}public String getDesc() {return desc;}
}
-
注解
注解(Annotation)也被称为元数据, 用于修饰包, 类, 方法, 属性, 构造器, 局部变量等数据信息
不影响程序逻辑, 但是注解可以被编译和执行
//1. @Override 标明方法为重写方法 //2. @Deprecated 标明某个类或方法已过时 //3. @SuppressWarnings 抑制编译器警告//元注解, 注解的注解 //1. @Retention(RetentionPolicy.SOURCE) 指定注解的作用范围(SOURCE(编译器使用后直接丢弃), CLASS(记录在class文件, 但jvm运行时不保留), RUNTIME) // 代码(SOURCE) ==> class 文件(class) ==> JVM运行(RUNTIME) //2. Target(ElementType.METHOD) 指定注解的使用范围(CONSTRUCTURE, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE) //3. Documented 指定该注解是否在javadoc中显示 //4. Inherited 子类会继承父类注解-
异常
运行时异常: 程序运行时发生异常
编译时异常: 编译器检测出的异常
Java.lang.Throwable-Error-Exception--编译时异常--IOException--SQLException--运行时异常 RuntimeException--NullPointException--ArrayindexOutOfBundsException--ArithmeticException--ClassCastExcetption// 1. try-catch-finally try{... }catch(NullPointerException e){e. sss; }catch(ArithmeticException e){}finally{}// 2. throwspublic void method() throws {} //当抛出的是运行时异常, 调用该方法的方法可以不处理 //当抛出的是编译时异常, 调用该方法的方法需要进行处理 -
1.3 集合
1.4 函数式编程
Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程
2. 重难点
2.1 设计模式
2.1.1 单例设计模式
所谓的单例设计模式,就是采取一定的方法保证在整个软件系统中,对于某个类只能存在一个实例,并且该类只提供一个取得其实例的方法。
单例模式有两种:懒汉式和饿汉式
-
饿汉式
(1) 构造器私有化
(2) 类的内部创建对象, 同时创建实例
(3) 向外暴露一个静态的公共方法
(4) 代码实现
class SingleTon{private SingleTon(){};private static SingleTon instance = new SingleTon();public static SingleTon getSingleTon(){return instance;} } -
懒汉式
(1) 构造器私有化
(2) 类的内部创建对象
(3) 向外暴露一个静态的公共方法, 同时创建实例
(4) 代码实现
class SingleTon{private SingleTone(){};private static SingleTon instance;public static SingleTon getSingleTon(){if(instance == null){ // 有线程风险instance = new SingleTon();}return instance;} }
2.1.2 模版设计模式
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
class abstract Template{public abstract void job();public void calculate(){int i = 1;job();int j = 3;}
}
3. JVM
3.1 JAVA和JVM简介
3.2 虚拟机与JAVA虚拟机
3.3 JVM整体结构
3.4 Java代码执行流程
3.5 JVM架构模型
3.6 JVM生命周期
3.7 JVM发展历程
JVM与JAVA体系结构
类加载子系统-修改
运行时数据区概述及线程
程序计数器
虚拟机栈
本地方法接口
本地方法栈
堆
方法区
直接内存
执行引擎
StringTable
垃圾回收概述
垃圾回收相关算法
垃圾回收相关概念
垃圾回收器
4. 多线程
开启线程方式
- 继承Thread类
- 实现Runable接口
- 实现Callable接口,并封装到Future对象中
Thread中的Priority
数字越大,优先级越高。
它优先级越高只是被先执行的概率更大,并不是严格按照优先级执行。
Thread中的Daemon
主线程结束后,Daemon也会立即结束。 thread.setDaemon(true);
Thread中的yeild:Thread.yeild();
礼让不一定成功,因为下次抢占cpu的时候或许又抢到了。
Thread中的join
插入一定成功
线程生命周期

线程安全
synchronized
同步方法
同步方法的锁无法指定
// 1. 非静态方法,锁就是this.class
public synchronized void method(){ ....
}// 2. 静态方法,锁就是类名.class
public static synchronized void method(){....
}
方法内的同步代码块
synchronized(Test.class){ // obj 保持唯一,Test是代码所在类....
}
Lock
Lock中实现了比synchronized更广泛的锁定操作
Lock中实现了获得锁和释放锁的方法
void lock();
void unlock();
Lock是接口,不能直接实例化, 通常采用它的子类ReentrantLock来实例化
new Thread(new Runnable() {private static final Lock lock = new ReentrantLock(); // 1. 保证lock唯一@Overridepublic void run() {while (true) {lock.lock();try{System.out.println(Thread.currentThread().getName());}catch (Exception e){throw new RuntimeException(e);}finally {lock.unlock(); // 2.保证最终都会被执行}}}
}).start();
死锁
代码中出现嵌套锁,多个线程都在等待对方线程释放锁,从而形成死锁状态。

生产者和消费者
生产者:生产数据
消费者:消费数据
等待唤醒
package com.thread.itcast;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ConsumerAndProducer {public static void main(String[] args) {Thread consumer = new Thread(new Runnable() {public void run() {while (true) {synchronized (Buffer.lock) {if(Buffer.count == 0){break;}else {if(Buffer.bufferFlag == 0){try {Buffer.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else {System.out.println("消费者消费 " + (11 - Buffer.count--));Buffer.bufferFlag = 0;Buffer.lock.notifyAll();}}}}}});Thread producer = new Thread(new Runnable() {public void run() {while (true) {synchronized (Buffer.lock) {if(Buffer.count == 0){break;}else {if(Buffer.bufferFlag == 0){System.out.println("生产者生产第 " + (11 - Buffer.count));Buffer.bufferFlag = 1;Buffer.lock.notifyAll();}else {try {Buffer.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}}}});consumer.start();producer.start();}
}class Buffer{public static final Object lock = new Object();public static int bufferFlag = 0; //状态为0消费者和生产才能进行public static int count = 10; // 消费次数}
等待唤醒机制(阻塞队列)
package com.thread.itcast;import java.util.concurrent.ArrayBlockingQueue;public class QueueBuffer {public static void main(String[] args) {Thread producer = new Thread(new MyRunnable() {@Overridepublic void run() {int i = 10;while (i > 0) {System.out.println("生产者生产" + (11 - i) );try {queue.put("产品" + (11 - i)); // 队列已自己加锁} catch (InterruptedException e) {throw new RuntimeException(e);}i--;}try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("生产完成");}});Thread consumer = new Thread(new MyRunnable() {@Overridepublic void run() {while (true) {//System.out.println("开始消费");String poll = null;try {poll = queue.take();} catch (Exception e) {throw new RuntimeException(e);}System.out.println("消费者消费" + poll);}}});consumer.setDaemon(true);producer.start();consumer.start();}
}abstract class MyRunnable implements Runnable {protected static ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);}
线程状态
- NEW
- RUNNABLE
- WAITING
- TIME_WAITING
- BLOCKED
- TERMINATED
RUNNING 没有定义,JVM 交给了宿主机
线程池
// 1. 获取一个没有大小限制的线程池
ExecuterService pool = Executors.newCachedThreadPool();
// 2. 设置一个最大容量为10的线程池
ExecuterService pool = Executors.newFixedThreadPool(10);//3. 提交任务
pool.submit(new Runnable() {public void run() {while (true) {System.out.println(Thread.currentThread().getName());}}});
核心线程数量
线程池中最大线程数量
空闲时间和时间单位
阻塞队列
创建线程方式
执行任务过多时解决方案

ThreadPoolExecutor pool = new ThreadPoolExecutor(3, // 一、核心线程数量,不能小于06, // 二、最大线程数量, 必须大于等于核心线程数量60, // 三、空闲线程最大存活时间, 不能小于0TimeUnit.SECONDS, // 四、时间单位,用TimeUnit指定new ArrayBlockingQueue<>(10), // 五、阻塞任务队列Executors.defaultThreadFactory(), // 六、创建线程工厂 // 可选new ThreadPoolExecutor.AbortPolicy()// 七、任务拒绝策略 // 可选 默认是Abor
);
4 JUC
JUC是java.util.concurrent在并发编程中使用的工具包
4.1 CompletableFuture
用于处理异步任务编排的一个类。是Future的功能增强版,减少阻塞和轮询,可以传入
4.1.1 Future接口理论知识
Future接口(FutureTask实现类)定义操作异步任务执行的一些方法. 如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。简单讲:Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。
4.1.2 Future 接口常用实现类FutureTask异步任务
FutureTask future = new FutureTask(new Callable(){public String call(){return "OJBK";}
});
Thread t1 = new Thread(future);
t1.start();
// Executor threadPool = new ThreadPoolExecutor(3,5,1,TimeUnit.SECONDS,new LinkedBlockingDeque<>());
-
优点
- Future+线程池异步多线程任务配合,能显著提高程序的执行效率
threadPool.execute(callableInterfaceImpl);
- Future+线程池异步多线程任务配合,能显著提高程序的执行效率
-
缺点:
- 直接调用Future接口的get方法获取线程返回值可能导致主线线程阻塞
future.get() - 通过Future接口的
isDone()方法轮询, 但是轮询会导致CPU空转,浪费计算资源。
- 直接调用Future接口的get方法获取线程返回值可能导致主线线程阻塞
4.1.3 CompletableFuture对Future的改进
- CompletableFuture为什么会出现
为了解决Future接口的缺点,阻塞和轮询资源浪费问题,提供了一种类似观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
CompletionState接口代表异步计算过程中的某一个阶段。一个阶段完成之后可能会触发另一个阶段。
- 核心的4个静态方法,来创建一个异步的任务
官方不推荐通过new创建CompletableFuture对象,而是使用静态的方法进行创建。
// runAsync无返回值
public static CompletableFuture<Void> runAsync(Runnable runnable) {return asyncRunStage(ASYNC_POOL, runnable); // 如果没有指定Executor,直接使用默认的`ForkJoinPool.commonPool()`线程池。}
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor) {return asyncRunStage(screenExecutor(executor), runnable);
}// =====================================//
// supplyAsync 有返回值 1
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {return asyncSupplyStage(ASYNC_POOL, supplier); // 如果没有指定Executor,直接使用默认的`ForkJoinPool.commonPool()`线程池。}
// SupplyAsync 有返回值 2
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor) {return asyncSupplyStage(screenExecutor(executor), supplier);}
如果没有指定Executor,直接使用默认的ForkJoinPool.commonPool()线程池。
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {System.out.println("异步任务supplyAsync");return "ok";}, threadPool);
如何解决阻塞问题的呢????
默认情况下,开启协程,线程池中的线程作为协程,主线程结束,协程直接终止执行。
将返回结果通知给主线程或者其他线程
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3,5,3,TimeUnit.SECONDS,new LinkedBlockingDeque<>(), new ThreadPoolExecutor.AbortPolicy()); // 工作中尽量使用自己创建的线程池
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {System.out.println(Thread.currentThread().getName() + "异步任务supplyAsync");return "ok";
}, threadPool).whenCompleteAsync((v,e)->{ // 异步任务成功结束回调方法System.out.println( Thread.currentThread().getName() + "对返回值进行处理:" + v);}
}, threadPool).exceptionallyAsync(e ->{// 异步任务出现异常回调方法System.out.println(Thread.currentThread().getName() + "异常处理:" + e.getMessage());return null;
}, threadPool);
System.out.println("主线程结束======");
CompletableFuture优点
-
异步任务结束时,会自动回调某个自定义的回调方法(
whenComplete()) -
异步任务出错时,会自动回调某个定义异常处理方法(
exceptionally()) -
主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行
whenComplete().exceptionally(),
4.1.4 案例-电商网站的比价需求开发
大厂业务需求说明:
- 切记: 先是功能实现,再说性能提升
- 方法论: 需求分析 -> 功能实现-> 性能提升
4.1.5 CompletableFuture的常用方法
(1) 获取结果和触发规则
- complete(T value): 获取结果,默认值为value
- get():等待获取,需要处理结果异常
- join(): 获取完成后的结果,无需强制捕获异常,代码更简洁
- getNow(T valueIfAbsent): 获取当前运行结果,当前还未完成则返回默认值
(2) 对计算结果进行处理
-
计算结果存在依赖关系,这两个线程串行化
-
thenApply(f->{}): 根据前述返回结果进一步处理,出现异常不走下一步
-
handle(f->{}):类似thenApply, 出现异常仍然走下一步。
(3)对计算结果进行消费
-
接受任务的处理结果,并消费处理,无返回结果
-
thenAccept() :无返回值
-
对比:
- thenRun(Runnable r):既没有输入参数,也没有返回结果
- thenAccept(Consumer action): 只有输入参数,没有返回结果
- thenApply(Function f): 有输入参数,也有返回值
-
CompletableFuture和线程池:
- thenRun和thenRunAsync区别:
(4)对计算速度进行选用
- 谁快用谁
- a.applyToEither(b,f->{}); a和b谁快用谁的结果
(5)对计算结果进行合并
- 两个CompletionStage任务都完成后, 最终把两个任务的结果一起交给thenCombine来处理
- a.thenCombine(b, (x,y) -> {}): 将两个任务的运行结果进行合并处理
(5)等待多个任务完成
- CompletableFuture.allOf(a,b,c,d).join();
4.2 线程锁
4.2.1 面试题复盘
-
如何理解多线程,如何处理并发,线程池有哪些核心参数?
-
Java加锁有哪几种锁?
-
synchronized原理是什么?为什么可重入?如何获取对象的锁?
-
JVM对原生锁做了哪些优化?
-
什么是锁清除和锁粗化?
-
乐观锁是什么?synchronized与乐观锁什么区别?
-
volatile有什么作用?
-
ReentrantLock原理是什么?
-
AQS框架原理介绍一下?
-
简单说说Lock
-
是否使用过CountDownLanch? 如何使用?
4.2.2 乐观锁与悲观锁
(1)悲观锁
synchronized和Lock都是悲观锁, 同一时间点,有且只有一个线程能够访问对应的资源。 写操作多的场景使用。
(2)乐观锁
认为自己在使用数据时,不会有别的线程修改数据或资源,所以不会添加锁。只是在更新资源的时候,需要去判断当前数据有没有别的线程更新过。判断规则有:
- 版本号机制version,每一次更新一个版本号。
- 采用CAS算法,Java原子类中的递增操作就通过
CAS自旋实现的。比较并交换
4.2.3 锁是什么
(1)锁案例演示 - synchronized的三种应用方式
// 1. 对象锁:对于非静态方法使用synchronized就是加的对象锁,获得的是这个对象(this)作为锁
public synchronized void sentEmail(){try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("sent email");
}// 2. 类锁:对于静态方法或使用synchronized就是加的类锁,获得的是这个类对象(.class)作为锁
public static synchronized void sentSMS(){System.out.println("sent SMS");
}// 3. 代码块,使用的是synchronized括号内的对象
public void testSynchronized(){synchronized (this){System.out.println("testSynchronized");}
}
(2)从字节码角度分析synchronized实现
javap -c ***.class文件反编译
javap -c a.class # 反汇编代码,输出每个方法的 Java 字节码指令(指令集)
-v或 -verbose # 输出最详细的附加信息,包括版本号、常量池、方法描述符(签名)、栈大小等
synchronized同步代码块
public void testSynchronized(){synchronized (this){System.out.println("testSynchronized");}
}
public void testSynchronized();Code:0: aload_01: dup2: astore_13: monitorenter # 获取锁4: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;7: ldc #44 // String testSynchronized9: invokevirtual #34 // Method java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_113: monitorexit # 释放锁14: goto 2217: astore_218: aload_119: monitorexit # 异常情况也可以释放锁20: aload_221: athrow22: returnException table:from to target type4 14 17 any17 20 17 any
}
synchronized对象锁
-v
public synchronized void sentEmail(){System.out.println("sent email");
}
public synchronized void sentEmail();descriptor: ()Vflags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED # 会检查对象的ACC_SYNCHRONIZED是否被设置,如果设置了,则会持有monitor直到方法完成释放Code:stack=2, locals=1, args_size=10: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #13 // String sent email5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 31: 0line 32: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 this Lcom/thread/sgg/juc/Phone;
synchronized类锁
public static synchronized void sentSMS(){System.out.println("sent SMS");
}
public static synchronized void sentSMS();descriptor: ()Vflags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED # 根据是否有ACC_STATIC是否存在判断应该是类锁还是对象锁Code:stack=2, locals=0, args_size=00: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #21 // String sent SMS5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 34: 0line 35: 8
(3)反编译synchronized锁的是什么
为什么任何一个对象都可以成为一个锁?
Object是任何类的父类
什么是管程monitor?
monitor是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。
这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
//结构体如下
ObjectMonitor::ObjectMonitor() { _header = NULL; _count = 0; // 该线程获取锁的次数_waiters = 0, _recursions = 0; //线程的重入次数_object = NULL; _owner = NULL; //标识拥有该monitor的线程_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点_WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁进入时的单向链表FreeNext = NULL ; _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点_SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ;
}
4.2.4 公平锁与非公平锁
公平锁:多个线程按照申请锁的顺序获取锁
非公平锁:不按照顺序获取锁,可能导致某些线程处于饥饿状态
为什么有公平和非公平锁的设计?为什么默认非公平锁?
非公平锁能减少线程切换,减少CPU空闲状态的时间,效率较高。
public class ReentrantLock implements Lock, java.io.Serializable {abstract static class Sync extends AbstractQueuedSynchronizer {...}/*** Sync object for non-fair locks*/static final class NonfairSync extends Sync {...}/*** Sync object for fair locks*/static final class FairSync extends Sync {...}public ReentrantLock() {sync = new NonfairSync();}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
4.2.5 可重入锁(递归锁)
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
- 可重入锁的种类
- 隐式:synchronized(自动多次释放)
- 显示:ReentrantLock(手动多次释放)
4.2.6 死锁及排查
死锁是指两个或两个以上的线程在执行过程中,因争夺双方持有的资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足!死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
jps # 查看Java进程编号
jstack 进程编号 # 查看死锁情况
4.2.7 写锁(独占)与读锁(共享)
4.2.8 自旋锁SpinLock
4.2.9 无锁-独占锁-读写锁-邮戳锁
4.2.10 无锁-偏向锁-轻量锁-重量锁
4.3 LockSupport与线程中断
4.3.1 内容介绍
一个线程不应该由其他线程来强制中断或停止,而是应该山线程自己自行停止,自己来决定自己的命运。
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制--中断,也即中断标识协商机制。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true; 接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
如何中断一个运行中的线程?
4.3.2 线程中断机制
// Thread类中
public void interrupt() // Just to set the interrupt flag
public static boolean interrupted() // 1. 判断当前的线程是否被中断,2. 清除当前中断状态
public boolean isInterrupted() // 判断当前线程是否被中断
- volatile标识一个flag,协商中断
- AtomicBoolean, 类似于volatile,协商中断
- 通过Thread类自带的api,协商中断(底层为volatile)
调用一个线程的interrupt()方法时:
- 如果线程正常运行,仅仅是把中断标志位修改为true
- 如果线程处于被阻塞状态(WATING, TIME_WATING, BLOCKED),线程会退出当前被阻塞状态,清除当前中断状态,并直接抛出
InterrruptedException异常。
4.3.3 LockSupport是什么
用于创建锁和其他同步类的基本线程阻塞原语。
LockSupport的park()和unpark()分别是阻塞线程和解除线程阻塞
4.3.4 线程等待唤醒机制
3种让线程等待和唤醒的方法
- 使用Object的wait()和notify()
- 使用JUC种Condition的await()和signal()
- LockSupport的park()和unpark()
(1)Object的wait()和notify()
// thread1
Object objectLock = new Object();
new Thread(()->{synchronized(object){ // 需要结合synchronized来进行等待唤醒操作。没有synchronized代码块包裹,会报错System.out.println("线程一");try {object.wait(); // 等待唤醒,直到被唤醒才被唤醒。如果不被唤醒则一直处于等待状态} catch (InterruptedException e) {throw new RuntimeException(e);}}
}).start();// thread2
new Thread(()->{synchronized(object){object.notify(); // 唤醒等待线程, 必须在锁块中}
}).start();
(2)JUC中Condition的await()和signal()
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// thread1
new Thread(()->{lock.lock();try {condition.await(); // 必须在锁块中,等待唤醒,直到被唤醒才开始运行。否则一直处于等待状态} catch (InterruptedException e) {throw new RuntimeException(e);}finally {lock.unlock();}
}).start();//thread2
new Thread(()->{lock.lock();try{condition.signal(); // 必须在锁块中}finally {lock.unlock();}
}).start();
(3)LockSupport的park()和unpark()
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit)
于Semaphore不同的是,许可的累加上线是1.
-
阻塞
park() / park(Object blocker): 阻塞当前线程 / 阻塞传入的具体线程public static void park(){UNSAFE.park(false,0L); } public static void park() {U.park(false, 0L); // | 0ms 没有通行证就一直等待 }permit许可证默认没有不能放行,所以一开始调用
park()方法,当前线程就会被阻塞,直到别的线程给当前线程发放permit, park方法才会被返回(唤醒) -
唤醒
unpark(Thread thread): 唤醒处于阻塞状态的指定线程public static void unpark(Thread thread) {if (thread != null)U.unpark(thread); }调用
unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park的线程,即之前阻塞中的LockSupport.park()方法会立即返回。
Thread thread1 = new Thread(() -> {System.out.println("线程一启动");LockSupport.park();System.out.println("线程一被唤醒");
});
thread1.start();try {TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {throw new RuntimeException(e);
}new Thread(()->{System.out.println("线程二启动");LockSupport.unpark(thread1); // 发放permit,放行thread1.(唤醒thread1)
}).start();
- 无需锁块包裹
- 可以实现提前唤醒的操作
park()和unpark()成对出现,单个线程重复调用unpark()不会累计permit,permit最多只能有一个!
为什么可以突破wait/notify原有的调用顺序?
unpark获取了一个permit凭证,调用park()时由于有permit会顺利进行,不会阻塞。
为什么唤醒两次后阻塞两次,最终还是阻塞?
因为permit凭证最多只能有一个,虽然有两次唤醒,但是只积累了一个permit,阻塞两次只有第一次有permit时能唤醒阻塞,第二次阻塞由于没有permit,无法唤醒。
4.4 Java内存模型JMM
4.4.1 常见面试题
什么是JMM?
JMM和volatile两个之间的关系是什么?
JMM有哪些特性?
为什么要有JMM?为什么出现?作用和功能是什么?
happens-before先行发生原则你有了解吗?
4.4.2 Java内存模型-JMM
CPU运行并不是直接操作物理内存而是先将内存中的数据读到CPU缓存,而CPU对内存的读写操作就可能造成数据不一致的问题。
JVM规范中定义了一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异。用来实现Java程序在各个平台下都能达到一致的内存访问效果.
它通过定义一套标准的规范,为Java程序员提供了一个稳定、跨平台的并发编程基石。
| 硬件/软件层面的挑战 | 导致的问题 | JMM的解决方案 |
|---|---|---|
| CPU多级缓存 | 缓存一致性问题,导致线程对共享变量的修改彼此不可见。 | 定义主内存与工作内存的交互规则,通过关键字(如volatile)强制刷新内存。 |
| 处理器优化与指令重排 | 为优化性能,编译器和处理器会重排指令执行顺序,破坏程序的有序性,可能导致意想不到的结果。 | 建立 happens-before 规则,并在关键位置插入内存屏障,禁止有害的重排序。 |
| 平台差异性 | 不同硬件架构(如x86、ARM)的内存模型差异,导致同一段Java并发程序在不同平台行为不一致。 | 提供统一的内存访问模型抽象,屏蔽底层差异,实现 “一次编写,到处运行” 的并发语义。 |
JMM(ava内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
作用:屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各个平台上都能达到一致的内存访问效果。
4.4.3 JMM三大特性
- 可见性
- 原子性
- 有序性(禁止指令重排)
(1)可见性
当一个线程修改了某个共享变量,其他线程是否能够立即获取变更,JMM规定了所有的变量都存储在主内存中.
系统主内存共享变量的数据修改被写入的时机是不确定的,多线程并发下很可能出现脏读,所以每个线程都有自己的工作内存,线程自己的工作内存保存了该线程使用到的变量的主内存数据副本,线程对变量所有的操作都必须在自己的工作内存进行。线程间变量值的传递需要依靠主内存来完成。
(2)原子性
指一个操作是不可以打断的,即多线程环境下,操作不能被其他线程干扰。
(3)有序性
JVM在执行Java代码指令的时候,执行顺序可能和Java代码顺序不一致,这个指令顺序重新编排的过程叫指令重排序。
指令重排可以保证单线程的语义一致,但是不能保证多线程的语义也一致。
4.4.4 JMM规范下,多线程对变量的读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成.
4.4.5 JMM规范下,多线程先行发生原则happends-before
在JVM中,如果一个操作执行的结果需要对另一个操作可见性,或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。逻辑上有先后关系。
包含可见性和有序性的约束。
我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下有一个“先行发生”(Happens-Before)的原则限制和规矩,给你立好了规矩!
这个原则非常重要: 它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
(1)happens-before总原则
- 可见:如果一个操作
happens-before另一个操作,那么第一个操作对第二个操作可见 - 重排:如果两个操作之间存在
happens-before关系,并不一定按照happens-before的顺序进行执行,只要结果一致,执行顺序可能改变。
(2)happens-before 八大原则
- 次序规则:
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作 - 锁定规则:获取锁一定是在锁空闲的时候,释放锁必须先拥有锁
- volatile变量规则:对于volatile变量的写操作,先行发生于后面的读操作。
- 传递规则:如果操作A先于操作B,操作B又先于操作C,那么操作A先于操作C
- 线程启动规则:一个线程的start()方法优先于线程内的每一个操作。
- 线程中断规则:先进行了
interrupt()后,才能被检查线程中断状态 - 线程终止规则:线程中的所有操作都先行于此线程的终止操作。
- 对象终结规则:创建过对象才能进行垃圾回收
finalize()
4.5 volatile与JMM
4.5.1 Volatile两大特性
- 可见性
- 有序性: 排序要求 -- 有时需要禁止指令重排
volatile内存语义:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
所以volatile的写内存语义是直接刷新到主内存中,读内存语义是直接从主内存中读取。
如何保证可见性和有序性?
可见性:写后立即刷新到主内存
有序性:禁止指令重排
依靠内存屏障
4.5.2 内存屏障
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此占之后的操作),避免代码重排序。内存展障其实就是一种JM指令,dava内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。
-
内存屏障之前,所有写操作都要写回到主内存
-
内存屏障之后,所有读操作都从主内存中读取最新结果
-
写屏障(Store Memory Barier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到
Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。 -
读屏障(Load Memory Barier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在
Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
内存屏障分类:
- 简单分类:
- 读屏障:在读指令之前执行读操作,让工作内存或CPU高速缓存中的数据失效,重新在主内存中获取最新数据。
- 写屏障:在写操作之前执行的操作,强制将写缓存区的数据刷新回主内存。
- 细分:
- LoadLoad:Load1; LoadLoad; Load2 : 保证load1的读取操作在load2即后续操作之前执行
- StoreStore:Store1: StoreStore: Store2: 保证Store2及之后的写操作执行前,store1的写操作已经写入主内存
- LoadStore:Load1: LoadStore: Store2: 保证Store2进行写操作之前,Load1读操作已经执行结束
- StoreLoad:Store1: StoreLoad: Load2: 保证Load2读操作执行前,Store1写操作的数据已经写入主内存
Java 内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作:
read(读取) >> load(加载) >> use(使用) >> assign(赋值) >> store(存储) >> write(写入) >> lock(锁定) >> unlock(解锁)
- read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
- load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
- use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
- assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
- store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
- write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令。
- lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
- unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
volatile 不具备原子性,比如count++
一般通过加锁实现原子性
重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
- 不存在数据依赖关系,可以重排序;
- 存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义! 这点在并发设计中必须要重点考虑!
数据依赖性: 若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。
volatile最佳实践
- 单一赋值,避免符合运算操作,比如i++
- 使用布尔状态标志,判断业务是否结束
- 读多写少场景:读不加锁,写操作加锁
- DCL(双端锁的发布)单例模式
4.5.3 总结
- 有可见性、有序性,没有原子性
- 内存屏障:读屏障、写屏障
- 内存屏障四大指令:LoadLoad, LoadStore, StoreStore, StoreLoad
- volatile写之前的操作,禁止重排到volatile之后
- volatile读之后的操作,禁止排序到volatile之前
- volatile写之后volatile读,禁止重排序
4.6 CAS
Compare And Swap, 比较并交换,实现并发算法时常用的一种技术。
它包含三个操作数:
- 内存位置
- 预期原值
- 更新值
执行CAS操作的时候,将内存位置的值与预期原值进行比较
- 如果相等,那么处理器会自动将该位置的值更新
- 如果不匹配,处理器不做任何操作,多线程同时执行CAS只会有一个成功
CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
4.6.1 Unsafe类
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsfe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意: Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
JVM提供的CAS机制,会在汇编层级禁止变量两侧的指令优化, 然后使用cmpxchg指令比较并更新变量值(原子性)
4.6.2 AtomicReference
public class CASTest {public static void main(String[] args) {AtomicReference<User> userAtomicReference = new AtomicReference<>();User tom = new User("tom",12);User jack = new User("jack", 43);userAtomicReference.set(tom);// 原子引用对象比较的是对象地址System.out.println(userAtomicReference.compareAndSet(tom, jack) + " : " + userAtomicReference.get().toString()); }
}
class User{String name;int age;User(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return this.name + " " + this.age;}
}
4.6.3 CAS与自旋锁
CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU资源。
CAS 是实现自旋锁的基础,自旋翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个CAS 操作,
当操作成功返回 true 时,循环结束;
当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
// AtomicInteger.compareAndSet对应的c++源码
/**/
public final int getAndAddInt(Object o, long offset, int delta){int v;do {v = getIntVolatile(o,offset);} while(!compareAndSwapInt(o,offset,v,v + delta));return v;
}
// 自定义自旋锁
class SpinLockDemo{AtomicReference<Thread> atomicReference = new AtomicReference<>();public void lock(){Thread thread = Thread.currentThread();while(!atomicReference.compareAndSet(null,thread)) {// 自定义自旋 ==> 空转}}public void unlock(){Thread thread = Thread.currentThread();atomicReference.compareAndSet(thread,null);}
}
4.6.4 CAS缺点
- 自旋操作使cpu空转,浪费CPU资源
- ABA问题:线程一从内存V中获取数据A,线程二将A修改成了B,然后又修改成了A,线程一进行CAS操作发现内存中仍是A,预期ok,线程一操作成功。
解决ABA问题:加版本号
// AtomicStampedReferencepublic class ABATest {public static void main(String[] args) {Book book1 = new Book("book1", "author1");Book book2 = new Book("book2", "author2");AtomicStampedReference<Book> bookAtomicStampedReference = new AtomicStampedReference<>(book1, 1); // 初始化需要版本号System.out.println(bookAtomicStampedReference.getReference() + " " + bookAtomicStampedReference.getStamp());bookAtomicStampedReference.compareAndSet(book1, book2, 1, 2); // 添加版本号System.out.println(bookAtomicStampedReference.getReference() + " " + bookAtomicStampedReference.getStamp());}
}
4.7 原子操作类
4.7.1 基本类型原子类
- AtomicInteger
- AtomicBoolean
- AtomicLong
常用API:
- get()
- getAndSet(int newValue)
- getAndIncrement();
- getAndDecrement();
- getAndAdd(int delta)
- compareAndSet(int expect, int newValue);
4.7.2 数组类型原子类
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
常用API
- get(int index)
- getAndSet(int index, int newValue)
4.7.3 引用类型原子类
- AtomicReference
- AtomicStampedReference: 带版本号:+1
- AtomicMarkableReference:带标识:是否被修改过:false -> true。一次性数据
4.7.4 对象的属性修改原子类
-
AtomicIntegerFieldUpdater
-
AtomicLongFieldUpdater
-
AtomicReferenceFieldUpdater
-
使用目的:以一种线程安全的方式修改非线程安全对象内的某些字段
-
使用要求:
- 更新的属性必须使用public volatile修饰
- 对象的属性修改原子类都是抽象类,使用时必须使用静态方法newUpdater()创建一个更新器
public class AtomicTest {public static void main(String[] args) {Student tom = new Student("Tom", 23);System.out.println(tom);AtomicIntegerFieldUpdater<Student> updater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "age"); // 使用静态方法创建updater.compareAndSet(tom, 23, 18);System.out.println(tom);}
}@Data
@AllArgsConstructor
class Student{private String name;public volatile int age; // 使用public volatile 修饰
}
4.7.5 原子操作增强类原理深度解析
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder : 如果是Java8,推荐使用LongAdder,比AtomicLong性能高(减少乐观锁重试次数)
点赞计数器:看看性能
public class AtomicTest {public static void main(String[] args) throws InterruptedException {int _1W = 100000;int threadNum = 50;CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);CountDownLatch countDownLatch3 = new CountDownLatch(threadNum);ClickNumber clickNumber = new ClickNumber();long start = System.currentTimeMillis();for(int i = 0; i < threadNum; i++) {new Thread(()->{try{for(int j = 0; j < _1W * 100; j++) {clickNumber.clickByLongAdder();}}finally {countDownLatch1.countDown();}}).start();}countDownLatch1.await();long end = System.currentTimeMillis();System.out.println( clickNumber.getLongAdder() + "LongAdder Time: " + (end - start));start = System.currentTimeMillis();for(int i = 0; i < threadNum; i++) {new Thread(()->{try {for(int j = 0; j < _1W * 100; j++) {clickNumber.clickByAtomicLong();}}finally {countDownLatch2.countDown();}}).start();}countDownLatch2.await();end = System.currentTimeMillis();System.out.println( clickNumber.getAtomicLong() + "AtomicLong Time: " + (end - start));start = System.currentTimeMillis();for(int i = 0; i < threadNum; i++) {new Thread(()->{try {for(int j = 0; j < _1W * 100; j++) {clickNumber.clickByLongAccumulator();}}finally {countDownLatch3.countDown();}}).start();}countDownLatch3.await();end = System.currentTimeMillis();System.out.println( clickNumber.getLongAccumulator() + "LongAccumulator Time: " + (end - start));}
}@Data
class ClickNumber{AtomicLong atomicLong = new AtomicLong(0);public void clickByAtomicLong(){atomicLong.getAndIncrement();}LongAdder longAdder = new LongAdder();public void clickByLongAdder(){longAdder.increment();}LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0L);public void clickByLongAccumulator(){longAccumulator.accumulate(1);}
}
500000000LongAdder Time: 209
500000000AtomicLong Time: 4241
500000000LongAccumulator Time: 205Process finished with exit code 0
- 原理分析
base变量: 低并发,直接累加到该变量上Cell[]数组: 高并发,累加进各个线程自己的Cell[i]槽位中- value =
base+sum(Cell[i])
- value =
- 空间换时间,分散热点数据,减少乐观锁的重试次数
4.8 ThreadLocal
线程局部变量。
4.8.1 常见面试题
- ThreadLocal中ThreadLocalMap的数据结构和关系?
- ThreadLocal的key是弱引用,为什么?
- ThreadLocal内存泄漏问题是什么?
- ThreadLocal中最后为什么要加remove方法?
4.8.2 ThreadLocal简介
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
必须回收自定的ThreadLocal变量,尤其在线程池的场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能影响后续业务和造成内存泄漏问题i。尽量在代码中使用try-finally块进行回收
objectThreadLocal.set(userInfo);try{}finally{objectThreadLocal.remove();
}
因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用
既然其它Thread不可访问,那就不存在多线程间共享的问题。
统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
如何保证不争抢:
- 加锁
- ThreadLocal: 每个线程一份数据
4.8.3 ThreadLocal源码分析
// ThreadLocal类/*** Returns the value in the current thread's copy of this* thread-local variable. If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {@link #initialValue} method.** @return the current thread's value of this thread-local*/public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}/*** Sets the current thread's copy of this thread-local variable* to the specified value. Most subclasses will have no need to* override this method, relying solely on the {@link #initialValue}* method to set the values of thread-locals.** @param value the value to be stored in the current thread's copy of* this thread-local.*/public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}}/*** Removes the current thread's value for this thread-local* variable. If this thread-local variable is subsequently* {@linkplain #get read} by the current thread, its value will be* reinitialized by invoking its {@link #initialValue} method,* unless its value is {@linkplain #set set} by the current thread* in the interim. This may result in multiple invocations of the* {@code initialValue} method in the current thread.** @since 1.5*/public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);}}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}/*** Create the map associated with a ThreadLocal. Overridden in* InheritableThreadLocal.** @param t the current thread* @param firstValue value for the initial entry of the map*/void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table. Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}...}
ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry集合。
JVM内部维护了一个线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个ThreadLocal的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竟争条件被彻底消除,在并发模式下是绝对安全的变量。
4.8.4 ThreadLocaln内存泄漏问题
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。
// 正例
objectThreadLocal.set(userInfo);
try{//...
}finally{objectThreadLocal.remove();
}
什么是内存泄漏:不再会使用的对象或者变量占用着内存,一直不被回收,就是内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> { // 为什么使用弱引用,不用会怎么样?/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k); // 关键!将 key (k) 传递给 WeakReference<ThreadLocal<?>> 的构造函数, value = v;}
}
ThreadLocalMap 从字面上就可以看出这是一个保存ThreadLocal对象的map(以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
-
第一层包装是使用
WeakReference<ThreadLocal<?>>将ThreadLocal对象变成一个弱引用的对象 -
第二层包装是定义了一个专门的类Entry来扩展
WeakReference<ThreadLocal<?>>;
| 引用类型 | 被垃圾回收(GC)的时机 | 是否可通过 get()方法获取对象 |
典型应用场景 |
|---|---|---|---|
| 强引用 (Strong Reference) | 永不回收(只要强引用存在) | 是 | 日常编程中的默认引用,用于持有需要长期存在的对象。 |
| 软引用 (SoftReference) | 内存不足时(在抛出 OutOfMemoryError之前) |
是 | 实现内存敏感的缓存(如图片缓存、网页缓存),在内存紧张时自动释放。 |
| 弱引用 (WeakReference) | 下一次 GC 发生时(无论内存是否充足) | 是 | 实现非强制的映射关系(如 WeakHashMap),防止因无用的条目积累导致内存泄漏。 |
| 虚引用 (PhantomReference) | 对象被 GC 时,但其回收过程会被跟踪 | 否(get()总是返回 null) |
跟踪对象被垃圾回收的时机,以便在回收后执行一些清理操作,如释放堆外内存。必须和引用队列ReferenceQueue结合使用 |
为什么使用弱引用?
public void func(){ThreadLocal<String> threadLocal = new ThreadLocal<>();threadLocal.set("123");threadLocal.get();
}
当方法func执行完之后,栈帧销毁,强引用threadLocal也就没有了。但此时线程的ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals里某个entry的key引用还指向threadLocal这个对象。
- 如果entry的这个key是强引用,就会导致key指向的ThreadLocal对象以及value不能被gc回收,造成内存泄漏
- 如果entry的这个key是弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向null。
为什么Entry里value不使用弱引用?
| 考量维度 | 如果Value使用弱引用 | 当前设计(Value为强引用) |
|---|---|---|
| 数据可靠性 | 极低:Value可能在任何时候被GC回收,导致get()返回null,业务逻辑出错。 |
高:只要ThreadLocal强引用存在且未调用remove(),就能保证随时取到值,业务稳定。 |
| 内存管理主动性 | 被动依赖GC,不可预测。 | 主动可控:通过get/set/remove方法清理无效Entry,或在线程结束时统一释放。 |
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
4.8.5 最佳实践
- ThreadLocal.withInitial(()->初始化值) ; 一定要进行初始化,避免空指针异常
- 建议把ThreadLocal修饰为static:
ThreadLocal实例在类加载时只初始化一次 - 用完记得手动remove
总结:
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expunaeStaleEntry,cleanSomeSlots,replaceStaleEntrv这三个方法回收键为 null 的 Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法
- 群雄逐鹿起纷争,人各一份天下安
4.9 Java对象内存布局和对象头
4.9.1 面试题
12、你觉得目前面试,你还有那些方面理解的比较好,我没问到的,我说了juc和 jvm以及 同步锁机制
那先说juc吧,说下aqs的大致流程3、
14、cas自旋锁,是获取不到锁就一直自旋吗? cas和synchronized区别在哪里,为什么cas好,具体优势在哪里,我说cas避免cpu切换线程的开销,又问我在自旋的这个线程能保证一直占用cpu吗?假如cpu放弃了这个线程,不是还要带来线程再次抢占cpu的开销?
15、synchronized底层如何实现的,实现同步的时候用到cas了吗?具体哪里用到了
16、我说上个问题的时候说到了对象头,问我对象头存储哪些信息,长度是多少位存储
4.9.2 对象在堆内存中布局
在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header),实例数据(Instance Data),和对齐填充(Padding)
- 对象实例
- 对象头
- 对象标记(Mark Word):HashCode,GC标记,GC存活次数,同步锁标记,偏向锁持有者
- 类元信息(又叫类型指针):指向该对象类元数据(klass)的首地址
- 长度(数组对象特有)
- 实例数据(属性数据)
- 对齐填充(保证8字节的倍数)
- 对象头
在64位系统中,Mark Word占用8字节,类元信息占用8字节,一共是16字节。
Object obj = new Object(); // obj占用16字节的数据
// 类元信息(类型指针) Customer c1 = new Customer(); // c1中的类型指针指向了Customer类class Customer{ int id; // 实例数据 }
JOL查看Java对象内存布局
<!-- JOL依赖 JOL查看Java对象内存布局-->
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version>
</dependency>
- 两种方式
- 开启偏向锁无等待
-XX:BiasedLockingStartupDelay=0 - 使用Sleep
- 开启偏向锁无等待
public class JOLTest {public static void main(String[] args) throws InterruptedException {Object o = new Object();TimeUnit.SECONDS.sleep(5);synchronized (o) {System.out.println(ClassLayout.parseInstance(o).toPrintable());}}
}
(1) 对象标记MarkWord

分代年龄最大为15,因为对象标记(MarkWord)中GC存活次数只占4个bit位,只能表示 0 ~ 15
4.10 synchronized与锁升级
4.10.1 面试题
- 谈谈你对synchronized的理解
- synchronized的锁升级机制是什么?
- 偏向锁和轻量锁有什么区别?
高并发时,同步调用应该去考量锁的性能损耗。能用无锁的数据结构,就不要用锁。能用锁块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能小,避免在锁代码块中调用RPC方法。
- 锁的升级过程
- 无锁 >> 偏向锁 >> 轻量级锁 >> 重量级锁
- 无锁 >> 独占锁 >> 读写锁 >> 邮戳锁
4.10.2 synchronized锁原理
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个ava线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的svnchronized效率低的原因
Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
Montor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。Java对象是天生的Monitor,每一个Java对象都有成为
Monitor的潜质,因为在Java的设计中 ,每一个java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor的本质是依赖于底层操作系统的MutexLock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。
JVM中的同步就是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。
Monitor是由ObjectMonitor实现,而ObjectMonitor是由C++的ObjectMonitor.hpp文件实现,如下所示:
//结构体如下
ObjectMonitor::ObjectMonitor() { _header = NULL; _count = 0; // 该线程获取锁的次数_waiters = 0, _recursions = 0; //线程的重入次数_object = NULL; _owner = NULL; //标识拥有该monitor的线程_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点_WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁进入时的单向链表FreeNext = NULL ; _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点_SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ;
}
Monitor是在ivm底层实现的,底层代码是c++。本质是依赖于底层操作系统的MtexLocK实现,操作系统实现线程之问的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作。
Monitor与Java对象 以及线程是如何关联的?
- 如果一个java对象被某个线程锁住,则该java对象的Mark Word字段中LockWord(重量级锁) 指向monitor的起始地址
- Monitor的Owner字段会存放拥有相关关联对象锁的线程ID。
Mutex Lock 的切换需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。
4.10.3 synchronized锁种类及升级步骤
(1)多线程访问情况
- 只有一个线程访问
- 有两个线程交替访问
- 有多个线程来访问
synchronized 用到锁是存在Java对象头的MarkWord中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
- 偏向锁:MarkWord存储的是偏向的线程ID
- 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
- 重量锁:MarkWord存储的是指向堆中的Monitor对象的指针
(2)升级流程

(3)无锁 001
初始状态: 如果一个对象被实例化之后,如果还没有被任何线程竞争锁,那么它就为无锁状态(001)
(4)偏向锁 101
偏向锁:单线程竞争
当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
主要作用:
- 当一段同步代码块一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时,便会自动获取锁。避免用户态到内核态的频繁切换。
多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况
偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。
备注:
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检査锁的MarkWord里面是不是放的自己的线程ID)。
-
如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程D与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
-
如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,
- 竞争成功,表示之前的线程不存在了, MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁
- 竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
技术实现:
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中判断是否有偏向锁指向本线程的ID,无需再进入ObjectMonitor 去竞争对象了。
举例:
假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改
偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是偏向于第一个获得它的线程的锁。执行完同步代码块后,线程并不会主动释放偏向锁。这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过对象的MarkWord判断,当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。如果自始有终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
结论: JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID, 就标示自己获得了当前锁,不用操作系统接入。
上述就是偏向锁; 在没有其他线程竞争的时候,一直偏向当前线程,当前线程可以一直执行。
默认偏向锁延时为4秒: JVM启动延迟:为了优化启动性能,JVM在启动后的头几秒(默认4秒)内是禁止偏向锁的。在此期间创建的对象,其Mark Word直接是无锁状态(
001),任何锁竞争都会直接触发轻量级锁的升级路径
如果确定锁通常处于竞争状态,则可以通过JVM参数-XX:-UseBiasedLocking关闭偏向锁,那么默认会进入轻量级锁。
偏向锁的撤销:
当有另外的线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁。
竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码) 撤销偏向锁.
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检査持有偏向锁的线程是否还在执行:
- 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。 - 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。
java15之后逐步废弃偏向锁
| JDK 版本 | 偏向锁状态 | 关键变化 |
|---|---|---|
| JDK 15 | 默认禁用 | 通过 JEP 374,将偏向锁的启用参数 (-XX:+UseBiasedLocking) 设置为默认 false,标志着官方开始弃用此功能 。 |
| JDK 17 | 完全移除 | 偏向锁相关的代码被从 HotSpot 虚拟机中彻底移除,相应的 JVM 参数也不再有效 |
为什么移除偏向锁?
- 维护成本高昂:偏向锁的实现逻辑非常复杂,涉及到锁的撤销、批量重偏向、批量撤销等精细操作,给 JVM 的开发和维护带来了巨大负担
- 收益有限:在现代多核和高并发应用成为主流的背景下,能够真正从“只有一个线程访问”这种理想场景中获益的情况越来越少。大多数情况下,锁都存在一定程度的竞争,这使得偏向锁带来的性能提升变得不显著
- 撤销开销:当出现第二个线程尝试获取锁时,就需要撤销偏向锁。这个撤销过程需要进入全局安全点,暂停所有用户线程,其开销有时甚至超过了偏向锁本身带来的收益
移除偏向锁后,synchronized的锁升级路径变得更加简洁和高效: 无锁 >> 轻量级锁 >> 重量级锁
(5)轻量级锁 000
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有了线程阻塞
主要作用:
- 有线程来参与锁的竞争,但是获取锁的冲突时间极短
- 本质就是自旋锁CAS
轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。此时线程B操作中有两种情况:
- 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A →B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位:
- 如果锁获取失败,则偏向锁开级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原拥有偏向锁的线程持有,继续执行其同出
代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
轻量级锁的加锁:
线程栈帧中: JVM会为每个线程在当前线程的方法栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word。
若一个线程获得锁时发现是轻量级锁,会把锁的Markword复制到自己的方法栈帧的Disolaced Mark word果面,然后线程尝试: CAS将锁的MarkWord替换为指向当前线程锁记录的指针。
- 如果成功,当前线程获得锁
- 如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
自旋CAS: 不断尝试去获取锁,能不升级就不往上捅,尽量不要阻塞
轻量级锁的释放:
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞
自旋达到一定程度后:
- Java6之前: 默认启用,默认情况下自旋的次数是10次 ;
-XX:PreBlockSpin=10 - Java6之后:自适应自旋锁
- 如果自旋成功,下次自旋最大次数会增加
- 如果自旋失败,下次自旋最大次数会减少
偏向锁和轻量级锁区别:
- 争夺轻量级锁失败,会尝试自旋抢占锁
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是竞争发生时才释放锁。
(6)重量级锁010
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,其他线程获取不到锁会处于被阻塞的状态(BLOCKED)。除非退出同步块,否则其他线程无法获取到这个Monitor。
(7)总结
-
锁升级后,hashcode去哪了?
- 锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码,
GC年龄了,那么这些信息被移动到哪里去了呢?- 如果处于无锁状态,又计算了hashcode,加锁时,则直接升级为轻量级锁
- 如果处于偏向锁状态,计算hashcode时,会先升级为重量级锁
- 升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
- 重量级锁,mark word存储monitor指针,monitor有字段记录非加锁状态时的Mark Word,释放锁之后写回到对象头中
- 锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码,
-
各种锁的优缺点
- 偏向锁:适用于只有一个线程访问同步代码块
- 轻量级锁:竞争的线程不会阻塞,提高了响应速度。长时间消耗的cpu资源较多。追求响应时间,同步块执行速度非常快的代码。
- 重量级锁:线程阻塞,响应时间慢。追求吞吐量,同步块执行时间长的代码。
-
synchronized锁升级和实现原理
- 先自旋,再升级阻塞
4.10.4 JIT编译器对锁的优化
JIT: Just In Compiler: 即时编译器
(1)锁消除
public void m1(){Object o = new Object();synchronized (o){ // JIT会优化,相当于不存在System.out.println(ClassLayout.parseInstance(o).toPrintable());}
}
(2)锁粗化
public void m1(){Object o = new Object();synchronized (o){ // JIT会优化四个锁为一个锁System.out.println("1");}synchronized (o){ // JIT会优化四个锁为一个锁System.out.println("2");}synchronized (o){ // JIT会优化四个锁为一个锁System.out.println("3");}synchronized (o){ // JIT会优化四个锁为一个锁System.out.println("4");}
}// 以上等同于
public void m1(){Object o = new Object();synchronized (o){ // JIT会优化四个锁为一个锁System.out.println("1");System.out.println("2");System.out.println("3");System.out.println("4");}
}
(3)总结
- 没有锁:自由自在
- 偏向锁:唯我独占
- 轻量锁:楚汉争霸
- 重量锁:群雄逐鹿
4.11 AQS
AbstractQueuedSynchronizer
AQS重要性:
Java => JVM
JUC => AQS
4.11.1 前置知识
- 公平锁和非公平锁
- 可重入锁
- 自旋思想
- LockSupport
- 双向链表数据结构
- 模板设计模式
4.11.2 AQS入门级理论知识
- AQS定义
抽象的队列同步器- 是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题
- 整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量来表示持有锁的状态。
- 为什么AQS是JUC内容的基石
- 与AQS有关的类
- ReentrentLock
- CountDownLatch
- ReentrantReadWriteLock
- Semaphere
- ....
- 锁和同步器的关系
- 锁:面向锁的使用者:隐藏实现细节,调用即可
- 同步器:面向锁的实现者:提出了统一规范并简化了锁的实现,将其抽象出来屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等。是一切锁和同步组件实现的基石--- 公共基础部分。
- 锁:面向锁的使用者:隐藏实现细节,调用即可
- 与AQS有关的类
- 作用
- 加锁会导致阻塞: 有阻塞就需要排队,实现排队就必然需要队列
- 基本结构
- 同步状态
state:volatile int state及相应的CAS操作 - FIFO等待队列:CLH锁队列的变体,是一个虚拟的双向链表(Node节点)
- 模板方法:由子类实现的
tryAcquire,tryRelease等方法
- 同步状态
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了。暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH (FIFO)队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSuppor.park()的方式,维护state变量的状态,使并发达到同步的效果。
public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {//CLH Nodesabstract static class Node {volatile Node prev; // initially attached via casTailvolatile Node next; // visibly nonnull when signallableThread waiter; // visibly nonnull when enqueuedvolatile int status; // written by owner, atomic bit ops by others...}// The synchronization state.private volatile int state;...
}
AQS使用一个volatie的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改
4.11.3 AQS源码分析前置知识储备
AQS内部体系架构:
- 同步状态
state:volatile int state及相应的CAS操作 - FIFO等待队列:CLH锁队列的变体,是一个虚拟的双向链表(Node节点)
- 模板方法:由子类实现的
tryAcquire,tryRelease等方法

4.11.4 AQS源码深度讲解和分析
(1)Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的
public static class ReadLock implements Lock, java.io.Serializable {private final Sync sync;...
}
(2)ReentrantLock的原理
从ReentranLock解读AQS源码
// 构造器
public ReentrantLock() {sync = new NonfairSync(); // 默认非公平锁
}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); // 根据fair来创建
}
// Sync extends AbstractQueuedSynchronizer@ReservedStackAccessprotected final boolean tryRelease(int releases) { // AQS 抽象方法int c = getState() - releases;if (getExclusiveOwnerThread() != Thread.currentThread())throw new IllegalMonitorStateException();boolean free = (c == 0);if (free)setExclusiveOwnerThread(null);setState(c);return free;}
对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()
hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
-
公平锁: 公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
-
非公平锁: 不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还是需要参加竞争锁(存在线程竞争的情况下),后来的线程可能不讲武德插队夺锁了。
(3)从最简单的lock方法开始看公平锁和非公平锁
// ReentrantLock类
public void lock() {sync.lock(); // FairSync extends Sync 非公平锁; NonfairSync extends Sync 公平锁
}// Sync类@ReservedStackAccessfinal void lock() {if (!initialTryLock())acquire(1);}// NonfairSync extends Syncfinal boolean initialTryLock() { // 定义首次尝试获取锁的方法。该方法被标记为 final,防止子类重写。// Sync 抽象方法Thread current = Thread.currentThread();if (compareAndSetState(0, 1)) { // first attempt is unguarded // 尝试CAS将锁状态从0改为1(无锁->锁定)setExclusiveOwnerThread(current); // 成功获取锁:将当前线程设置为独占所有者return true; // 返回成功获取锁} else if (getExclusiveOwnerThread() == current) { // 检查当前线程是否已是锁的持有者(可重入性检查)int c = getState() + 1; // 是持有者:计算新的重入计数(当前状态值+1)if (c < 0) // overflow // 检查是否超过最大重入次数(int溢出)throw new Error("Maximum lock count exceeded"); // 抛出错误:重入次数超过上限setState(c); // 安全更新锁状态(增加重入计数)return true; // 返回重入成功} elsereturn false; // 获取失败:锁被其他线程持有}/** FairSync extends Sync * Acquires only if reentrant or queue is empty.*/final boolean initialTryLock() {Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedThreads() && compareAndSetState(0, 1)) {setExclusiveOwnerThread(current);return true;}} else if (getExclusiveOwnerThread() == current) {if (++c < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(c);return true;}return false;}// AQS类
public final void acquire(int arg) {if (!tryAcquire(arg))acquire(null, arg, false, false, false, 0L);
}// NonfairSync extends Sync 非公平锁类/*** Acquire for non-reentrant cases after initialTryLock prescreen*/protected final boolean tryAcquire(int acquires) { // AQS 抽象方法if (getState() == 0 && compareAndSetState(0, acquires)) { // 锁空闲时直接CAS抢锁,不检查等待队列,体现“插队”非公平性setExclusiveOwnerThread(Thread.currentThread()); // 立即记录当前线程为独占所有者,支持可重入return true;}return false; // 获取锁失败(锁被占用或CAS竞争失败)}// FairSync extends Sync 公平锁类//Acquires only if thread is first waiter or empty protected final boolean tryAcquire(int acquires) {if (getState() == 0 && !hasQueuedPredecessors() && // 锁空闲时,先检查等待队列是否有更早的线程(hasQueuedPredecessors)compareAndSetState(0, acquires)) { // 保证FIFO公平性再进行CAS枪锁setExclusiveOwnerThread(Thread.currentThread()); // 抢锁成功,记录当前线程为独占所有者return true;}return false; // 获取锁失败(锁被占用、有更早等待线程或CAS失败)}// AQS的aquire方法: 入队node.waiter = current;Node t = tail;node.setPrevRelaxed(t); // avoid unnecessary fenceif (t == null)tryInitializeHead();else if (!casTail(t, node))node.setPrevRelaxed(null); // back outelset.next = node;
(4)unlock
// ReentrantLock
public void unlock() {sync.release(1);
}@ReservedStackAccess
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (getExclusiveOwnerThread() != Thread.currentThread())throw new IllegalMonitorStateException();boolean free = (c == 0);if (free)setExclusiveOwnerThread(null);setState(c);return free;
}// AQS 类
public final boolean release(int arg) {if (tryRelease(arg)) {signalNext(head);return true;}return false;
}
(5)总结
- 获取锁流程
- 非公平锁:
- CAS尝试加锁
- 加锁成功返回true,失败返回false
- 公平锁
- 尝试加锁
- 前面有线程排队,加锁失败,将排在队列尾部
- 前面没有线程排队,CAS尝试加锁
- 加锁成功,返回true
- 加锁失败,返回false
- 非公平锁:
- 释放锁流程
- 当前线程不是加锁线程,抛出异常
int c = getStatus() - 1;- c 等于0,则将当前owner置null
- 如果
c != 0, 则status(重入次数) - 1, 并且返回false
4.12 读写锁
无锁 => 独占锁 => 读写锁 => 邮戳锁
ReentrantLock, ReentrantReadWriteLock, StampedLock
4.12.1 面试题
- Java有哪些锁?
- 对于读写锁,锁饥饿问题是什么?
- 有没有比读写锁更快的锁?邮戳锁
- StampedLock知道码?(邮戳锁/票据锁)
- ReentrantReadWriteLock的锁降级机制是什么?
4.12.2 ReentrantReadWriteLock
读写锁的定义:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
class MyResource{Map<String,String> map = new HashMap<>();// Lock lock = new ReentrantLock();ReadWriteLock readWriteLock = new ReentrantReadWriteLock();public void write(String key, String value) throws InterruptedException {// lock.lock();readWriteLock.writeLock().lock();try {System.out.println("正在写入");map.put(key, value);TimeUnit.MILLISECONDS.sleep(50);System.out.println("完成写入");}finally {//lock.unlock();readWriteLock.writeLock().unlock();}}public void read(String key) throws InterruptedException {//lock.lock();readWriteLock.readLock().lock();try {System.out.println("正在读取");map.get(key);TimeUnit.MILLISECONDS.sleep(5000);System.out.println("完成读取");}finally {//lock.unlock();readWriteLock.readLock().unlock();}}
ReentrantReadWriteLock锁降级:将写锁降级为读锁,反之叫锁升级
- 一个线程锁降级流程
- 获取写锁:线程首先获取写锁,确保在修改数据时排它访问。
- 获取读锁:在写锁保持的同时,线程可以再次获取读锁。
- 释放写锁:线程保持读锁的同时释放写锁。
- 释放读锁:最后线程释放读锁。
如果先获取读锁,并且没有释放时,线程是无法获取写锁的。因此ReentrantReadWriteLock无法完成锁升级
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();readWriteLock.writeLock().lock(); // 1. 获取写锁
// 写锁状态
readWriteLock.readLock().lock(); // 2. 获取读锁
readWriteLock.writeLock().unlock();// 3. 释放写锁// 锁降级之后为读锁状态readWriteLock.readLock().unlock();
统一线程先获取写锁再去获取读锁,相当于重入。
4.12.3 StampedLock
- 定义
- StampedLock是JDK1.8新增的一个读写锁,是对ReentrantReadWriteLock的优化。
- 邮戳锁, 也成为票据锁
- stamp(戳记,long类型):代表了锁的状态。当stamp返回0时,表示线程获取锁失败,并且,当释放锁或者转换锁时,都要传入最初获取的stamp值。
- 作用
- 解决锁饥饿:对于短的只读代码块,使用乐观模式通常可以减少争用并提高吞吐量。
- 锁降
锁饥饿案例:假如当前有1000个线程,999个在进行读操作,1个在进行写操作。有可能999个读操作长期获取锁,导致1个写操作长时间获取不到锁。导致锁饥饿
ReentrantReadWriteLock 和 StampedLock对比:
ReentrantReadWVriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态。
读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock文持读并发,读读可以共享
StampedLock
- ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
- 但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
对于短的只读代码块,使用乐观模式通常可以减少争用并提高吞吐量。
(1)特点
-
所有获取锁的方法,都返回一个邮戳
Stamp,Stamp为零表示获取失败,其余都表示成功; -
所有释放锁的方法都需要一个邮戳
Stamp,这个Stamp必须是和成功获取锁时得到的Stamp一致; -
StampedLock是不可重入的,危险 !!!(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁) -
StampedLock有三种访问模式- Reading(悲观读模式):和
ReentrantReadWriteLock.readLock().lock()一样 - Writing(悲观写模式):和
ReentrantReadWriteLock.writeLock().lock()一样 - Optimistic Reading(乐观读模式): 无锁机制,类似于数据库中的乐观锁,支持读写并发,乐观认为读取时没有人修改,如果被修改再升级为悲观读模式
- Reading(悲观读模式):和
乐观读模式:仅当锁定当前未处于写入模式时,方法
tryOptimisticead()才返回非零戳记。如果自获得给定标记以来未在写入模式下获取锁定,则方法validate(long)返回tue。这种模式可以被认为是读锁的极弱版本,可以随时被作者破坏。对短的只读代码段使用乐观模式通常可以减少争,用并提高吞吐量。但是,它的使用本质上是脆弱的。乐观读取部分应该只读取字段并将它们保存在局部变量中,以便以后在验证后使用。在乐观摸式下读取的字段可能非常不一致,因此仅在您熟采数据表示以拾查一致性和/或重复调用方法validate()。例如,在首次读取对象或数组引用,然后访问其中一个字段,元素或方法时,通常需要执行此类步。
(2)代码演示
// writelong writeStamp = stampedLock.writeLock();
//业务代码
stampedLock.unlock(writeStamp);// read
long readStamp = stampedLock.readLock();
stampedLock.unlock(writeStamp);
- 普通读写
public void write(){long stamp = stampedLock.writeLock();System.out.println("WW 写线程准备修改");try{num = num + 1;}finally {stampedLock.unlockWrite(stamp);}System.out.println("WW 写线程修改结束");
}/*** 悲观读*/
public void read(){long stamp = stampedLock.readLock();System.out.println("RR 读线程准备读取");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}try {System.out.println("读线程读取" + num);}finally {stampedLock.unlockRead(stamp);}System.out.println("RR 读线程完成读取");
}
- 乐观读
/*** 乐观读*/
public void optimisticRead(){long stamp = stampedLock.tryOptimisticRead();int result = num;//故意间隔4秒 乐观认为读取中没有其他线程修改过number值,具体靠判断System.out.println("4s前stampedLock.validate方法(true无修改,false有修改): " + stampedLock.validate(stamp));for (int i = 0; i < 4; i++) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("OR 正在读取 " + i + "::" + num);System.out.println("OR STAMP Validate标记:" + stampedLock.validate(stamp));}if (!stampedLock.validate(stamp)) { // 有写操作,可以对其进行手动升级System.out.println("有人修改");long readStamp = stampedLock.readLock();try{System.out.println("OR RR 升级悲观读");result = num;System.out.println("OR RR 悲观读后 result " + result);}finally {stampedLock.unlockRead(readStamp);}}System.out.println("OR 最终值 " + result);
}
(3)缺点
- StampedLock不支持重入,没有Reentrant开头。读写锁原理上都不是可重入锁 ,读锁是伪重入,需要多次释放
- StampedLock的悲观读锁和写锁都不支持条件变量(Condition),
- 使用StampedLock一定不要调用中断操作,即不要调用
interrupt()方法。
4.13 总结
听课 + 实践
zzyybs@126.com
-
CompletableFuture
-
JAVA锁
- 悲观锁
- 乐观锁
- 自旋锁
- 可重入锁
- 写锁/读锁
- 公平锁/非公平锁
- 死锁
- 偏向锁
- 轻量锁
- 重量锁
- 邮戳锁
-
JMM
-
synchronized及升级优化
- 锁是什么
- 无锁 ->偏向锁 -> 轻量锁 -> 重量锁
- 对象布局
- 对象头
- mark word
- 无锁:基础数据 + 001
- 偏向锁:线程ID + 101
- 轻量锁:线程记录指针 + 000
- 重量锁:monitor指针 + 010
- 类型指针(模板类对象)
- 长度(数组特有)
- mark word
- 实例指针
- 对其填充 8字节倍数
- 对象头
-
CAS
- 定义:比较并交换
- 自旋锁:循环 + CAS
-
volatile
- 特性
- 可见性
- 禁止指令重排
- 内存屏障
- 读屏障
- 写屏障
- 特性
-
LockSupport和线程中断
-
AQS
-
ThreadLocal
-
原子类