在构建安稳的并发程序时,有必要正确的运用线程和锁。但这些仅仅一些机制,要编写线程安全的代码,核心在于要对状况拜访操作进行管理,特别是对同享的(shared)和可变的(mutable)状况的拜访

从非正式意义上来说,目标的状况指存储在状况变量(实例或静态域)中的数据,目标的状况或许包含其他依靠目标的域。在目标的状况中包含了任何或许影响其他外部可见行为的数据(能够以为目标的属性便是它的状况)。

同享意味着变量能够由多个线程一起拜访,可变意味着变量的值在其生命周期内能够产生变化。

一个目标是否需求时线程安全的,取决于他是否被多个线程拜访。要使目标是线程安全的,需求选用同步机制来协同目标可变状况的拜访,假如无法完成协同,那么或许会导致数据损坏以及其他不应呈现的成果。

当多个线程拜访某个状况变量而且其间有一个线程履行写入操作时,有必要选用同步机制来协同这些线程对变量的拜访。java中的首要同步机制是关键字 synchronized,它提供了一种独占加锁的办法,同步还包含volatile 变量、显现锁以及原子变量。

** 假如当多个线程拜访同一个可变的状况变量时没有合适的同步,那么程序就会呈现过错。有三种办法能够修复这个问题:**

  • 不在线程之间同享该状况变量。
  • 将状况变量修正为不可变的变量
  • 在拜访状况变量时运用同步。

假如在规划类的时分没有考虑并发拜访的状况,那么后续的保护可谓知易行难,假如一开始就规划一个线程安全的类,那么在今后再将这个类修正为线程安全的类要简单的多。

** 拜访某个变量的代码越少,就越简单保证对变量的一切拜访都完成正确同步,一起也更简单找出变量在哪些条件下被拜访**。程序的封装性越好,就越简单完成程序的线程安全性。

当规划线程安全的类时,杰出的面向目标技能、不可修正性,以及明晰的不变性标准都能起到必定的帮助效果。

在某些状况中,杰出的面向目标规划技能与实际状况的需求并不一致,在这些状况中,或许需求献身一些杰出的规划准则,以换取功能或许对留传代码的向后兼容。

1 什么是线程安全性

在线程安全性的界说中,最核心的概念是正确性。其意义是,某个类的行为与其标准完全一致,在杰出的标准中通常会界说各种不变性条件来束缚目标的状况,以及界说各种后验条件来描述目标操作的成果。因而,从正确性的视点界说线程安全性:当多线程拜访某个类时,这个类一直都能表现出正确的行为,那么就称这个类是线程安全的

当多个线程拜访某个类时,不论运转环境选用何种调度办法或许这些线程将如何替换履行,而且在主调代码中不需求任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。

在线程安全类中封装了必要的同步机制,因而客户端无需进一步采纳同步办法。

一个无状况的Servlet

public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("do some thing");
    }
    public void init(ServletConfig servletConfig) throws ServletException {
    }
    public ServletConfig getServletConfig() {
        return null;
    }
    public String getServletInfo() {
        return null;
    }
    public void destroy() {
    }
}

StatelessFactorizer是无状况的,它不包含任何域也不包含任何其他类中域的引用。核算过程中的暂时状况仅存在线程栈上的局部变量中,而且只能由正在履行的线程拜访。拜访StatelessFactorizer的线程不会影响另一个拜访同一个StatelessFactorizer线程的核算成果,因为这两个线程没有同享状况,就好像它们都在拜访不同的实例,因为线程拜访无状况的行为并不会影响其他线程中操作的正确性。

无状况的目标必定是线程安全的。

2 原子性

public class UnsafeCountingFactorizer implements Servlet {
    private long count = 0;
    public void init(ServletConfig servletConfig) throws ServletException {
        count++;
    }
}

count++操作并非原子的,因为它并不会作为一个不可分割的操作来履行,它包含了三个独立的操作:读取count值,加1,然后将核算成果写入count。多线程在没有同步机制的状况下读取到count值时,或许读取到相同的count值,这个核算就失去准确性。

在并发编程中,因为不恰当履行时序而呈现不正确的成果是一种非常重要的状况,这种状况也被称之为:竞态条件(Race Condition)。

2.1 竞态条件

