本文作者:王一飞,叩丁狼高级讲师。原创文章,转载请注明出处。
前面几篇讲完了并发环境下各种集合的使用, 今天来聊一聊多线程设计模式。进入正题前显示说下,多线程的设计准则:
1:安全性(safety)
程序正常运行的必要条件之一,对象进行逻辑操作时,对象结果状态要和设计者原意一致。举个简单例子: 多线程对ArrayList对象操作时,会得到意想不到的的结果,因为ArrayList是线程不安全的集合类,单纯的多线程操作时,不具有安全性。
2:生存性/活性(liveness)
程序正常运行的必要条件之一,无论是什么时候,必要执行的处理一定能够执行,如果死活执行不到,这个代码有问题。比如说,程序运行过程中,突然停止了,又不死机,后续代码不执行了。常见的死循环,死锁等。这种情况表示代码失去活性,多线程代码要必须强调生存性/活性。
3:可复用性(reusablility)
属于代码优化范畴,类设计成为组件,可以重复使用。编写多线程程序时,如果能巧妙的将线程的互斥机制/资源竞争机制隐藏到类中,这就是一个可复用性高的程序。
4:性能(performance)
也属于代码优化范畴,如果代码能设计成可以快速,大批量的执行,这个代码就是高性能代码。这时候就得考虑,吞吐量: 单位时间处理数量, 响应时间:开始到结束的花费时间,容量:同时进行处理的数量。
居于上面准则, 来看下本篇的主角, Single Threaded Execution模式. 这个模式是多线程设计模式中最简单,最基础的一个. 按字面意思解释 : 同一时刻某个资源只允许一个线程操作.
假设一个需求: 多个线程共同操作一个资源Resoure,先执行setData, 再执行showData方法
//竞争资源
public class Resource {
private String data;
public void setData(String data){
this.data = data;
try {
Thread.sleep(1000); //放大问题
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void showData(){
System.out.println(Thread.currentThread().getName() + "--:: data:" + data);
}
}
public class App {
public static void main(String[] args) {
//共用同一个资源对象
final Resource resource = new Resource();
new Thread(new Runnable() {
public void run() {
resource.setData("A");
//期望:t1--:: data:A
//结果:t1--:: data:B
resource.showData();
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
resource.setData("B");
//期望:t2--:: data:B
//结果:t2--:: data:B
resource.showData();
}
}, "t2").start();
}
}
t1--:: data:B
t2--:: data:B
看执行结果, t1线程打印出来并不是它setData进去的A, 原因: 当线程t1 通过setData方法改data=A 然后休眠1s. 紧接着线程t2进入setData方法修改data=B,并休眠. 这是t1线程休眠结束, 马上执行showData, 此时的data已经被t2改为data=B, 所以出现上面t1–:: data:B 的结果.
上述案例对resource资源不进行安全防护,就让多个线程进行操作, 最终得到非期望的结果.这就是我们常说的线程不安全的操作. 要解决这问题最简单做法就是将resource资源保护起来即可, 保证同一时刻只允许一个线程操作.
方案: synchronized 将 setData 跟 showData 方法包裹起来
public class App {
public static void main(String[] args) {
final Resource resource = new Resource();
new Thread(new Runnable() {
public void run() {
synchronized (resource) {
resource.setData("A");
resource.showData();
}
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
synchronized (resource) {
resource.setData("B");
resource.showData();
}
}
}, "t2").start();
}
}
线程t1操作前, 先获取resource资源对象锁,持有锁之后,可以执行setData/showData方法, 线程t2则必须等待线程t1释放资源对象锁才可以参与执行.
这种类型的多线程操作模式,我们称之为:Single Threaded Execution模式. 其核心点就是保证同一时刻某个资源只允许一个线程操作.
1>多线程编程环境
2>多个线程同时访问同一个共享资源;
3>共享资源的状态会因线程的访问而改变;
Single Thread Execution 模式使用不当时,可能引起死锁问题:
比如, 如果resource资源对象不是1个对象,而是多个对象呢?
假设场景: t1 先持有resource1, 再持有resource2 2个对象锁之后,才可以执行showData方法, t2更好相反.
public class App {
public static void main(String[] args) {
final Resource resource1 = new Resource();
final Resource resource2 = new Resource();
new Thread(new Runnable() {
public void run() {
synchronized (resource1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
resource2.setData("A");
resource2.showData();
}
}
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
synchronized (resource2) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
resource1.setData("B");
resource1.showData();
}
}
}
}, "t2").start();
}
}
所以, 在使用Single Threaded Execution模式一定要看清楚使用场合.