资讯专栏INFORMATION COLUMN

并发学习笔记 (5)

meteor199 / 2844人阅读

摘要:执行会重新将设置为,并且通知唤醒其中一个若有的话在方法中调用了函数而处于等待状态的线程。除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。竞争失败的线程处于就绪状态,长期竞争失败的线程就会饥饿。

tutorials site

Locks in java
  

Locks (and other more advanced synchronization mechanisms) are created using synchronized blocks, so it is not like we can get totally rid of the synchronized keyword.
锁的实现是利用synchonized, wait(),notify()方法实现的。所以不可以认为锁可以完全脱离synchonized实现。

Java包 JUC java.util.concurrent.locks 包括了很多lock接口的实现了类,这些类足够使用。
但是需要知道如何使用它们,以及这些类背后的理论。JUC包教程

用synchonized:可以保证在同一时间只有一个线程可以执行 return ++count:

public class Counter{
  private int count = 0;

  public int inc(){
    synchronized(this){
      return ++count;
    }
  }
}

以下的Counter类用Lock代替synchronized 达到同样的目的:
lock() 方法会对 Lock 实例对象进行加锁,因此所有其他对该对象调用 lock() 方法的线程都会被阻塞,直到该 Lock 对象的 unlock() 方法被调用。

public class Counter{
  private Lock lock = new Lock();
  private int count = 0;

  public int inc(){
    lock.lock();
    int newCount = ++count;
    lock.unlock();
    return newCount;
  }
}

那么问题来了, Lock类是怎么设计的?

Lock 类的设计

一个Lock类的简单实现:

javapublic class Lock{
  private boolean isLocked = false;

  public synchronized void lock() throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}

while(isLocked) 循环, 又被称为spin lock自旋锁。当 isLockedtrue 时,调用 lock() 的线程在 wait() 调用上阻塞等待。为防止该线程没有收到 notify() 调用也从 wait() 中返回(也称作虚假唤醒),这个线程会重新去检查 isLocked 条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果 isLocked 为 false,当前线程会退出 while(isLocked) 循环,并将 isLocked 设回 true,让其它正在调用 lock() 方法的线程能够在 Lock 实例上加锁。

当线程完成了临界区(位于 lock() 和 unlock() 之间)中的代码,就会调用 unlock()。执行 unlock() 会重新将 isLocked 设置为 false,并且通知(唤醒)其中一个(若有的话)在 lock() 方法中调用了 wait() 函数而处于等待状态的线程。

锁的可重入性

synchronized 同步块是可重入的。这意味着: 如果一个java线程进入了代码中的同步块synchonzied block,并因此获得了该同步块使用的同步对象对应的管程monitor object上的锁那么这个线程可以进入由同一个管程对象所同步的另一个 java 代码块

前面的Lock的设计就不是可重入的:

