Java并发编程之锁的活跃性问题

在安全性和活跃性之间一般存在一种制衡。当咱们使用锁来保证线程的安全的同时,若是过分使用加锁,可能会致使死锁。 应用没法从死锁中恢复过来,因此在设计时必定要避免会排除这些可能会出现的活跃性问题。数据库

死锁

死锁描述了这样一种情景,两个或多个线程永久阻塞,互相等待对方释放资源编程

若是线程1锁住了A,而后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,而且它们永远也不会知道发生了这样的事情。为了获得彼此的对象(A和B),它们将永远阻塞下去。安全

pubic class LeftRightDeadlock{
    
    private Object left =new Object();
    private Object right =new Object();
    
    public void leftRight(){
        
        synchronized(left){
          synchronized(right){
              
              dosomething();
          }
        }
    }
    punlic void rightLeft(){
        synchronized(right){
            synchronized(left){
                dosomething();
            }
        }
    }
}

很常见的错误,若是同时2个线程去请求这2个方法就会出现死锁数据结构

更加复杂的死锁场景发生在数据库事务中。一个数据库事务可能由多条SQL更新请求组成。当在一个事务中更新一条记录,这条记录就会被锁住避免其余事务的更新请求,直到第一个事务结束。同一个事务中每个更新请求均可能会锁住一些记录 当多个事务同时须要对一些相同的记录作更新操做时,就颇有可能发生死锁并发

不过好在数据库设计中考虑了检测死锁和死锁恢复,数据库会选择牺牲一个事物来释放锁,从而让其余事物继续进行。 不过Java应用并无这种处理,因此咱们在设计时要格外注意。数据库设计

避免死锁

加锁顺序

当多个线程须要相同的一些锁,可是按照不一样的顺序加锁,死锁就很容易发生。this

若是能确保全部的线程都是按照相同的顺序得到锁,那么死锁就不会发生spa

按照顺序加锁是一种有效的死锁预防机制。可是,这种方式须要你事先知道全部可能会用到的锁,但总有些时候是没法预知的线程

加锁限时

另一个能够避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程当中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功得到全部须要的锁,则会进行回退并释放全部已经得到的锁,而后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,而且让该应用在没有得到锁的时候能够继续运行设计

须要注意的是,因为存在锁的超时,因此咱们不能认为这种场景就必定是出现了死锁。也多是由于得到了锁的线程(致使其它线程超时)须要很长的时间去完成它的任务

时和重试机制是为了不在同一时间出现的竞争,可是当线程不少时,其中两个或多个线程的超时时间同样或者接近的可能性就会很大,所以就算出现竞争而致使超时后,因为超时时间同样,它们又会同时开始重试,致使新一轮的竞争,带来了新的问题

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁而且锁超时也不可行的场景。

每当一个线程得到了锁,会在线程和锁相关的数据结构中将其记下。除此以外,每当有线程请求锁,也须要记录在这个数据结构中。

输入图片说明

当一个线程请求锁失败时,这个线程能够遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,可是锁7这个时候被线程B持有,这时线程A就能够检查一下线程B是否已经请求了线程A当前所持有的锁。若是线程B确实有这样的请求,那么就是发生了死锁

当出现死锁的时候能够给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁同样继续保持着它们须要的锁。若是赋予这些线程的优先级是固定不变的,同一批线程老是会拥有更高的优先级。为避免这个问题,能够在死锁发生的时候设置随机的优先级

饥饿

若是一个线程由于CPU时间所有被其余线程抢走而得不到CPU运行时间,这种状态被称之为饥饿。而该线程被饥饿致死正是由于它得不到CPU运行时间的机会。解决饥饿的方案被称之为公平性 – 即全部线程均能公平地得到运行机会

在Java中,下面几个常见的缘由会致使线程饥饿:

  • 高优先级线程吞噬全部的低优先级线程的CPU时间。

你能为每一个线程设置独自的线程优先级,优先级越高的线程得到的CPU时间越多,线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来讲,你最好是不要改变其优先级值

  • 线程被永久堵塞在一个等待进入同步块的状态,由于其余线程老是能在它以前持续地对该同步块进行访问。

Java的同步代码区也是一个致使饥饿的因素。Java的同步代码区对哪一个线程容许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,由于其余线程老是能持续地先于它得到访问

实现公平性

public class FairLock {
    private boolean           isLocked       = false;
    private Thread            lockingThread  = null;
    private List<QueueObject> waitingThreads =
            new ArrayList<QueueObject>();

  public void lock() throws InterruptedException{
    QueueObject queueObject           = new QueueObject();
    boolean     isLockedForThisThread = true;
    synchronized(this){
        waitingThreads.add(queueObject);
    }

    while(isLockedForThisThread){
      synchronized(this){
        isLockedForThisThread =
            isLocked || waitingThreads.get(0) != queueObject;
        if(!isLockedForThisThread){
          isLocked = true;
           waitingThreads.remove(queueObject);
           lockingThread = Thread.currentThread();
           return;
         }
      }
      try{
        queueObject.doWait();
      }catch(InterruptedException e){
        synchronized(this) { waitingThreads.remove(queueObject); }
        throw e;
      }
    }
  }

  public synchronized void unlock(){
    if(this.lockingThread != Thread.currentThread()){
      throw new IllegalMonitorStateException(
        "Calling thread has not locked this lock");
    }
    isLocked      = false;
    lockingThread = null;
    if(waitingThreads.size() > 0){
      waitingThreads.get(0).doNotify();
    }
  }
}


public class QueueObject {

    private boolean isNotified = false;

    public synchronized void doWait() throws InterruptedException {

    while(!isNotified){
        this.wait();
    }

    this.isNotified = false;

}

public synchronized void doNotify() {
    this.isNotified = true;
    this.notify();
}

public boolean equals(Object o) {
    return this == o;
}

}

FairLock新建立了一个QueueObject的实例,并对每一个调用lock()的线程进行入队列。调用unlock()的线程将从队列头部获取QueueObject,并对其调用doNotify(),以唤醒在该对象上等待的线程。经过这种方式,在同一时间仅有一个等待线程得到唤醒,而不是全部的等待线程。这也是实现FairLock公平性的核心所在

 

活锁

活锁是另外一种形式的活跃性问题,该问题不会阻塞线程,但也不能继续执行,由于线程将不断重复相同的操做,并且总会失, 这就至关于两个在走廊相遇的人:A 向他本身的左边靠想让 B 过去,而B 向他的右边靠想让 A 过去。可见他们阻塞了对方。A 向他的右边靠,而B向他的左边靠,他们仍是阻塞了对方。

解决方案是,让彼此重试的时候随机等待一段时间,这样就会有效避免活锁的发生。

总结

活跃性问题是很是严重的问题,当应用出现活跃性故障,每每只有中断应用程序才能解决,死锁最为常见,咱们要注意锁的顺序性问题,对于此类方法调用,有良好的封装,对使用者透明也能避免锁使用的错误,欢迎加入【Java并发编程交流组】:https://jq.qq.com/?_wv=1027&k=5mOvK7L

相关文章
相关标签/搜索