文章目录
5.5.共享模型之工具
5.5.1. 线程池
1、自定义线程池(TODO手写)
-
步骤1:自定义拒绝策略接口
@FunctionalInterface // 拒绝策略,任务定义为泛型,因为不一定是 Runable,也有可能是 Callable interface RejectPolicy<T> { void reject(BlockingQueue<T> queue, T task); }
-
步骤2:自定义任务队列
@Slf4j(topic = "c.BlockingQueue") class BlockingQueue<T> { // 1. 任务队列 private Deque<T> queue = new ArrayDeque<>(); // 2. 锁 private ReentrantLock lock = new ReentrantLock(); // 3. 生产者条件变量 private Condition fullWaitSet = lock.newCondition(); // 4. 消费者条件变量 private Condition emptyWaitSet = lock.newCondition(); // 5. 容量 private int capcity; public BlockingQueue(int capcity) { this.capcity = capcity; } // 带超时阻塞获取 public T poll(long timeout, TimeUnit unit) { lock.lock(); try { // 将 timeout 统一转换为 纳秒 long nanos = unit.toNanos(timeout); while (queue.isEmpty()) { try { // 返回值是剩余时间 if (nanos <= 0) { return null; } nanos = emptyWaitSet.awaitNanos(nanos); } catch (InterruptedException e) { e.printStackTrace(); } } T t = queue.removeFirst(); fullWaitSet.signal(); return t; } finally { lock.unlock(); } } // 阻塞获取 public T take() { lock.lock(); try { while (queue.isEmpty()) { try { emptyWaitSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } T t = queue.removeFirst(); fullWaitSet.signal(); return t; } finally { lock.unlock(); } } // 阻塞添加 public void put(T task) { lock.lock(); try { while (queue.size() == capcity) { try { log.debug("等待加入任务队列 {} ...", task); fullWaitSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("加入任务队列 {}", task); queue.addLast(task); emptyWaitSet.signal(); } finally { lock.unlock(); } } // 带超时时间阻塞添加 public boolean offer(T task, long timeout, TimeUnit timeUnit) { lock.lock(); try { long nanos = timeUnit.toNanos(timeout); while (queue.size() == capcity) { try { if(nanos <= 0) { return false; } log.debug("等待加入任务队列 {} ...", task); nanos = fullWaitSet.awaitNanos(nanos); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("加入任务队列 {}", task); queue.addLast(task); emptyWaitSet.signal(); return true; } finally { lock.unlock(); } } public int size() { lock.lock(); try { return queue.size(); } finally { lock.unlock(); } } public void tryPut(RejectPolicy<T> rejectPolicy, T task) { lock.lock(); try { // 判断队列是否满 if(queue.size() == capcity) { rejectPolicy.reject(this, task); } else { // 有空闲 log.debug("加入任务队列 {}", task); queue.addLast(task); emptyWaitSet.signal(); } } finally { lock.unlock(); } } }
-
步骤3:自定义线程池
@Slf4j(topic = "c.ThreadPool") class ThreadPool { // 任务队列 private BlockingQueue<Runnable> taskQueue; // 线程集合,非线程安全 private HashSet<Worker> workers = new HashSet<>(); // 核心线程数 private int coreSize; // 获取任务时的超时时间 private long timeout; private TimeUnit timeUnit; // 构建线程池的时候,就应当确定拒绝策略 private RejectPolicy<Runnable> rejectPolicy; // 执行任务 public void execute(Runnable task) { // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行 // 如果任务数超过 coreSize 时,加入任务队列暂存,执行拒绝策略 // 这段代码保护起来线程安全 synchronized (workers) { if(workers.size() < coreSize) { Worker worker = new Worker(task); log.debug("新增 worker{}, {}", worker, task); workers.add(worker); worker.start(); } else { // taskQueue.put(task); // 将拒绝策略全体下放,下放给调用者,也就是函数式接口,不写死,策略模式 // 1) 死等 // 2) 带超时等待 // 3) 让调用者放弃任务执行 // 4) 让调用者抛出异常 // 5) 让调用者自己执行任务 taskQueue.tryPut(rejectPolicy, task); } } } public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) { this.coreSize = coreSize; this.timeout = timeout; this.timeUnit = timeUnit; this.taskQueue = new BlockingQueue<>(queueCapcity); this.rejectPolicy = rejectPolicy; } // 线程对象封装 class Worker extends Thread{ private Runnable task; // 初始化时的任务对象 public Worker(Runnable task) { this.task = task; } @Override public void run() { // 执行任务 // 1) 当 task 不为空,执行任务 // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行 // while(task != null || (task = taskQueue.take()) != null) { // 短路或的用法 while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) { try { log.debug("正在执行...{}", task); task.run(); } catch (Exception e) { e.printStackTrace(); } finally { // 执行完,任务没用了 task = null; } } synchronized (workers) { log.debug("worker 被移除{}", this); workers.remove(this); } } } }
-
步骤4:测试
@Slf4j(topic = "c.TestPool") public class TestPool { public static void main(String[] args) { ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{ // 1. 死等 // queue.put(task); // 2) 带超时等待 // queue.offer(task, 1500, TimeUnit.MILLISECONDS); // 3) 让调用者放弃任务执行 // log.debug("放弃{}", task); // 4) 让调用者抛出异常,让剩余的任务不执行了 // throw new RuntimeException("任务执行失败 " + task); // 5) 让调用者自己执行任务,主线程将run方法当做普通方法执行 task.run(); }); for (int i = 0; i < 4; i++) { int j = i; threadPool.execute(() -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("{}", j); }); } } }
2、ThreadPoolExecutor
-
1)线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING(有符号位)
这些信息存储在一个原子变量ctl
中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值,减少 CAS 操作
-
2)构造方法
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 可以为线程创建时起个好名字
- handler 拒绝策略
-
工作方式:
-
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
-
当线程数达到
corePoolSize
并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue
队列排队,直到有空闲的线程。 -
如果队列选择了有界队列,那么任务超过了队列大小时,会创建
maximumPoolSize - corePoolSize
数目的线程来救急。 -
如果线程到达
maximumPoolSize
仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现:AbortPolicy
让调用者抛出 RejectedExecutionException 异常,这是默认策略CallerRunsPolicy
让调用者运行任务DiscardPolicy
放弃本次任务DiscardOldestPolicy
放弃队列中最早的任务,本任务取而代之Dubbo
的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题Netty
(网络框架)的实现,是创建一个新线程来执行任务ActiveMQ
的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略PinPoint
(链路追踪的框架)的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
-
当高峰过去后,超过
corePoolSize
的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime
和unit
来控制。
根据这个构造方法,JDKExecutors
类(工具类)中提供了众多工厂方法来创建各种用途的线程池
3)newFixedThreadPool
特点:
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
- 评价:适用于任务量已知,相对耗时的任务
@Slf4j(topic = "c.TestThreadPool")
public class TestThreadPool {
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}
t.setDaemon(false);
线程池中的线程是非守护线程,所以并不会结束。
@Slf4j(topic = "c.TestThreadPool")
public class TestThreadPool {
public static void main(String[] args) throws InterruptedException {
// 通过工厂,自己定义线程的名字
ExecutorService threadPool = Executors.newFixedThreadPool(2, new ThreadFactory() {
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "my_pool_thread_" + t.getAndIncrement());
}
});
for (int i = 0; i < 3; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}
4)newCachedThreadPool
特点:
- 核心线程数是 0, 最大线程数是
Integer.MAX_VALUE
,救急线程的空闲生存时间是 60s,意味着- 全部都是救急线程(60s 后可以回收)
- 救急线程可以无限创建
- 队列采用了
SynchronousQueue
实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货,放的线程因为没有人来取从而阻塞住,像是两个线程之间交换任务的队列) - 评价:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
@Slf4j(topic = "c.TestSynchronousQueue")
public class TestSynchronousQueue {
public static void main(String[] args) {
SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {
try {
log.debug("putting {} ", 1);
integers.put(1);
log.debug("{} putted...", 1);
log.debug("putting...{} ", 2);
integers.put(2);
log.debug("{} putted...", 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
sleep(1);
new Thread(() -> {
try {
log.debug("taking {}", 1);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
sleep(1);
new Thread(() -> {
try {
log.debug("taking {}", 2);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t3").start();
}
}
5)newSingleThreadExecutor
使用场景:
希望多个任务排队执行(串行效果)。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
@Slf4j(topic = "c.TestExecutors")
public class TestExecutors {
public static void main(String[] args) throws InterruptedException {
// test1();
test2();
}
public static void test2() {
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(() -> {
log.debug("1");
int i = 1 / 0;
});
pool.execute(() -> {
log.debug("2");
});
pool.execute(() -> {
log.debug("3");
});
}
private static void test1() {
ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "mypool_t" + t.getAndIncrement());
}
});
pool.execute(() -> {
log.debug("1");
});
pool.execute(() -> {
log.debug("2");
});
pool.execute(() -> {
log.debug("3");
});
}
}
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。
- Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
- FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
- Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
- 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
TODO 异步编排
- 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
6)提交任务
@Slf4j(topic = "c.TestSubmit")
public class TestSubmit {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(1);
method1(pool);
}
private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
Future<String> future = pool.submit(() -> {
log.debug("running");
Thread.sleep(1000);
return "ok";
});
// 主线程中执行的方法,在这里阻塞住,等待结果的返回
log.debug("{}", future.get());
}
private static void method3(ExecutorService pool) throws InterruptedException, ExecutionException {
String result = pool.invokeAny(Arrays.asList(
() -> {
log.debug("begin 1");
Thread.sleep(1000);
log.debug("end 1");
return "1";
},
() -> {
log.debug("begin 2");
Thread.sleep(500);
log.debug("end 2");
return "2";
},
() -> {
log.debug("begin 3");
Thread.sleep(2000);
log.debug("end 3");
return "3";
}
));
log.debug("{}", result);
}
private static void method2(ExecutorService pool) throws InterruptedException {
// 接收任务的集合,并返回执行结果的集合
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
() -> {
log.debug("begin");
Thread.sleep(1000);
return "1";
},
() -> {
log.debug("begin");
Thread.sleep(500);
return "2";
},
() -> {
log.debug("begin");
Thread.sleep(2000);
return "3";
}
));
futures.forEach( f -> {
try {
log.debug("{}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
method1(pool);
执行结果:
method2(pool);
执行结果:
method3(pool);
执行结果:
7)关闭线程池
- shutdown
- shutdownNow
- 其他方法
@Slf4j(topic = "c.TestShutDown")
public class TestShutDown {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> result1 = pool.submit(() -> {
log.debug("task 1 running...");
Thread.sleep(1000);
log.debug("task 1 finish...");
return 1;
});
Future<Integer> result2 = pool.submit(() -> {
log.debug("task 2 running...");
Thread.sleep(1000);
log.debug("task 2 finish...");
return 2;
});
Future<Integer> result3 = pool.submit(() -> {
log.debug("task 3 running...");
Thread.sleep(1000);
log.debug("task 3 finish...");
return 3;
});
log.debug("shutdown");
pool.shutdown();
// pool.awaitTermination(3, TimeUnit.SECONDS);
// List<Runnable> runnables = pool.shutdownNow();
log.debug("other.... {}" , runnables);
}
}
pool.shutdown();
结果演示:
pool.shutdownNow();
结果演示:
8)模式之 Worker Thread (异步模式之工作线程)
-
定义
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式(重用对象)。例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)
注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。
例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工
-
饥饿
固定大小线程池会有饥饿现象(线程数量不足导致的饥饿,解决方法是线程池划分,不同的任务类型使用不同的线程池)- 两个工人是同一个线程池中的两个线程
- 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
- 1)客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
- 2)后厨做菜:没啥说的,做就是了
- 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
- 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿。解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程池,例如:
@Slf4j(topic = "c.TestDeadLock") public class TestStarvation { static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅"); static Random RANDOM = new Random(); static String cooking() { return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { // 分了两类线程池 ExecutorService waiterPool = Executors.newFixedThreadPool(1); ExecutorService cookPool = Executors.newFixedThreadPool(1); waiterPool.execute(() -> { log.debug("处理点餐..."); Future<String> f = cookPool.submit(() -> { log.debug("做菜"); return cooking(); }); try { // 阻塞等待 log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); waiterPool.execute(() -> { log.debug("处理点餐..."); Future<String> f = cookPool.submit(() -> { log.debug("做菜"); return cooking(); }); try { log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); } }
ExecutorService pool = Executors.newFixedThreadPool(2);
饥饿现象:@Slf4j(topic = "c.TestDeadLock") public class TestStarvation { static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅"); static Random RANDOM = new Random(); static String cooking() { return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(2); pool.execute(() -> { log.debug("处理点餐..."); Future<String> f = pool.submit(() -> { log.debug("做菜"); return cooking(); }); try { // 阻塞等待 log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); pool.execute(() -> { log.debug("处理点餐..."); Future<String> f = pool.submit(() -> { log.debug("做菜"); return cooking(); }); try { log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); } }
-
创建多少线程合适
过小会导致程序不能充分地利用系统资源、容易导致饥饿。过大会导致更多的线程上下文切换,占用更多内存。-
CPU 密集型运算数据分析运算()
通常采用cpu 核数 + 1
能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费 -
I/O 密集型运算
CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。经验公式如下
线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 50% = 8
例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 10% = 40
-
-
自定义线程池(见上)
9)任务调度线程池
希望某个任务被反复的被执行。在『任务调度线程池』功能加入之前,可以使用 java.util.Timer
来实现定时功能,Timer
的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个
任务的延迟或异常都将会影响到之后的任务。
输出:
使用 ScheduledExecutorService
改写(延时执行任务):
输出:
scheduleAtFixedRate 例子(固定速率执行任务):
输出:
scheduleAtFixedRate 例子(任务执行时间超过了间隔时间):
输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s(任务的执行时间较长,影响到了执行的间隔)
scheduleWithFixedDelay 例子(单独延时的执行):
输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所以间隔都是 3s
评价 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务
10)正确处理执行任务异常
- 方法1:主动捉异常
输出:
- 方法2:使用 Future(Submit 方法的返回值,callable 配合 Future,有结果返回结果,有异常返回异常)
输出:
11)应用之定时任务
如何让每周四 18:00:00 定时执行任务?
public class TestSchedule {
// 如何让每周四 18:00:00 定时执行任务?
public static void main(String[] args) {
// 获取当前时间,线程安全,Java 8 新增的日期
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
// 获取本周四时间
LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
// 如果 当前时间 > 本周周四,必须找到下周周四
if(now.compareTo(time) > 0) {
time = time.plusWeeks(1);
}
System.out.println(time);
// initailDelay 代表当前时间和周四的时间差
// period 一周的间隔时间
long initailDelay = Duration.between(now, time).toMillis();
long period = 1000 * 60 * 60 * 24 * 7;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(() -> {
System.out.println("running...");
}, initailDelay, period, TimeUnit.MILLISECONDS);
}
}
12)Tomcat 线程池
Tomcat 在哪里用到了线程池呢?
- LimitLatch 用来限流,可以控制最大连接个数(防止太多的连接将服务器压垮),类似 J.U.C 中的 Semaphore 后面再讲
- Acceptor (死循环的线程)只负责【接收新的 socket 连接】
- Poller (死循环的线程)只负责监听 socket channel 是否有【可读的 I/O 事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
- 如果总线程数达到 maximumPoolSize
- 1)这时不会立刻抛 RejectedExecutionException 异常
- 2)而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
源码 tomcat-7.0.42
TaskQueue.java
Connector 配置
Executor 线程配置
3、Fork/Join
1)概念
Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
2)使用
提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务
public class TestForkJoin {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask1(5)));
// System.out.println(pool.invoke(new AddTask3(1, 5)));
}
}
@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {
int n;
public AddTask1(int n) {
this.n = n;
}
@Override
public String toString() {
return "{" + n + '}';
}
// invoke 调用的方法
@Override
protected Integer compute() {
// 终止条件
if (n == 1) {
log.debug("join() {}", n);
return n;
}
AddTask1 t1 = new AddTask1(n - 1);
t1.fork();
log.debug("fork() {} + {}", n, t1);
int result = n + t1.join();
log.debug("join() {} + {} = {}", n, t1, result);
return result;
}
}
@Slf4j(topic = "c.AddTask")
class AddTask2 extends RecursiveTask<Integer> {
int begin;
int end;
public AddTask2(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
AddTask2 t1 = new AddTask2(begin, mid - 1);
t1.fork();
AddTask2 t2 = new AddTask2(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} + {} = ?", mid, t1, t2);
int result = mid + t1.join() + t2.join();
log.debug("join() {} + {} + {} = {}", mid, t1, t2, result);
return result;
}
}
用图来表示
改进
public class TestForkJoin {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
// System.out.println(pool.invoke(new AddTask1(5)));
System.out.println(pool.invoke(new AddTask3(1, 5)));
}
}
@Slf4j(topic = "c.AddTask")
class AddTask3 extends RecursiveTask<Integer> {
int begin;
int end;
public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
AddTask3 t1 = new AddTask3(begin, mid);
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}
用图来表示
5.5.2. J.U.C
5.5.2.1. AQS 原理
1)概述
全称是 AbstractQueuedSynchronizer
,是阻塞式锁和相关的同步器工具的框架。先明白AQS是一个接口,规范,这个接口定义了一系列规则,而是否要设定公平锁与否由实现它的类来决定。
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
- 1)getState - 获取 state 状态
- 2)setState - 设置 state 状态
- 3)compareAndSetState - cas 机制设置 state 状态,保证赋值时候的原子性
- 4)独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(C++实现,而AQS纯Java实现)
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
获取锁的姿势。
释放锁的姿势。
2)实现不可重入锁
-
自定义同步器
// 独占锁 同步器类 class MySync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int arg) { if(compareAndSetState(0, 1)) { // 加上了锁,并设置 owner 为当前线程 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } @Override protected boolean tryRelease(int arg) { // 表示没有线程占用,以下两行代码存在顺序 setExclusiveOwnerThread(null); // private volatile int state; 防止指令重排序,写前读后,将 setExclusiveOwnerThread(null); // 放在前面,有写屏障,共享变量写入主存,确保前面的都写到主存 setState(0); return true; } @Override // 是否持有独占锁 protected boolean isHeldExclusively() { return getState() == 1; } public Condition newCondition() { return new ConditionObject(); } }
-
自定义锁
有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁
// 自定义锁(不可重入锁)
class MyLock implements Lock {
private MySync sync = new MySync();
@Override // 加锁(不成功会进入等待队列)acquire 尝试多次,数字都没有用上
public void lock() {
sync.acquire(1);
}
@Override // 加锁,可打断,等待过程中打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override // 尝试加锁(一次)
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override // 尝试加锁,带超时
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override // 解锁
public void unlock() {
sync.release(1);
}
@Override // 创建条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
public final void acquire(int arg) {
// 尝试加锁不成功,将线程放入队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
测试一下:
@Slf4j(topic = "c.TestAqs")
public class TestAqs {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
sleep(1);
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
},"t2").start();
}
}
不可重入测试
如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)
3)心得TODO
5.5.2.2. ReentrantLock 原理
1)非公平锁实现原理
-
加锁解锁流程
先从构造器开始看,默认为非公平锁实现
NonfairSync 继承自 AQS
没有竞争时
第一个竞争出现时:
Thread-1 执行了
1) CAS 尝试将 state 由 0 改为 1,结果失败
2)进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
3)接下来进入 addWaiter 逻辑,构造 Node 队列- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程。
当前线程进入 acquireQueued 逻辑
1)acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
2)如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
3)进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
4)shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败
5) 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
6)进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子:
Thread-0 释放锁,进入 tryRelease 流程,如果成功
1)设置 exclusiveOwnerThread 为 null
2)state = 0
可重入的情况有可能 tryRelease 为 false
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程。
找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1。(队列内是公平的,不公平体现在入队列的时候就有机会取获得锁)
回到 Thread-1 的 acquireQueued 流程。
如果加锁成功(没有竞争),会设置
1)exclusiveOwnerThread 为 Thread-1,state = 1
2)head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
3)原本的 head 因为从链表断开,而可被垃圾回收如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
如果不巧又被 Thread-4 占了先
1)Thread-4 被设置为 exclusiveOwnerThread,state = 1
2)Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞 -
加锁源码(TOSEE 文档中的源码部分P46)
-
解锁源码(TOSEE 文档中的源码部分P49)
2、可重入原理
3、可打断原理
1)不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。
2)可打断模式
4、公平锁实现原理
5、条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。
1)await 流程
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程。
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部。
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁。
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功。
park 阻塞 Thread-0。
2)signal 流程
假设 Thread-1 (锁的持有者)要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node。
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1
Thread-1 释放锁,进入 unlock 流程,略。
源码(TOSEE 文档中的源码部分P57)
5.5.2.3. 读写锁
1、 ReentrantReadWriteLock
当读操作远远高于写操作时,这时候使用 读写锁
让 读-读
可以并发,提高性能。 类似于数据库中的 select ...from ... lock in share mode
提供一个 数据容器类
内部分别使用读锁保护数据的 read()
方法,写锁保护数据的 write()
方法。
演示:
@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("获取读锁...");
r.lock();
try {
log.debug("读取");
sleep(1);
return data;
} finally {
log.debug("释放读锁...");
r.unlock();
}
}
public void write() {
log.debug("获取写锁...");
w.lock();
try {
log.debug("写入");
sleep(1);
} finally {
log.debug("释放写锁...");
w.unlock();
}
}
}
测试 读锁-读锁
可以并发(加读锁的原因是因为为了让其他线程来并发读。防止其他线程来写)
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start();
}
}
输出结果,从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响。
测试 读锁-写锁
相互阻塞。
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
Thread.sleep(10);
new Thread(() -> {
dataContainer.write();
}, "t2").start();
}
}
输出结果:
写锁-写锁
也是相互阻塞的,这里就不测试了。
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
- 重入时降级支持:即持有写锁的情况下去获取读锁
2、应用之缓存
1)缓存更新策略
更新时,是先清缓存还是先更新数据库
先清缓存(导致数据库与缓存不一致)
先更新数据库(做不到强一致,但能最终一致)
补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询。这种情况的出现几率非常小,见 facebook 论文
2)读写锁实现一致性缓存
public class TestGenericDao {
public static void main(String[] args) {
// 缓存的优化
GenericDao dao = new GenericDaoCached();
System.out.println("============> 查询");
String sql = "select * from emp where empno = ?";
int empno = 7369;
Emp emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
System.out.println("============> 更新");
dao.update("update emp set sal = ? where empno = ?", 800, empno);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
}
}
class GenericDaoCached extends GenericDao {
private GenericDao dao = new GenericDao();
// 缓存的 Map (SQL语句和查询结果)
// HashMap 作为缓存非线程安全, 需要保护
private Map<SqlPair, Object> map = new HashMap<>();
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
@Override
public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
return dao.queryList(beanClass, sql, args);
}
@Override
public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
// 先从缓存中找,找到直接返回
SqlPair key = new SqlPair(sql, args);
// 加读锁, 防止其它线程对缓存更改
rw.readLock().lock();
try {
T value = (T) map.get(key);
if(value != null) {
return value;
}
} finally {
rw.readLock().unlock();
}
// 加写锁, 防止其它线程对缓存读取和更改
rw.writeLock().lock();
try {
// 多个线程
// get 方法上面部分是可能多个线程依次获得锁进来的, 可能已经向缓存填充了数据
T value = (T) map.get(key);
// 为防止重复查询数据库, 再次验证,双重检查
if(value == null) {
// 缓存中没有,查询数据库
value = dao.queryOne(beanClass, sql, args);
map.put(key, value);
}
return value;
} finally {
rw.writeLock().unlock();
}
}
@Override
public int update(String sql, Object... args) {
// 加锁,原子整体,强一致性(并发能力低)
// 加写锁, 防止其它线程对缓存读取和更改
rw.writeLock().lock();
try {
// 先更新库
int update = dao.update(sql, args);
// 清空缓存
map.clear();
return update;
} finally {
rw.writeLock().unlock();
}
}
class SqlPair {
private String sql;
private Object[] args;
public SqlPair(String sql, Object[] args) {
this.sql = sql;
this.args = args;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SqlPair sqlPair = (SqlPair) o;
return Objects.equals(sql, sqlPair.sql) &&
Arrays.equals(args, sqlPair.args);
}
@Override
public int hashCode() {
int result = Objects.hash(sql);
result = 31 * result + Arrays.hashCode(args);
return result;
}
}
}
注意
- 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
- 1)适合读多写少,如果写操作比较频繁,以上实现性能低
- 2)没有考虑缓存容量
- 3)没有考虑缓存过期
- 4)只适合单机
- 5)并发性还是低,目前只会用一把锁
- 6)更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
- 乐观锁实现:用 CAS 去更新
3、 读写锁原理
1)图解流程
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个
t1 w.lock,t2 r.lock
-
1) t1 成功上锁,流程与
ReentrantLock
加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
-
2)t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示
- -1 表示失败
- 0 表示成功,但后继节点不会继续唤醒
- 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
-
3)这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
-
4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
-
5)如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次 for 循环执行 tryAcquireShared 成功则让读锁计数加一
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次 for 循环执行 tryAcquireShared 成功则让读锁计数加一(唤醒所有的读锁节点)
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点(以上就是为什么读读可以并发的原因,因为只要是shared节点被唤醒,就会将一连串的读锁节点都唤醒,直到遇到一个独占节点,才停止唤醒)
t2 r.unlock,t3 r.unlock
t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
2)源码分析(TOSEE文档P68)
2、 StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用加解读锁(不加锁,无锁并发)
加解读锁
加解写锁
乐观读,StampedLock
支持 tryOptimisticRead()
方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
提供一个 数据容器类
内部分别使用读锁保护数据的 read()
方法,写锁保护数据的 write() 方法。
@Slf4j(topic = "c.DataContainerStamped")
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
// 乐观读,获取一个戳
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
// 校验戳
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
// 返回到读取的数据
return data;
}
// 验证戳失败
// 锁升级 - 乐观读锁-->读锁
log.debug("updating to read lock... {}", stamp);
try {
// 加读锁,产生互斥
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}
测试 读-读
可以优化
@Slf4j(topic = "c.TestStampedLock")
public class TestStampedLock {
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}
}
输出结果,可以看到实际没有加读锁
测试 读-写
时优化读补加读锁
@Slf4j(topic = "c.TestStampedLock")
public class TestStampedLock {
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.write(0);
}, "t2").start();
}
}
输出结果
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
5.5.2.4.Semaphore
基本使用
[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限(共享资源有多个,也允许有多个共享线程来共享访问,和读写锁的独占模式不一样)。
@Slf4j(topic = "c.TestSemaphore")
public class TestSemaphore {
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3); // 3 表示许可的数量,类似于例子中的停车位
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 先获得许可
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
// 释放许可
semaphore.release();
}
}).start();
}
}
}
输出:
Semaphore 应用
限制对共享资源的使用,semaphore 实现。
- 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
- 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
public class TestPoolSemaphore {
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection conn = pool.borrow();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(conn);
}).start();
}
}
}
@Slf4j(topic = "c.Pool")
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
// 信号量用于保护共享资源数
private Semaphore semaphore;
// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
// 让许可数与资源数一致
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i+1));
}
}
// 5. 借连接
public Connection borrow() {// t1, t2, t3
// 获取许可
try {
semaphore.acquire(); // 没有许可的线程,在此等待,阻塞住,实现 wait 功能
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 不会执行到这里
return null;
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
log.debug("free {}", conn);
// 归还许可
semaphore.release();
break;
}
}
}
}
class MockConnection implements Connection {
private String name;
public MockConnection(String name) {
this.name = name;
}
@Override
public String toString() {
return "MockConnection{" +
"name='" + name + '\'' +
'}';
}
// TODO ...
}
Semaphore 原理
1)加锁解锁流程
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一
刚开始,permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞
Thread-0获取资源失败,进入 doAcquireSharedInterruptibly(arg);
这时 Thread-4 释放了 permits,状态如下
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
2)源码分析(TOSEE文档P76)
5.5.2.5.CountdownLatch
倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时。其中构造参数用来初始化等待计数值,await()
用来等待计数归零,countDown()
用来让计数减一。(Join与此功能类似。但是更加底层,在线程池中使用不了)
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException, ExecutionException {
test4();
}
private static void test4() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
log.debug("begin...");
sleep(1);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
new Thread(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
new Thread(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
log.debug("waiting...");
latch.await();
log.debug("wait end...");
}
}
输出
可以配合线程池使用,改进如下
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException, ExecutionException {
test5();
}
private static void test5() {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(() -> {
log.debug("begin...");
sleep(1);
// 计数器减一
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(1.5);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
log.debug("begin...");
sleep(2);
latch.countDown();
log.debug("end...{}", latch.getCount());
});
service.submit(() -> {
try {
log.debug("waiting...");
// await
latch.await();
log.debug("wait end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
输出
应用之同步等待多线程准备完毕
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException, ExecutionException {
test2();
}
private static void test2() throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
return new Thread(r, "t" + num.getAndIncrement());
});
CountDownLatch latch = new CountDownLatch(10);
String[] all = new String[10];
Random r = new Random();
for (int j = 0; j < 10; j++) {
int x = j;
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
try {
// 模拟网络延迟
Thread.sleep(r.nextInt(100));
} catch (InterruptedException e) {
}
all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";
// "\r" 能让后面的打印回退到当前行,覆盖掉当前行的内容
System.out.print("\r" + Arrays.toString(all));
}
latch.countDown();
});
}
// 等所有的 latch.countDown(); 减为0
latch.await();
System.out.println("\n游戏开始...");
service.shutdown();
}
}
中间输出
最后输出
应用之同步等待多个远程调用结束
@RestController
public class TestCountDownlatchController {
@GetMapping("/order/{id}")
public Map<String, Object> order(@PathVariable int id) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("total", "2300.00");
sleep(2000);
return map;
}
@GetMapping("/product/{id}")
public Map<String, Object> product(@PathVariable int id) {
HashMap<String, Object> map = new HashMap<>();
if (id == 1) {
map.put("name", "小爱音箱");
map.put("price", 300);
} else if (id == 2) {
map.put("name", "小米手机");
map.put("price", 2000);
}
map.put("id", id);
sleep(1000);
return map;
}
@GetMapping("/logistics/{id}")
public Map<String, Object> logistics(@PathVariable int id) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", "中通快递");
sleep(2500);
return map;
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
rest 远程调用
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException, ExecutionException {
test3();
}
private static void test3() throws InterruptedException, ExecutionException {
RestTemplate restTemplate = new RestTemplate();
log.debug("begin");
ExecutorService service = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(4);
// 将 RPC 做成任务,交给线程池运行
Future<Map<String, Object>> f1 = service.submit(() -> {
Map<String, Object> response = restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
log.debug("end order:{}", response);
latch.countDown();
return response;
});
Future<Map<String, Object>> f2 = service.submit(() -> {
Map<String, Object> response1 = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
log.debug("end product:{}", response1);
latch.countDown();
return response1;
});
Future<Map<String, Object>> f3 = service.submit(() -> {
Map<String, Object> response1 = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
log.debug("end product:{}", response1);
latch.countDown();
return response1;
});
Future<Map<String, Object>> f4 = service.submit(() -> {
Map<String, Object> response3 = restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
log.debug("end product:{}", response3);
latch.countDown();
return response3;
});
// 利用future做线程执行完毕的返回结果
/*System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());*/
latch.await();
log.debug("执行完毕");
service.shutdown();
}
}
执行结果(没有返回结果,用CountDownLatch做线程同步方便,有返回值结果用Future方便)
5.5.2.6.CyclicBarrier
@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {
public static void main(String[] args) {
test1();
}
private static void test1() {
ExecutorService service = Executors.newFixedThreadPool(5);
// 反复运行 3 遍
for (int i = 0; i < 3; i++) {
CountDownLatch latch = new CountDownLatch(2);
service.submit(() -> {
log.debug("task1 start...");
sleep(1);
latch.countDown();
});
service.submit(() -> {
log.debug("task2 start...");
sleep(2);
latch.countDown();
});
try {
// 主线程恢复运行
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("task1 task2 finish...");
}
service.shutdown();
}
}
[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作(可以重用),等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行:
@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);// 线程数与任务数保持一致
// 参数2是其他两个任务执行完了,最后再执行的
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
log.debug("task1, task2 finish...");
});
// 计数重复被使用
for (int i = 0; i < 3; i++) {
// 线程池中若有三个线程,导致两个task1先执行完,达不到效果task1 task2 task1
service.submit(() -> {
log.debug("task1 begin...");
sleep(1);
try {
barrier.await(); // 2-1=1
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.submit(() -> {
log.debug("task2 begin...");
sleep(2);
try {
barrier.await(); // 1-1=0
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
service.shutdown();
}
}
注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』
5.5.2.7.线程安全集合类概述
线程安全集合类可以分为三大类:
- 遗留的线程安全集合如
Hashtable
,Vector
- 使用 Collections 装饰的线程安全集合(装饰器模式,在所有的装饰方法上上加入了
synchronized
性能并没有太大的提升),如:(TODO查看源码)- 1)
Collections.synchronizedCollection
- 2)
Collections.synchronizedList
- 3)
Collections.synchronizedMap
- 4)
Collections.synchronizedSet
- 5)
Collections.synchronizedNavigableMap
- 6)
Collections.synchronizedNavigableSet
- 7)
Collections.synchronizedSortedMap
- 8)
Collections.synchronizedSortedSet
- 1)
java.util.concurrent.*
重点介绍 java.util.concurrent.*
下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent
- Blocking 大部分实现基于锁(lock),并提供用来阻塞的方法(在线程不满足要求时,进行等待)
- CopyOnWrite 之类容器修改开销相对较重(修改时拷贝,写时复制)
- Concurrent 类型的容器
- 1)内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 2)弱一致性(最终一致性)
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
- 求大小弱一致性,size 操作未必是 100% 准确
- 读取弱一致性
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出
ConcurrentModificationException
,不再继续遍历
5.5.2.8.ConcurrentHashMap
(ConcurrentHashMap 单个方法是线程安全的,但是方法组合在一起,保证不了线程安全性,指令还是会交错执行。)
练习:单词计数(TOSEE文档P177)
ConcurrentHashMap 原理(摆烂了一波)
1)JDK 7 HashMap 并发死链
测试代码
注意
- 要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
- 以下测试代码是精心准备的,不要随便改动
public class TestDeadLink {
public static void main(String[] args) {
// 测试 java 7 中哪些数字的 hash 结果相等
System.out.println("长度为16时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 16 == 1) {
System.out.println(i);
}
}
System.out.println("长度为32时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 32 == 1) {
System.out.println(i);
}
}
// 1, 35, 16, 50 当大小为16时,它们在一个桶内
final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
// 放 12 个元素,四分之三
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
map.put(6, null);
map.put(7, null);
map.put(8, null);
map.put(9, null);
map.put(10, null);
map.put(16, null);
// 扩容之后,35 1 还是在一个桶内
map.put(35, null);
map.put(1, null);
System.out.println("扩容前大小[main]:"+map.size());
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-0]:"+map.size());
}
}.start();
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-1]:"+map.size());
}
}.start();
}
final static int hash(Object k) {
int h = 0;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
}
死链复现(TOSEE看视屏P279)
调试工具使用 idea
在 HashMap 源码 590 行加断点(问题在于一个线程1已经完成了rehash扩容【1–>35–>16–>null】变为【35–>1–>null】,另外一个线程2还准备去rehash,此时2指向的引用已经发生变化【35–>1–>null】和【1–>null】而并非【1–>35–>16–>null】)
断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来
断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行
运行代码,程序在预料的断点位置停了下来,输出
小结
- 究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
- JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
2)JDK 8 ConcurrentHashMap
重要属性和内部类
重要方法
构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建。
get 流程
put 流程
以下数组简称(table),链表简称(bin)
public V put(K key, V value) {
return putVal(key, value, false);
}
// onlyIfAbsent 表示覆盖操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 不允许有空的键值
if (key == null || value == null) throw new NullPointerException();
// 其中 spread 方法会综合高位低位, 具有更好的 hash 性,保证正整数(负数 表示 扩容或者在红黑树中搜索)
int hash = spread(key.hashCode());
int binCount = 0;
// 进入死循环
for (Node<K,V>[] tab = table;;) {
// f 是链表头节点
// fh 是链表头结点的 hash
// i 是链表在 table 中的下标
Node<K,V> f; int n, i, fh;
// 要创建 table
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
tab = initTable();
// 要创建链表头节点,找到桶下标,如果头结点是空,第一次放入桶下标,没有冲突
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加链表头节点使用了 cas, 无需 synchronized
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 检测到 fnode 节点,帮忙扩容,锁住当前列表
else if ((fh = f.hash) == MOVED)
// 帮忙之后, 进入下一轮循环
tab = helpTransfer(tab, f);
// 能进入则说明你的哈希表既不是初始创建,也不是扩容过程中,普通的放入键值对,并产生桶下标冲突
else {
V oldVal = null;
// 锁住链表头节点
synchronized (f) {
// 再次确认链表头节点没有被移动
if (tabAt(tab, i) == f) {
// 链表,普通节点的哈希码都是大于0的,只有红黑树或者fnode才是负数(扩容)
if (fh >= 0) {
binCount = 1;
// 遍历链表,看是更新,还是追加
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同的 key
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 更新
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 已经是最后的节点了, 新增 Node, 追加至链表尾
if ((e = e.next) == null) {
// 创建新节点
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 是否是红黑树,TreeBin 是红黑树的头结点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
// 释放链表头节点的锁
}
// 元素出现冲突,哈希碰撞
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加 size 计数,类似于 LongAdder
addCount(1L, binCount);
return null;
}
// 创建哈希表,懒惰初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // 盲等
// 尝试将 sizeCtl 设置为 -1(表示初始化 table,正在创建哈希表)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
// check 是之前 binCount 的个数
private final void addCount(long x, int check) {
// LongAdder 的直接实现 CounterCell[],累加单元数组
CounterCell[] as; long b, s;
if (
// 已经有了 counterCells, 向 cell 累加
(as = counterCells) != null ||
// 还没有, 向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (
// 还没有 counterCells
as == null || (m = as.length - 1) < 0 ||
// 还没有 cell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// cell cas 增加计数失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
) {
// 创建累加单元数组和cell, 累加重试
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// 获取元素个数
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// newtable 已经创建了,帮忙扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要扩容,这时 newtable 未创建
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
size 计算流程
size 计算实际发生在 put,remove 改变集合元素的操作之中
- 没有竞争发生,向 baseCount 累加计数
- 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
- 1)counterCells 初始有两个 cell
- 2)如果计数竞争比较激烈,会创建新的 cell 来累加计数
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
// 将 baseCount 计数与所有 cell 计数累加
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
- 初始化,使用 cas 来保证并发安全,懒惰初始化 table
- 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
- put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
- get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新table 进行搜索
- 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
- size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可
3)JDK 7 ConcurrentHashMap
它维护了一个 segment 数组,每个 segment 对应一把锁
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
- 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
构造器分析
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift 默认是 32 - 4 = 28
// 移位属性
this.segmentShift = 32 - sshift;
// segmentMask 默认是 15 即 0000 0000 0000 1111
// 掩码属性
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建 segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
// 直接创建,并不是懒惰初始化
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
构造完成,如下图所示
可以看到 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位
结果再与 this.segmentMask 做位于运算,最终得到 1010 即下标为 10 的 segment
put 流程
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 计算出 segment 下标
int j = (hash >>> segmentShift) & segmentMask;
// 获得 segment 对象, 判断是否为 null, 是则创建该 segment
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) {
// 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,
// 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
s = ensureSegment(j);
}
// 进入 segment 的put 流程
return s.put(key, hash, value, false);
}
segment 继承了可重入锁(ReentrantLock),它的 put 方法为
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试加锁
HashEntry<K,V> node = tryLock() ? null :
// 如果不成功, 进入 scanAndLockForPut 流程
// 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程
// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
scanAndLockForPut(key, hash, value);
// 执行到这里 segment 已经被成功加锁, 可以安全执行
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
// 更新
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 新增
// 1) 之前等待锁时, node 已经被创建, next 指向链表头
if (node != null)
node.setNext(first);
else
// 2) 创建新 node
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 3) 扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 将 node 作为链表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
rehash 流程(TOSEE文档P95)
5.5.2.9.BlockingQueue
(TOSEE文档P179)
5.5.2.10. ConcurrentLinkedQueue
(TOSEE文档P179)
5.5.2.10. CopyOnWriteArrayList
(TOSEE文档P180)