javapublic class Reentrant2{
    Lock lock = new Lock();

    public outer(){
        lock.lock();
        inner();
        lock.unlock();
    }

    public synchronized inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

一个线程是否被允许退出 lock() 方法是由 while 循环(自旋锁)中的条件决定的。当前的判断条件是只有当 isLocked 为 false 时 lock 操作才被允许,而没有考虑是哪个线程锁住了它。
所以需要对Lock的设计做出如下修改,才能可重入。

javapublic class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;

    public synchronized void lock()
        throws InterruptedException{
        Thread callingThread =
            Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
  }

    public synchronized void unlock(){
        if(Thread.curentThread() ==
            this.lockedBy){
            lockedCount--;

            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }

    ...
}

注意到现在的 while 循环(自旋锁)也考虑到了已锁住该 Lock 实例的线程。如果当前的锁对象没有被加锁 (isLocked = false),或者当前调用线程已经对该 Lock 实例加了锁,那么 while 循环就不会被执行,调用 lock() 的线程就可以退出该方法(译者注:“被允许退出该方法” 在当前语义下就是指不会调用 wait() 而导致阻塞)。

除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。否则,一次 unblock() 调用就会解除整个锁,即使当前锁已经被加锁过多次。在 unlock() 调用没有达到对应 lock() 调用的次数之前,我们不希望锁被解除。

现在这个 Lock 类就是可重入的了。

锁的公平性
 Starvation and Fairness 饥饿和公平

一个线程因为其他线程长期占有CPU而自己获得不到,这种状态称为Starvation. 解决线程饥饿的方法是公平机制fairness公平机制,让所有线程都能公平的有机会去获得CPU。

导致饥饿的原因

高优先级的线程占有了所有CPU处理时间,这样低优先级的线程获得不到;

处于阻塞状态的线程无限期被阻塞
Java 的同步代码区也是一个导致饥饿的因素。Java 的同步代码区对哪个线程允许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,因为其他线程总是能持续地先于它获得访问,这即是 “饥饿” 问题,而一个线程被 “饥饿致死” 正是因为它得不到 CPU 运行时间的机会

  

Java"s synchronized code blocks can be another cause of starvation.

处于等待状态的对象无限期等待
如果多个线程处在 wait() 方法执行上,而对其调用 notify() 不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。

这里细说一下:多线程通过共享一个object对象,来调用对象的wait/notifyAll 来导致线程等待或者唤醒; 每次一个线程进入同步块,其他所有线程陷入等待状态;然后active线程调用notifyALL()函数唤醒所有等待线程,所有线程竞争,只有一个线程竞争成功,获得CPU执行。竞争失败的线程处于就绪状态,长期竞争失败的线程就会饥饿。

线程之间的对资源(object)竞争导致的饥饿,为了避免竞争,所以想办法一次唤醒一个线程。也就是下面讲的FairLock 公平锁机制。

Implementing Fairness in Java

使用锁lock来代替同步块synchonized block

  

每一个调用 lock() 的线程都会进入一个队列,当解锁后,只有队列里的第一个线程 (队首)被允许锁住 Fairlock 实例,所有其它的线程都将处于等待状态,直到他们处于队列头部。
公平锁实现机制:为每一个线程创建一个专属锁对象(而非多个线程共享一个对象,来wait/notify()),然后用一个队列来管理这些锁对象,尝试加锁的线程会在各自的对象上等待,当一个线程unlock的时候,只通知队列头的锁对象,以唤醒其对应的线程

为了让这个 Lock 类具有可重入性,我们需要对它做一点小的改动:

javapublic class FairLock {
    private boolean           isLocked       = false;
    private Thread            lockingThread  = null;
    private List waitingThreads =
            new ArrayList();

  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();
    }
  }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64366.html

相关文章

  • 并发学习笔记 (6)

    摘要:每个通过网络到达服务器的连接都被包装成一个任务并且传递给线程池。线程池的线程会并发的处理连接上的请求。用线程池控制线程数量,其他线程排队等候。实现包,线程池顶级接口是但是严格意义讲并不是一个线程。此线程池支持定时以及周期性执行任务的需求。 tutorial site1tutorial site2 一个问题: 每启动一个新线程都会有相应的性能开销(涉及到OS的交互:创建线程,销毁线程...

    superw 评论0 收藏0
  • Java 并发学习笔记(二)

    摘要:请参看前一篇文章并发学习笔记一原子性可见性有序性问题六等待通知机制什么是等待通知机制当线程不满足某个条件,则进入等待状态如果线程满足要求的某个条件后,则通知等待的线程重新执行。经极客时间并发编程实战专栏内容学习整理 请参看前一篇文章:Java 并发学习笔记(一)——原子性、可见性、有序性问题 六、等待—通知机制 什么是等待通知—机制?当线程不满足某个条件,则进入等待状态;如果线程满足要...

    zgbgx 评论0 收藏0
  • Java 并发学习笔记(一)——原子性、可见性、有序性问题

    摘要:最后,总结一下,导致并发问题的三个源头分别是原子性一个线程在执行的过程当中不被中断。可见性一个线程修改了共享变量,另一个线程能够马上看到,就叫做可见性。 计算机的 CPU、内存、I/O 设备的速度一直存在较大的差异,依次是 CPU > 内存 > I/O 设备,为了权衡这三者的速度差异,主要提出了三种解决办法: CPU 增加了缓存,均衡和内存的速度差异 发明了进程、线程,分时复用 CP...

    Chao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<