限制

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中关于连接池的实现