单线程执行模式
案例-1
背景
模拟3个人频繁地经过同一个只能容许一个人经过的门 。
(模拟三个线程调用同一个对象的方法)
当人通过门的时候,这个程序会在计数器中,递增通过的人数。另外, 还会记录通过的人的 “ 姓名与出生地”。
(当某一个线程调用该对象方法的时候,都会进行计数(使该对象的counter属性+1),另外还会记录当前对象的name和address属性,并检查name和address属性关系是否正确对应)
Main.class
创建一 个门,并操作了个人不断地穿越门的 类
Gate.class
表 示 门 的 类 , 当 人经 过 时 会 记 录姓 名 与 出 生 地 表示人的类,
UserThread.class
只负责处理不断地在门间穿梭通过
Gate.class
class Gate{
//表示已经通过这道门的人数
private int counter = 0;
//表示通过这道门的人名
private String name = "Nobody";
//表示通过这道门的人的出生地
private String address = "Nowhere";
public void pass (String name, String address) {
this.counter++;
this.name = name;
this.address = address;
check();
}
public String toString() {
return "No." + counter + ": " + name + ", " + address;
}
/**
* 这里为了判断人名和地址是否匹配直接看 人名 和 出生地 的首字母是否一致进行判断了
*/
private void check(){
if (name.charAt(0) != name.charAt(0)){
System.out.println("********* broken *********** " + toString());
}
}
}
UserThread.class
class UserThread extends Thread{
private final Gate gate;
private final String myname;
private final String myaddress;
public UserThread (Gate gate, String myname, String myaddress){
this.gate = gate;
this.myname = myname;
this.myaddress = myaddress;
}
public void run() {
System.out.println (myname + " start");
while (true) {
gate.pass(myname, myaddress);
}
}
}
Main.class
public class Demo {
public static void main(String[] args) {
System.out.println("Testing Gate, hit CIRLtC to exit");
Gate gate = new Gate();
new UserThread(gate,"Axxxx","Alaska").start();
new UserThread(gate,"Bxxxx","Brazil").start();
new UserThread(gate,"Cxxxx","Canada").start();
}
}
执行结果
分析
我们看一下这个执行的结果,每个线程开始执行的时候都会先打印一下穿过这个门的人姓名,同时我们预想中的
broken
出现了,但是好像又和预想的有点不一样。我们预想的结果是出现broken时,后面的人名和出生地不对应的,但是咱们这里出现broken时,人名和出生地大部分都是对应的,从结果来看只有一条记录是不对应的。这是为什么呢?同时我们也能看到第一次出现broken的时候已经是第128次了,侧面的说明了有些时候仅仅靠测试是无法验证程序的安全性。
从第一条broken的打印结果来看,明明已经是姓名和出生地不匹配了,但是打印的结果是匹配的,这也从侧面说明了,在多线程的程序调试过程中,调试的结果是不可信的,值得注意。
原因分析:
在这里是以语句当作线程的基本操作单位,事实上线程可能是以更小的单位在切换的。
我们的pass方法;
public void pass (String name, String address) { this.counter++; this.name = name; this.address = address; check(); }
pass( ) 方法里面有四条语句,每个多线程在执行的时候,可能会是交错依次执行这四条语句的。通常线程是不会去考虑其他线程的,它只会自己一直不停的跑下去。所以在这里,可能线程A刚给name赋值完,此时线程A会继续给address赋值,但是在线程A给address赋值的同时,线程B也在给name重新赋值,然后线程A去调用check( )方法时就会出现broken,当出现broken的时候线程A会继续调用toString( )方法,此时线程B又给address赋值完了,然后toString()方法打印的就会是线程B操作的name和address,线程A在进行check( )方法检查的时候,使用的是线程B的name和线程A的address进行判断的。所以就会出现我们最终在控制台打印的结果。
修改成线程安全版本
这里我们只需要加固这个门就可以实现安全问题,我们将门的pass( )和check( )方法进行线程安全处理就可以了 。
class Gate{ //表示已经通过这道门的人数 private int counter = 0; //表示通过这道门的人名 private String name = "Nobody"; //表示通过这道门的人的出生地 private String address = "Nowhere"; public synchronized void pass (String name, String address) { this.counter++; this.name = name; this.address = address; check(); } public String toString() { return "No." + counter + ": " + name + ", " + address; } /** * 这里为了判断人名和地址是否匹配直接看 人名 和 出生地 的首字母是否一致进行判断了 */ private synchronized void check(){ if (name.charAt(0) != name.charAt(0)){ System.out.println("********* broken *********** " + toString()); } } }
.
在本案例中,Gate类就担任了共享资源参与者的角色。
对于pass( )和check( )这两个不安全的方法我们使用
synchronized
修饰符进行修饰后,就可以保证它的线程安全问题。
synchronized
synchronized
修饰符并不会对程序的安全造成危害,但是调用synchronized
修饰的方法时会比调用一般方法花费比较多的一些时间,所以会使程序的性能降低。在单线程程序中使用
synchronized
方法,就好像在外独居的人,即使一个人在 家中还是将厕所的门锁住的意思一样。既然是 一个人住,即使不锁门,也不需要担 心有人会突然打开厕所的门 了。就算是多线程程序,如果所有线程完全地独立运行,那也没有使用 单线程执行模式 的必要。我们將这个状态称为线程互不干涉(interfere)。 有些管理多线程的环境,会帮我们确保线程的独立性,这种情况下这个环境的 用户就不必考虑需不需要使用单线程执行模式。
生命性与死锁
使用 单线程执行模式 时,可能会有发生死锁 (deadlock )的危险。 所谓的死锁,是指两个线程分别获取了锁定,互相等待另一个线程解除锁定的 现象。发生死锁时,哪个线程都无法继续执行下去,所以程序会失去生命性。
死锁小案例背景
假设 A 与 B 同吃一个大盘子所盛放的意大利面。盘子的旁边只有一支汤匙与一支叉子,而要吃意大利面时,同时需要用到汤匙与叉子。
只有 一支的汤匙,被 A 拿去了,两只有一支的叉子,却被 B 拿走了。 就造成以 下的情况:
- 握着汤匙的 A ,一直等着B 把叉子放下
- 握着叉子的 B , 一直等着A 把汤匙放下
这么一来 A 与 B 只有面面相觑,就这样不动了。像这样,多个线程僵持不下,使程序无法继续运行的状态,就称为死锁。
死锁出现条件
单线程执行达到下面这些条件时,可能会出现死锁的现象:
- 具有多个SharedResource(共享资源) 参与者
- 线程锁定一个SharedResource 时,还没解除前就去锁定另 一个SharedResource。
- 获取SharedResource 参与者的顺序不固定 (和SharedResource 参与者是 对 等 的 )。
对比一下上面吃意大利面的例子:
多个SharedResource 参与者,相当于汤匙与叉子。
锁定某个SharedResource 参与者后,就去锁定其他SharedResource。就相当于握着汤匙而想 要获取对方的叉子,或握着叉子而想要获取对方的汤匙这些操作。
SharedResource角色是对等的,就像“拿汤匙一拿叉子”与“拿叉子一拿汤匙 〞两个操作都可能发生。也就是说在里汤匙与叉子并没有优先级 。
上面的三个条件只要破坏一种条件,就可以避免死锁的发生。
synchronized 在保护什么
请读者阅读程序代码的时候,看到synchronized 时,就要思考:
“这个synchronized 是在保护什么?”
无论是synchronized 方法或是synchronized块,syncbronized势必保护着“某个 东西”。
确认 “ 保护什么” 之后,接 下来应该要思考的是:
“ 其他的地方也有妥善保护到吗?”
若这些字段还在其他许多地方使用着,这里使用synchronized 小心保护,但其他地方并没有做好保护措施, 那其实这个字段还是没被保护的。就像就算小心翼翼地把大门跟后门都锁得好好的, 如果窗户敞开, 还是没有意义一样
“ 获取谁的锁定来保护的呢?,
要调用synchronized 实例方法 (instance method)的线程, 一定会获取this 的锁 定。一个实例的锁定,同一时问内只能有 一线程可以得到。因为这个惟一性,我们 才能使用synchronized 来做到单线程执行模式 如果实例不同,那锁定也不同了。虽然我们说“使用synchronized 来保护”,但 如果有多个相异实例,那多个线程仍然可以分别执行不同实例的 synchronized 方法 。
案例-2
##### 背景:
案例-1的死锁小案例简化版,汤匙和叉子,我们这里简称左手工具和右手工具,
A和B吃饭需要左手和右手配合,两只手都需要拿到对应的工具才可以吃饭。
代码:
public class Demo {
public static void main(String[] args) {
System.out.println("Testing Gate, hit CIRLtC to exit");
Tools left = new Tools("left");
Tools right = new Tools("right");
new EatThread("A",left,right).start();
new EatThread("B",right,left).start();
}
}
class EatThread extends Thread{
private String name ;
private final Tools leftTool;
private final Tools rightTool;
public EatThread(String name, Tools leftTool, Tools rightTool) {
this.name = name;
this.leftTool = leftTool;
this.rightTool = rightTool;
}
@Override
public void run() {
while (true){
eat();
}
}
public void eat(){
synchronized (leftTool){//左手工具拿起
System.out.println(name + " " + "拿到了 " + leftTool.toString());
synchronized (rightTool){//右手工具拿起
System.out.println(name + " " + "拿到了 " + rightTool.toString());
System.out.println(name + "开始吃东西了========> eat");
System.out.println(name + "放下了右手" + leftTool.toString());
}//右手工具放下
System.out.println(name + "放下了左手" + rightTool.toString());
}//左手工具放下
}
}
class Tools{
private final String name ;
public Tools(String name) {
this.name = name;
}
@Override
public String toString() {
return name ;
}
}
分析
上面的代码运行后会发生死锁,是因为main方法中A和B线程传参数是对称传入,这就造成了A和B拿起左右手工具时顺序不一致,就会造成A和B分别拿到一个工具,并且另一个工具都在对方手中,都在等对方放下手中的工具。
纠正
方案1
修改main方法中的A和B的传参数顺序一致,这样A和B都同时按照同一种顺序拿取工具,先拿左手工具再去拿右手工具,当左手工具没有拿到的时候就不会再去拿右手工具。
public static void main(String[] args) { System.out.println("Testing Gate, hit CIRLtC to exit"); Tools left = new Tools("left"); Tools right = new Tools("right"); new EatThread("A",left,right).start(); new EatThread("B",left,right).start(); }
方案2
将左右手工具放在一起,不分开,这样工具只有一个,谁拿到就是谁的,拿到了就可以直接开吃,不用再去拿第二个工具。
public static void main(String[] args) { System.out.println("Testing Gate, hit CIRLtC to exit"); Tools left = new Tools("left"); Tools right = new Tools("right"); Pair pair = new Pair(left,right); new EatThread("A",pair).start(); new EatThread("B",pair).start(); }