一、组件问题
DBRouter注解实现分库。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouter {
/** 分库分表字段 */
String key() default "";
}
DBRouterStrategy实现是否分表,默认为否。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouterStrategy {
boolean splitTable() default false;
}
1. 框架总体总结
1. DBRouter
自定义路由注解。目的是为了切面提供切点,并获取方法中入参属性的某个字段。作为路由字段的存在,比如我们想对哪个方法进行路由处理,在该方法上加上该注解。并写明需要路由的字段
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouter {
String key() default "";
}
2. DataSourceAutoConfig
配置和加载数据源该类实现了EnvironmentAware接口。
1. EnvironmentAware接口简介
EnvironmentAware 接口是 spring 的 context 包中的一个接口,凡注册到Spring容器内的Bean,如果实现了EnvironmentAware接口并充血setEnvironment方法后,在工程启动时可以获得application.yml的配置文件配置的属性值。
@Override
public void setEnvironment(Environment environment) {
// 设置前缀
final String prefix = "router.jdbc.datasource.";
// 读取数据库实例数量
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
// 读取数据源列表配置
String dataSources = environment.getProperty(prefix + "list");
if (dataSources != null) {
for (String dbInfo : dataSources.split(",")) {
// 获取单个数据源配置
Map<String, Object> dataSourceProps = PropertyUtil.handle(
environment,
prefix + dbInfo,
Map.class
);
// 存储数据源配置
dataSourceMap.put(dbInfo, dataSourceProps);
}
}
}
3. PropertyUtil
ProperUtil方法中我们主要加载俩个类:RelaxedPropertyResolver和Binder
1. RelaxedPropertyResolver和Binder
简介:首先它俩都是Spring Framework 提供的用于处理配置属性的工具类,但它们的功能和用法有所不同。
- RelaxedPropertyResolver 是一个轻量级的工具类,它主要用于读取和解析配置属性。它可以读取以任意前缀开始的属性,例如 spring.datasource.url、db.url 等,不需要严格匹配属性名和类型。它不需要使用 Spring Boot 或其他 Spring 模块,可以单独使用。使用方式是通过创建 RelaxedPropertyResolver对象,然后使用 getProperty() 方法获取属性值。
- Binder 是一个更为强大和复杂的工具类,它主要用于将配置属性绑定到 Java 对象上,并支持数据格式转换、校验、注入等功能。它是 Spring Boot 提供的一个核心工具类,需要使用 Spring Boot 来启动应用程序。使用方式是通过创建 Binder 对象,并调用 bind() 方法将属性绑定到 Java 对象上。
private static Object v2(final Environment environment, final String prefix, final Class<?> targetClass) {
try {
Class<?> binderClass = Class.forName("org.springframework.boot.context.properties.bind.Binder");
// 获取Binder核心方法
Method getMethod = binderClass.getDeclaredMethod("get", Environment.class);
Method bindMethod = binderClass.getDeclaredMethod("bind", String.class, Class.class);
// 创建Binder实例
Object binderObject = getMethod.invoke(null, environment);
// 处理前缀格式
String prefixParam = prefix.endsWith(".")
? prefix.substring(0, prefix.length() - 1)
: prefix;
// 执行绑定操作
Object bindResultObject = bindMethod.invoke(
binderObject,
prefixParam,
targetClass
);
// 获取最终结果
Method resultGetMethod = bindResultObject.getClass().getDeclaredMethod("get");
return resultGetMethod.invoke(bindResultObject);
} catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
该方法主要是通过反射的方式获取配置文件中的俩个数据源
- 配置数据源
@Bean
public DataSource dataSource() {
// 创建目标数据源映射
Map<Object, Object> targetDataSources = new HashMap<>();
for (String dbInfo : dataSourceMap.keySet()) {
Map<String, Object> datasourceConfig = dataSourceMap.get(dbInfo);
// 构建数据源实例
DriverManagerDataSource dataSource = new DriverManagerDataSource(
datasourceConfig.get("url").toString(),
datasourceConfig.get("username").toString(),
datasourceConfig.get("password").toString()
);
targetDataSources.put(dbInfo, dataSource);
}
// 配置动态数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
该方法就是将上面我们从配置文件中获取到的俩个存入dataSourceMap的数据源设置到环境中。
4. DynamicDataSource
介绍:DynamicDataSource的主要作用就是解决多数据源的场景下数据源切换的问题,实现数据的读写分离或者数据的分库分表等需求,其具体实现方式是通过配置多个数据源,在需要切换数据源的时,通过设置当前线程的数据源路由键,让 AbstractRoutingDataSource 根据路由表选择对应的数据源。就是通过key-value的形式绑定数据源。
- 切面处理--核心
@Around("aopPoint() && @annotation(dbRouter)")
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
// 获取路由键
String dbKey = dbRouter.key();
if (StringUtils.isBlank(dbKey)) {
throw new RuntimeException("annotation DBRouter key is null!");
}
// 计算路由属性值
String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数计算索引
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
// 设置路由上下文
DBContextHolder.setDBKey(String.format("%02d", dbIdx));
DBContextHolder.setTBKey(String.format("%02d", tbIdx));
logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);
try {
// 执行目标方法
return jp.proceed();
} finally {
// 清理路由上下文
DBContextHolder.clearDBKey();
DBContextHolder.clearTBKey();
}
}
本方法主要就是计算数据库和数据表索引,然后将计算好的数据库和表索引存入DBContextHolder中。
- 方法执行-拿查询举例
<select id="queryUserInfoByUserId"
parameterType="cn.bugstack.middleware.test.infrastructure.po.User"
resultType="cn.bugstack.middleware.test.infrastructure.po.User">
SELECT
id,
userId,
userNickName,
userHead,
userPassword,
createTime
FROM
user_${tbIdx}
WHERE
userId = #{userId}
</select>
1. DynamicDataSource 动态切换数据源类
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "db"+ DBContextHolder.getDBKey();
}
}
该方法继承了AbstractRoutingDataSource类。用于动态切换数据源
- AbstractRoutingDataSource类介绍:AbstractRoutingSource是SpringFramework提供的一个抽象类,用于多数据源动态切换。主要作用就是根据路由的一个Key决定使用哪个数据源。核心方法就是 determineCurrentLookupKey()用来表示当前使用的数据源。 我们需要继承该类并重写该方法来决定使用哪个数据源。当应用程序请求一个连接时,Spring会通过AbstractRoutingDataSource的determineCurrentLookupKey()方法来获取当前使用的数据源的Key,然后根据Key从预先定义的多个数据源中选择一个具体的数据源。选择完数据源之后,Spring就会将请求路由到这个数据源上,并将请求转发给这个数据源进行处理。
数据源的设置我们在DataSourceAutoConfig类初始化的时候就已经设置了,并且就是key-value的结构。
在DataSourceAutoConfig初始化并执行dataSource方法时,我们就将从配置环境中解析到的俩个数据源设置到了DynamicDataSource中。key分别是db01和db02;
- AbstractRoutingDataSource的执行时机。我们可以分为俩种就是初始化阶段和运行阶段。我们主要介绍一下运行阶段。运行阶段就是当我们需要访问数据库的时候。Spring会通过AbstractRoutingDataSource来获取当前需要使用的数据源的Key。 它会根据key选择对应的数据源。 通过determineCurrentLookupKey方法来获取数据源的key。然后将请求路由到对应的数据源上。
至此我们已经知道了这个查询会选择哪个数据库。接下来看一下选择哪个表
在SQL中。有一个user_${tbIdx}这个字段 tbIdx这个值就是DBRouterBase类中的实例字段。我们将需要路由sql的参数实体类继承DBRouterBase类。为此来获取路由数据表的索引。
public class DBRouterBase {
private String tbIdx;
public String getTbIdx() {
return DBContextHolder.getTBKey();
}
}
执行到这里 就代表我们的queryUserInfoByUserId查询方法执行结束了。其实就是我们切面中的jp.proceed();执行结束了然后执行DBContextHolder.clearDBKey();DBContextHolder.clearTBKey();清理本次路由计算中的threadLocal中的值。
2、DB-Router
1、分库分表
首先我们要知道为什么要用分库分表,其实就是由于业务体量较大,数据增长较快,所以需要把用户数据拆分到不同的库表中去,减轻数据库压力。
分库分表操作主要有垂直拆分和水平拆分:
- 垂直拆分:指按照业务将表进行分类,分布到不同的数据库上,这样也就将数据的压力分担到不同的库上面。最终一个数据库由很多表的构成,每个表对应着不同的业务,也就是专库专用。
- 水平拆分:如果垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而水平拆分是把同一个表拆到不同的数据库中。如:user_001、user_002
而本章节我们要实现的也是水平拆分的路由设计
那么,这样的一个数据库路由设计要包括哪些技术知识点呢?
- 是关于 AOP 切面拦截的使用,这是因为需要给使用数据库路由的方法做上标记,便于处理分库分表逻辑。
- 数据源的切换操作,既然有分库那么就会涉及在多个数据源间进行链接切换,以便把数据分配给不同的数据库。
- 数据库表寻址操作,一条数据分配到哪个数据库,哪张表,都需要进行索引计算。在方法调用的过程中最终通过 ThreadLocal 记录。
- 为了能让数据均匀的分配到不同的库表中去,还需要考虑如何进行数据散列的操作,不能分库分表后,让数据都集中在某个库的某个表,这样就失去了分库分表的意义。
综上,可以看到在数据库和表的数据结构下完成数据存放,我需要用到的技术包括:AOP、数据源切换、散列算法、哈希寻址、ThreadLocal以及SpringBoot的Starter开发方式等技术。而像哈希散列、寻址、数据存放,其实这样的技术与 HashMap 有太多相似之处,那么学完源码造火箭的机会来了 如果你有过深入分析和学习过 HashMap 源码、Spring 源码、中间件开发,那么在设计这样的数据库路由组件时一定会有很多思路的出来。
2、技术调研
在 JDK 源码中,包含的数据结构设计有:数组、链表、队列、栈、红黑树,具体的实现有 ArrayList、LinkedList、Queue、Stack,而这些在数据存放都是顺序存储,并没有用到哈希索引的方式进行处理。而 HashMap、ThreadLocal,两个功能则用了哈希索引、散列算法以及在数据膨胀时候的拉链寻址和开放寻址,所以我们要分析和借鉴的也会集中在这两个功能上。
1. ThreadLocal
- 数据结构:散列表的数组结构
- 散列算法:斐波那契(Fibonacci)散列法
- 寻址方式:Fibonacci 散列法可以让数据更加分散,在发生数据碰撞时进行开放寻址,从碰撞节点向后寻找位置进行存放元素。公式:
f(k) = ((k * 2654435769) >> X) << Y对于常见的32位整数而言,也就是 f(k) = (k * 2654435769) >> 28,黄金分割点:(√5 - 1) / 2 = 0.61803398871.618:1 == 1:0.618 - 学到什么:可以参考寻址方式和散列算法,但这种数据结构与要设计实现作用到数据库上的结构相差较大,不过 ThreadLocal 可以用于存放和传递数据索引信息。
2. HashMap
public static int disturbHashIdx(String key, int size) {
return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
}
- 数据结构:哈希桶数组 + 链表 + 红黑树
- 散列算法:扰动函数、哈希索引,可以让数据更加散列的分布
- 寻址方式:通过拉链寻址的方式解决数据碰撞,数据存放时会进行索引地址,遇到碰撞产生数据链表,在一定容量超过8个元素进行扩容或者树化。
- 学到什么:可以把散列算法、寻址方式都运用到数据库路由的设计实现中,还有整个数组+链表的方式其实库+表的方式也有类似之处。
3、设计实现
1. 定义路由注解
定义
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouter {
String key() default "";
}
使用
@Mapper
public interface IUserDao {
@DBRouter(key = "userId")
User queryUserInfoByUserId(User req);
@DBRouter(key = "userId")
void insertUser(User req);
}
- 首先我们需要自定义一个注解,用于放置在需要被数据库路由的方法上。
- 它的使用方式是通过方法配置注解,就可以被我们指定的 AOP 切面进行拦截,拦截后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上。
2. 解析路由配置
- 以上就是我们实现完数据库路由组件后的一个数据源配置,在分库分表下的数据源使用中,都需要支持多数据源的信息配置,这样才能满足不同需求的扩展。
- 对于这种自定义较大的信息配置,就需要使用到
org.springframework.context.EnvironmentAware接口,来获取配置文件并提取需要的配置信息。
数据源配置提取
@Override
public void setEnvironment(Environment environment) {
String prefix = "router.jdbc.datasource.";
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
String dataSources = environment.getProperty(prefix + "list");
for (String dbInfo : dataSources.split(",")) {
Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
dataSourceMap.put(dbInfo, dataSourceProps);
}
}
- prefix,是数据源配置的开头信息,你可以自定义需要的开头内容。
- dbCount、tbCount、dataSources、dataSourceProps,都是对配置信息的提取,并存放到 dataSourceMap 中便于后续使用。
3. 数据源切换
在结合 SpringBoot 开发的 Starter 中,需要提供一个 DataSource 的实例化对象,那么这个对象我们就放在 DataSourceAutoConfig 来实现,并且这里提供的数据源是可以动态变换的,也就是支持动态切换数据源。
创建数据源
@Bean
public DataSource dataSource() {
// 创建数据源
Map<Object, Object> targetDataSources = new HashMap<>();
for (String dbInfo : dataSourceMap.keySet()) {
Map<String, Object> objMap = dataSourceMap.get(dbInfo);
targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));
}
// 设置数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
- 这里是一个简化的创建案例,把基于从配置信息中读取到的数据源信息,进行实例化创建。
- 数据源创建完成后存放到
DynamicDataSource中,它是一个继承了 AbstractRoutingDataSource 的实现类,这个类里可以存放和读取相应的具体调用的数据源信息。
4. 切面拦截
在 AOP 的切面拦截中需要完成;数据库路由计算、扰动函数加强散列、计算库表索引、设置到 ThreadLocal 传递数据源,整体案例代码如下:
@Around("aopPoint() && @annotation(dbRouter)")
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
String dbKey = dbRouter.key();
if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");
// 计算路由
String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 库表索引
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
// 设置到 ThreadLocal
DBContextHolder.setDBKey(String.format("%02d", dbIdx));
DBContextHolder.setTBKey(String.format("%02d", tbIdx));
logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);
// 返回结果
try {
return jp.proceed();
} finally {
DBContextHolder.clearDBKey();
DBContextHolder.clearTBKey();
}
}
- 简化的核心逻辑实现代码如上,首先我们提取了库表乘积的数量,把它当成 HashMap 一样的长度进行使用。
- 接下来使用和 HashMap 一样的扰动函数逻辑,让数据分散的更加散列。
- 当计算完总长度上的一个索引位置后,还需要把这个位置折算到库表中,看看总体长度的索引因为落到哪个库哪个表。
- 最后是把这个计算的索引信息存放到 ThreadLocal 中,用于传递在方法调用过程中可以提取到索引信息。
二、多线程
1、两个线程交替打印1-100
1。 synchronized
public class AlternatePrinting {
private static int count = 1;
private static final Object lock = new Object();
public static void main(String[] args) {
// 线程A负责打印奇数
Thread threadA = new Thread(() -> {
while (count <= 100) {
synchronized (lock) {
if (count % 2 == 1) { // 打印奇数
System.out.println(Thread.currentThread().getName() + ": " + count++);
lock.notify(); // 唤醒线程B
} else {
try {
lock.wait(); // 让出锁并等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "Thread-A");
// 线程B负责打印偶数
Thread threadB = new Thread(() -> {
while (count <= 100) {
synchronized (lock) {
if (count % 2 == 0) { // 打印偶数
System.out.println(Thread.currentThread().getName() + ": " + count++);
lock.notify(); // 唤醒线程A
} else {
try {
lock.wait(); // 让出锁并等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "Thread-B");
threadA.start();
threadB.start();
}
}
2、线程池+ReentrantLock+Condition
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class ThreadPoolAlternatingPrint {
private static final int MAX_NUMBER = 100; // 最大打印数字
private static int currentNumber = 1; // 当前待打印的数字
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static final int THREAD_COUNT = 2; // 线程数量
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
// 提交任务到线程池
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(new PrinterTask(i));
}
executor.shutdown();
}
static class PrinterTask implements Runnable {
private final int threadId;
public PrinterTask(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
while (true) {
lock.lock();
try {
// 检查是否超出范围或当前线程是否应该打印
if (currentNumber > MAX_NUMBER) break;
if (currentNumber % THREAD_COUNT != threadId) {
condition.await();
continue;
}
// 打印并更新数字
System.out.println("Thread-" + threadId + ": " + currentNumber);
currentNumber++;
// 唤醒其他线程
condition.signalAll();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
}
}
2、生产者-消费者模型(含缓冲区)
要求:生产者生产数据存入缓冲区,消费者从缓冲区取出数据。缓冲区满时生产者阻塞,空时消费者阻塞。
解法1:使用 wait/notify 和同步锁
public class ProducerConsumer {
private static final int CAPACITY = 5;
private static Queue<Integer> queue = new LinkedList<>();
public static void main(String[] args) {
// 生产者线程
new Thread(() -> {
int i = 0;
while (true) {
synchronized (queue) {
while (queue.size() == CAPACITY) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(i++);
System.out.println("Produced: " + (i - 1));
queue.notifyAll();
}
}
}, "Producer").start();
// 消费者线程
new Thread(() -> {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int val = queue.poll();
System.out.println("Consumed: " + val);
queue.notifyAll();
}
}
}, "Consumer").start();
}
}
解法2:使用 BlockingQueue(无锁化)
import java.util.concurrent.ArrayBlockingQueue;
public class ProducerConsumerBlockingQueue {
public static void main(String[] args) {
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
// 生产者
new Thread(() -> {
int i = 0;
while (true) {
try {
queue.put(i++);
System.out.println("Produced: " + (i - 1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 消费者
new Thread(() -> {
while (true) {
try {
int val = queue.take();
System.out.println("Consumed: " + val);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
3、多线程打印数字(按顺序交替)
要求:三个线程交替打印 1-26(A打印1,B打印2,C打印3,依此类推)
解法1:使用 ReentrantLock + Condition
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class OrderedPrinting {
private static int num = 1;
private static ReentrantLock lock = new ReentrantLock();
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
// 线程A(打印1,4,7...)
new Thread(() -> {
while (num <= 26) {
lock.lock();
try {
while (num % 3 != 1) {
conditionA.await();
}
System.out.print((char) ('A' + num - 1));
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}).start();
// 线程B(打印2,5,8...)
new Thread(() -> {
while (num <= 26) {
lock.lock();
try {
while (num % 3 != 2) {
conditionB.await();
}
System.out.print((char) ('A' + num - 1));
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}).start();
// 线程C(打印3,6,9...)
new Thread(() -> {
while (num <= 26) {
lock.lock();
try {
while (num % 3 != 0) {
conditionC.await();
}
System.out.println((char) ('A' + num - 1));
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}).start();
}
}
解法2:使用 Semaphore 控制信号量
import java.util.concurrent.Semaphore;
public class OrderedPrintingSemaphore {
private static int num = 1;
private static Semaphore semaphoreA = new Semaphore(1);
private static Semaphore semaphoreB = new Semaphore(0);
private static Semaphore semaphoreC = new Semaphore(0);
public static void main(String[] args) {
// 线程A
new Thread(() -> {
while (num <= 26) {
try {
semaphoreA.acquire();
System.out.print((char) ('A' + num - 1));
semaphoreB.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 线程B
new Thread(() -> {
while (num <= 26) {
try {
semaphoreB.acquire();
System.out.print((char) ('A' + num - 1));
semaphoreC.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 线程C
new Thread(() -> {
while (num <= 26) {
try {
semaphoreC.acquire();
System.out.println((char) ('A' + num - 1));
semaphoreA.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
4、线程安全的单例模式
要求:实现一个线程安全的单例模式,要求延迟初始化。
解法1:双重检查锁定(Double-Checked Locking)
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;
}
}
解法2:静态内部类(推荐)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
5、资源竞争控制
要求:三个线程(A/B/C)分别持有资源 X/Y/Z,只有当线程A持有X且线程B持有Y时,线程C才能执行操作。
使用 ReentrantLock 实现
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ResourceControl {
private static Lock lockX = new ReentrantLock();
private static Lock lockY = new ReentrantLock();
private static Lock lockZ = new ReentrantLock();
public static void main(String[] args) {
// 线程A(获取X)
new Thread(() -> {
lockX.lock();
System.out.println("Thread A acquired X");
// 模拟业务逻辑
try { Thread.sleep(100); } catch (InterruptedException e) {}
}).start();
// 线程B(获取Y)
new Thread(() -> {
lockY.lock();
System.out.println("Thread B acquired Y");
// 模拟业务逻辑
try { Thread.sleep(100); } catch (InterruptedException e) {}
}).start();
// 线程C(需要X和Y都释放后才能执行)
new Thread(() -> {
while (true) {
if (!lockX.isLocked() && !lockY.isLocked()) {
lockZ.lock();
System.out.println("Thread C executed!");
lockZ.unlock();
break;
}
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}).start();
}
}