0%

多线程设计模式总结

避免共享的设计模式

Immutability模式、Copy-on-Write模式、线程本地存储模式本质上都是为了避免共享,只是实现手段不同而已。
使用Immutability模式需要注意对象属性的不可变性,使用Copy-on-Write模式需要注意性能问题,使用线程本地存储模式需要注意异步执行问题。

Immutability中Account这个类不具备不可变性。初看这个类属于不可变对象的实现,实质上StringBuffer不同于String,StringBuffer不具备不可变性,通过getUser()方法获取user之后,是可以修改user的。一个简单的解决方案是让getUser()方法返回String对象。

String类中的value定义:private final char value[];
StringBuffer类中的value定义:char[] value;并且提供了append(Object object)和setCharAt(int index, char ch)修改value.

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Account {
private final StringBuffer user;
public Account(String user) {
this.user = new StringBuffer(user);
}
//返回的StringBuffer并不具备不可变性
public StringBuffer getUser() {
return this.user;
}
public String toString() {
return "user" + user;
}
}

Copy-on-WriteJava SDK中不提供CopyOnWriteLinkedList。完整的复制LinkedList性能开销太大。

线程本地存储中在异步场景中,不可以使用Spring的事务管理器。Spring使用ThreadLocal来传递事务信息,因此这个事务信息是不能跨线程共享的。实际工作中有很多类库都是用ThreadLocal传递上下文信息的,这种场景下如果有异步操作,一定要注意上下文信息是不能跨线程共享的。

多线程版本IF的设计模式

Guarded Suspension模式和Balking模式都可以简单的理解为“多线程版本的if”,它们的区别在于前者会等待if条件变为真,而后者则不需要等待。

Guarded Suspension模式的经典实现是使用管程,初学会简单的用线程sleep的方式实现,Guarded Suspension中就是使用sleep方式实现的。不推荐,如果sleep的时间太长,会影响响应时间;sleep的时间太短,会导致线程频繁的被唤醒,消耗系统资源。

同时由于obj不是volatile变量,所以即便obj被设置了正确的值,执行while(!p.test(obj))的线程也可能看不到,从而导致更长时间的sleep。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取受保护对象
T get(Predicate<T> p) {
try {
//obj的可见性无法保证
while(!p.test(obj)) {
TimeUnit.SECONDS.sleep(timeout);
}
} catch(InterruptedExceotion e) {
throw new RuntimeException(e);
}
//返回非空的保护对象
return obj;
}
//事件通知方法
void onChanged(T obj) {
this.obj = obj;
}

实现Balking模式最容易忽视的就是竞态条件问题。Balking中就存在竞态条件问题,在多线程场景中使用if语句时,一定要多问一遍,是否存在竞态条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {
volatile boolean inited = false;
int count = 0;
void init() {
//存在竞态条件
if(inited) {
return;
}
//有可能多个线程执行到这里
inited = true;
//计算count的值
count = calc();
}
}

三种最简单的分工模式

Thread-Per-Message模式、Worker Thread模式、生产者-消费者模式是三种最简单实用的多线程分工方法。

Thread-Per-Message模式在实现的时候需要注意是否存在线程的频繁创建、销毁以及是否可能导致OOM。Thread-Per-Message中探讨快速解决OOM问题的方法。在高并发场景中,最简单的办法是限流。

Worker Thread模式的实现,需要注意潜在的线程死锁问题。Worked Thread中示例代码存在线程死锁。“工厂里只有一个工人,它的工作就是同步的等待工程里其他人给他提供东西,然而并没有其他人,他将等到天荒地老、海枯石烂。”因此,共享线程池虽然能够提供线程池的使用效率,但一定要保证一个前提:任务之间没有依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
ExecutorService pool = Executors.newSingleThreadExecutor();
//提交主任务
pool.submit(()->{
try {
//提交子任务并等待其完成
//会导致线程死锁
String qq. == pool.submit(()->"QQ").get();
System.out.println(qq);
} catch(Exception e) {

}
});

Java线程池本身就是一种生产者-消费者模型的实现,所以大部分场景都不需要自己实现,直接使用Java的线程池就可以了。自定义生产者-消费者模式可以实现批量执行和分阶段提交,需要注意优雅的终止线程。生产者-消费者模式

两阶段终止模式是一种通用的优雅终止线程的解决方案。
终止生产者-消费者服务还有一种更简单的方案-“毒丸”对象。简单来讲,“毒丸”对象是生产者生产的一条特殊任务,当消费者线程读到“毒丸”对象时,会立即终止自身的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Logger {
//用于终止日志执行的“毒丸”
final LogMsg poisonPill = new LogMsg(LEVER.ERROE, "");
//任务队列
final BlockingQueue<LogMsg> bq = new BlockingQueue<>();
//只需要一个线程写日志
ExecutorService es = Executors.newFixedThreadPool(1);
//启动写日志线程
void start() {
File file = File.createTempFile("foo", ".log");
final FileWriter writer = new FileWriter(file);
this.es.execute(()->{
try {
while(true) {
LogMsg log = bq.poll(5, TimeUnit.SECONDS);
//如果是“毒丸”,终止执行
if(poisonPill.equals(logMsg)) {
break;
}
//省略执行逻辑
}
} catch(Exception e) {

} finally {
try {
writer.flush();
writer.close();
} catch(IOException e) {

}
}
});
}
//终止写日志线程
public void stop() {
//将“毒丸”对象加入阻塞队列
bq.add(poisonPill);
es.shutdown();
}
}

《图解Java多线程设计模式》