线程同步

        1.竞争条件。在大多数实际的多线程应用中,两个或两个以上的线程须要共享对同一数据的存取。当两个线程存取相同的对象,而且每个线程都调用了一个修改该对象状态的方法的时候,根据各线程访问数据的次序,可能会产生讹误的对象。这样的状况一般称为“竞争条件”。为了不多线程引发的对共享数据的讹误,就必须学习如何“同步存取”。 java

        2.锁对象。从Java SE 5.0开始,有两种机制防止代码块受并发访问的干扰。即Java语言关键字synchronized和Java SE 5.0引入的ReentrantLock类。synchronized关键字自动提供一个锁以及相关的“条件”(Condition),对于大多数须要显式锁的状况,这是很便利的。使用ReentrantLock保护代码块的基本结构以下:
程序员

myLock.lock();//a ReentrantLock object
    try{
        critical section
    }finally{
        myLock.unlock();//make sure the lock is unlocked even if an exception is thrown
    }
        这一结构确保任什么时候刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其它任何线程都没法经过lock语句。当其它线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
        锁是可重入的(若一个程序或子程序能够“安全的被并行执行”,则称其为“可重入”的),由于线程能够重复地得到已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。因为这一特性,被一个锁保护的代码能够调用另外一个使用相同的锁的方法。
     一般,可能想要保护需若干个操做来更新或检查共享对象的代码块。要确保这些操做都完成后,另外一个线程才能使用相同对象。
        要留心临界区中的代码,不要由于异常的抛出而跳出了临界区。若是在临界区代码结束以前抛出了异常,finally子句将释放锁,但会使对象处于一种受损状态。
        另外,ReentrantLock还提供一个带有公平策略的构造方法——ReentrantLock(boolean fair)。一个公平锁偏心等待时间最长的线程。可是,这一公平的保证将大大下降性能。因此,默认状况下,锁没有被强制为公平的。不过,遗憾的是,即便使用公平锁,也没法确保线程调度器是公平的。


        3.条件对象。一般,线程进入临界区,却发如今某一条件知足以后它才能执行。这时候,就须要一个条件对象来管理那些已经得到了一个锁可是却不能作有用工做的线程(因为历史缘由,条件对象常常被称为条件变量(conditional variable))。
        一个锁对象能够有一个或多个相关的条件对象,可使用newCondition方法得到一个条件对象。习惯上给每个条件对象命名为能够反映它所表达的条件的名字。
        等待得到锁的线程和调用await方法的线程存在本质上的不一样。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能立刻解除阻塞。相反,它处于阻塞状态,直到另外一个线程调用同一条件上的signalAll方法时为止。
        当另外一条线程调用signalAll方法时,这一调用从新激活了由于这一条件而等待的全部线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图从新进入该对象。一旦锁成为可用的,他们中的某个将从await调用返回,得到该锁并从阻塞的地方继续执行。此时,线程应该再次测试该条件。因为没法确保该条件被知足——signalAll方法仅仅是通知正在等待的线程:此时有可能已经知足条件,值得再次去检测该条件。
        一般,await的调用应该在以下形式的循环体中:
