JUC-模式

两阶段终止

在一个线程 T1 中如何 优雅 终止线程 T2 ?这里的【优雅】指的是给 T2 一个料理后事的机会

1.错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都停止

2. 两阶段终止模式

2.1 利用interrupted

class TPTInterrupt {
            private Thread thread;
            public void start(){
                thread = new Thread(() -> {
                    while(true) {
                        Thread current = Thread.currentThread();
                        if(current.isInterrupted()) {
                            log.debug("料理后事");
                            break;
                        }
                        try {
                            Thread.sleep(1000);
                            log.debug("将结果保存");
                        } catch (InterruptedException e) {
                            current.interrupt();
                        }
                        // 执行监控操作
                    }
                },"监控线程");
                thread.start();
            }
            public void stop() {
                thread.interrupt();
            }
        }
    }

调用

TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();

输出

11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop 
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事

2.2 利用停止标记

利用 volatile 关键字修饰的变量作为停止标记

// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
     private Thread thread;
     private volatile boolean stop = false;
     public void start(){
         thread = new Thread(() -> {
             while(true) {
                 Thread current = Thread.currentThread();
                 if(stop) {
                     log.debug("料理后事");
                     break;
                 }
                 try {
                     Thread.sleep(1000);
                     log.debug("将结果保存");
                 } catch (InterruptedException e) {
                 }
                 // 执行监控操作
             }
         },"监控线程");
         thread.start();
     }

     public void stop() {
         stop = true;
         thread.interrupt();
     }
}

调用 

TPTVolatile t = new TPTVolatile();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();

 结果

11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存
11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存
11:54:54.502 c.TestTwoPhaseTermination [main] - stop 
11:54:54.502 c.TPTVolatile [监控线程] - 料理后事

保护性暂停

定义

Guarded Suspension ,用在一个线程等待另一个线程的执行结果
要点
  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

实现

GuardedObject代码实现

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();
            }
        }
    }

应用:一个线程等待另一个线程给出结果

public static void main(String[] args) {
        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());
    }

设置超时时间

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();
            }
        }
    }

拓展-多任务版

如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此需要设计一个解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理

解耦:就是将不同部分的代码或功能尽量独立开来,降低它们之间的相互影响

在解耦之前,消费者 需要等待 生产者 产生 产品,这导致双方耦合度较高,同时,如果要维护代码或者增加新模块也较为困难,因为需要同时顾及到消费者与生产者。如果有多个消费者与生产者,没有中间类的情况下也较难处理。

解耦之后,设计了一个中间类来存储 生产者 生成的 产品,消费者也只需要在中间类取走 产品 即可,无需再等待生产者。即使有多个生产者与消费者,这样也很方便,不会出现虚假唤醒(消费者A等待生产者A,结果被唤醒之后产品被消费者B取走)

在GuardedObject中新增id属性

// 标识 Guarded Object
 private int id;
 public GuardedObject(int id) {
 this.id = id;
 }
 public int getId() {
 return id;
 }

中间解耦类

class Mailboxes {
        private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
        private static int id = 1;
        // 产生唯一 id
        private static synchronized int generateId() {
            return id++;
        }
        public static GuardedObject getGuardedObject(int id) {
            return boxes.remove(id);
        }
        public static GuardedObject createGuardedObject() {
            GuardedObject go = new GuardedObject(generateId());
            boxes.put(go.getId(), go);
            return go;
        }
        public static Set<Integer> getIds() {
            return boxes.keySet();
        }
    }

业务相关类

class People extends Thread{
 @Override
 public void run() {
 // 收信
 GuardedObject guardedObject = Mailboxes.createGuardedObject();
 log.debug("开始收信 id:{}", guardedObject.getId());
 Object mail = guardedObject.get(5000);
 log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
 }
}
class Postman extends Thread {
 private int id;
 private String mail;
 public Postman(int id, String mail) {
 this.id = id;
 this.mail = mail;
 }
 @Override
 public void run() {
 GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
 log.debug("送信 id:{}, 内容:{}", id, mail);
 guardedObject.complete(mail);
 }
}

测试

public static void main(String[] args) throws InterruptedException {
 for (int i = 0; i < 3; i++) {
 new People().start();
 }
 Sleeper.sleep(1);
 for (Integer id : Mailboxes.getIds()) {
 new Postman(id, "内容" + id).start();
 }
}

结果

10:35:05.689 c.People [Thread-1] - 开始收信 id:3
10:35:05.689 c.People [Thread-2] - 开始收信 id:1
10:35:05.689 c.People [Thread-0] - 开始收信 id:2
10:35:06.688 c.Postman [Thread-4] - 送信 id:2, 内容:内容2
10:35:06.688 c.Postman [Thread-5] - 送信 id:1, 内容:内容1
10:35:06.688 c.People [Thread-0] - 收到信 id:2, 内容:内容2
10:35:06.688 c.People [Thread-2] - 收到信 id:1, 内容:内容1
10:35:06.688 c.Postman [Thread-3] - 送信 id:3, 内容:内容3
10:35:06.689 c.People [Thread-1] - 收到信 id:3, 内容:内容3

