文章目录
- JUC笔记2
- 练习题:手写线程池
- 代码解释:
- AdvancedThreadPool 类:
- WorkerThread 内部类:
- AdvancedThreadPoolExample 类:
- 线程池的思考
- CPU密集型
- IO密集型
- 练习题:手写自动重试机
- 练习题:手写定时任务机
- Job封装
- 定时任务执行
- 附录1:常见函数式接口
- 附录2:ForkJoin
- 1. ForkJoin 框架概述
- 2. 核心组件
- 2.1 ForkJoinPool
- 2.2 ForkJoinTask
- 3. 使用案例
- 3.1 计算数组元素之和(使用 `RecursiveTask`)
- 3.2 打印数组元素(使用 `RecursiveAction`)
- 4. 总结
- 附录3:异步回调
- 重点:JMM,VOLATILE,单例,CAS,原子引用,各种锁的深入理解
- JMM(Java Memory Model)
- 实际的执行模型参考
- 现在考虑更复杂一点的状况,B线程对Flag做了修改
- VOLATILE
- 单例
- 1. 饿汉式(静态常量)
- 2. 饿汉式(静态代码块)
- 3. 懒汉式(线程不安全)
- 4. 懒汉式(同步方法,线程安全)
- 5. 双重检查锁定(Double-Checked Locking,线程安全)
- 6. 静态内部类(线程安全)
- 7. 枚举(线程安全)
- 实现原理
- 代码示例
- 调用示例
- 代码解释
- 优势
- 总结
- 附录:双重检查锁定的详细解释
- CAS
- 基本概念
- 原理
- 在 Java 中的实现
- CAS 在 JUC 中的应用
- 优缺点
- 优点
- 缺点
- 附录:unsafe的底层C++
- 原子引用
- 常见锁的提法
- 公平与非公平锁
- 乐观锁与悲观锁
- 可重入锁
- 偏向锁
- 自旋锁
- 表锁行锁间隙锁及MVCC
- 死锁及排查
JUC笔记2
练习题:手写线程池
注意,要实现的目标如下:
- 核心线程不会在空闲时自动死亡,而辅助线程会在空闲一段时间后自动消亡
- 任务添加失败时会自旋重新尝试添加任务若干次,等待时间和重试次数可自定义
- 线程池可以手动强停
- 线程池的核心线程数可以动态调整(缩减时需要格外注意)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
// 自定义线程池类
class AdvancedThreadPool {
private int corePoolSize;
private int maximumPoolSize;
private final BlockingQueue<Runnable> taskQueue;
private final List<WorkerThread> workerThreads;
private boolean isShutdown = false;
private int retryCount;
private long retryDelay;
private long keepAliveTime;
// 构造函数,初始化线程池
public AdvancedThreadPool(int corePoolSize, int maximumPoolSize, int queueCapacity, int retryCount, long retryDelay, long keepAliveTime) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.taskQueue = new LinkedBlockingQueue<>(queueCapacity);
this.workerThreads = new ArrayList<>(maximumPoolSize);
this.retryCount = retryCount;
this.retryDelay = retryDelay;
this.keepAliveTime = keepAliveTime;
// 初始化核心线程
for (int i = 0; i < corePoolSize; i++) {
WorkerThread worker = new WorkerThread(true);
workerThreads.add(worker);
worker.start();
}
}
// 动态设置核心线程数
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0) {
throw new IllegalArgumentException("核心线程数不能为负数");
}
this.corePoolSize = corePoolSize;
adjustThreadCount();
}
// 动态设置最大线程数
public void setMaximumPoolSize(int maximumPoolSize) {
if (maximumPoolSize < 0) {
throw new IllegalArgumentException("最大线程数不能为负数");
}
this.maximumPoolSize = maximumPoolSize;
adjustThreadCount();
}
// 调整线程数量
private void adjustThreadCount() {
int currentSize = workerThreads.size();
if (corePoolSize > currentSize) {
// 需要增加线程
for (int i = currentSize; i < corePoolSize; i++) {
WorkerThread worker = new WorkerThread(true);
workerThreads.add(worker);
worker.start();
}
} else if (corePoolSize < currentSize) {
// 需要减少线程(高危操作,实际生产中未必需要这个部分)
Iterator<WorkerThread> iterator = workerThreads.iterator();
int removedCount = 0;
while (iterator.hasNext() && removedCount < currentSize - corePoolSize) {
WorkerThread worker = iterator.next();
if (!worker.isCore) {
worker.interrupt();
iterator.remove();
removedCount++;
}
}
}
}
// 向线程池提交任务
public void submit(Runnable task) {
if (isShutdown) {
throw new IllegalStateException("线程池已关闭,不能再提交任务。");
}
int attempts = 0;
while (attempts < retryCount) {
try {
if (taskQueue.offer(task, retryDelay, TimeUnit.MILLISECONDS)) {
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
attempts++;
}
if (attempts == retryCount) {
if (workerThreads.size() < maximumPoolSize) {
// 创建新的辅助线程来处理任务
WorkerThread worker = new WorkerThread(false);
workerThreads.add(worker);
worker.start();
try {
taskQueue.put(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
throw new RuntimeException("任务队列已满,且达到最大线程数,无法处理任务。");
}
}
}
// 关闭线程池
public void shutdown() {
isShutdown = true;
for (WorkerThread worker : workerThreads) {
worker.interrupt();
}
}
// 工作线程类,负责从任务队列中取出任务并执行
private class WorkerThread extends Thread {
private final boolean isCore;
public WorkerThread(boolean isCore) {
this.isCore = isCore;
}
@Override
public void run() {
while (!isShutdown ||!taskQueue.isEmpty()) {
Runnable task = null;
try {
if (isCore) {
// 核心线程使用 take 方法,会一直阻塞直到有任务
task = taskQueue.take();
} else {
// 辅助线程使用 poll 方法,在空闲时间后没有任务则退出
task = taskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS);
if (task == null) {
// 辅助线程空闲超时,从线程列表中移除自身
synchronized (workerThreads) {
workerThreads.remove(this);
}
break;
}
}
} catch (InterruptedException e) {
// 如果线程被中断,跳出循环
break;
}
if (task != null) {
try {
task.run();
} catch (Exception e) {
// 处理任务执行时的异常
e.printStackTrace();
}
}
}
}
}
}
// 测试线程池的使用
public class AdvancedThreadPoolExample {
public static void main(String[] args) {
// 创建一个线程池,核心线程数为 2,最大线程数为 5,任务队列容量为 3,重试次数为 3,重试延迟为 500 毫秒,辅助线程空闲存活时间为 2 秒
AdvancedThreadPool threadPool = new AdvancedThreadPool(2, 5, 3, 3, 500, 2000);
// 提交 10 个任务到线程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
threadPool.submit(() -> {
System.out.println("正在执行任务: " + taskId + ",线程: " + Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 动态调整核心线程数
threadPool.setCorePoolSize(3);
try {
// 等待一段时间,让辅助线程有机会空闲消亡
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 关闭线程池
threadPool.shutdown();
}
}
代码解释:
AdvancedThreadPool 类:
-
新增成员变量:
keepAliveTime
:辅助线程的空闲存活时间,当辅助线程空闲时间超过该值时,会自动退出。
-
构造函数:
- 增加了
keepAliveTime
参数的初始化,并创建核心线程。
- 增加了
-
adjustThreadCount()
方法:- 在调整线程数量时,优先移除辅助线程,以保证核心线程不被误移除。
-
submit(Runnable task)
方法:- 与之前的实现相同,处理任务提交和重试逻辑。
-
shutdown()
方法:- 标记线程池为关闭状态,并中断所有工作线程。
WorkerThread 内部类:
-
isCore
字段:- 用于标记该线程是核心线程还是辅助线程。
-
run()
方法:- 核心线程使用
taskQueue.take()
方法,该方法会一直阻塞直到有任务到来,因此核心线程不会因空闲而死亡。 - 辅助线程使用
taskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS)
方法,在空闲keepAliveTime
时间后,如果没有任务则退出,并从workerThreads
列表中移除自身。
- 核心线程使用
AdvancedThreadPoolExample 类:
- 创建线程池时传入
keepAliveTime
参数。 - 提交任务并动态调整核心线程数。
- 主线程休眠一段时间,让辅助线程有机会空闲消亡。
- 最后关闭线程池。
线程池的思考
究竟该怎么决定核心线程数是多少? 此处特别感谢B站UP@狂神说Java 在JUC相关视频中关于核心线程数的一些考量
CPU密集型
直接把CPU的核数作为最大线程数:
new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(), // 根据最大核心数决定
3,
TimeUnit.SECONDS,
// 阻塞队列,
// ThreadFactory
// 淘汰策略
)
IO密集型
很好理解,你的项目里有若干个,比如K个需要IO的大任务,这些任务的总数拿来设定最大核心数,起码保证这些任务不崩
练习题:手写自动重试机
目标:
- 允许泛型出入参
- 允许自定义循环重试的条件
- 允许重试开始前自定义回调函数
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 用于自动重试某个函数,T为入参,R为返回值
*
* @param <T> action入参
* @param <R> 返回值
*/
public class MineActionUpgrade<T, R> {
private static final Logger LOGGER = Logger.getLogger(MineActionUpgrade.class.getName());
/**
* 待执行函数
*/
private final Function<T, R> action;
/**
* 重试次数
*/
private final int retryLimitTimes;
/**
* 重试间隔
*/
private final long retryInterval;
/**
* 重试判断函数
*/
private final Function<Optional<R>, Boolean> retryCondition;
/**
* 重试前回调函数
*/
private final Function<Integer, Void> beforeRetryCallback;
public MineActionUpgrade(Function<T, R> action, int retryLimitTimes, long retryInterval) {
this(action, retryLimitTimes, retryInterval, Optional::isEmpty, null);
}
public MineActionUpgrade(Function<T, R> action, int retryLimitTimes, long retryInterval,
Function<Optional<R>, Boolean> retryCondition,
Function<Integer, Void> beforeRetryCallback) {
if (action == null) {
throw new IllegalArgumentException("待执行体为null");
}
if (retryLimitTimes <= 0) {
throw new IllegalArgumentException("可重试次数不合法");
}
if (retryInterval <= 0) {
throw new IllegalArgumentException("重试间隔不合法");
}
this.action = action;
this.retryLimitTimes = retryLimitTimes;
this.retryInterval = retryInterval;
this.retryCondition = Objects.requireNonNullElse(retryCondition, Optional::isEmpty);
this.beforeRetryCallback = beforeRetryCallback;
}
/**
* 自动执行
*
* @param param 执行参数
* @return 返回值
*/
public Optional<R> exec(T param) {
for (int i = 0; i < retryLimitTimes; i++) {
if (i > 0 && beforeRetryCallback != null) {
beforeRetryCallback.apply(i);
}
Optional<R> result = execOnce(param);
if (!retryCondition.apply(result)) {
return result;
}
try {
TimeUnit.MILLISECONDS.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("执行过程中线程被中断", e);
}
}
throw new RuntimeException("函数重试次数耗尽仍未获得结果");
}
private Optional<R> execOnce(T param) {
R res = null;
try {
R apply = action.apply(param);
if (Objects.nonNull(apply)) {
res = apply;
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "执行失败,参数为:" + param, e);
}
return Optional.ofNullable(res);
}
}
练习题:手写定时任务机
特别地:此处感谢B站UP提供的思路@学习Java的基尔兽
目标:
- 支持自定义时间定时执行某个函数部分
Job封装
import java.util.Objects;
/**
* 任务封装
*/
public class Job implements Comparable<Job> {
/**
* 待执行任务
*/
private Runnable task;
/**
* 下次开始时间
*/
private long startTime;
/**
* 需要等待时间
*/
private long delay;
public Runnable getTask() {
return task;
}
public long getStartTime() {
return startTime;
}
public long getDelay() {
return delay;
}
private Job() {
}
public Job(Runnable task, long startTime, long delay) {
if (Objects.isNull(task)) {
throw new IllegalArgumentException("待执行任务不能为Null");
}
if (startTime <= 0) {
throw new IllegalArgumentException("开始时间非法");
}
if (delay <= 0) {
throw new IllegalArgumentException("等待时间非法");
}
this.task = task;
this.startTime = startTime;
this.delay = delay;
}
/**
* 用于排序任务
*
* @param o the object to be compared.
* @return 排序结果
*/
@Override
public int compareTo(Job o) {
return Long.compare(this.startTime, o.startTime);
}
}
定时任务执行
public class MineSchedule {
private final ExecutorService service = Executors.newFixedThreadPool(6);
private final Trigger trigger = new Trigger();
class Trigger {
/**
* 优先级队列,会自动排序
*/
PriorityBlockingQueue<Job> queue = new PriorityBlockingQueue<>();
Thread machine = new Thread(() -> {
while (true) {
while (queue.isEmpty()) {
LockSupport.park();
}
Job latelyJob = queue.peek();
if (latelyJob.getStartTime() < System.currentTimeMillis()) {
latelyJob = queue.poll();
if (latelyJob != null) {
service.execute(latelyJob.getTask());
queue.offer(rebuildJob(latelyJob));
}
} else {
LockSupport.parkUntil(latelyJob.getStartTime());
}
}
});
{
machine.start();
System.out.println("触发器启动");
}
void wakeUp() {
LockSupport.unpark(machine);
}
private Job rebuildJob(Job old) {
return new Job(old.getTask(), old.getStartTime() + old.getDelay(), old.getDelay());
}
}
/**
* 每隔delay毫秒数,自动执行一次task
*
* @param task 需要周期执行的任务
* @param delay 延迟时间
*/
public void schedule(Runnable task, long delay) {
// 最开始的想法,搞一个线程池,每次有新任务的时候把任务丢进去,睡delay毫秒后执行
// 但是这是有问题的,线程耗尽就完了,而且线程不可复用,创建线程消耗资源很大
// 那我们就考虑这么一种设计:
// 1. 有一个定时触发器,每隔delay时间被唤醒,然后去尝试执行任务
// 2. 线程池只负责执行任务,不负责处理时间
// 那么这个触发器需要什么信息呢?第一,所有需要执行的任务,第二,需要delay的时间
// 那么我们封装一个Job类,专门用来记录任务和时间
// 再写一个trigger,用于时间触发
Job job = new Job(task, System.currentTimeMillis(), delay);
trigger.queue.offer(job);
trigger.wakeUp();
}
}
附录1:常见函数式接口
Function: 自定义一个带有出入参的接口
Consumer:自定义一个消费接口,只有入参
Supplier:自定义一个生产接口,只有出参
Predicate:自定义一个判断接口,只返回布尔值
附录2:ForkJoin
1. ForkJoin 框架概述
ForkJoin
框架是 Java 7 引入的一个并行执行框架,它主要用于处理可以递归分解成更小任务的计算密集型任务。其核心思想是将一个大任务拆分成多个小任务(fork
操作),并行地执行这些小任务,然后将小任务的结果合并(join
操作)得到最终结果。ForkJoin
框架利用多线程并行计算,充分发挥多核处理器的性能,提高程序的执行效率。
2. 核心组件
2.1 ForkJoinPool
ForkJoinPool
是 ForkJoin
框架的核心,它是一个特殊的线程池,用于管理和执行 ForkJoinTask
任务。ForkJoinPool
采用工作窃取算法(Work-Stealing Algorithm),每个工作线程都有自己的双端队列(Deque),当一个线程完成了自己队列中的任务后,它可以从其他线程的队列尾部窃取任务来执行,从而充分利用线程资源,提高并行度。
2.2 ForkJoinTask
ForkJoinTask
是所有任务的抽象基类,它有两个重要的子类:
RecursiveAction
:用于没有返回值的任务。RecursiveTask
:用于有返回值的任务。
3. 使用案例
3.1 计算数组元素之和(使用 RecursiveTask
)
下面是一个使用 ForkJoin
框架计算数组元素之和的示例代码:
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
// 继承 RecursiveTask 用于有返回值的任务
class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 10;
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
// 任务足够小,直接计算
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// 任务过大,拆分任务
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
// 执行左子任务
leftTask.fork();
// 执行右子任务并获取结果
int rightResult = rightTask.compute();
// 获取左子任务的结果
int leftResult = leftTask.join();
// 合并结果
return leftResult + rightResult;
}
}
}
public class ForkJoinSumExample {
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
// 创建 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建任务
SumTask sumTask = new SumTask(array, 0, array.length);
// 执行任务并获取结果
int result = forkJoinPool.invoke(sumTask);
System.out.println("数组元素之和为: " + result);
}
}
代码解释:
SumTask
类:继承自RecursiveTask<Integer>
,表示这是一个有返回值的任务,返回值类型为Integer
。compute
方法:该方法是任务的核心逻辑,首先判断任务是否足够小(即元素数量是否小于等于阈值THRESHOLD
),如果足够小则直接计算元素之和;否则,将任务拆分成两个子任务(左子任务和右子任务),分别调用fork()
方法异步执行左子任务,调用compute()
方法同步执行右子任务,最后调用join()
方法获取左子任务的结果,并将两个结果合并。ForkJoinPool
:创建一个ForkJoinPool
实例,调用invoke()
方法执行任务并获取最终结果。
3.2 打印数组元素(使用 RecursiveAction
)
下面是一个使用 RecursiveAction
打印数组元素的示例代码:
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
// 继承 RecursiveAction 用于无返回值的任务
class PrintTask extends RecursiveAction {
private static final int THRESHOLD = 10;
private int[] array;
private int start;
private int end;
public PrintTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected void compute() {
if (end - start <= THRESHOLD) {
// 任务足够小,直接打印元素
for (int i = start; i < end; i++) {
System.out.print(array[i] + " ");
}
} else {
// 任务过大,拆分任务
int mid = (start + end) / 2;
PrintTask leftTask = new PrintTask(array, start, mid);
PrintTask rightTask = new PrintTask(array, mid, end);
// 执行左子任务
leftTask.fork();
// 执行右子任务
rightTask.compute();
// 等待左子任务完成
leftTask.join();
}
}
}
public class ForkJoinPrintExample {
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
// 创建 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建任务
PrintTask printTask = new PrintTask(array, 0, array.length);
// 执行任务
forkJoinPool.invoke(printTask);
}
}
代码解释:
PrintTask
类:继承自RecursiveAction
,表示这是一个没有返回值的任务。compute
方法:与SumTask
类似,首先判断任务是否足够小,如果足够小则直接打印元素;否则,将任务拆分成两个子任务,分别调用fork()
方法异步执行左子任务,调用compute()
方法同步执行右子任务,最后调用join()
方法等待左子任务完成。ForkJoinPool
:创建一个ForkJoinPool
实例,调用invoke()
方法执行任务。
4. 总结
ForkJoin
框架适用于处理可以递归分解的计算密集型任务,通过并行执行小任务和合并结果,可以充分利用多核处理器的性能。在使用时,需要根据任务是否有返回值选择继承 RecursiveTask
或 RecursiveAction
,并在 compute
方法中实现任务的拆分和合并逻辑。
附录3:异步回调
其实就是Future,我们在Callable中已经使用过
这里要说的是实际开发中大家经常用CompletableFuture,用起来很简单很爽,但是这个东西是有隐患的
import java.util.concurrent.CompletableFuture;
public class test {
public static void main(String[] args) {
// 常见但有问题的用法
CompletableFuture.runAsync(() -> {
// 代码
});
}
}
这里的问题在于CompletableFuture只给一个Runnable作为参数那么它会直接使用JVM的公共线程池,这对项目的压力是很大的,并发拉上来以后,压力会均匀传导到所有功能上拖慢服务。
正确的用法:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test {
public static void main(String[] args) {
// 自定义线程池。注意,这里是为了简便所以用newFixedThreadPool,实际工程中应当根据情况使用ThreadPoolExecutor来手动创建可靠线程池,避免压力传导
ExecutorService service = Executors.newFixedThreadPool(6);
CompletableFuture.runAsync(() -> {
// 代码
}, service);
}
}
重点:JMM,VOLATILE,单例,CAS,原子引用,各种锁的深入理解
JMM(Java Memory Model)
是一个虚拟概念,并不实际存在,用于模拟JVM的内存模型,是一种约定而非实在。相关资料可以参考:
JMM参考文档1
JMM参考文档2
JMM可以做到什么?
- 保证可见性
- 不保证原子性
- 禁止指令重排
指令重排:开发者所写的程序,会被编译器编译到字节码
过程为:源码->编译器优化重排->指令并行重排->内存系统重排->执行。
使用volatile可以禁止代码重新排序。这个部分一般不会问到,牵涉到C++的一些功能,和java的关系有但是不那么大
约定的内容:
-
当线程解锁前,必须把共享变量立刻刷回:a.变量存储在主存中;b.线程需要使用变量;c.线程把主存中的变量拷贝到线程的工作空间中;d.线程修改变量;e.线程即将释放,马上把修改后的变量刷回
-
当线程加锁前,必须读取主存的最新值到工作空间
-
加解锁必须是同一把锁
实际的执行模型参考
其中,read和load是一组,Use和Assign是一组,write和store是一组,同组操作不可分割。
除了这三组操作以外,我们还需要注意Lock和UnLock,这两个操作没有出现在图中,但多线程时必定使用,下面说。
现在考虑更复杂一点的状况,B线程对Flag做了修改
B线程把Flag修改为false,但A这里仍然在使用True,这就会出现问题。
/**
* 实际案例看没有volatile,变量会出什么问题
*/
public class TestJmm01 {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (num == 0) {
}
}).start();
TimeUnit.SECONDS.sleep(1);
// 由于num对上面的Thread不可见,所以虽然我们把num修改为1,但线程仍然无限死循环
num = 1;
System.out.println(num);
}
}
这时候VOLATILE就有用了
public class TestJmm02 {
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (num == 0) {
}
}).start();
TimeUnit.SECONDS.sleep(1);
// 由于num对上面的Thread不可见,所以虽然我们把num修改为1,但线程仍然无限死循环
num = 1;
System.out.println(num);
}
}
VOLATILE
轻量级的同步机制,不能保证原子性
如上所述,Volatile标记的变量只能保证自己对所有线程都可见,但不能确保在一组操作中,可以让一组操作都成功或失败
可以从下面这个案例看到:
图中add函数虽然只是num++一行代码,但是在字节码中我们很容易发现,这是一个复合操作,分为读,改,写回几步,这个过程中并没有事务特性,不保证原子性。
那这里如果我们就是不想用synchronized或者lock,应该怎么办呢?
用atomicInteger,原子整型。
为什么原子类可以做到?因为底层是UnSafe,用native方法通过CAS实现的。这个部分在下面说。
单例
在 Java 中,单例模式是一种常见的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。为了保证在多线程环境下也能正确地创建和使用单例实例,需要实现线程安全的单例模式。以下是几种常见的实现方式:
1. 饿汉式(静态常量)
public class Singleton {
// 静态常量,在类加载时就创建实例
private static final Singleton INSTANCE = new Singleton();
// 私有构造函数,防止外部实例化
private Singleton() {}
// 提供公共的静态方法获取实例
public static Singleton getInstance() {
return INSTANCE;
}
}
解释:
- 这种方式在类加载时就创建了单例实例,由于类加载过程是线程安全的,所以这种实现方式天然就是线程安全的。
- 缺点是如果这个单例实例在程序运行过程中可能不会被使用,那么它会一直占用内存,造成资源浪费。
2. 饿汉式(静态代码块)
public class Singleton {
private static Singleton INSTANCE;
static {
// 在静态代码块中创建实例
INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
解释:
- 同样是在类加载时创建实例,利用静态代码块的特性保证线程安全。
- 缺点和静态常量方式一样,可能会造成资源浪费。
3. 懒汉式(线程不安全)
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
解释:
- 这种方式在第一次调用
getInstance()
方法时才创建实例,实现了懒加载。 - 但在多线程环境下,可能会有多个线程同时进入
if (INSTANCE == null)
语句块,从而创建多个实例,因此是线程不安全的。
4. 懒汉式(同步方法,线程安全)
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
// 使用 synchronized 关键字保证线程安全
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
解释:
- 通过在
getInstance()
方法上添加synchronized
关键字,保证了同一时间只有一个线程可以进入该方法,从而避免了多线程创建多个实例的问题。 - 缺点是每次调用
getInstance()
方法都需要进行同步,会带来一定的性能开销。
5. 双重检查锁定(Double-Checked Locking,线程安全)
public class Singleton {
// 使用 volatile 关键字保证可见性和禁止指令重排序
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
解释:
- 先进行一次非同步的检查,如果实例已经存在则直接返回,避免了每次调用都进行同步的性能开销。
- 当第一次检查发现实例不存在时,进入同步代码块,再次检查实例是否存在,这是为了防止多个线程同时通过第一次检查,从而保证只创建一个实例。
- 使用
volatile
关键字是为了禁止指令重排序,防止在实例还未完全初始化时就被其他线程使用。
6. 静态内部类(线程安全)
public class Singleton {
private Singleton() {}
// 静态内部类,在类加载时创建实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
解释:
- 利用静态内部类的特性,当外部类被加载时,静态内部类不会被加载,只有当调用
getInstance()
方法时,才会加载静态内部类并创建实例,实现了懒加载。 - 由于类加载过程是线程安全的,所以这种方式也是线程安全的。
7. 枚举(线程安全)
public enum Singleton {
INSTANCE;
// 可以添加其他方法
public void doSomething() {
System.out.println("Doing something...");
}
}
解释:
- 枚举类型的实例是由 Java 虚拟机保证线程安全和唯一性的,是实现单例模式的最佳方式之一。
- 枚举还可以防止反序列化重新创建新的对象,避免了单例模式被破坏的问题。
综上所述,推荐使用双重检查锁定、静态内部类或枚举的方式来实现线程安全的单例模式,它们在性能和线程安全方面都有较好的表现。
在 Java 中,使用枚举实现线程安全的单例模式是一种简单、高效且安全的方式。以下为你详细介绍如何正确使用枚举实现单例模式,包含原理、代码示例、调用示例及优势说明。
实现原理
Java 的枚举类型本质上是一种特殊的类,它的实例是全局唯一且在类加载时由 JVM 保证线程安全地创建。这意味着枚举实例的创建过程是线程安全的,并且不会被反射或反序列化破坏,非常适合用于实现单例模式。
代码示例
// 定义一个枚举类型作为单例类
public enum Singleton {
// 定义单例实例
INSTANCE;
// 可以在枚举中添加其他方法
public void doSomething() {
System.out.println("单例实例正在执行操作...");
}
}
调用示例
public class Main {
public static void main(String[] args) {
// 获取单例实例
Singleton singleton = Singleton.INSTANCE;
// 调用单例实例的方法
singleton.doSomething();
// 验证单例的唯一性
Singleton anotherSingleton = Singleton.INSTANCE;
System.out.println("两个实例是否相同: " + (singleton == anotherSingleton));
}
}
代码解释
- 枚举定义:
public enum Singleton
定义了一个名为Singleton
的枚举类型。 - 单例实例:
INSTANCE
是枚举类型的一个实例,由于枚举类型的特性,它是全局唯一的。 - 方法定义:
doSomething()
是一个自定义的方法,用于演示单例实例可以执行的操作。 - 调用单例:在
Main
类的main
方法中,通过Singleton.INSTANCE
获取单例实例,并调用其doSomething()
方法。 - 验证唯一性:通过比较两个获取到的实例是否相同,验证了单例的唯一性。
优势
- 线程安全:枚举实例的创建由 JVM 保证线程安全,无需额外的同步措施。
- 防止反射攻击:Java 的反射机制无法实例化枚举类型,避免了通过反射破坏单例的问题。
- 防止反序列化破坏:枚举类型在反序列化时,JVM 会确保返回的是原有的枚举实例,而不是创建新的实例,保证了单例的唯一性。
总结
使用枚举实现单例模式简洁、安全,并且能有效避免多线程环境下的各种问题。在实际开发中,若需要实现单例模式,推荐优先考虑使用枚举方式。
附录:双重检查锁定的详细解释
双重检查锁定是一种用于实现线程安全的单例模式的方法。它的基本思路是在获取单例实例时,先进行一次非同步的检查,如果实例还未创建,再进入同步块进行第二次检查并创建实例,以此来减少同步带来的性能开销。以下是双重检查锁定实现单例模式的基本代码框架:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
在上述代码中,如果不使用 volatile
关键字修饰 instance
变量,可能会出现实例还未完全初始化就被其他线程使用的问题,这主要和 Java 中的指令重排序有关。
指令重排序原理
在 Java 中,为了提高程序的执行效率,编译器和处理器会对指令进行重排序。instance = new Singleton();
这行代码在底层实际上会分为三个步骤:
- 分配内存空间:为
Singleton
对象分配内存。 - 初始化对象:调用
Singleton
的构造函数,对对象进行初始化。 - 将引用指向对象:将
instance
引用指向刚分配的内存地址。
正常情况下,步骤的执行顺序是 1 -> 2 -> 3。但由于指令重排序,步骤 2 和步骤 3 的执行顺序可能会被交换,变成 1 -> 3 -> 2。
假设线程 A 进入同步块创建 Singleton
实例,由于指令重排序,线程 A 先执行了步骤 1 和步骤 3,此时 instance
已经指向了分配的内存地址,但对象还未完成初始化(步骤 2 未执行)。
这时,线程 B 执行第一次检查 if (instance == null)
,发现 instance
不为 null
,就会直接返回 instance
,并使用这个未完全初始化的对象,从而导致程序出现错误。
当使用 volatile
关键字修饰 instance
变量时,volatile
会禁止指令重排序,保证步骤 2 一定在步骤 3 之前执行。即保证对象在初始化完成后,instance
引用才会指向该对象。以下是添加 volatile
关键字后的代码:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
在这个代码中,volatile
关键字确保了在 instance
引用指向对象时,对象已经完成了初始化。这样,当其他线程通过第一次检查发现 instance
不为 null
时,获取到的就是一个已经完全初始化的对象,避免了使用未完全初始化的实例而导致的问题。
综上所述,volatile
关键字在双重检查锁定的单例模式中是必不可少的,它通过禁止指令重排序保证了单例实例在被其他线程使用时已经完全初始化。
CAS
在 Java 中,CAS(Compare-And-Swap)是一种实现并发算法时常用到的技术,它是一种无锁的原子操作,被广泛应用于多线程编程中以实现高效的并发控制。下面将从基本概念、原理、在 Java 中的实现、优缺点等方面详细分析说明 Java 中的 CAS。
基本概念
CAS 是一种乐观锁的实现机制,它假设在并发环境下,大多数情况下不会发生冲突,因此在进行操作时不会加锁。CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置 V 的值等于预期原值 A 时,才会将该位置的值更新为新值 B;否则,不做任何操作。一般情况下,“更新”是一个原子操作,这个过程是不可被中断的。
原理
CAS 操作是由 CPU 提供的原子指令来实现的,不同的 CPU 架构可能有不同的指令来支持 CAS 操作,例如 x86 架构中的 cmpxchg
指令。Java 中的 CAS 操作是基于底层的 CPU 指令实现的,因此具有很高的性能。
CAS 的基本流程如下:
- 读取内存位置 V 的当前值。
- 比较该值是否等于预期原值 A。
- 如果相等,则将内存位置 V 的值更新为新值 B;如果不相等,则表示有其他线程已经修改了该位置的值,当前操作失败,通常会重试或者放弃操作。
在 Java 中的实现
在 Java 中,java.util.concurrent.atomic
包提供了一系列基于 CAS 实现的原子类,例如 AtomicInteger
、AtomicLong
、AtomicReference
等,这些类可以在多线程环境下安全地进行原子操作。
以下是一个使用 AtomicInteger
的示例:
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
public static void main(String[] args) {
// 创建一个 AtomicInteger 对象,初始值为 0
AtomicInteger atomicInteger = new AtomicInteger(0);
// 获取当前值
int expectedValue = atomicInteger.get();
// 新值
int newValue = expectedValue + 1;
// 使用 CAS 操作更新值
boolean success = atomicInteger.compareAndSet(expectedValue, newValue);
if (success) {
System.out.println("CAS 操作成功,新值为: " + atomicInteger.get());
} else {
System.out.println("CAS 操作失败,当前值为: " + atomicInteger.get());
}
}
}
在上述示例中,compareAndSet
方法就是一个 CAS 操作,它会比较 atomicInteger
的当前值是否等于 expectedValue
,如果相等则将其更新为 newValue
,并返回 true
;否则返回 false
。
CAS 在 JUC 中的应用
Java 并发包(JUC,java.util.concurrent
)中的很多类都使用了 CAS 技术来实现高效的并发控制,例如 ReentrantLock
中的 AbstractQueuedSynchronizer
(AQS)、ConcurrentHashMap
等。
以 ReentrantLock
为例,它的底层使用了 AQS,而 AQS 在更新同步状态时就使用了 CAS 操作,保证了在多线程环境下对同步状态的原子更新,避免了使用传统的锁机制带来的性能开销。
优缺点
优点
- 无锁并发:CAS 是一种无锁的并发机制,避免了传统锁机制带来的线程阻塞和上下文切换开销,因此在高并发场景下具有更好的性能。
- 原子性:CAS 操作是原子的,由 CPU 指令保证,不会被其他线程中断,确保了数据的一致性。
- 乐观锁思想:基于乐观锁的思想,假设大多数情况下不会发生冲突,减少了锁的使用,提高了并发性能。
缺点
- ABA 问题:CAS 操作只比较值是否相等,当一个值从 A 变为 B 再变回 A 时,CAS 操作会认为值没有发生变化,但实际上中间已经发生了修改。在某些场景下,这种变化可能会对程序的逻辑产生影响。可以使用
AtomicStampedReference
或AtomicMarkableReference
来解决 ABA 问题。 - 自旋开销:当 CAS 操作失败时,通常会进行重试(自旋),如果长时间自旋会消耗大量的 CPU 资源,特别是在竞争激烈的情况下。
- 只能保证一个变量的原子操作:CAS 只能保证对一个变量的原子操作,如果需要对多个变量进行原子操作,CAS 就无法直接满足需求,可能需要使用锁或者其他并发机制。
综上所述,CAS 是 Java 中一种非常重要的并发技术,它在提高并发性能方面具有很大的优势,但也存在一些缺点,在使用时需要根据具体的场景进行权衡和选择。
多线程代码案例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
// 定义一个任务类,用于对计数器进行自增操作
class IncrementTask implements Runnable {
private final AtomicInteger counter;
private final int incrementCount;
public IncrementTask(AtomicInteger counter, int incrementCount) {
this.counter = counter;
this.incrementCount = incrementCount;
}
@Override
public void run() {
for (int i = 0; i < incrementCount; i++) {
// 循环进行 CAS 操作,直到成功
while (true) {
int current = counter.get();
int next = current + 1;
// 使用 CAS 操作尝试更新计数器的值
if (counter.compareAndSet(current, next)) {
break;
}
}
}
}
}
public class CASTest {
public static void main(String[] args) {
// 创建一个初始值为 0 的 AtomicInteger 计数器
AtomicInteger counter = new AtomicInteger(0);
// 定义每个线程需要自增的次数
int incrementCount = 10000;
// 定义线程的数量
int threadCount = 10;
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
// 提交任务到线程池
for (int i = 0; i < threadCount; i++) {
executorService.submit(new IncrementTask(counter, incrementCount));
}
// 关闭线程池并等待所有任务完成
executorService.shutdown();
while (!executorService.isTerminated()) {
// 等待所有任务完成
}
// 输出最终的计数器值
System.out.println("Final counter value: " + counter.get());
}
}
附录:unsafe的底层C++
在 Java 里,CAS(Compare-And-Swap)操作主要是借助 java.util.concurrent.atomic
包下的原子类(像 AtomicInteger
、AtomicLong
等)来达成的,而这些原子类中的 CAS 操作大多是依靠 native 方法实现的。下面以 AtomicInteger
类为例,深入探讨其 native 方法的具体内容并加以分析。
- Java 层面的 CAS 方法调用
在AtomicInteger
类中,compareAndSet
方法是一个典型的 CAS 操作方法,其源码如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE;
static {
try {
VALUE = U.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
private volatile int value;
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}
从这段代码可知,compareAndSet
方法调用了 jdk.internal.misc.Unsafe
类的 compareAndSetInt
方法,此方法是一个 native 方法。
Unsafe
类的 native 方法
Unsafe
类提供了众多底层操作的 native 方法,compareAndSetInt
方法便是其中之一,它的声明如下:
public final class Unsafe {
// 其他代码...
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
// 其他代码...
}
- 参数说明:
o
:表示要操作的对象。offset
:表示对象中要操作的字段的内存偏移量。expected
:表示预期的旧值。x
:表示要设置的新值。
- native 方法的具体实现(以 HotSpot JVM 为例)
Unsafe
类的 native 方法实现在 HotSpot JVM 的源码里,对应的 C++ 代码文件一般是unsafe.cpp
。compareAndSetInt
方法的具体实现会依据不同的操作系统和硬件架构有所差异,下面以 x86 架构为例:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
上述代码调用了 Atomic::cmpxchg
函数,该函数是一个原子操作函数,其实现如下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
- 代码分析:
os::is_MP()
:用于判断当前系统是否为多处理器系统。LOCK_IF_MP(%4)
:如果是多处理器系统,会在cmpxchgl
指令前添加lock
前缀,以此确保该操作的原子性。lock
前缀能够保证在执行该指令期间,处理器会锁定总线,防止其他处理器对同一内存地址进行操作。cmpxchgl %1,(%3)
:这是 x86 架构的汇编指令,cmpxchgl
是比较并交换指令。它会比较eax
寄存器的值(即compare_value
)和内存地址dest
处的值,如果相等,则将exchange_value
写入dest
地址,并将ZF
(零标志位)置为 1;如果不相等,则将dest
地址处的值加载到eax
寄存器,并将ZF
置为 0。
- 总结
- Java 中的 CAS 操作通过
Unsafe
类的 native 方法实现,这些 native 方法会调用底层操作系统和硬件提供的原子指令(如 x86 架构的cmpxchg
指令)。 - 在多处理器系统中,为保证操作的原子性,会在原子指令前添加
lock
前缀,锁定总线。 - CAS 操作是一种无锁的原子操作,性能优于传统的锁机制,不过在高竞争场景下可能会出现自旋等待,进而影响性能。
原子引用
带版本号的CAS,专门解决ABA问题
常见锁的提法
公平与非公平锁
- 公平锁需要先来后到
- 非公平锁相反
- 大多数情况下用非公平锁,让小任务先跑
乐观锁与悲观锁
- 乐观锁默认资源抢占不严重
- 悲观锁相反
可重入锁
- 拿到一次锁的可以再次自动获得锁
偏向锁
偏向锁是 Java 虚拟机(JVM)为了减少无竞争情况下的锁开销而引入的一种锁优化机制,主要应用于 synchronized 关键字所修饰的同步块场景,旨在提升单线程环境下的性能。下面将从基本概念、原理、优缺点、使用场景、JVM 相关参数几个方面详细介绍偏向锁。
基本概念
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的核心思想是,当一个线程第一次获得锁时,会在对象头中记录该线程的 ID,这个过程称为偏向。此后,该线程再次进入和退出同步块时,无需进行任何同步操作,如加锁、解锁,从而提高了单线程环境下的性能。
原理
偏向锁的实现主要涉及对象头和锁记录的变化,其具体原理如下:
- 偏向锁的初始化:当一个对象被创建时,它的对象头中标记位为“01”,表示未锁定状态,此时偏向锁标志位为“0”,表示该对象还未偏向任何线程。
- 线程首次获取锁:当有线程第一次访问同步块并获取锁时,JVM 会使用 CAS(Compare-And-Swap)操作将该线程的 ID 记录在对象头的偏向线程 ID 字段中,并将偏向锁标志位设置为“1”。这个过程一旦成功,该线程就获得了偏向锁,以后该线程再次进入这个同步块时,无需进行任何同步操作,直接进入同步块执行代码。
- 其他线程竞争锁:当有其他线程尝试获取该锁时,会发现对象头中的偏向线程 ID 不是自己的线程 ID,此时偏向锁会被撤销。撤销偏向锁需要等待全局安全点(Safe Point),在这个时间点上所有线程都处于暂停状态。撤销后,对象头会恢复到未锁定状态或者升级为轻量级锁,具体取决于竞争情况。
优缺点
优点
- 性能提升显著:在单线程环境下,偏向锁避免了传统锁机制中的加锁和解锁操作,减少了同步开销,大大提高了程序的执行效率。
- 实现简单:偏向锁的实现相对简单,只需要在对象头中记录线程 ID 即可,不需要额外的数据结构来维护锁状态。
缺点
- 撤销成本较高:当出现锁竞争时,需要撤销偏向锁,这需要等待全局安全点,会导致一定的性能开销。
- 不适合多线程竞争场景:如果一个锁经常被多个线程竞争,偏向锁的撤销和升级操作会频繁发生,反而会降低性能。
使用场景
偏向锁适用于大多数情况下只有一个线程访问同步块的场景,例如:
- 单例模式中的同步块:在单例模式的实现中,通常会使用 synchronized 关键字来保证线程安全。如果单例对象的创建和使用主要由一个线程完成,偏向锁可以显著提高性能。
- 单线程循环中的同步操作:在一些单线程的循环代码中,如果存在同步块,使用偏向锁可以避免不必要的同步开销。
JVM 相关参数
在 JVM 中,可以通过以下参数来控制偏向锁的行为:
-XX:+UseBiasedLocking
:开启偏向锁,这是 JDK 6 及以后版本的默认设置。-XX:-UseBiasedLocking
:关闭偏向锁。-XX:BiasedLockingStartupDelay=0
:设置偏向锁的启动延迟为 0,即 JVM 启动后立即启用偏向锁。默认情况下,JVM 会有一个 4 秒的启动延迟,以避免在 JVM 启动初期出现大量的偏向锁撤销操作。
综上所述,偏向锁是一种针对单线程环境的锁优化机制,通过减少无竞争情况下的锁开销来提高程序性能。但在多线程竞争频繁的场景下,需要谨慎使用,必要时可以通过 JVM 参数进行调整。
自旋锁
这个很好理解,不多说
表锁行锁间隙锁及MVCC
主要针对mysql的锁机制
在 MySQL 中,不同的存储引擎支持不同的锁机制,其中 InnoDB 引擎支持表锁、行锁和间隙锁,下面详细介绍哪些操作会触发这些锁:
- 表锁
表锁会锁定整个表,在锁定期间,其他事务无法对该表进行写操作,读操作可能会受到不同程度的限制,具体取决于锁的类型。
显式加表锁操作
LOCK TABLES
语句:这是一种显式对表加锁的方式,可以指定不同的锁模式,如读锁(READ
)和写锁(WRITE
)。- 读锁示例:
-- 给 table_name 表加读锁
LOCK TABLES table_name READ;
-- 其他事务可以读取该表,但不能写入
-- 当前事务可以读取该表,但不能对其他表进行操作
-- 释放锁
UNLOCK TABLES;
- **写锁示例**:
-- 给 table_name 表加写锁
LOCK TABLES table_name WRITE;
-- 其他事务不能读取和写入该表
-- 当前事务可以读写该表,但不能对其他表进行操作
-- 释放锁
UNLOCK TABLES;
ALTER TABLE
语句:当执行ALTER TABLE
语句修改表结构时,MySQL 会自动对表加写锁,以防止其他事务在修改过程中对表进行读写操作,确保数据的一致性。例如:
ALTER TABLE table_name ADD COLUMN new_column INT;
隐式加表锁操作
在某些存储引擎(如 MyISAM)中,默认使用表锁。当执行 INSERT
、UPDATE
、DELETE
等写操作时,会自动对表加写锁;执行 SELECT
操作时,会自动对表加读锁。不过 InnoDB 引擎通常不会因为简单的读写操作隐式加表锁,除非在特定的隔离级别或使用特定的语句。
- 行锁
行锁会锁定表中的某一行或多行,其他事务可以对未锁定的行进行读写操作,从而提高并发性能。InnoDB 引擎默认使用行锁。
基于索引的 UPDATE
、DELETE
操作
当使用 UPDATE
或 DELETE
语句时,如果使用了索引进行条件过滤,并且索引是唯一索引或主键索引,MySQL 会使用行锁锁定匹配的行。例如:
-- 假设 id 是主键
UPDATE table_name SET column1 = 'new_value' WHERE id = 1;
DELETE FROM table_name WHERE id = 2;
基于索引的 SELECT ... FOR UPDATE
操作
SELECT ... FOR UPDATE
语句会对查询结果中的行加排他锁,防止其他事务对这些行进行修改。例如:
-- 假设 id 是主键
SELECT * FROM table_name WHERE id BETWEEN 1 AND 10 FOR UPDATE;
插入操作(针对唯一索引冲突)
当插入数据时,如果违反了唯一索引的约束,MySQL 会对冲突的行加锁。例如:
-- 假设 unique_column 是唯一索引
INSERT INTO table_name (unique_column) VALUES ('duplicate_value');
间隙锁
间隙锁是 InnoDB 引擎在可重复读(REPEATABLE READ)隔离级别下为了防止幻读而引入的一种锁机制,它会锁定索引记录之间的间隙,防止其他事务在这些间隙中插入新的记录。
范围查询
当使用 UPDATE
、DELETE
或 SELECT ... FOR UPDATE
语句进行范围查询时,如果查询条件使用了索引,并且没有完全匹配的记录,MySQL 会使用间隙锁锁定查询范围对应的间隙。例如:
-- 假设 id 是索引
SELECT * FROM table_name WHERE id BETWEEN 10 AND 20 FOR UPDATE;
如果表中 id
在 10 到 20 之间没有记录,MySQL 会锁定这个区间的间隙,防止其他事务插入 id
在 10 到 20 之间的记录。
插入操作引发的间隙锁
在某些情况下,插入操作也可能会触发间隙锁。例如,当插入数据时,如果需要检查唯一索引是否冲突,MySQL 可能会对相邻的间隙加锁。
需要注意的是,间隙锁只在可重复读隔离级别下生效,在读已提交(READ COMMITTED)隔离级别下,MySQL 不会使用间隙锁。
特别地,MVCC在可重复读级别不能完全解决幻读问题,只有串行化能完全解决
死锁及排查
死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。下面为你介绍常见的死锁排查方案。
- 日志分析
- 原理:在代码中添加详细的日志记录,记录锁的获取和释放操作,以及关键的业务步骤。当发生死锁时,通过查看日志可以了解各个线程的执行顺序和锁的持有情况,从而找出可能导致死锁的原因。
- 操作步骤
- 在代码里,在获取锁之前和释放锁之后添加日志,例如:
import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggingExample {
private static final Logger LOGGER = Logger.getLogger(LoggingExample.class.getName());
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock1", Thread.currentThread().getName());
synchronized (lock1) {
LOGGER.log(Level.INFO, "Thread {0} has acquired lock1", Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock2", Thread.currentThread().getName());
synchronized (lock2) {
LOGGER.log(Level.INFO, "Thread {0} has acquired lock2", Thread.currentThread().getName());
}
LOGGER.log(Level.INFO, "Thread {0} has released lock2", Thread.currentThread().getName());
}
LOGGER.log(Level.INFO, "Thread {0} has released lock1", Thread.currentThread().getName());
}
public void method2() {
LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock2", Thread.currentThread().getName());
synchronized (lock2) {
LOGGER.log(Level.INFO, "Thread {0} has acquired lock2", Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock1", Thread.currentThread().getName());
synchronized (lock1) {
LOGGER.log(Level.INFO, "Thread {0} has acquired lock1", Thread.currentThread().getName());
}
LOGGER.log(Level.INFO, "Thread {0} has released lock1", Thread.currentThread().getName());
}
LOGGER.log(Level.INFO, "Thread {0} has released lock2", Thread.currentThread().getName());
}
}
- 当程序出现死锁后,查看日志文件,分析各个线程的锁获取和释放顺序,找出循环等待的情况。
- jstack 工具
- 原理:
jstack
是 JDK 自带的一个命令行工具,用于生成 Java 虚拟机(JVM)中线程的快照。线程快照是当前 JVM 内每一条线程正在执行的方法堆栈的集合,通过分析这些堆栈信息,可以找出处于死锁状态的线程及其持有和等待的锁。 - 操作步骤
a. 使用jps
命令查找 Java 进程的 PID(进程 ID),例如:
jps
b. 使用 `jstack` 命令生成指定进程的线程快照,例如:
jstack <PID> > thread_dump.txt
c. 打开生成的 `thread_dump.txt` 文件,查找包含 “`Found one Java-level deadlock`” 的信息,下面会详细列出死锁的线程及其持有和等待的锁。
- VisualVM 工具
- 原理:VisualVM 是一个可视化的 Java 性能分析工具,它可以监控 Java 应用程序的性能、线程状态等信息。通过 VisualVM 的线程分析功能,可以直观地查看各个线程的状态,找出死锁线程。
- 操作步骤
a. 启动 VisualVM 工具。
b. 在左侧的应用程序列表中选择要分析的 Java 进程。
c. 切换到 “线程” 标签页,查看线程的状态。如果存在死锁,VisualVM 会在界面上标记出死锁线程,并提供详细的线程堆栈信息。
- 数据库死锁排查(以 MySQL 为例)
- 原理:数据库中的死锁通常是由于多个事务同时竞争资源(如锁表、锁行)导致的。MySQL 提供了一些系统表和命令来帮助排查死锁问题。
- 操作步骤
- 查看死锁日志:在 MySQL 的错误日志文件中查找包含 “
Deadlock found when trying to get lock
” 的信息,日志中会详细记录死锁发生的时间、涉及的事务和 SQL 语句。 - 使用
SHOW ENGINE INNODB STATUS
命令:该命令可以显示 InnoDB 存储引擎的状态信息,其中包含了最近一次死锁的详细信息,如死锁发生的时间、涉及的事务 ID、锁的类型等。例如:
- 查看死锁日志:在 MySQL 的错误日志文件中查找包含 “
SHOW ENGINE INNODB STATUS;
- 代码静态分析工具
- 原理:代码静态分析工具可以在不运行代码的情况下,对代码进行分析,找出可能存在的死锁隐患。这些工具会检查代码中的锁获取和释放逻辑,识别出可能导致死锁的代码模式。
- 常见工具:FindBugs、SonarQube 等。
- 操作步骤
- 安装并配置代码静态分析工具。
- 使用工具对代码进行扫描,工具会生成分析报告,指出可能存在死锁隐患的代码位置和原因。