其实就是多个线程操做同一个资源,但动做不一样。
示例:在某个数据库中,Input输入人的姓名,性别,Output输出,两个线程同时做用。
思考:1.明确哪些代码是多线程操做的?2.明确共享数据。3.明确多线程代码中哪些是共享数据的。
思考后发现,Input和Output类中的run方法对Res类的Field数据同时操做。故须要考虑使用同步。
同步前提:1.是多线程。2.必须是多个线程使用同一个锁
惟一的锁有:类字节码文件(非静态同步函数不推荐),资源对象rjava
class Res //共同处理的资源库,包含两个属性 { String name; String sex; } class Input implements Runnable { private Res r; Input (Res r) { this.r = r; } public void run() { int x = 0; while (true) { synchronized (r) { if (x==0) { r.name="mike"; r.sex="male"; x=1; } else { r.name="莉莉"; r.sex="女女女"; x=0; } } } } } class Output implements Runnable { private Res r; Output (Res r) { this.r = r; } public void run() { while (true) { synchronized (r) { System.out.println(r.name+"————"+r.sex); } } } } class InputoutputDemo { public static void main(String[] args) { Res r = new Res(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } }
观察结果:
因为输入线程一直抢夺资源,致使输出线程长时间属于阻塞状态。为了使其达到输入-输出的行为,考虑等待唤醒机制。数据库
注意:如下三种方法使用时要求必须有监视器(锁),所以必须使用在同步里。须要标示他们所操做线程持有的锁。等待和唤醒必须是同一个锁。
-wait();将该线程载入线程池,等待唤醒。(该方法抛出异常,故须要配合try catch使用)
-notify();随机唤醒线程池中一线程。
-notifyAll();唤醒线程池中全部线程。
代码以下:数据结构
class Res //共同处理的资源库 { String name; String sex; boolean flag = false; //标识位来表示和判断已输入or已输出 } class Input implements Runnable { private Res r; Input (Res r) { this.r = r; } public void run() { int x = 0; while (true) { synchronized (r) { if (r.flag) //若是标识位为真,说明已经输入,此时关闭输入,等待输出 { try { r.wait();//wait配合try catch使用,且要标识锁。 } catch (Exception e) { } } else //不然输入数据,置标识位为真并唤醒输出。 { if (x==0) { r.name="mike"; r.sex="male"; x=1; } else { r.name="莉莉"; r.sex="女女女"; x=0; } r.flag = true; r.notify(); //唤醒输出 } } } } } class Output implements Runnable { private Res r; Output (Res r) { this.r = r; } public void run() { while (true) { synchronized (r) { if (r.flag) //若是标识位为真,则有数据等待输出,此时取出数据后置标识位为假,唤醒输入 { System.out.println(r.name+"————"+r.sex); r.flag = false; r.notify(); } else //不然关闭输出。等待输入 try { r.wait(); } catch (Exception e) { } } } } } class InputoutputDemo { public static void main(String[] args) { Res r = new Res(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); } }
最后考虑到设计惯例,封装数据和操做方法,优化后代码以下(参考设计思路和设计惯例)多线程
class Res //共同处理的资源库 { private String name; private String sex; private boolean flag = false; //标识位来表示和判断已输入or已输出 public synchronized void set(String name,String sex) { if (flag) try { this.wait(); //非静态同步函数的锁为this } catch (Exception e) { } this.name = name; this.sex = sex; flag = true; this.notify(); } public synchronized void out() { if (!flag) try { this.wait(); } catch (Exception e) { } System.out.println(name+"......."+sex); flag = false; this.notify(); } } class Input implements Runnable { private Res r; Input (Res r) { this.r = r; } public void run() { int x = 0; while (true) { if (x==0) r.set("mike","male"); else r.set("莉莉","女女女女"); x = (x+1)%2; } } } class Output implements Runnable { private Res r; Output (Res r) { this.r = r; } public void run() { while (true) { r.out(); } } } class InputoutputDemo { public static void main(String[] args) { Res r = new Res(); new Thread(new Input(r)).start(); //匿名对象,简化代码 new Thread(new Output(r)).start(); /* Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start(); t2.start(); */ } }
1.加锁顺序
当多个线程须要相同的一些锁,可是按照不一样的顺序加锁,死锁就很容易发生。
若是能确保全部的线程都是按照相同的顺序得到锁,那么死锁就不会发生。看下面这个例子:函数
Thread 1: lock A lock B Thread 2: wait for A lock C (when A locked) Thread 3: wait for A wait for B wait for C
若是一个线程(好比线程3)须要一些锁,那么它必须按照肯定的顺序获取锁。它只有得到了从顺序上排在前面的锁以后,才能获取后面的锁。按照顺序加锁是一种有效的死锁预防机制。可是,这种方式须要你事先知道全部可能会用到的锁,并对这些锁作适当的排序,但总有些时候是没法预知的。工具
2.加锁时限
另一个能够避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程当中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功得到全部须要的锁,则会进行回退并释放全部已经得到的锁,而后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,而且让该应用在没有得到锁的时候能够继续运行。此外,若是有很是多的线程同一时间去竞争同一批资源,就算有超时和回退机制,仍是可能会致使这些线程重复地尝试但却始终得不到锁,由于这些线程等待相等的重试时间的几率就高的多。
这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你须要建立一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。优化
3.死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁而且锁超时也不可行的场景。this
每当一个线程得到了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此以外,每当有线程请求锁,也须要记录在这个数据结构中。spa
当一个线程请求锁失败时,这个线程能够遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,可是锁7这个时候被线程B持有,这时线程A就能够检查一下线程B是否已经请求了线程A当前所持有的锁。若是线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。线程
固然,死锁通常要比两个线程互相持有对方的锁这种状况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它须要递进地检测全部被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,而后又找到了线程D,发现线程D请求的锁被线程A本身持有着。这是它就知道发生了死锁。
下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就能够被用来检测死锁。
那么当检测出死锁时,这些线程该作些什么呢?
一个可行的作法是释放全部锁,回退,而且等待一段随机的时间后重试。这个和简单的加锁超时相似,不同的是只有死锁已经发生了才回退,而不会是由于加锁的请求超时了。虽然有回退和等待,可是若是有大量的线程竞争同一批锁,它们仍是会重复地死锁,缘由同超时相似,不能从根本上减轻竞争。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁同样继续保持着它们须要的锁。若是赋予这些线程的优先级是固定不变的,同一批线程老是会拥有更高的优先级。为避免这个问题,能够在死锁发生的时候设置随机的优先级。