L02_并发编程知识图谱

这些知识点你都掌握了吗?大家可以对着问题看下自己掌握程度如何?对于没掌握的知识点,大家自行网上搜索,都会有对应答案,本文不做知识点详细说明,只做简要文字或图示引导。

并发理论

并发编程Bug源头

为了合理平衡CPU、内存、I/O 设备三者速度差异,主要有这几方面优化:

  • CPU 增加了缓存,均衡与内存的速度差异;
  • 操作系统增加了进程、线程,分时复用CPU,均衡CPU和I/O设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

Java内存模型

解决可见性、有序性最直接的办法就是禁用缓存和编译优化。考虑程序的性能,合理的方案应该是按需禁用缓存及编译优化。

在Java 语言里面, Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。

  • 程序次序规则:在一个线程内,按照程序代码执行顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

互斥锁:解决原子性问题

Java 语言提供的锁技术:synchronized

class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object()void baz() {
    synchronized(obj) {
      // 临界区
    }
  }
}  

保护有关联关系的多个资源

死锁了,怎么办?

class Account {
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    // 锁定转出账户
    synchronized(this){// 锁定转入账户
      synchronized(target){if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

上面转账的代码是怎么发生死锁的呢?我们假设线程 T1 执行账户 A 转账户 B 的操作,账户 A.transfer(账户 B);同时线程 T2 执行账户 B 转账户 A 的操作,账户 B.transfer(账户 A)。当 T1 和 T2 同时执行完①处的代码时,T1 获得了账户 A 的锁,而 T2 获得了账户 B 的锁。之后 T1 和 T2 在执行②处的代码时,T1 试图获取账户 B 的锁时,发现账户 B 已经被锁定(被 T2 锁定),所以 T1 开始等待;T2 则试图获取账户 A 的锁时,发现账户 A 已经被锁定(被 T1 锁定),所以 T2 也开始等待。于是 T1 和 T2 会无期限地等待下去,也就是我们所说的死锁了。
如何预防死锁?
要预防死锁,那就需要分析死锁发生的条件:

  • 互斥,共享资源X和Y只能被一个线程占用;
  • 占有且等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;
  • 不可抢占,其他线程不能强行抢占线程T1占有的资源;
  • 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源。

也就是说,我们只要破坏其中一个,就可以避免死锁的发生。
互斥没法破坏,因为我们用锁为的就是互斥。其他三个条件如何破坏呢?

  • 对于“占用且等待”,我们可以一次性申请所有资源,就不存在等待了;
  • 对于“不可抢占”,占用资源的线程申请其他资源时,如果申请不到,主动释放占有的资源;
  • 对于“循环等待”,可以靠有序申请资源来预防。
class Allocator {
  private List<Object> als =
    new ArrayList<>();
  // 一次性申请所有资源
  synchronized boolean apply(
    Object from, Object to){
    if(als.contains(from) ||
         als.contains(to)){
      return false;  
    } else {
      als.add(from);
      als.add(to);  
    }
    return true;
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
  }
}

class Account {
  // actr应该为单例
  private Allocator actr;
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    // 一次性申请转出账户和转入账户,直到成功
    while(!actr.apply(this, target))try{
      // 锁定转出账户
      synchronized(this){              
        // 锁定转入账户
        synchronized(target){           
          if (this.balance > amt){
            this.balance -= amt;
            target.balance += amt;
          }
        }
      }
    } finally {
      actr.free(this, target)
    }
  } 
}

优化性能,用synchronized 实现等待-通知机制

class Allocator {
  private List<Object> als;
  // 一次性申请所有资源
  synchronized void apply(
    Object from, Object to){
    // 经典写法
    while(als.contains(from) ||
         als.contains(to)){
      try{
        wait();
      }catch(Exception e){
      }   
    } 
    als.add(from);
    als.add(to);  
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}
class Account {
  private int id;
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    Account left = thisAccount right = target;if (this.id > target.id) { ③
      left = target;           ④
      right = this;}// 锁定序号小的账户
    synchronized(left){
      // 锁定序号大的账户
      synchronized(right){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

安全性、活跃性和性能问题

并发编程是一个复杂的技术领域,微观上涉及到原子性问题、可见性问题和有序性问题,宏观则表现为安全性、活跃性以及性能问题。
安全性:

  • 数据竞争: 多个线程同时访问一个数据,并且至少有一个线程会写这个数据。
  • 竞态条件: 程序的执行结果依赖程序执行的顺序。 程序的执行依赖于某个状态变量,在判断满足条件的时候执行,但是在执行时其他变量同时修改了状态变量。 if (状态变量 满足 执行条件) { 执行操作 }

活跃性:

  • 死锁:破坏造成死锁的条件。
  • 活锁:虽然没有发生阻塞,但仍会存在执行不下去的情况。
  • 饥饿:如果遇到长作业,会导致短作业饥饿。如果优先处理短作业,则会饿死长作业。长作业就可以类比持有锁的时间过长,而时间片可以让cpu资源公平地分配给各个作业。

**性能: **
核心就是在保证安全性和活跃性的前提下,根据实际情况,尽量降低锁的粒度。

管程:并发编程的万能钥匙

管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。
MESA 模型
在管程的发展史上,先后出现过三种不同的管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。其中,现在广泛应用的是 MESA 模型,并且 Java 管程的实现参考的也是 MESA 模型。

public class BlockedQueue<T>{
  final Lock lock =
    new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull =
    lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty =
    lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满 
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

Java 线程:线程的生命周期

Java 线程的生命周期如图所示:

Java线程:创建多少线程是合适的?

创建多少线程是合适的,其实只要把握住一条原则就可以了,这条原则就是将硬件的性能发挥到极致。理论初始线程数计算公式参考:
CPU 密集型:

最佳线程数 =CPU 核数 + 1

IO 密集型:

最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

并发工具类

Lock & Condition

Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
背景:
synchronized 死锁问题,没办法解决,原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了。
解决:

// 支持中断的API
void lockInterruptibly() 
  throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit) 
  throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();

原理:
利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的 ReetrantLock,内部持有一个 volatile 成员变量 state,获取锁的时候,会读写 state 的值,解锁的时候好,也会读取 state 的值。

class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public int get() {
    // 获取锁
    rtl.lock();try {
      return value;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value = 1 + get();} finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

Semaphore: 如何快速实现一个限流器?

信号量 1965 年提出,而管程 1980 被提出。
信号量模型

class Semaphore{
  // 计数器
  int count;
  // 等待队列
  Queue queue;
  // 初始化操作
  Semaphore(int c){
    this.count=c;
  }
  // 
  void down(){
    this.count--;
    if(this.count<0){
      //将当前线程插入等待队列
      //阻塞当前线程
    }
  }
  void up(){
    this.count++;
    if(this.count<=0) {
      //移除等待队列中的某个线程T
      //唤醒线程T
    }
  }
}

如何使用信号量

static int count;
//初始化信号量
static final Semaphore s 
    = new Semaphore(1);
//用信号量保证互斥    
static void addOne() {
  s.acquire();
  try {
    count+=1;
  } finally {
    s.release();
  }
}
class ObjPool<T, R> {
  final List<T> pool;
  // 用信号量实现限流器
  final Semaphore sem;
  // 构造函数
  ObjPool(int size, T t){
    pool = new Vector<T>(){};
    for(int i=0; i<size; i++){
      pool.add(t);
    }
    sem = new Semaphore(size);
  }
  // 利用对象池的对象,调用func
  R exec(Function<T,R> func) {
    T t = null;
    sem.acquire();
    try {
      t = pool.remove(0);
      return func.apply(t);
    } finally {
      pool.add(t);
      sem.release();
    }
  }
}
// 创建对象池
ObjPool<Long, String> pool = 
  new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行  
pool.exec(t -> {
    System.out.println(t);
    return t.toString();
});

ReadWriteLock: 快速实现一个完备的缓存?

class Cache<K,V> {
  final Map<K, V> m =
    new HashMap<>();
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  // 读锁
  final Lock r = rwl.readLock();
  // 写锁
  final Lock w = rwl.writeLock();
  // 读缓存
  V get(K key) {
    r.lock();
    try { return m.get(key); }
    finally { r.unlock(); }
  }
  // 写缓存
  V put(K key, V value) {
    w.lock();
    try { return m.put(key, v); }
    finally { w.unlock(); }
  }
}

实现缓存的按需加载

class Cache<K,V> {
  final Map<K, V> m =
    new HashMap<>();
  final ReadWriteLock rwl = 
    new ReentrantReadWriteLock();
  final Lock r = rwl.readLock();
  final Lock w = rwl.writeLock();
 
  V get(K key) {
    V v = null;
    //读缓存
    r.lock();try {
      v = m.get(key);} finally{
      r.unlock();}
    //缓存中存在,返回
    if(v != null) {return v;
    }  
    //缓存中不存在,查询数据库
    w.lock();try {
      //再次验证
      //其他线程可能已经查询过数据库
      v = m.get(key);if(v == null){//查询数据库
        v=省略代码无数
        m.put(key, v);
      }
    } finally{
      w.unlock();
    }
    return v; 
  }
}

StampedLock:有没有比读写锁更快的锁?

ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。而 StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。示例代码如下。

final StampedLock sl = 
  new StampedLock();
  
// 获取/释放悲观读锁示意代码
long stamp = sl.readLock();
try {
  //省略业务相关代码
} finally {
  sl.unlockRead(stamp);
}

// 获取/释放写锁示意代码
long stamp = sl.writeLock();
try {
  //省略业务相关代码
} finally {
  sl.unlockWrite(stamp);
}
class Point {
  private int x, y;
  final StampedLock sl = 
    new StampedLock();
  //计算到原点的距离  
  int distanceFromOrigin() {
    // 乐观读
    long stamp = 
      sl.tryOptimisticRead();
    // 读入局部变量,
    // 读的过程数据可能被修改
    int curX = x, curY = y;
    //判断执行读操作期间,
    //是否存在写操作,如果存在,
    //则sl.validate返回false
    if (!sl.validate(stamp)){
      // 升级为悲观读锁
      stamp = sl.readLock();
      try {
        curX = x;
        curY = y;
      } finally {
        //释放悲观读锁
        sl.unlockRead(stamp);
      }
    }
    return Math.sqrt(
      curX * curX + curY * curY);
  }
}

CountDownLatch 和 CyclicBarrier

一个单线程里面循环查询订单、派送单,然后执行对账,最后将写入差异库。

while(存在未对账订单){
  // 查询未对账订单
  pos = getPOrders();
  // 查询派送单
  dos = getDOrders();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
} 

利用并行优化对账系统

// while 循环每次都会创建新的线程?
while(存在未对账订单){
  // 查询未对账订单
  Thread T1 = new Thread(()->{
    pos = getPOrders();
  });
  T1.start();
  // 查询派送单
  Thread T2 = new Thread(()->{
    dos = getDOrders();
  });
  T2.start();
  // 等待T1、T2结束
  T1.join();
  T2.join();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
} 

用 CountDownLatch 实现线程等待

// 创建2个线程的线程池
Executor executor = 
  Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 计数器初始化为2
  CountDownLatch latch = 
    new CountDownLatch(2);
  // 查询未对账订单
  executor.execute(()-> {
    pos = getPOrders();
    latch.countDown();
  });
  // 查询派送单
  executor.execute(()-> {
    dos = getDOrders();
    latch.countDown();
  });
  
  // 等待两个查询操作结束
  latch.await();
  
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
}

用 CyclicBarrier 实现线程同步

// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池 
Executor executor = 
  Executors.newFixedThreadPool(1);
final CyclicBarrier barrier =
  new CyclicBarrier(2, ()->{
    executor.execute(()->check());
  });
  
void check(){
  P p = pos.remove(0);
  D d = dos.remove(0);
  // 执行对账操作
  diff = check(p, d);
  // 差异写入差异库
  save(diff);
}
  
void checkAll(){
  // 循环查询订单库
  Thread T1 = new Thread(()->{
    while(存在未对账订单){
      // 查询订单库
      pos.add(getPOrders());
      // 等待
      barrier.await();
    }
  });
  T1.start();  
  // 循环查询运单库
  Thread T2 = new Thread(()->{
    while(存在未对账订单){
      // 查询运单库
      dos.add(getDOrders());
      // 等待
      barrier.await();
    }
  });
  T2.start();
}

原子类:无锁工具类的典范

public class Test {
  AtomicLong count = 
    new AtomicLong(0);
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count.getAndIncrement();
    }
  }
}

无锁方案的实现原理:
CPU 为了解决并发问题,提供了 CAS 指令。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和 共享变量的新值 C.作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。

class SimulatedCAS{
  int count;
  synchronized int cas(
    int expect, int newValue){
    // 读目前count的值
    int curValue = count;
    // 比较目前count值是否==期望值
    if(curValue == expect){
      // 如果是,则更新count的值
      count = newValue;
    }
    // 返回写入前的值
    return curValue;
  }
}

CompletableFuture

//任务1:洗水壶->烧开水
CompletableFuture<Void> f1 = 
  CompletableFuture.runAsync(()->{
  System.out.println("T1:洗水壶...");
  sleep(1, TimeUnit.SECONDS);

  System.out.println("T1:烧开水...");
  sleep(15, TimeUnit.SECONDS);
});
//任务2:洗茶壶->洗茶杯->拿茶叶
CompletableFuture<String> f2 = 
  CompletableFuture.supplyAsync(()->{
  System.out.println("T2:洗茶壶...");
  sleep(1, TimeUnit.SECONDS);

  System.out.println("T2:洗茶杯...");
  sleep(2, TimeUnit.SECONDS);

  System.out.println("T2:拿茶叶...");
  sleep(1, TimeUnit.SECONDS);
  return "龙井";
});
//任务3:任务1和任务2完成后执行:泡茶
CompletableFuture<String> f3 = 
  f1.thenCombine(f2, (__, tf)->{
    System.out.println("T1:拿到茶叶:" + tf);
    System.out.println("T1:泡茶...");
    return "上茶:" + tf;
  });
//等待任务3执行结果
System.out.println(f3.join());

void sleep(int t, TimeUnit u) {
  try {
    u.sleep(t);
  }catch(InterruptedException e){}
}
// 一次执行结果:
T1:洗水壶...
T2:洗茶壶...
T1:烧开水...
T2:洗茶杯...
T2:拿茶叶...
T1:拿到茶叶:龙井
T1:泡茶...
上茶:龙井

CompletionService

用三个线程异步执行询价,通过三次调用 Future 的 get() 方法获取询价结果,之后将询价结果保存在数据库中。

// 创建线程池
ExecutorService executor =
  Executors.newFixedThreadPool(3);
// 异步向电商S1询价
Future<Integer> f1 = 
  executor.submit(
    ()->getPriceByS1());
// 异步向电商S2询价
Future<Integer> f2 = 
  executor.submit(
    ()->getPriceByS2());
// 异步向电商S3询价
Future<Integer> f3 = 
  executor.submit(
    ()->getPriceByS3());
    
// 获取电商S1报价并保存
r=f1.get();
executor.execute(()->save(r));
  
// 获取电商S2报价并保存
r=f2.get();
executor.execute(()->save(r));
  
// 获取电商S3报价并保存  
r=f3.get();
executor.execute(()->save(r));

// 创建线程池
ExecutorService executor = 
  Executors.newFixedThreadPool(3);
// 创建CompletionService
CompletionService<Integer> cs = new 
  ExecutorCompletionService<>(executor);
// 异步向电商S1询价
cs.submit(()->getPriceByS1());
// 异步向电商S2询价
cs.submit(()->getPriceByS2());
// 异步向电商S3询价
cs.submit(()->getPriceByS3());
// 将询价结果异步保存到数据库
for (int i=0; i<3; i++) {
  Integer r = cs.take().get();
  executor.execute(()->save(r));
}

利用 CompletionService 可以快速实现 Forking 这种集群模式,只要我们拿到一个正确返回的结果,就可以取消所有任务并且返回最终结果了。

// 创建线程池
ExecutorService executor =
  Executors.newFixedThreadPool(3);
// 创建CompletionService
CompletionService<Integer> cs =
  new ExecutorCompletionService<>(executor);
// 用于保存Future对象
List<Future<Integer>> futures =
  new ArrayList<>(3);
//提交异步任务,并保存future到futures 
futures.add(
  cs.submit(()->geocoderByS1()));
futures.add(
  cs.submit(()->geocoderByS2()));
futures.add(
  cs.submit(()->geocoderByS3()));
// 获取最快返回的任务执行结果
Integer r = 0;
try {
  // 只要有一个成功返回,则break
  for (int i = 0; i < 3; ++i) {
    r = cs.take().get();
    //简单地通过判空来检查是否成功返回
    if (r != null) {
      break;
    }
  }
} finally {
  //取消所有任务
  for(Future<Integer> f : futures)
    f.cancel(true);
}
// 返回结果
return r;

并发设计模式

并发设计模式是前人在做并发编程时已经归纳好的,在不同场景下具有可行性的设计模式。我们在设计并发程序时应当优先考虑这些设计模式。

Immutablity 模式:利用不变性解决并发问题

解决并发问题,简单的办法就是让共享变量只有读操作,没有写操作。Immutablity 模式充分利用了面向对象的封装特性,将类的入口全部取消,自身的状态仅允许创建时设置。状态的改变通过新建一个对象来达成,为了避免频繁创建新对象,通过响元模式或对象池来解决该问题。因此,适用于对象状态较少或不变的场景。

Copy-on-Write 模式

不可变对象的写操作往往都是 Copy-on-Write 方法解决的。
应用:
CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个容器的读操作都是无锁的。

线程本地存储模式:没有共享,就没有伤害

解决并发问题的一个重要方法:避免共享。
应用:
ThreadLocal 。利用线程本地存储空间(TLAB)来存储线程级别的对象以保证各线程操作对象的隔离行,一定程度上可以等同于能够携带上下文信息的局部变量。Java 是在用户空间实现的ThreadLocal 控制的,目前的实现可以保证 map 的生命周期和 Thread 绑定,但。value 需要我们手动 remove 来避免内存泄漏。

class Thread {
    //内部持有ThreadLocalMap
    ThreadLocal.ThreadLocalMap 
    threadLocals;
}
class ThreadLocal<T>{
    public T get() {
        //首先获取线程持有的
        //ThreadLocalMap
        ThreadLocalMap map =
        Thread.currentThread()
        .threadLocals;
        //在ThreadLocalMap中
        //查找变量
        Entry e = 
        map.getEntry(this);
        return e.value;  
    }
    static class ThreadLocalMap{
        //内部是数组而不是Map
        Entry[] table;
        //根据ThreadLocal查找Entry
        Entry getEntry(ThreadLocal key){
            //省略查找逻辑
        }
        //Entry定义
        static class Entry extends
        WeakReference<ThreadLocal>{
            Object value;
        }
    }
}

在线程池中,如何正确使用 ThreadLocal 呢?

ExecutorService es;
ThreadLocal tl;
es.execute(()->{
  //ThreadLocal增加变量
  tl.set(obj);
  try {
    // 省略业务逻辑代码
  }finally {
    //手动清理ThreadLocal 
    tl.remove();
  }
});

Guarded Suspension 模式

该模式本质上是一种等待唤醒机制的实现,它还有一个非官方名字:多线程版本的if。

 class GuardedObject<T>{
  //受保护的对象
  T obj;
  final Lock lock = 
    new ReentrantLock();
  final Condition done =
    lock.newCondition();
  final int timeout=1;
  //获取受保护对象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推荐写法
      while(!p.test(obj)){
        done.await(timeout, 
          TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保护对象
    return obj;
  }
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

实践扩展:
Web 版的文件浏览器,通过它用户可以在浏览器里查看服务器上的目录和文件。而这个文件浏览器只支持消息队列方式接入。
在这个项目中,用户通过浏览器发送一个请求过来,然后被转换为一个异步消息发送给MQ,等MQ返回结果后,再将这个结果返回至浏览器。那么问题来了,给MQ 发送消息的线程是处理Web 请求的线程 T1,但消费 MQ 结果的线程并不是线程 T1,那线程如何等待 MQ 得返回结果呢?

class GuardedObject<T>{
  //受保护的对象
  T obj;
  final Lock lock = 
    new ReentrantLock();
  final Condition done =
    lock.newCondition();
  final int timeout=2;
  //保存所有GuardedObject
  final static Map<Object, GuardedObject> 
  gos=new ConcurrentHashMap<>();
  //静态方法创建GuardedObject
  static <K> GuardedObject 
      create(K key){
    GuardedObject go=new GuardedObject();
    gos.put(key, go);
    return go;
  }
  static <K, T> void 
      fireEvent(K key, T obj){
    GuardedObject go=gos.remove(key);
    if (go != null){
      go.onChanged(obj);
    }
  }
  //获取受保护对象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推荐写法
      while(!p.test(obj)){
        done.await(timeout, 
          TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保护对象
    return obj;
  }
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}
//处理浏览器发来的请求
Respond handleWebReq(){
  int id=序号生成器.get();
  //创建一消息
  Message msg1 = new 
    Message(id,"{...}");
  //创建GuardedObject实例
  GuardedObject<Message> go=
    GuardedObject.create(id);  
  //发送消息
  send(msg1);
  //等待MQ消息
  Message r = go.get(
    t->t != null);  
}
void onMessage(Message msg){
  //唤醒等待的线程
  GuardedObject.fireEvent(
    msg.id, msg);
}

Balking 模式

本质上是一种规范话解决“多线程版本的 if”的方案,依赖于互斥保证多线程版本的if 正确性。经典实现如下示例:

boolean changed=false;
//自动存盘操作
void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  //执行存盘操作
  //省略且实现
  this.execSave();
}
//编辑操作
void edit(){
  //省略编辑逻辑
  ......
  change();
}
//改变状态
void change(){
  synchronized(this){
    changed = true;
  }
}

Thead-Per-Message 模式:简单实用的分工方法

简言之就是每个任务分配一个独立的线程。Java 领域不知名,根因在于 Java 语言里的线程是一个重量级的对象,为每个任务创建一个线程成本太高。

Work Thread 模式:避免重复创建线程

Thread-Per-Message 模式和 Thread-Per-Message 模式的区别有哪些呢?从现实世界的角度看,你委托代办人做事,往往是和代办人直接沟通的;对应到编程领域,其实现也是主线程直接创建了一个子线程,主子线程之间是可以直接通信的。而车间工人的工作方式则是完全围绕任务展开的,一个具体的任务被哪个工人执行,预先是无法知道的;对应到编程领域,则是主线程提交任务到线程池,但主线程并不关心任务被哪个线程执行。
Worker Thread 模式能避免线程频繁创建、销毁的问题,而且能够限制线程的最大数量。Java 语言里可以直接使用线程池来实现 Worker Thread 模式。

两阶段终止模式:如何优雅地终止线程?

两阶段终止模式在线程粒度的管理中通过中断操作和置位标记来保证正常终止,JAVA中在线程池粒度的管理中可以通过 showdown 方法来对线程池进行操作,源码中可以看到,其实质也是通过第一种方式来达成目的的。
如何理解两阶段终止模式?
第一阶段主要是线程T1向线程T2发送终止指令,第二阶段则是线程T2响应终止指令。

Java:interrupt() 方法和线程终止的标志位。示例如下:

class Proxy {
  //线程终止标志位
  volatile boolean terminated = false;
  boolean started = false;
  //采集线程
  Thread rptThread;
  //启动采集功能
  synchronized void start(){
    //不允许同时启动多个采集线程
    if (started) {
      return;
    }
    started = true;
    terminated = false;
    rptThread = new Thread(()->{
      while (!terminated){
        //省略采集、回传实现
        report();
        //每隔两秒钟采集、回传一次数据
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e){
          //重新设置线程中断状态
          Thread.currentThread().interrupt();
        }
      }
      //执行到此处说明线程马上终止
      started = false;
    });
    rptThread.start();
  }
  //终止采集功能
  synchronized void stop(){
    //设置中断标志位
    terminated = true;
    //中断线程rptThread
    rptThread.interrupt();
  }
}

如何优雅地终止线程池?
线程池提供了两个方法:shutdown() 和 shutdowNow()。
showdown() 执行后,就会拒绝接收心的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务抖执行完之后最终关闭线程池。
showdownNow() 执行后,会拒绝接收新任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也会被剥夺被执行的机会。

生产者-消费者模式:用流水线思想提高效率


文章整体思路和图示参考来自于极客时间 王宝令 老师的 Java 并发编程实战,该课程通过深入浅出的方式,系统地讲解了Java并发编程的核心概念、机制与实战技巧。如有侵权,麻烦及时联系作者删除。

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值