当某个核算的正确性取决于多个线程的替换履行时序时,那么就会产生竞态条件。

最常见的竞态条件类型是“先查看后履行”,即经过一个或许失败的观测成果来决定下一步的动作。

2.2 示例:推迟初始化中的竞态条件

public class LazyInitRace {
    private LazyInitRace instance = null;
    public LazyInitRace getInstance(){
        if(instance == null){
            instance = new LazyInitRace();
        }
        return instance;
    }
}

关于getInstance,就存在竞态条件。A线程和B线程一起拜访该办法时,A线程履行new操作没完成,B线程也会进来履行new操作,相当于这个new操作有或许履行2次。

竞态条件并不总是会产生过错,还需求某种不恰当的履行时序。

2.3 复合操作

要防止竞态条件问题,就有必要在某个线程修正该变量时,经过某种办法防止其他线程运用这个变量,然后保证其他线程只能在修正操作完成之前或之后读取和修正状况,而不是在修正过程中。

假定有两个操作A和B,假如从履行A的线程来看,当另一个线程履行B时,要么将B悉数履行完,要么完全不履行B,那么A和B对彼此来说是原子的。原子操作是指,关于拜访同一个状况的一切操作,这个操作是一个原子的办法履行的操作。

为了保证线程安全性,先查看后履行 读取-修正-写入等操作有必要是原子的,这类型操作统称为复合操作:包含一组有必要以原子办法履行的操作以保证线程的安全性。

public class SafeCount {
    private final AtomicInteger integer = new AtomicInteger(0);
    public void c(){
        integer.incrementAndGet();
    }
}

运用AtomicInteger系列原子变量保证核算操作的原子性,关于SafeCount,它的线程状况取决于integer的线程状况,因而它是一个线程安全类。在一个无状况的类中增加一个状况时,假如该状况完全由线程安全的目标来管理,那么这个类仍然是线程安全的。

** 在实际状况中,应尽或许地运用现有的线程安全目标来管理类的状况。与非线程安全的目标比较,判别线程安全目标或许状况及其状况转化状况要更为简单,然后也更简单保护和验证过线程安全性。**

3 加锁机制

关于包含多个状况的类,即使这些类时原子性的,假如存在不恰当的竞态条件,这个类仍然是线程不安全的。

public class UnSafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> last = new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = (BigInteger) servletRequest;
        if(last.get().equals(i)){
        }else {
            last.set(i);
            lastFactors.set(new BigInteger[]{});
        }
    }
}

关于原子类来说,set操作是原子的,但是关于lastFactors来说,他的set取决于last.get的判别,这种竞态条件对错原子的。

要坚持状况的一致性,就需求在单个原子操作中更新一切相关的状况变量。

3.1 内置锁

java运用内置的锁机制来支持原子性,同步代码块(synchronized block),同步代码块包含两个部分:一个是作为锁的目标引用,一个座位由这个锁保护的代码块,以关键字 synchronized润饰的办法便是一种横跨整个办法体的同步代码块,其间该同步代码块的锁便是办法调用所在的目标,静态的synchronized办法以Class目标为锁。

synchronized(lock){
    //拜访或修正由所保护的同享状况
}

每个Java目标都能够用做一个完成同步的锁,这个锁被称之为内置锁(intrinsic lock)或监督锁(monitor lock),线程在进入同步代码块之前会主动获取锁,在退出同步代码快后主动开释锁,无论是正常退出还是反常退出,获取内置锁的唯一办法便是进入锁保护的同步代码块或办法。

Java的内置锁相当于一种互斥体(互斥锁),这意味着最多只有一个线程能持有这种锁,当xianchengA尝试获取一个由B线程持有的锁时,线程A有必要等候或许堵塞,直到线程B开释这个锁,假如B永久不开释锁,那么线程A也将永久等下去。

因为每次只能由一个线程履行内置锁保护的代码块,因而,这个锁保护的代码块会以原子的办法履行,多个线程在履行该代码块时也不会彼此搅扰。并发环境中的原子性与实务使用程序中的原子性有相同的意义——一组语句作为一个不可分割的单元被履行。任何一个履行同步代码块的线程,都不或许看到其他线程正在履行由同一个锁保护的同步代码块。

public class SynchronizedFactorizer implements Servlet {
    private BigInteger lastNumber;
    public synchronized void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        lastNumber = new BigInteger("10");
    }
}