编程

    while(!(Ok to proceed))
        condition.await();
        相当重要的是最终须要某个其它线程调用signalAll方法。当一个线程调用await时,它没有办法从新激活自身。它寄但愿于其它线程,而若是没有其它线程来从新激活等待的线程的话,它就永远不在运行了。这将致使使人不快的死锁(deathlock)现象。若是全部其它线程被阻塞,最后一个活动线程在解除其它线程的阻塞状态以前就调用了await方法,那么它也就被阻塞了。没有任何线程能够解除其它线程的阻塞,那么该程序就挂起了。
        从经验上讲,应该在对象的状态有利于等待线程的方向改变时调用signalAll方法。
        注意:调用signalAll方法不会当即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,经过竞争实现对对象的访问。
        另外一个方法signal,则是随机解除等待集中某个线程的阻塞状态。这比解除全部线程的阻塞更加有效,但也存在危险。若是随机选择的线程发现本身仍然不能运行,那么它再次被阻塞。若是没有其它线程再次调用signal,那么系统就死锁了。


        4.synchronized关键字。咱们先总结一下锁和条件的关键之处:1)锁用来保护代码片断,任什么时候刻只能有一个线程执行被保护的代码。2)锁能够管理试图进入被保护代码段的线程。3)锁能够拥有一个或多个相关的条件对象。4)每一个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
        Lock和Condition接口被添加到Java SE 5.0中,这也向程序设计人员提供了高度的封锁机制。然而,大多数状况下,并不须要那样的控制,而且可使用一种嵌入到Java语言内部的机制。从1.0版开始,Java中的每个对象都有一个内部锁。若是一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须得到内部的对象锁。
     换句话说,
数组

    public synchronized void method(){
        method body
    }
     等价于
    public void method(){
        this.intrinsicLock.lock();
        try{
            method body
        }finally{
            this.intrinsicLock.unlock();
        }
    }
        内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。换句话说,调用wait或者notifyAll方法等价于:
    intrinsicCondition.await();
    intrinsicCondition.signalAll();
        能够看出,使用synchronized关键字来编写代码要简洁的多。固然,要理解这些代码,就必须了解每个对象有一个内部锁,而且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
        将静态方法声明为synchronized也是合法的。若是调用这种方法,该方法得到相关的类对象(Class)的内部锁。所以,没有其它线程能够调用同一个类的这个或任何其它的同步静态方法。
        内部锁和条件存在一些局限,包括:1)不能中断一个正在试图得到锁的线程。2)试图得到锁时不能设置超时。3)每一个锁仅有单一的条件,多是不够的。
        在代码中选择使用Lock和Condition对象仍是同步方法的一些建议:1)最好既不使用Lock/Condition也不使用synchronized关键字。在许多状况下你可使用java.util.concurrent包中的一种机制(阻塞队列),它会为你处理全部的加锁。2)若是synchronized关键字适合你的程序,那么请尽可能使用它,这样能够减小编写的代码数量,减小出错的概率。3)若是特别须要Lock/Condition结构提供的独有特性时,才使用Lock/Condition。


        5.同步阻塞。每一个Java对象都有一个锁。线程能够经过调用同步方法得到锁,也能够经过进入一个同步阻塞,得到该对象的锁: 缓存

synchronized(obj){
    critical section
}
        有时程序员使用一个对象的锁来实现额外的原子操做,实际上被称为客户端锁定(client-side locking)。可是客户端锁定是很是脆弱的,一般不推荐使用。由于这种方法彻底依赖于这样一个事实,obj对本身的全部可修改方法都使用内部锁。


        6.监视器概念。锁和条件是线程同步的强大工具,可是严格地讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,能够在不须要程序员考虑如何加锁的状况下,就能够保证多线程的安全性。最成功的解决方案之一是监视器(monitor),这一律念最先是由Per Brinch Hansen和Tony Hoare 在20世纪70年代提出的。用Java的术语讲,监视器具备以下特性:
        1)监视器是只包含私有域的类。
        2)每一个监视器类的对象有一个相关的锁。
        3)使用该锁对全部的方法进行加锁。换句话说,若是客户端调用obj.method,那么obj对象的锁是在方法调用开始时自动得到,而且当方法返回时自动释放该锁。由于全部的域是私有的,这样的安排能够确保一个线程在对对象操做上,没有其它线程能访问该域。
        4)该锁能够有任意多个相关条件。
    监视器的早期版本只有单一的条件,不使用任何显式的条件变量。然而,研究代表盲目地从新测试条件是低效的。显式的条件变量解决了这一问题,每个条件变量管理一个独立的线程集。
        Java设计者以不是很精确的方式采用了监视器概念,Java中的每个对象有一个内部的锁和内部的条件。若是一个方法用synchronized关键字声明,那么,它表现的就像是一个监视器方法。经过调用wait/notifyAll/notify来访问条件变量。
        然而,在下面三个方面Java对象不一样于监视器,从而使得线程的安全性降低:
        1)域不要求是私有的。
        2)方法不要求必须是synchronized。
        3)内部锁对客户是可用的。
