博客记录-day124-组件问题+多线程手撕

116 阅读14分钟

一、组件问题

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 提供的用于处理配置属性的工具类,但它们的功能和用法有所不同。

  1. RelaxedPropertyResolver 是一个轻量级的工具类,它主要用于读取和解析配置属性。它可以读取以任意前缀开始的属性,例如 spring.datasource.urldb.url 等,不需要严格匹配属性名和类型。它不需要使用 Spring Boot 或其他 Spring 模块,可以单独使用。使用方式是通过创建 RelaxedPropertyResolver对象,然后使用 getProperty()  方法获取属性值。
  2. 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);
    }
}

该方法主要是通过反射的方式获取配置文件中的俩个数据源

  1. 配置数据源
@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的形式绑定数据源。

  1. 切面处理--核心
@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中。

  1. 方法执行-拿查询举例
<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类。用于动态切换数据源

  1. AbstractRoutingDataSource类介绍:AbstractRoutingSource是SpringFramework提供的一个抽象类,用于多数据源动态切换。主要作用就是根据路由的一个Key决定使用哪个数据源。核心方法就是 determineCurrentLookupKey()用来表示当前使用的数据源。 我们需要继承该类并重写该方法来决定使用哪个数据源。当应用程序请求一个连接时,Spring会通过AbstractRoutingDataSource的determineCurrentLookupKey()方法来获取当前使用的数据源的Key,然后根据Key从预先定义的多个数据源中选择一个具体的数据源。选择完数据源之后,Spring就会将请求路由到这个数据源上,并将请求转发给这个数据源进行处理。

数据源的设置我们在DataSourceAutoConfig类初始化的时候就已经设置了,并且就是key-value的结构。

image.png

在DataSourceAutoConfig初始化并执行dataSource方法时,我们就将从配置环境中解析到的俩个数据源设置到了DynamicDataSource中。key分别是db01和db02;

  1. 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

而本章节我们要实现的也是水平拆分的路由设计

图 1-1

那么,这样的一个数据库路由设计要包括哪些技术知识点呢?

  • 是关于 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.6180339887 1.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();
    }
}