关于synchronized润饰办法时,线程安全视点考虑这个办法时安全的,但假定办法时间履行过长,其他线程无法履行,功能视点考虑很糟糕。

3.2 重入

当某个线程恳求一个由其他线程持有的锁时,宣布的恳求就会堵塞,因为内置锁是能够重入的,因而假如某个线程企图获取一个已经由他自己持有的锁,那么这个恳求就会成功。“重入”意味着获取锁的操作粒度时线程而不是调用。

public class Widget {
    public synchronized void doSomething(){}
}
class LoggingWidget extends Widget{
    public synchronized void doSomething(){
        super.doSomething();
    }
}

因为Widget 和 LoggingWidget 的 doSomething办法都是synchronized润饰的,调用doSometing时都会获取Widget的锁,那么在履行super.doSometing时,若没有重入机制就会产生死锁。

4 用锁来保护状况

因为锁能使其保护的代码途径以串行的形式来拜访,因而能够经过锁来构造一些协议以完成对同享状况的独占拜访。只需一直遵从这些协议,就能保证状况的一致性。

复合操作都有必要是原子操作以防止产生竞态条件。假如在复合操作的履行过程中持有一个锁,那么会使复合操作称为原子操作。但是,仅仅将复合操作封装到一个同步代码块中是不够的,假如用同步来协调对某个变量的拜访,那么在拜访这个变量的一切方位上都需求运用同步,而且,当运用锁来协调对某个变量的拜访时,在拜访变量的一切方位上都要运用同一个锁。

关于或许被多个线程一起拜访的可变状况变量,在拜访它时都需求持有同一个锁,在这种状况下,我们称状况变量是由这个锁保护的。

目标的内置锁与其状况之间没有内涵的相关。虽然大多数类都将内置锁用作一种有用的加锁机制,但目标的域并不必定要经过内置锁来保护,当获取域目标相关的锁时,并不能阻挠其他线程拜访该目标,某个线程在取得目标锁之后,只能阻挠其他线程获取同一个锁,之所以每个目标都有一个内置锁,仅仅为了免去显现地创建所目标。

每个同享的和可变的变量都应该只有一个锁来保护,然后使保护人员知道是哪一个锁。

一种常见的加锁约定是,将一切的可变状况都封装在目标内部,并经过目标的内置锁对一切拜访可变状况的代码途径上进行同步,使得在该目标上不会产生并发拜访,例如Vector类。在这种状况下,目标状况中的一切变量都是由目标的内置锁保护起来,但是,假如再增加新的办法或许代码途径时忘了运用同步,那么这种加锁协议会很简单被损坏。

并非一切的数据都需求保护,只有被多个线程一起拜访的可变数据才需求经过锁来保护

乱用synchronized会形成功能问题,而且在上层调用并不能保证不产生竞态条件。

关于每个包含多个变量的不变性条件,其间触及的一切变量都需求由同一个锁来保护。

5 活跃性与功能

synchronized加到办法时,假如办法履行时间过长,其他线程就进入不了这个办法,形成了整体堵塞,关于web使用来说,这将明显降低接口的吞吐量。确认synchronized的规模有利于保证程序的并发性与线程安全性,同步代码快不要过小,而且不要将本应是原子操作拆分到多个同步代码块中,应该尽量将不影响同享状况且履行时间较长的操作从同步代码块中别离出去,然后这些操作的履行过程中,其他线程能够拜访同享状况。

public class CachedFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    public synchronized long getHits() {
        return hits;
    }
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / hits;
    }
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = null;
        synchronized (this){
            ++hits;
            if(i.equals(lastNumber)){
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if(factors == null){
            factors = factor(i);
            synchronized (this){
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
    }
}

synchronized代码块尽量仅保护状况的原子操作。

要判别同步代码块的合理大小,需求在各种规划需求之间进行权衡,包含安全性、简单性和功能,有时分,咋简单性与功能之间会产生冲突。

通常,在简单性与功能之间存在着彼此限制的要素,当完成某个同步战略时,必定不要盲目地为了功能而献身简单性。

当履行时间较长的核算或许或许无法快速完成的操作时(例如网络io或控制台io),必定不要持有锁。

总结

chapter02 线程安全性