安全

        7.Volatile域。有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。那么,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大:
        1)多处理器的计算机可以暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不一样处理器上的线程可能在同一个内存位置取到不一样的值。
        2)编译器能够改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,可是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值能够被另外一个线程改变!
        若是你使用锁来保护能够被多个线程访问的代码,那么能够不考虑这种问题。编译器被要求经过在必要的时候刷新本地缓存来保持锁的效应,而且不能不正当地从新排序指令。详细解释参见JSR 133的Java内存模型和线程规范(http://www.jcp.org/en/jsr/detail?id=133)。
        Brian Goetz给出了下述“同步格言”:若是向一个变量写入值,而这个变量接下来可能会被另外一个线程读取,或者,从一个变量读值,而这个变量多是以前被另外一个线程写入的,此时必须使用同步。
        volatile关键字为实例域的同步访问提供了一种免锁机制。若是声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另外一个线程并发更新的。
        例如,假定一个对象有一个布尔标记done,它的值被一个线程设置却被另外一个线程查询,如以前所述,你可使用锁: 数据结构

public synchronized boolean isDone(){ return done; }
public synchronized void setDone(){ done = true; }
private boolean done;
        或许使用内部锁不是个好主意。若是另外一个线程已经对该对象加锁,isDone和setDone方法可能阻塞。若是注意到这个方面,一个线程能够为这一变量使用独立的Lock。可是,这也会带来许多麻烦。
        在这种状况下,就能够将域声明为volatile:
public boolean getDone(){ return done; }
public void setDone(){ done = true; };
private volatile boolean done;
        这地方须要注意的一点是,Volatile不能提供原子性。例如,方法
public void flipDone(){ done = !done; }//not atomic
    不能确保改变域中的值。
        在这样一种很是简单的状况下,存在第三种可能性,使用AtomicBoolean。这个类有方法get和set,且确保是原子的(就像它们是同步的同样)。该实现使用有效的机器指令,在不使用锁的状况下确保原子性。在java.util.concurrent.atomic中有许多包装器类用于原子的整数、浮点数、数组等。这些类是为编写并发实用程序的系统程序员提供使用的,而不是应用程序员。
        总之,在如下三个条件下,域的并发访问是安全的:
        1)域是final的,而且在构造器调用完成以后被访问。
        2)对域的访问由公有的锁进行保护。
        3)域是volatile的。
        语言的设计者试图在优化使用volatile域的代码的性能方面给实现人员留有余地。可是,旧规范太复杂,实现人员难以理解,这带来了混乱和非预期的行为。例如,不可变对象不是真的不可变。


        8.死锁。锁和条件不能解决多线程中的全部问题,好比死锁。在一个程序中,全部线程都被阻塞,这样的状态被称为死锁。遗憾的是,Java编程语言中没有任何东西能够避免或打破这种死锁现象。必须仔细设计程序,以确保不会出现死锁。 多线程

        9.锁测试与超时。线程在调用lock方法来得到另外一个线程锁持有的锁的时候,极可能发生阻塞。应该更加谨慎地申请锁。tryLock方法试图申请一个锁,在成功得到锁后返回true,不然,当即返回false,并且线程能够当即离开去作其它事情。 并发

if(myLock.tryLock()){
    // now the thread owns the lock
    try {
        ...
    }finally{
        myLock.unlock();
    }
}else{
    do something else
}
        能够调用tryLock时,使用超时参数,例如:
