限制
1、限制对 CPU 的使用
单核 CPU 下,while (true) 里如果什么都不干, CPU 会空转占用会很快达到 100% 。这时 while(true) 里哪怕用 sleep(1) 也会大幅降低 cpu 占用
sleep 实现
while(true) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } }
- 可以用 wait 或 条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep 适用于无需锁同步的场景
两阶段终止模式(Two phase termination)
在一个线程 T1 中如何优雅的终止线程 T2,这里的优雅是指给 T2 一个料理后事的机会
1、错误思路
- 使用线程对象的 stop() 方法停止线程
- stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个程序都停止
2、两阶段终止
通常有监控线程,每隔几秒(sleep())进行一次系统状态记录,会一直让它 while(true) 运行。但是必须有可以让它停止下来的方法
- 情况1:如果在 sleep 时被打断了,那么就会有 InterruptException 异常,执行 catch(InterruptException e) 里的内容,这时打断标志位为 false,所以要重新打断一次,让它的标志位变为 true,这样下一次 while 循环,就会判断标志位然后继续走下面料理后事退出循环
- 情况2:如果在正常执行时(如记录监控日志时)被打断了,那么打断标志位为 true,这样下一次 while 循环,就会判断标志位然后继续走下面的料理后事退出循环
同步模式之保护性暂停
1. 定义
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
2.实现
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get() { synchronized (lock) { // 条件不满足则等待 while (response == null) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return response; } } public void complete(Object response) { synchronized (lock) { // 条件满足,通知等待线程,返回响应 this.response = response; lock.notifyAll(); } }
}
3.应用
public static void main(String[] args) { // 两线程使用同一个 guardedObject GuardedObject guardedObject = new GuardedObject(); new Thread(() -> { try { // 子线程执行下载 List<String> response = download(); log.debug("download complete..."); guardedObject.complete(response); } catch (IOException e) { e.printStackTrace(); } }).start(); log.debug("waiting..."); // 主线程阻塞等待 Object response = guardedObject.get(); log.debug("get response: [{}] lines", ((List<String>) response).size()); }
执行结果
08:42:18.568 [main] c.TestGuardedObject - waiting... 08:42:23.312 [Thread-0] c.TestGuardedObject - download complete... 08:42:23.312 [main] c.TestGuardedObject - get response: [3] lines
4.优点
相比 join ,notify 完了还可以干点别的事
5.带超时版 GuardedObject
控制超时时间
class GuardedObjectV2 { private Object response;
private final Object lock = new Object(); public Object get(long millis) { synchronized (lock) { // 1) 记录最初时间 long begin = System.currentTimeMillis(); // 2) 已经经历的时间 long timePassed = 0; while (response == null) { // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等 long waitTime = millis - timePassed; log.debug("waitTime: {}", waitTime); if (waitTime <= 0) { log.debug("break..."); break; } try { lock.wait(waitTime); } catch (InterruptedException e) { e.printStackTrace(); } // 3) 如果提前被唤醒,这时已经经历的时间假设为 400 timePassed = System.currentTimeMillis() - begin; log.debug("timePassed: {}, object is null {}", timePassed, response == null); } return response; } }
public void complete(Object response) { synchronized (lock) { // 条件满足,通知等待线程 this.response = response; log.debug("notify..."); lock.notifyAll(); } } }
6.多任务版 GuardedObject
图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员
如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理,传递结果
但是这样 结果等待者 和 结果生产者 必须是一一对应的(因为是一一对应,所以生产好了消费者就能立刻拿到)
RPC 框架常用???
异步模式之生产者/消费者
要点:
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK 中各种阻塞队列,采用的就是这种模式
实现
class Message { private int id; private Object message;
public Message(int id, Object message) { this.id = id; this.message = message; }
public int getId() { return id; }
public Object getMessage() { return message; } }
class MessageQueue { private LinkedList<Message> queue; private int capacity;
public MessageQueue(int capacity) { this.capacity = capacity; queue = new LinkedList<>(); }
public Message take() { synchronized (queue) { //队列空,取出操作阻塞 while (queue.isEmpty()) { log.debug("没货了, wait"); try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列不空了,可以取出了 Message message = queue.removeFirst(); //通知等待的所有线程(因为刚取出,所以因为队列满而put等待的线程会被真正唤醒) queue.notifyAll(); return message; } }
public void put(Message message) { synchronized (queue) { //队列满,放入操作阻塞 while (queue.size() == capacity) { log.debug("库存已达上限, wait"); try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //队列不空了,可以取出了 queue.addLast(message); //通知等待的所有线程(因为刚取出,所以因为队列空而take等待的线程会被真正唤醒) queue.notifyAll(); } } }
顺序模式(面试常考)
1.固定顺序
比如,必须先 2 后 1 打印
1.1 wait/notify
// 用来同步的对象 static Object lockobj = new Object();
// t2 运行标记, 代表 t2 是否执行过 static boolean t2runed = false;
public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lockobj) { // 如果 t2 没有执行过 while (!t2runed) { try { // t1 先等一会 lockobj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println(1); });
Thread t2 = new Thread(() -> { System.out.println(2); synchronized (lockobj) { // 修改运行标记 t2runed = true; // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll) lockobj.notifyAll(); } });
t1.start(); t2.start(); }
1.2 park/unpark
t1 在调用输出之前 park() 了,那么就相当于要等 t2 通过 unpark(t1) 通知它才可以继续
Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { } // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行 LockSupport.park(); System.out.println("1"); }); Thread t2 = new Thread(() -> { System.out.println("2"); // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) LockSupport.unpark(t1); }); t1.start(); t2.start();
1.3 t.join()
// 必须把 t2 定义在 t1 的前面。这样才能在 t1 里调用 t2.join() Thread t2 = new Thread(() -> { System.out.println("t2 run"); }); Thread t1 = new Thread(() -> { try { t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 run"); }); t1.start(); t2.start();
2.交替输出
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现
2.1 sychronized - wait / notify
public class TestThreadJTPrint1 { public static void main(String[] args) { /* 线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现 线程 输出内容 flag nextFlag t1 a 1 2 t2 b 2 3 t3 c 3 1 */ // 初始的 nowFlag 是 1,交替打印的循环次数是 5 WaitNotify waitNotify = new WaitNotify(1, 5); Thread t1 = new Thread(()-> { waitNotify.print("a", 1, 2); }); Thread t2 = new Thread(()-> { waitNotify.print("b", 2, 3); }); Thread t3 = new Thread(()-> { waitNotify.print("c", 3, 1); }); t1.start(); t2.start(); t3.start(); } // 内部类要声明为 static public static class WaitNotify { // 每个线程的循环次数 private int loopNumber = 0; // 当前的 flag private int nowFlag = 0; // 构造方法 WaitNotify(int initFlag, int loopNumber) { this.nowFlag = initFlag; this.loopNumber = loopNumber; } /* 线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现 线程 输出内容 flag nextFlag t1 a 1 2 t2 b 2 3 t3 c 3 1 */ public void print(String printContent, int myFlag, int nextFlag) { // loopNumber 次循环 for (int i=0;i<loopNumber;i++) { // 同步对象是当前 waitNotify synchronized (this) { // 不满足条件,就 wait() while (myFlag != nowFlag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 满足条件了,会退出循环,此时真正打印内容 System.out.println(printContent); // 该下个线程打印了,修改 nowFlag 为 nextFlag nowFlag = nextFlag; // 唤醒等待的线程 this.notifyAll(); } } } } }
2.2 ReentrantLock - await/signal
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class TestThreadJTPrint2 { public static void main(String[] args) { // 循环次数, 五次 AwaitSignal awaitSignal = new AwaitSignal(5); // 每个线程都有自己的休息室 Condition aCondition = awaitSignal.newCondition(); Condition bCondition = awaitSignal.newCondition(); Condition cCondition = awaitSignal.newCondition(); Thread t1 = new Thread(() -> { // 打印"a", myCondition 是 aCondition, nextCondition 是 bCondition awaitSignal.print("a", aCondition, bCondition); }); Thread t2 = new Thread(() -> { awaitSignal.print("b", bCondition, cCondition); }); Thread t3 = new Thread(() -> { awaitSignal.print("c", cCondition, aCondition); }); // 依次 start 的时候,都会先去自己的休息室等待 t1.start(); t2.start(); t3.start(); // t1 t2 t3 启动后,要等等待一段时间,等它们都到各自的休息室准备好。再由主线程唤醒a休息室里的a线程 // 不然会有错误 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // t1 t2 t3 依次 start 的时候,都会先去自己的休息室等待 // 所以要由主线程来先唤醒休a休息室里的线程。主线程也要先 lock 才能 signal() awaitSignal.lock(); try { // 注意是 signal() 不是 notifyAll() aCondition.signal(); } finally { // unlock 一定要在 finally 里 awaitSignal.unlock(); } } // 继承了 ReentrantLock 后 lock() 就相当于 lock(this) public static class AwaitSignal extends ReentrantLock { private int loopNumber; AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; } public void print(String printContent, Condition myCondition, Condition nextCondition) { for (int i=0;i<loopNumber;i++) { // 因为 extends ReentrantLock,所以可以直接有这个lock方法,相当于lock住了this lock(); try { // 打印前都先去自己的休息室休息。有人来唤醒才会去下面真正打印 myCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } // 真正打印 System.out.println(printContent); // 唤醒下一个休息室的线程。注意是 signal() 不是 notifyAll() nextCondition.signal(); } } } }
2.2 LockSupport - park/unPark
import java.util.concurrent.locks.LockSupport; public class TestThreadJTPrint3 { // 由于 t1 要 unpark 的 nextThread 是 t2 ,那么 t2 定义要在 t1 前面吗? // 但是这是一个循环,所以也不行,所以全都弄成静态的 private static Thread t1; private static Thread t2; private static Thread t3; public static void main(String[] args) { ParkUnpark parkUnpark = new ParkUnpark(5); t1 = new Thread(() -> { // 打印 "a",下一个要唤醒的线程是 t2 parkUnpark.print("a", t2); }); t2 = new Thread(() -> { parkUnpark.print("b", t3); }); t3 = new Thread(() -> { parkUnpark.print("c", t1); }); t1.start(); t2.start(); t3.start(); // 一开始都是 park 住的,所以要主线程先来唤醒t1开始打印 LockSupport.unpark(t1); } public static class ParkUnpark { int loopNumber; ParkUnpark(int loopNumber) { this.loopNumber = loopNumber; } public void print(String printContent, Thread nextThread) { for (int i=0;i<loopNumber;i++) { // 刚开始都是 park 住的 LockSupport.park(); // 等有人用 unpark(自己的线程) 唤醒自己,这里才真正打印 System.out.println(printContent); // 唤醒下一个要打印的线程,a唤醒b,b唤醒c,c唤醒a LockSupport.unpark(nextThread); } } } }
享元模式 - 自定义连接池实现
例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时 预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约 了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
class Pool { // 1. 连接池大小 private final int poolSize; // 2. 连接对象数组 private Connection[] connections; // 3. 连接状态数组 0 表示空闲, 1 表示繁忙 private AtomicIntegerArray states; // 4. 构造方法初始化 public Pool(int poolSize) { this.poolSize = 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() { while(true) { 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]; } } } // 如果没有空闲连接,当前线程进入等待。必须先 sychronized 才能调用 .wait() 方法 synchronized (this) { try { log.debug("wait..."); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 6. 归还连接 public void free(Connection conn) { for (int i = 0; i < poolSize; i++) { if (connections[i] == conn) { states.set(i, 0); synchronized (this) { log.debug("free {}", conn); this.notifyAll(); } break; } } } } class MockConnection implements Connection { // 实现略 }
测试代码:使用连接池
Pool pool = new Pool(2); for (int i = 0; i < 5; i++) { new Thread(() -> { Connection conn = pool.borrow(); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } pool.free(conn); }).start(); }
只使用 CAS 等待 state 重新变为0,而不使用 wait notify 似乎也可以啊?
- CAS 只适用于短时间使用的代码片段,让 CPU 陷入循环一直尝试 CAS 操作。但是数据库连接要去做增删改查,使用时间较长。所以不建议用 CAS
以上实现没有考虑:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理
- 分布式 hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache commons pool,例如redis连接池可以参考jedis中关于连接池的实现