《图解Java多线程设计模式》之二:Single Thread Execution 模式
一,什么是SingleThreadExecution模式?
同一时间内只能让一个线程执行处理
二,例子
1.不安全的情况
用程序模拟 三个人频繁地通过一个只允许一个人经过的门。当人通过时,统计人数便会增加,并记录通行者的姓名和地址
门:
public 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) != address.charAt(0)){ System.out.println("*****borken******"+toString()); } } }
通行者:
public 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; } @Override public void run() { System.out.println(myName+" is ready....");
//频繁通过门 while (true){ gate.pass(myName,myAddress); } } }
创建三个通过门的人
public class Test { public static void main(String[] args) { Gate gate = new Gate(); new UserThread(gate,"aaa","aa").start(); new UserThread(gate,"bbb","bb").start(); new UserThread(gate,"ccc","cc").start(); } }
运行结果:
ccc is ready....
*****borken******NO.4717:aaa, bb
分析:
当人通过门时,会记录人的名字和人的地址。但从这行结果看,这和我们预期的结果不一样。
2.安全的例子
上面的问题就出现在同一时刻不只有一个人通过(可能多个人同时通过这个门),导致记录人的名字和地址时就会出现混乱。如何解决呢? 其实,我们只要确保某一时刻只能有一个人通过这个门,问题就解决了。我们可以考虑使用Single Thread Execution模式。
线程安全的门:
public class SafeGate { 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 synchronized String toString(){ return "NO."+counter+":"+name+", "+address; } private void check() { if (name.charAt(0) != address.charAt(0)){ System.out.println("*****borken******"+toString()); } } }
其他的不用改变。
运行结果:
aaa is ready....
bbb is ready....
ccc is ready....
3.synchronized的作用
synchronized方法能够确保该方法同时只能由一个线程执行
三,SingleThreadExecution模式中的登场角色
SharedResource资源:可以被多个线程访问的类,包含很多方法,分为两类
安全方法:多个线程同时访问也没有关系
不安全方法:多个线程访问出现问题,必须加以保护
SingleThreadExecution模式会保护不安全的方法,使其同时只能由一个线程访问
临界区:只允许单个线程执行的程序范围
四,什么时候使用SingleThreadExecution模式?
1.多线程时
2.多个线程访问时:当ShareResource角色的实例有可能被多个线程同时访问时
3.状态有可能发生变化时:ShareResource角色的状态发生变化
4.需要确保安全性时:
五,生存性与死锁
1.在使用SingleThreadExecution模式时,会存在发生死锁的危险
2.死锁是指两个线程分别持有着锁,并相互等待对方释放锁的现象。
3.发生死锁条件:
存在多个SharedResource角色
线程在持有某个SharedResource角色的锁的同时,还去获取其他SharedResource角色的锁
获取SharedResource角色的锁的顺序并不固定
解决:只要破坏上面条件中的一个,就可以防止死锁发生了。
4.死锁的例子:
比如,有两个人A和B一起吃一份意大利面,但是桌子上只有一把勺子和一把叉子。吃面必须要同时用勺子和叉子。
假如A先拿到了勺子,而这时B拿到了叉子。这是会出现什么情况?
拿到勺子的A一直等着B放下叉子
拿到叉子的B一直等着A放下勺子
而他们会一直僵持下去,对应到程序中,就是两个线程分别持有锁,并等待对方释放锁。导致程序无法运行,这就是死锁
5.用代码来实现:
/** * 用餐工具 */ public class Tool { private final String name; public Tool(String name){ this.name = name; } @Override public String toString() { return "["+name+"]"; } }
public class EaterThread extends Thread { private String name; private final Tool lefthand; private final Tool righthand; public EaterThread(String name,Tool lefthand,Tool right){ this.name = name; this.lefthand = lefthand; this.righthand = right; } @Override 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 now....."); System.out.println(name +"puts down "+righthand+" (right)."); } System.out.println(name+ "puts down "+lefthand+" (left)."); } } }
public class Test { public static void main(String[] args) { System.out.println("test....."); //创建吃饭的工具 Tool spoon = new Tool("Spoon"); Tool fork = new Tool("Fork"); //两个人抢夺工具吃饭 new EaterThread("Alice",spoon,fork).start(); new EaterThread("Bobby",fork,spoon).start(); } }
运行结果: 两个人分别持有锁,互相等待对方释放锁
Bobby takes up [Fork] (left).
Alice takes up [Spoon] (left).
6.解决:
方法一:A和B以相同的顺序拿餐具
public class Test { public static void main(String[] args) { System.out.println("test....."); //创建吃饭的工具 Tool spoon = new Tool("Spoon"); Tool fork = new Tool("Fork"); //两个人抢夺工具吃饭,相同的顺序拿餐具 new EaterThread("Alice",spoon,fork).start(); new EaterThread("Bobby",spoon,fork).start(); } }
方法二: 勺子和叉子成对拿取,那就只用一把锁就可以了。
public class Tool { private final String name; public Tool(String name){ this.name = name; } @Override public String toString() { return "["+name+"]"; } }
public class Pair { private final Tool lefthand; private final Tool righthand; public Pair(Tool lefthand, Tool righthand) { this.lefthand = lefthand; this.righthand = righthand; } @Override public String toString() { return "Pair{" + "lefthand=" + lefthand + ", righthand=" + righthand + '}'; } }
public class EaterThread extends Thread { private String name; private final Pair pair; public EaterThread(String name,Pair pair){ this.name = name; this.pair = pair; } @Override public void run() { while (true){ eat(); } } public void eat(){ synchronized (pair){ System.out.println(name+" takes up "+pair+" ."); System.out.println(name+ " is eating now....."); System.out.println(name+ "puts down "+pair+" ."); } } }
public class Test { public static void main(String[] args) { System.out.println("test....."); //创建吃饭的工具 Tool spoon = new Tool("Spoon"); Tool fork = new Tool("Fork"); Pair pair = new Pair(spoon,fork); //两个人抢夺工具吃饭 new EaterThread("Alice",pair).start(); new EaterThread("Bobby",pair).start(); } }
六,临界区大小和性能
SingleThreadExecution模式降低性能的原因:
1.获取锁花费时间
进入synchronized方法时,线程需要获取对象的锁,这个处理花费时间。若SharedResource角色的数量减少了,那么获取锁的数量减少,花费时间就会减少
2.线程冲突引起的等待
当线程A执行临界区的代码时,其他线程想要进入临界区的线程会阻塞,这称之为线程冲突。若尽可能缩小临界区的范围,可以减少线程冲突的概率。
七,Before/After模式
1.synchronized语法
synchronized(this){
....
}
上面的语法可以看作在 { 处获取锁,在 } 处释放锁。
2.显示处理锁
void method(){
lock();//加锁
...
unlock();//解锁
}
缺点:如果在lock方法和unlock方法之间存在return,那么锁就无法释放了。当lock和unlock之间抛出异常,锁也无法释放。而synchronized无论是return
还是抛出异常,都一定能够释放锁。
3.解决:
我们想在调用lock()后,无论执行什么操作,unlock()都会被调用,我们可以使用finally来处理
void method(){
lock();
try{
...
}finally{
unlock();
}
}
finally的这种用法是Before/After模式(事前/事后模式)的实现方法之一。
八,思考
使用synchronized时思考:
synchronized在保护什么
其他地方也妥善保护了吗
以什么单位保护
使用哪个锁保护
2.原子操作
从多线程观点来看,这个synchronized方法执行的操作是不可分割的操作,可以看成是原子操作
九,计数信号量和Semaphore类
Single Thread Execution模式用于确保某个区域只能由一个线程执行。如果我们想让某个区域最多能由N个线程执行,该如何实现?
java.util.concurrent包提供了表示计数信号量的Semaphore类。 可以用来控制并发数量
public class SemaphoreTest { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore sp = new Semaphore(3); for (int i = 0; i < 10; i++) { Runnable runnable = new Runnable() { @Override public void run() { try { //拿信号灯 sp.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程"+Thread.currentThread().getName()+ "进入,当前已经有"+(3-sp.availablePermits())+"个并发"); try { Thread.sleep((long) (Math.random()*10000)); } catch (InterruptedException e) { e.printStackTrace(); } //处理业务 try { System.out.println("处理业务中......."); }finally { System.out.println("线程"+Thread.currentThread().getName()+"即将离开"); //释放信号灯 sp.release(); } System.out.println("线程"+Thread.currentThread().getName()+ "已离开,当前有"+(3-sp.availablePermits()+"个并发")); } }; executorService.execute(runnable); } } }
当Semaphore sp = new Semaphore(1);只有一个信号灯时,作用相当于synchronized
sp.acquire和sp.release中间的代码最多允许N个线程同时访问
十,Mutex
Mutex是Mutual Exclusion(互斥)的缩写,使用Mutex类可以完成synchronized的功能。像Mutex类这样执行互斥处理的机制称为Mutex。
1.自己实现一个Mutex类
public class UserThread extends Thread{ private final MutexGate gate; private final String myName; private final String myAddress; public UserThread(MutexGate gate, String myName,String myAddress){ this.gate = gate; this.myName = myName; this.myAddress = myAddress; } @Override public void run() { System.out.println(myName+" is ready...."); while (true){ gate.pass(myName,myAddress); } } }
public class MutexGate { private int counter =0; private String name = "Nobody"; private String adress = "Nowhere"; private final Mutex mutex = new Mutex(); public void pass(String name,String address){ mutex.lock(); try { this.counter++; this.name = name; this.adress = address; }finally { mutex.unlock(); } } public String toString(){ String s = null; mutex.lock(); try { s = "NO."+counter+": "+name+", "+adress; }finally { mutex.unlock(); } return s; } public void check(){ if(name.charAt(0)!= adress.charAt(0)){ System.out.println("*******broken+++++"+toString()); } } }
public final class Mutex { private boolean busy = false; public synchronized void lock(){ while (busy){ try { wait(); }catch (InterruptedException e){ } } } public synchronized void unlock(){ busy = false; notifyAll(); } }
public class Test { public static void main(String[] args) { MutexGate gate = new MutexGate(); new UserThread(gate,"aaa","aa").start(); new UserThread(gate,"bbb","bb").start(); new UserThread(gate,"ccc","cc").start(); } }
2.问题:
从Mutex类中可以分析:
(1,一个线程不能重入
假如某一个线程连续调用两次lock方法,当第二次调用时,由于busy字段已经变为true,所以会执行wait。相当于自己把自己所在了外面
(2,任何人都可以unlock
即使线程自己没有调用lock方法,也能调用unlock方法。相当于不是自己上的锁,自己也可以打开一样
3.改良Mutex
public final class MutexPlus { private long locks = 0;//记录当前锁的个数 (锁的个数 = lock的调用次数 - unlock的调用次数) private Thread owner = null;//记录当前线程 (把调用lock方法的线程赋值给owner) public synchronized void lock(){ Thread me = Thread.currentThread(); while (locks>0 && owner != me){ try { wait(); }catch (InterruptedException e){ } } assert locks ==0 || owner ==me;//断言,显式地表达此处肯定可以成立的条件 owner = me; locks++; } public synchronized void unlock(){ Thread me = Thread.currentThread(); if (locks ==0 || owner != me){ return; } assert locks > 0 || owner == me; locks--; if (locks == 0){ owner =null; notifyAll(); } } }
4.使用java并法包提供的类
Lock lock = new ReentrantLock(); lock.lock(); //...... lock.unlock();