if(myLock.tryLock(100,TimeUnit.MILLISECONDS))......
        TimeUnit是一个枚举类型,能够取的值包括SECONDS、MILLISECONDS、MICROSECONDS和NANOSECONDS。
        lock方法不能被中断。若是一个线程在等待得到一个锁时被中断,中断线程在得到锁以前一直处于阻塞状态。若是出现死锁,lock方法就没法终止。
        然而,若是调用带有超时参数的tryLock,那么若是线程在等待期间被中断,将抛出InterruptedException异常。这是一个很是有用的特性,由于容许程序打破死锁。
        也能够调用lockInterruptibly方法。它就至关于一个超时设为无限的tryLock方法。
     在等待一个条件时,也能够提供一个超时:
myCondition.await(100,TimeUnit.MILLISECONDS);
        若是一个线程被另外一个线程经过调用signalAll或signal激活,或者超时时限已达到,或者线程被中断,那么await方法将返回。
        若是等待的线程被中断,await方法将抛出一个InterruptedException异常。在你但愿出现这种状况时线程继续等待,可使用awaitUninterruptibly方法代替await。


        10.读/写锁。java.util.concurrent.locks包定义了两个锁类,除了前面的ReentrantLock类,还有ReentrantReadWriteLock类。若是不少线程从一个数据结构读取数据而不多线程修改其中数据的话,后者是十分有用的。在这种状况下,容许对读者线程共享访问是合适的。固然,写者线程依然必须是互斥访问的。
        下面是使用读/写锁的必要步骤:
        1)构造一个ReentrantReadWriteLock对象: 框架

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        2)抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
        3)对全部的访问者加读锁。
        4)对全部的修改者加写锁。

        11.为何弃用stop和suspend方法?初始的Java版本定义了一个stop方法用来终止一个线程,以及一个suspend方法用来阻塞一个线程直至另外一个线程调用resume。stop和suspend方法有一个共同点:都试图控制一个给定线程的行为。
        从Java1.2开始就弃用了这两个方法。stop方法天生就不安全,而经验证实suspend方法会常常致使死锁。
        首先来看stop方法,该方法终止全部未结束的方法,包括run方法。当线程被终止,当即释放被它锁住的全部对象的锁。这会致使对象处于不一致的状态。例如,假定一个银行转帐的线程TransferThread,在从一个帐户向另外一个帐户转帐的过程当中被终止,钱款已经转出,却没有转入目标帐户,如今银行对象就被破坏了,这种破坏会被其余还没有中止的线程观察到。
        当线程要终止另外一个线程时,没法知道何时调用stop方法是安全的,何时会致使对象被破坏。所以,该方法被弃用了。在但愿中止线程的时候应该中断线程,被中断的线程会在安全的时候中止。
        接下来,看看suspend方法有什么问题。与stop方法不一样,suspend不会破坏对象。可是若是用suspend挂起一个持有一个锁的线程,那么,该锁在恢复以前是不可用的。若是调用suspend方法的线程试图得到同一个锁,那么程序死锁:被挂起的线程等待恢复,而将其挂起的线程等待得到锁。
        若是想安全地挂起线程,引入一个变量suspendRequested并在run方法的某个安全的地方测试它,安全的地方是指该线程没有封锁其它线程须要的对象的地方。当该线程发现suspendRequested变量已经设置,将会保持等待状态直到它再次得到为止。下面的代码框架实现这一设计:
        

public void run(){
    while(...){
        ...
        if(suspendRequested){
            suspendLock.lock();
            try{
                while(suspendRequested)
                    suspendCondition.await();
            }finally{
                suspendLock.unlock();
            }
        }
    }
}

public void requestSuspend(){
    suspendRequested = true;
}

public void requestResume(){
    suspendRequested = false;
    suspendLock.lock();
    try{
        suspendCondition.signalAll();
    }finally{
        suspendLock.unlock();
    }
}

private volatile boolean suspendRequested = false;
private Lock suspendLock = new ReentrantLock();
private Condition suspendCondition = suspendLock.newCondition();
相关文章
相关标签/搜索