并发系列之二Single Threaded Execution

前言

Single Threaded Execution是指以一个线程执行,简单来说就是在多线程中限制同时只让一个线程运行

线程不安全

首先来模拟一个线程不安全的例子

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
package com.zwl.utest2;
public class Gate {
//记录走过门的人数,默认为0
private int count=0;
//记录通过门的人的名字
private String name="nobody";
//记录通过门的人的地址
private String address="noplace";
//穿越这道门使用的方法
public void pass(String name,String address){
this.name=name;
this.address=address;
count++;
check();
}
//检查通过门的人的合法性,即人的名字和地址首字母是否相同
public void check(){
if(name.charAt(0)!=address.charAt(0)){
System.out.println("***broken***"+this.toString());
}
}
//打印出人数、姓名、地址
public String toString(){
return "No."+count+":"+name+" ,"+address;
}
}

目前上面的Gate类是线程不安全的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.zwl.utest2;
//不断穿越门的行人
public class UserThread extends Thread {
//用final限制门,姓名,地址不能重复
private final Gate gate;
private final String myaddress;
private final String myname;
public UserThread(Gate gate, String myname, String myaddress) {
super();
this.gate = gate;
this.myname = myname;
this.myaddress = myaddress;
}
//发生错误,即行人不合法,则处于死循环,并且执行toString方法
@Override
public void run() {
System.out.println(myname+" BEGIN");
while(true){
gate.pass(myname, myaddress);
}
}
}

测试类

1
2
3
4
5
6
System.out.println("test begin:");
Gate gate=new Gate();
//构造三个行人
new UserThread(gate, "A1", "A2").start();
new UserThread(gate, "B1", "B2").start();
new UserThread(gate, "C1", "C2").start();

测试结果(截取部分)

test begin:
B1 BEGIN
A1 BEGIN
brokenNo.1305356:A1 ,A2
brokenNo.1347999:B1 ,B2
brokenNo.1357011:A1 ,A2
brokenNo.1370176:B1 ,B2
brokenNo.1377075:B1 ,B2

从结果来看,明显发生了错误,即出现了线程不安全,但是也有点不对劲

为什么会出错了

问题的出错地方就在Gate类中,因为其是线程不安全的,打个比方
线程A1 线程A2 this.name值 this.address值
count++ count++ 之前的值 之前的值
this.name=name A1 之前的值
this.name=name A2 之前的值
this.address=address A2 B2
this.address=address A1 B2
check() check() A1 B2

发生错误

以上只是发生错误的一种情况,事实上只有当数据量非常大时,极有可能发生错误,所以才会出现错误的结果,并且是在count达到大数量的时候出现的
那么如何修改了?
因为发生在Gate类的pass和toString方法中,所以只要把这个两个方法声明为同步就可以了

public synchronized void pass(String name,String address){}
public synchronized String toString(){}

对应的测试结果

test begin:
A1 BEGIN
B1 BEGIN
C1 BEGIN

归纳single Threaded

上面例子的解决方案就是该模式的一种应用,可以将其抽象为以下几个参与者
SharedResource(共享资源)参与者:可有多个线程访问的类,如Gate
SafeMethod:从多个线程同时调用也不会发生问题
UnsafeMethod:从多个线程同时调用会发生问题,需要防范的方法
所以必须将上述不安全的方法加以同步锁,当时如果通过其他方法改变不安全方法中数据也会发生错误,就像大门上锁,但是窗户开着一样,所以有时也是一个隐患的问题

那么此种模式何时适用了?
多线程时、数据可被多个线程访问的时候、状态可能变化的时候、需要确保安全性的时候

生命线与死锁

使用该模式,可能会发生死锁的危险
而死锁是指:两个线程分别获取了锁定,互相等待另一个线程解除锁定的线程。发生死锁时,哪个线程都无法继续执行下去,所以程序会失去生命线
最经典的死锁问题就是汤勺与叉子模型,问题大概是这样的
假设A与B同吃一碗面,盘子旁有一支汤勺与一支叉子,而吃面必须同时需要汤勺和叉子
而现在叉子被其中一人假设是A拿走,而汤勺被B拿走,就会造成如下现象:
A和B一直等待对方放下叉子(或者汤勺),即一直处于等待,僵持阶段,从而程序无法继续运行,故称为死锁现象

而只要上述模式达到下面的条件,就会出现死锁
1.具有多个SharedResource参与者
2.线程锁定一个SharedResource时,还没接触前就去锁定另一个SharedResource
3.获取SharedResource参与者的顺序不固定

当然只要破坏上述三个条件之一就可以避免死锁的发生

下面就开始模拟死锁
首先是表示餐具的类(这里只有叉子和汤勺)

1
2
3
4
5
6
7
8
9
10
11
12
package com.zwl.utest2;
public class Tool {
private final String name;
public Tool(String name){
this.name=name;
}
public String toString(){
return "["+name+"]";
}
}

开始用餐的类

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
41
42
43
44
package com.zwl.utest2;
public class EaterThread extends Thread {
//用餐者名字
private String name;
//左手拿餐具
private final Tool lefthand;
//右手拿餐具
private final Tool righthand;
public EaterThread(String name, Tool lefthand, Tool righthand) {
super();
this.name = name;
this.lefthand = lefthand;
this.righthand = righthand;
}
public void run(){
//不断吃
while(true){
eat();
}
}
public void eat(){
//锁定左手获取餐具
synchronized (lefthand) {
//左手拿餐具
System.out.println(name+"takes up"+lefthand+"left.");
//锁定右手拿餐具
synchronized (righthand) {
//右手拿餐具
System.out.println(name+" takes up"+righthand+"right.");
//开吃
System.out.println(name+" is eating");
//右手放下餐具
System.out.println(name+" puts down"+righthand+"right.");
}
//解除右手锁定锁,并且左手放下餐具
System.out.println(name+"puts down"+lefthand+"left.");
}
//解除左手餐具的锁定
}
}

测试类

1
2
3
4
5
System.out.println("test begin:");
Tool spoon=new Tool("Spoon");
Tool fork=new Tool("Fork");
new EaterThread("A", spoon, fork).start();
new EaterThread("B", fork, spoon).start();

测试结果:

test begin:
Atakes up[Spoon]left.
A takes up[Fork]right.
A is eating
A puts down[Fork]right.
Aputs down[Spoon]left.
Atakes up[Spoon]left.
A takes up[Fork]right.
A is eating
A puts down[Fork]right.
Aputs down[Spoon]left.
重复。。直到
Atakes up[Spoon]left.
Btakes up[Fork]left.
该处程序停止不动了,也就是a和b分别拿着汤勺和叉子,即处于死锁状态了。

一种最简单的方法就是改变顺序,即以相同顺序拿餐具
即:

Tool spoon=new Tool(“Spoon”);
Tool fork=new Tool(“Fork”);
new EaterThread(“A”, spoon, fork).start();
new EaterThread(“B”, spoon,fork).start();

最后程序就会一直进行下去,不过这样的话,前面的共享资源就不存在了!

热评文章