Single Thread Execution 模式是指在同一时刻只能有一个线程去访问共享资源,就像独木桥一样每次只允许一人通行,简单来说, Single Thread Execution 就是采用排他式的操作保证在同一时刻只能有一个线程访问共享资源。下面我们以机场过安检进行该模式的讲解:
机场过安检
先模拟一个非线程安全的安检口类,旅客(线程)分别手持登机牌和身份证接受工作人员的检查,示例代码如下所示。
public class FlightSecurity {
private int count = 0;// 已过安检的人数
private String boardingPass = "null";// 登机牌
private String idCard = "null";// 身份证
public synchronized void pass(String boardingPass, String idCard) {
this.boardingPass = boardingPass;
this.idCard = idCard;
this.count++;
check();
}
private void check() {
System.out.println(count + "安检通过");
// 简单的业务,当登机牌和身份证首位不相同时则表示检查不通过
if (boardingPass.charAt(0) != idCard.charAt(0))
throw new RuntimeException("-----Exception-----" + toString());
}
@Override
public String toString() {
return "FlightSecurity{" +
"count=" + count +
", boardingPass='" + boardingPass + '\'' +
", idCard='" + idCard + '\'' +
'}';
}
}
FlightSecurity 比较简单,提供了一个 pass 方法,将旅客的登机牌和身份证传递给 pass 方法,在 pass 方法中调用 check 方法对旅客进行检查,检查的逻辑也足够的简单, 只需要检测登机牌和身份证首位是否相等(这里的逻辑你可以自己定义,我只是为了方便测试),我们看以下代码所示的测试。
public class FlightSecurityTest {
static class Passengers extends Thread {
// 机场安检类
private final FlightSecurity flightSecurity;
// 旅客身份证
private final String idCard;
// 旅客登机牌
private final String boardingPass;
public Passengers(FlightSecurity flightSecurity, String idCard, String boardingPass) {
this.flightSecurity = flightSecurity;
this.idCard = idCard;
this.boardingPass = boardingPass;
}
@Override
public void run() {
while (true) {
// 旅客不断地过安检
flightSecurity.pass(boardingPass, idCard);
}
}
}
public static void main(String[] args) {
// 定义三个旅客,身份证和登机牌首位均相同
final FlightSecurity flightsecurity = new FlightSecurity();
new Passengers(flightsecurity, "Al23456", "AF123456").start();
new Passengers(flightsecurity, "B123456", "BF123456").start();
new Passengers(flightsecurity, "C123456", "CF123456").start();
}
}
看起来每一个客户都是合法的,因为每一个客户的身份证和登机牌首字母都一样,运行 上面的程序却出现了错误,而且错误的情况还不太一样,运行多次,发现了两种类型的错误 信息,程序输出如下:
Exception in thread "Thread-2" Exception in thread "Thread-1" java.lang.RuntimeException: -----Exception-----FlightSecurity{count=79152, boardingPass='AF123456', idCard='Al23456'}
at threaddemo.chapter14.FlightSecurity.check(FlightSecurity.java:19)
at threaddemo.chapter14.FlightSecurity.pass(FlightSecurity.java:13)
at threaddemo.chapter14.FlightSecurityTest$Passengers.run(FlightSecurityTest.java:23)
java.lang.RuntimeException: -----Exception-----FlightSecurity{count=79152, boardingPass='AF123456', idCard='Al23456'}
at threaddemo.chapter14.FlightSecurity.check(FlightSecurity.java:19)
at threaddemo.chapter14.FlightSecurity.pass(FlightSecurity.java:13)
at threaddemo.chapter14.FlightSecurityTest$Passengers.run(FlightSecurityTest.java:23)
首字母相同检查不能通过和首字母不相同检查不能通过,为什么会出现这样的情况呢? 首字母相同却不能通过?更加奇怪的是传入的参数明明全都是首字母相同的,为什么会出现 首字母不相同的错误呢。
问题分析:
首字母相同却未通过检查
- 线程 A 调用 pass 方法,传人”A123456”“AF123456”并且对 idcard 赋值成功,由于 CPU调度器时间片的轮转,CPU 的执行权归 B 线程所有。
- 线程 B 调用 pass 方法,传入”B123456”“BF123456”并且对idcard 赋值成功, 覆盖 A 线程赋值的 idCard。
- 线程 A 重新获得 CPU 的执行权,将 boardingPass 赋于AF123456,因此 check 无法通过。
- 在输出 toString 之前,B线程成功将 boardingPass 覆盖为 BF123456。
首字母不相同的情况
- 线程 A 调用 pass 方法,传入”A123456”“AF123456”并且对 id Card 赋值成功,由于CPU调度器时间片的轮转,CPU 的执行权归B 线程所有。
- 线程 B 调用 pass 方法,传入”B123456”“BF123456”并且对 id Card 赋值成功,覆盖 A 线程赋值的 idCard。
- 线程 A 重新获得 CPU 的执行权,将 boardingPass 赋于 AF123456,因此 check 无法通过。
- 线程 A 检查不通过,输出 idcard=”A123456”和 boardingPass=”BF123456”。
问题解决:
上面出现的问题说到底就是数据同步的问题,虽然线程传递给 pass 方法的两个参数能够百分之百地保证首字母相同,可是在为 FlightSecurity 中的属性赋值的时候会出现多个线程交错的情况,结合我们之前所讲内容可知,需要对共享资源增加同步保护,改进代 码如下。
public synchronized void pass(String boardingPass, String idCard) {
this.boardingPass = boardingPass;
this.idCard = idCard;
this.count++;
check();
}
修改后的 pass 方法,无论运行多久都不会再出现检查出错的情况了,为什么只在 pass方法增加 synchronized 关键字, check 以及 toString 方法都有对共享资源的访问,难道它们不加同步就不会引起错误么?由于 check 方法是在 pass 方法中执行的,pass 方法加同步已经保证了 single thread execution,因此 check 方法不需要增加同步, toString 方法原因与此相同。
何时适合使用 single thread execution 模式:
- 多线程访问资源的时候,被 synchronized 同步的方法总是排他性的。
- 多个线程对某个类的状态发生改变的时候,比如 Flightsecurity 的登机牌以及身份证。
总结:
在 Java 中经常会听到线程安全的类和线程非安全的类,所谓线程安全的类是指多个线程在对某个类的实例同时进行操作时,不会引起数据不一致的问题,反之则是线程非安全的 类,在线程安全的类中经常会看到 synchronized 关键字的身影。