异步模式

生产者/消费者

要点

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式


Worker Thread

定义

让有限的工作线程( Worker Thread )来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。
注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率
例如,如果一个餐馆的工人既要招呼客人(任务类型 A ),又要到后厨做菜(任务类型 B )显然效率不咋地,分成服务员(线程池A )与厨师(线程池 B )更为合理

饥饿

固定大小线程池会有饥饿现象
  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿
public class TestDeadLock {
     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 executorService = Executors.newFixedThreadPool(2);
     executorService.execute(() -> {
         log.debug("处理点餐...");
         Future<String> f = executorService.submit(() -> {
             log.debug("做菜");
             return cooking();
         });
         try {
             log.debug("上菜: {}", f.get());
         } catch (InterruptedException | ExecutionException e) {
             e.printStackTrace();
         }
     });

     executorService.execute(() -> {
         log.debug("处理点餐...");
         Future<String> f = executorService.submit(() -> {
             log.debug("做菜");
             return cooking();
         });
         try {
             log.debug("上菜: {}", f.get());
         } catch (InterruptedException | ExecutionException e) {
             e.printStackTrace();
         }
     });
 }
}

合理的解决方法并不是盲目的增加线程数量,因为线程数量总是有限的,但是任务数量并不是。

更合理的解决办法是,不同的任务类型,采用不同的线程池。例如,点菜单独一个线程池,做菜单独一个线程池,就不会出现所以线程都点菜而没有线程做菜的饥饿现象了。

如果A线程需要等待B线程的结果才能继续执行,那么这两个线程就不适合放在同一个线程池里。


创建多少线程合适

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

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

同步模式

顺序控制

1. 固定顺序运行

如先打印2后1

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();

使用park/unpark的好处是比较灵活,不用考虑先后顺序,并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』


2. 交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现
wait / notify 实现

class SyncWaitNotify {
     private int flag;
     private int loopNumber;
     public SyncWaitNotify(int flag, int loopNumber) {
         this.flag = flag;
         this.loopNumber = loopNumber;
 }
     public void print(int waitFlag, int nextFlag, String str) {
         for (int i = 0; i < loopNumber; i++) {
             synchronized (this) {
                 while (this.flag != waitFlag) {
                     try {
                         this.wait();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 System.out.print(str);
                 flag = nextFlag;
                 this.notifyAll();
             }
         }
     }
}
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {
     syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {
     syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {
     syncWaitNotify.print(3, 1, "c");
}).start();

Balking

定义

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回

实现

public class MonitorService {
     // 用来表示是否已经有线程已经在执行启动了
     private volatile boolean starting;
     public void start() {
         log.info("尝试启动监控线程...");
         synchronized (this) {
             if (starting) {
                 return;
             }
             starting = true;
         }
 
         // 真正启动监控线程...
     }
}

还可以用来实现线程安全的单例

public final class Singleton {
     private Singleton() {
     }
     private static Singleton INSTANCE = null;
     public static synchronized Singleton getInstance() {
         if (INSTANCE != null) {
             return INSTANCE;
         }
 
         INSTANCE = new Singleton();
         return INSTANCE;
     }
}

保护性暂停:A线程等待B线程的结果,当结果未产生时,A线程等待

Balking:A线程判断某个线程是否已运行某方法,是,结束运行;否,运行此方法


享元模式

1. 简介

定义 英文名称: Flyweight pattern. 当需要重用数量有限的同一类对象时
wikipedia A flyweight is an object that minimizes memory usage by sharing as much data aspossible with other similar objects
出自 "Gang of Four" design patterns
归类 Structual patterns

2. 体现

在JDK中 Boolean, Byte, Short, Integer, Long, Character 等包装类都实现了 valueOf 方法,例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围内会重用对象,大于这个范围,才会新建 Long 对象:

public static Long valueOf(long l) {
 final int offset = 128;
 if (l >= -128 && l <= 127) { // will cache
     return LongCache.cache[(int)l + offset];
 }
 return new Long(l);
}
注意:
  • Byte, Short, Long 缓存的范围都是 -128~127
  • Character 缓存的范围是 0~127
  • Integer的默认范围是 -128~127
    • 最小值不能变
    • 但最大值可以通过调整虚拟机参数 `-Djava.lang.Integer.IntegerCache.high` 来改变
  • Boolean 缓存了 TRUE FALSE

除此之外,还有 String 串池 和 BigDecimal 和 BigInteger等也体现了享元模式 


3. 自定义连接池

    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];
                    }
                }
            }
            // 如果没有空闲连接,当前线程进入等待
            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 {
        // 实现略
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值