什么是Single Threaded Execution Pattern?
想象一个情景,一群人想要过一座桥,但是这个桥只能容忍一个人的质量,当一个人以上经过这座桥,这座桥就会断裂,我们先使用线程不安全的代码来实现这一过桥情景。
首先写桥类:
package singleThreadedExecution;
public class Gate {
private int count = 0;//计数已经过桥的人
private String name;//正在桥上的人名
private String nameBackUp;//人名备份 如果和桥上人不一致说明桥上不止一个人
public void pass(String name){//过桥
this.name = name;
this.nameBackUp = name;//做一个备份
this.count++;
}
@Override
public String toString(){
return "name:"+this.name+" nameBackUp:"+this.nameBackUp;
}
public void check(){//检测桥断没断
if(this.name != this.nameBackUp){
System.out.println("Oops! The gate is broken! There are "+this.count+"men has passed this gate.");
}
}
}
然后写实现了Runnable接口的类:
package singleThreadedExecution;
public class UsingThread implements Runnable {
private Gate gate;
private String name;
public UsingThread(Gate gate , String name){
this.gate = gate;
this.name = name;
}
@Override
public void run() {
while(true){
gate.pass(name);//name这个人过桥
gate.check();//检测
}
}
}
最后做一个测试:
package singleThreadedExecution;
public class Test {
public static void main(String[] args) {
Gate gate = new Gate();
UsingThread t1 = new UsingThread(gate, "Cherry");
UsingThread t2 = new UsingThread(gate, "Tommy");
UsingThread t3 = new UsingThread(gate, "Diana");
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
}
}
运行结果如下:
果然桥断了,为什么会这样呢?
因为在执行pass()方法和check()方法的时候,这两个方法内部的语句可能时交错执行的(这样说时把语句当成线程的基本操作单位,事实上线程可能以更小的单位在切换),而交错执行就导致了预料之外的情况。
我们看,第一次桥短的时候已经有3146个人通过了,这说明当运行次数少的时候我们可能根本无法发现这个问题。
怎么解决这个问题呢?
我们只需要在check()和pass()方法加上synchronized关键字进行修饰就行。因为这个关键字保证了只有一个线程(一个想过桥的人)能进行check()和pass(),而在这个阶段其他人想过桥都会被拦下。
这是一个很具体的例子,现在我们来抽象一下这个单线程模型:
首先介绍一个概念:sharedResource(共享资源)参与者,sharedResource参与者是可由多个线程访问的类,它的方法可以分为
1、safeMethod(安全的方法),不需要做特别处理
2、unsafeMethod(不安全的方法),被多个线程同时执行会产生预想不到的问题,必须加以防卫,使得多个线程不能同时访问这个方法。只能让单线程执行的程序范围叫做临界区(critical section)。
什么时候使用Single Threaded Execution?
1、多线程时,单线程程序显然不需要考虑这个问题。
2、数据可以被多个线程访问的时候。
3、状态可能变化的时候。
4、需要确保安全性的时候。
jdk提供的线程安全的包装方法
1、synchronizedCollection方法
2、synchronizedList方法
3、synchronizedMap方法
4、synchronizedSet方法
5、synchronizedSortedMap方法
6、synchronizedSortedSet方法
生命性与死锁
使用Single Threaded Execution pattern可能会发生死锁。
所谓死锁就是两个线程分别获得了锁,互相等待另一个锁释放锁,两个程序都无法继续,程序失去了生命性。
死锁的产生条件
1、具有多个sharedResource参与者。
2、线程锁定一个sharedResource时,还没接触钱就去锁定另一个sharedResource。
3、获取sharedResource参与者的顺序不固定(和sharedResource参与者对等,也就是没有固定的优先级)。
Single Threaded Execution使得程序执行性能减少的原因:
1、获取锁要花时间,可以减少sharedResource参与者的数量,以减少性能损失
2、线程冲突时没有锁的线程要进行等待,可以减少临界区范围,以减少出现线程冲突的机会。
原子性
所谓原子性就是不能再分的东西,Java语言规范中,一些基础类型的赋值和引用操作是原子的。对象等引用类型的赋值和引用操作也是原子的,因此即使不加上synchronized,也不会被分割。
比如现在有一个int型字段n,一个线程进行 n = 123; 另一个进行 n = 456;这样的操作导致n不是123就是456。
然而Java的long和double的指定、引用操作并不是不可分割的,对于一个线程进行l = 123L; 另一个进行l = 456L;这样的操作之后l会变成什么无法保证,也许是123L,也许是456L,也许是0L,甚至31415926L,当然现在大部分Java执行环境都将long和double当作原子操作来实现了。
对于想要保证这样的字段操作的原子性问题可以在synchronized方法内进行操作,还有一种方法就是声明变量的时候加上volatile关键字,使得对这个字段的操作变得不可分。