Java并发编程实践——笔记

Java并发编程实践

第一章 简介

1.1 并发简史

  • 不一样进程之间能够经过一些粗粒度的通讯机制进行数据交换,包括:套接字,信号处理器,共享内存,信号量以及文件等
  • 线程容许在同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,例如内存句柄,文件句柄,但每一个线程都有各自的程序计数器,栈以及局部变量等。

1.2 线程带来的优点

  • 多核处理器
  • 建模简单
  • 异步事件简化处理
  • 响应灵敏

1.3 线程带来的风险

  • 安全性
  • 活跃性

某件正确的事情最终会发生, 活跃性问题包括死锁、饥饿、活锁java

  • 性能问题

CPU将更多的时间花在线程调度而不是线程运行上程序员

第二章 线程安全性

编写线程安全的代码,核心在于要对状态访问操做进行管理,特别是对共享的和可变状态的访问算法

共享意味着能够有多个线程访问
可变意味着变量的值在其生命周期内能够发生变化数据库

Java主要的同步机制包括synchronized关键字,volatile变量,显示锁以及原子变量编程

若是多个线程访问同一个可变的变量,且没有合适的同步,那么就会出现错误
修复手段:
- 不在线程之间共享该变量
- 将状态变量修改成不可变
- 在访问状态变量时使用同步
设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到必定的帮助

彻底由线程安全类构成的程序不必定就是安全的,而在线程安全类中也能够包含非线程安全的类。api

线程安全性是一个在代码上使用的术语,但它只是与状态相关的,所以只能应用于封装其状态的整个代码,这可能时一个对象,也可能时整个程序缓存

2.1 什么是线程安全性

线程安全性的核心概念就是正确性。
正确性的含义时,某个类的行为与其规范彻底一致。
对于单线程,正确性近似定义为,所见即所知安全

当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类时线程安全的
当多个线程访问某个类时,无论运行时采用何种调度方式,或者这些线程将如何交替执行,而且在主调代码中不须要额外的同步或者协同,这个类都表现出正确的行为,那么就称这个类时线程安全的。网络

无状态对象必定是线程安全的多线程

2.2 原子性

++操做时一种紧凑的语法,但实际上操做并不是原子操做。
因为不恰当的执行时序而出现不正确的结果是一种很是重要的状况——竞态条件(Race condition

2.2.1 竞态条件

最多见的竞态条件就是先检查后执行(Check Then Act), 即经过一个可能失效的观测结果来决定下一步的操做

2.2.2 延迟初始化中的竞态条件
@NotThreadSafe
public class LazyInitRace{
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance(){
        if(instance = null){
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

竞态条件并不总会产生错误,还须要某种不恰当的执行顺序

2.2.3 复合操做

原子操做是指,对于访问同一个状态的全部操做,这个操做是以原子方式执行的

2.3 加锁机制

要保持状态一致性,就须要在单个原子操做中更新全部相关的状态变量

2.3.1 内置锁

java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)
同步代码块包括两部分

  • 一个做为锁的对象引用
  • 一个有这个锁保护的代码块

静态的synchronized方法以Class对象做为锁

synchronized(lock) {
     //访问或修改由锁保护的共享状态
 }

每一个java对象均可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)
线程在进入同步代码块以前会自动得到锁,而且在推出代码块时自动释放锁,而不论时经过正常的控制路径退出,仍是经过从代码块中抛出异常退出。
得到内置锁的惟一途径就时进入由这个锁保护的同步代码块或方法。

2.3.2 重入

重入意味着获取锁的操做的粒度时线程,而不是调用

这与pthread(POSIX线程)互斥体默认的枷锁行为不一样,pthread互斥体的获取操做时以调用为粒度

public class Widget{
    public synchronized void doSomethind(){

    }
}

public class LoggingWidget extends Widget{
    public synchronized void doSomethind(){
        System.out.println(this.toString() + ": classing doSomething");
        super.doSomething();
    }
}

子类改写了父类的synchronized方法,若是没有可重入锁,那么这段代码将产生死锁。
因为Widget和LoggingWidget中doSomething方法都时synchronized方法,所以每一个doSomething方法在执行前都会获取Widget上的锁,若是锁时不可重入的,那么在调用super.doSomething时将永远没法得到Widget上的锁。

2.4 用锁来保护状态

对于可能被多个线程同时访问的可变状态变量,在访问它时都须要持有同一个锁,在这种状况下,咱们常状态变量时由这个锁保护的

对象的内置锁与其状态之间没有内在的关联,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不必定要经过内置锁来保护。当获取与对象关联的锁时,并不能阻止其余线程访问该对象,某个线程在得到对象的锁以后,并不能你组织其余线程访问该对象,某个线程在得到对象的锁以后,只能组织其余线程得到同一个锁。

每一个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道时哪个锁。

一种常见的额加锁约定是,将全部的可变状态都封装在对象内部,并经过对对象的内置锁对全部的可变状态的代码路径进行同步。

当类的不变性条件设计多个状态变量时,还有另外一个要求:不变性条件中的每一个变量必须由同一个锁来保护。所以能够在单个原子操做中访问或更新这些变量,从而确保不变性条件不被破坏。

2.5 活跃性与性能

两种不一样的同步机制不只会带来混乱,也不会在性能或安全性上带来任何好处。

当执行时间较长的计算或者可能没法快速我弄成的操做时(例如,网络I/O或控制台I/O),必定不要持有锁

第三章 对象的共享

3.1 可见性

在在多个线程执行读写操做时,咱们没法保证执行读操做的线程能适时的看到其余线程写入的值。

在没有同步的状况下,编译器、处理器以及运行时均可能对操做的执行顺序进行一些意想不到的调整。在缺少足够同步的多线程程序中,要想对内存操做的执行顺序进行判断,几乎没法得出正确结论。
3.1.1 失效数据
3.1.2 非原子的64位操做

非volatile类型的64位数值变量, Java内存模型要求变量的读取和写入都是原子操做。对于long和double,虚拟机容许将64位的读取和写入分为两个32位的操做。 当读取一个非volatile的long变量时,若是对这个变量的操做在不一样的线程中执行,那么极可能会出现错误

3.1.3 加锁与可见性

加锁的含义不只仅局限于互斥行为,还包括内存可见性,为了确保全部线程都能看到共享变量的最新值

3.1.4 Volatile变量
  • volatile: 编译器和运行时都会注意到这个变量是共享的,所以不会将该变量的操做与其余内存操做一块儿排序
  • volatile时一种比synchronized关键字更轻量级的同步机制

volatile在代码中用来公职状态的可见性,一般比锁的代码更脆弱

volatile的正确用法包括:确保自身状态的可见性,确保他们所引用对象的状态的可见性,以及标识一些重要程序生命周期时间的发生

3.2 发布与逸出

发布 一个对象的意思是指:时对象可以在当前做用域以外的代码中使用

如:将一个指向该对象的引用保存早其余代码能够访问的地方,或者在某一个非私有的方法中返回该引用。或者将引用传递到其余类的方法中。

发布内部状态可能会破坏封装性,并使得程序难以维持不变条件。
若是在对象构造完成以前就发布该对象,就会破坏线程安全性。当某个不该该发布的对象被发布时,这种状况就被称为逸出

假定有一个类C,对于C来讲,外部方法(Alien method)是指行为并不彻底由C来规定的方法,包括其它类中定义的方法以及类中能够被改写的方法
当把一个对象传递给某个外部方式的时候,就至关于发布了这个对象。

当某个对象逸出后,你必须假设由某个类或线程可能会无用该对象。这正是须要使用封装的最主要缘由:封装可以使得对程序的正确性进行分析变得可能。

public class ThisEscape{
    public ThisEscape(EventSource source){
        source.registerListener(
            new EventListener(){
                public void onEvent(Event e){
                    doSomething(e);
                }
            });
    }
}

ThisEscape发布EventListener时,也隐含发布了ThisEscape,由于在这个内部类实例中包含了对ThisEscape实力的隐含引用。

不要在构造过程当中使this逸出

在构造过程当中使this引用逸出的常见错误是,在构造函数中启动一个线程。
若是想在构造函数中注册一个事件监听器或启动线程,那么可使用一个私有的构造函数和一个公共的工厂方法。

//使用工厂方法防止this引用在构造过程当中逸出
pubilc class SafeListener{
    private final EventListener listener;

    private SafeListener(){
        listener = new EventListener(){
            public void onEvent(Event e){
                doSomething(e);
            }
        }
    }

    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();
        source.registerListener(sfae.listener);
        return sfae;
    }
}

3.3 线程封闭

访问共享的可变数据时,一般须要使用同步。一种避免使用同步的方法就是不共享数据。
Thread Confinement

java语言中并无强制规定某个变量必须由锁保护,一样在java语言中也没法强制将对象封闭在某个线程中。
java语言提供了一些机制来帮助维持线程封闭性,例如局部变量ThreadLocal类

3.3.1 Ad-hoc线程封闭

Ad-hoc线程封闭是指维护线程封闭性的职责彻底由程序实现来承担。

可是线程封闭技术的脆弱 程序中尽量少使用

3.3.2 栈封闭

局部变量的固有属性之一就是封闭在执行线程中。

位置对象引用的栈封闭性时,程序员须要多作一些工做已确保引用不会逸出

3.3.3 ThreadLocal类

ThreadLocal类能使线程中的某个值与保存值的对象关联起来。每一个线程都存有一份独立的副本

ThreadLocal对象一般用于防止对可变的单实例变量或局部变量进行共享。

从概念上能够将ThreadLocal<T> 视为 Map<Thread,T>,其中保存了特定线程的值。 这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会做为垃圾回收

ThreadLocal的实现

3.4 不变性

对象不可变必定是线程安全的

知足如下条件对象才是不可变的

- 对象建立之后其状态不能修改
- 对象的全部域都是final类型
- 对象时正确建立的(在对象建立期间,this引用没有逸出)
3.4.1 Final域

在java内存模型中,final域还有着特殊的语义。final域能确保初始化过程的安全性,从而能够不受限制的访问不可变对象,并在共享这些对象时无需同步

经过将域声明为final类型,也至关于告诉维护人员这些域是不会变化的。

3.4.2 用volatile

3.5 安全发布

//可见性 致使的不安全发布
public Holder holder;

public void initialize(){
    holder = new Holder(42);
}
3.5.1 不正确的发布:正确的对象被破坏
3.5.2 不可变对象域初始化安全性

即便某个对象的引用对其余线程是可见的,不意味着对象状态对于使用该对象的线程来讲必定是可见的。

任何线程均可以在不须要额外同步的状况下安全的访问不可变对象,即便在发布这些对象的时候没有使用同步

若是final类型的域所指向的对象是可变的,那么在访问这些域所指的对象的状态是仍须要同步

3.5.3 安全发布的经常使用模式

能够经过如下方式来安全的发布

  • 在静态初始化函数中初始化一个对象引用

= 将对象的引用保存早volatile类型或者AtomicReferance对象中

  • 将对象的引用保存早某个正确构造对象final类型域中
  • 将对象的引用保存早一个由锁保护的域中

静态初始化器由JVM在类的初始化阶段执行。因为在JVM内部存在着同步机制,所以经过这种方式初始化的任何对象均可以被安全的发布

3.5.3 事实不可变对象

在没有额外的同步的状况下,任何线程均可以安全的使用被安全发布的事实不可变对象

3.5.4 可变对象
不可变对象能够经过任意机制发布
事实不可变都西昂必须经过安全方式发布
可变对象必须经过安全方式来发布,而且必须是线程安全的或者由某个锁保护起来
3.5.6 安全地共享
  • 线程封闭
  • 只读共享
  • 线程安全共享
  • 保护对象

第四章 对象的组合

4.1 设计线程安全的类

包含三个基本要素

  • 找出构成对象状态的全部变量
  • 找出约束状态变量的不变性条件
  • 创建对象状态的并发方位管理策略

同步策略定义了如何在不违背对象不变性条件

4.1.2 以来状态的操做
4.1.3 状态的全部权

许多状况下 全部与封装性是相互关联的:对象封装它拥有的状态。反之也成立,即它对它封装的状态拥有全部权。

容器类一般便显出一种全部权分离的状态, 其中容器类拥有自身的状态,而客户代码则拥有容器中各个对象的状态。

4.2 实例封闭

Instance confinement
当一个对象封闭到另外一个对象中是,可以访问被封装对象的路径都是可知的。

  • 对象能够被封闭在实例中
  • 也能够封闭在做用域中
  • 或者线程内
封闭机制更容易构造线程安全的类,由于当封闭类的状态时,分析类的线程安全性就无须检查整个程序

4.3 线程安全性的委托

若是一个类是由多个独立且线程安全的状态变量组成,而且在全部的操做中都不包含无效状态转换,那么能够将线程安全性委托给底层的状态变量
若是一个状态变量是线程安全的,而且没有任何不变性条件来约束它的值,在变量的操做上也不存在任何不容许的状态转换,那么就能够安全的发布这个变量

第五章 基础构建模块

5.1 同步容器类

同步容器类包括 Vector/ Hashtable 还包括JSK中添加的功能类似的类
他们实现线程安全的方式:将状态封装起来,每一个公有方法都进行同步,每次只有一个线程能访问容器的状态

5.1.1 同步容器类的问题

并发操做修改容器时可能会出现意料以外的行为(迭代器操做)

能够经过客户端佳做来实现不可靠迭代问题,可是要牺牲掉一些伸缩性。

5.1.2 迭代器与ConcurrentModificationmException

不但愿对迭代器加锁 ---解决办法 克隆容器, 缺点就是会存在显著的性能开销

5.1.3 隐藏的迭代器

编译器将字符串的链接操做转换为调用StringBuilder.append(Object)

正如封装对象的状态有助于维持不变性条件同样,封装对象的同步机制有助于确保实施同步策略

5.2 并发容器

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • BlockingQue
5.2.1 ConcurrentHashMap

ConcurrentHashMap采用一种更细粒度的加锁机制——分段所(Locking Striping)
带来的结果就是并发访问环境下将实现更高的吞吐量,但线程中损失性能很是小。

ConcurrentHashMap与其余并发容器一块儿加强了同步容器类,它们提供的迭代器不会抛出ConcurrentModificationException,所以不须要对容器枷锁
返回的迭代器具备弱一致性,并不是及时失败,弱一致性的迭代器能够容忍并发的修改。
尽管有这些改进,但仍有一些须要权衡的因素,如size/isEmpty, 这些方法的语义被减弱,以反映容器的并发特性。

5.2.2 额外的原子Map操做

put if absent
remove if equal

5.2.3 CopyOnWriteArrayList

5.3 阻塞队列和生产者消费者模式

5.4 阻塞方法与中断方法

在代码中Dion共用一个将抛出InterruptedException异常的方法时,你本身的方法也就变成了一个阻塞方法,

  • 传递InterruptedException
  • 恢复中断
Thread.currentThread().interrupt();

5.5 同步工具类

  • 信号量
  • 栅栏
  • 闭锁
5.5.1 闭锁 latch

能够用来确保某些活动知道其余活动都完成以后才执行

  • CoundDownLatch
public long timeTasks(int nThreads, final Runnable task) thorw InterruptedException{
    final CountDownLatch startGate = new CountDownLatch();
    final CountDownLatch endGate = new CountDownLatch();

    for (int i = i < nThreads; i++){
        Thread t = new Thread(){
            public void run(){
                try{
                    startGate.await();
                    try{
                        task.run();
                    }finally {
                        endGate.countDown();
                    }
                } catch(InterrupetedException ignored) {}

            }
        }
        t.start();
    }

    long start = System.nanoTIme();
    startGate.countDown();
    endGate.await();
    long end = System.nanoTime();
    return end - start;
}
5.5.2 FUtureTask

FutureTask 表示的计算是经过Callable实现的至关于一种可生成结果的Runnable
FutureTask在Executor框架中表示异步任务

public class Preloader{
    private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>(){
            public ProductInfo call() throws DataLoadException{
                return loadProductInfo();
            }
        });
    private final Thread thread = new Thread(future);

    public void start(){
        thread.start();
    }
    
    public ProductInfo get() throws DataLoadException, InterruptedException{
        try{
            return future.get();
        }catch(ExecutionException e){
            Throwable cause = e.getCause();
            if(cause instanceof DataLoadException)
                throw (DateLoadException) cause;
            else
                throw launderThrowable(cause);
        }

    }


}
5.5.3 信号量
public class BoundedHashSet<T>{
    private final Set<T> set;
    private final Semaphore sem;

    public BoundedHashSet(int bound){
        this.get = Collections.synchronizedSet(new HasSet<T>());
        sem = new Semaphore(bound);
    }

    public boolean add(T o) throws InterruptedException{
        sem.acquire();
        boolean wasAdded = false;
        try{
            wasAdded = set.add(o);
            return wasAdded;
        }
        finally {
            if(!wasAdded)
                sem.release();
        }
    }

    public boolean remove(Object o) {
        boolean wasRemoved = set.remove(o);
        if(wasRemoved)
            sem.release();
        return wasRemoved;
    }
}
5.5.4 栅栏

闭锁时一次性对象,一旦进入终止状态就不能被重置
Barried相似于闭锁,区别在于,全部线程必须同时到达栅栏位置才能继续执行。 闭锁用域等待时间,栅栏用域等他其余线程。

当线程到达栅栏位置时,调用await方法,这个方法将阻塞,直到全部线程都到达栅栏位置。若是全部线程都到达了栅栏位置,那么栅栏将打开。全部线程都被释放。此时栅栏被重置,以便下次使用。

若是对await调用超市,或者await阻塞的线程被中断,那么栅栏就被认为时打破了。全部阻塞的线程终止,并抛出BrokenBarrierException

CyclicBarrier还会是你将一个栅栏操做传递给构造函数,这是一个Runnable,当成功经过栅栏时,会执行它。

public class CellularAutomata{
    private final Board mainBoard;
    private final CyclicBarrier barrier;
    private final Worker[] workers;

    public CellularAutomata(Board board){
        this.mainBoard = board;
        int count = Runtime.getRuntime().availableProcessors();
        this.barrier = new CyclicBarrier(count,
            new Runnable(){
                public void run(){
                    mainBoard.commitNewValues();
                }
            });
        this.workers = new Worker[count];
        for(int i = 0; i < count; i++){
            workers[i] = new Worker(mainBoard.getSubBoard(count,i));
        }
    }

    private class Worker implements Runnable{
        private final Board board;

        public Worker(Board board){
            this.board = board;
        }
        public void run(){
            while (!board.hasConverged()){
                for(int x = 0; x < board.getMaxX();x++){
                    for(int y = 0, y < board.getMaxY(); y++){
                        board.setNewValue(x,y,computeValue(x,y));
                    }
                }
                tyr{
                    barrier.await();
                }catch(InterruptedException e){
                    return;
                }catck(BrokenBarrierException e){
                    return;
                }
            }
        }

    }

    public void start(){
        for(int i = 0; i < workers.length;i++){
            new Thread(worker[i]).start();
        }
        mainBoard.waitForConvergence();
    }
}

第六章 任务执行

#### 6.1.3 无限制建立线程的不足

  • 现成的生命周期的开销很是高
  • 资源消耗

= 稳定性

### 6.2 Executor框架

public interface Executor{
    void execute(Runnable command);
}

Executor是基于生产者消费者模式

6.2.3 线程池

线程池的优点

- 减少开销
= 提升响应
- 调整线程池的大小,能够建立足够多的线程以是处理器保持忙碌同时还避免了竞争
  • newFixedThreadPool 建立固定长度的线程池
  • newCachedThreadPool 建立一个可缓存的线程池
  • newSingleThreadExecutor 建立一个单线程的Executor
  • newScheduledThreadPool 建立一个固定长度的线程池,并且能够延迟或定时的方式执行任务,相似于Timer
6.2.4 Executor生命周期
public interface ExecutorService extends Executor{
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) thorws InterruptedException;
}

运行中、关闭、终止

6.2.5 延迟任务与周期任务

6.3 找出可利用的并行性

6.3.2 携带结果的任务Callable与Future
6.3.4 在异构任务并行化中存在的局限

只有当大量相互独立且同构的任务能够并发处理时才能体现出将程序的工做负载分配到多个任务中带来的真正性能提高

第七章 取消与关闭

行为良好的软件能很完善的处理失败、关闭和取消等过程

7.1 任务取消

-在java中没有一种安全的抢占式方法来中止线程,所以也就没有安全的抢占式方法来中止任务。只有一些协做机制,使请求取消的任务和代码都遵循一种协商好的协议。

  • 如使用volatile变量
7.1.1 中断

一般中断是实现取消最合理的方式

在java的api或语言规范中,并无将中断与任何取消语义关联起来,但实际上,若是在取消以外的其余操做使用中断,都是不合适的,而且很难支撑其更大的应用???
调用interrupt并不意味着当即中止目标线程正在进行的工做,而只是传递了请求中断的消息
7.1.2 中断策略

每一个线程拥有各自的中断策略,所以除非你知道中断对该线程的含义,不然就不该该中断这线程

7.1.3 响应中断
  • 传递异常
  • 恢复中断

只有实现了线程中断策略的代码才能够屏蔽中断请求,在常规的任务和库代码中都不该该屏蔽中断请求

join 指定线程加入当前线程
yield 暂停当前线程

7.1.4 经过Future实现取消

当Future.get抛出InterruptedException或TimeoutException时,若是你知道再也不须要结果,那么就能够调用Future.cancel来取消任务

7.1.6 处理不可中断的阻塞

并不是全部的可阻塞方法或者阻塞机制都能响应中断

例如 一个线程因为执行同步的socket I/O或者等待得到内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此以外并无其余任何做用。

对于那些因为执行不可中断操做而被阻塞的线程,可使用相似于中断的手段来中止这些线程,可是要求咱们必须知道线程阻塞的缘由。

  • Java.io包中的同步socket I/O
  • java.io包中的同步I/O
  • Selector的异步I/O

7.2 中止基于线程的服务

7.2.2 关闭ExecutorService

shutdown正常关闭
shutdownNow 强行关闭

7.3 处理非正常的线程终止

7.4 JVM关闭

正常关闭的触发方式有多种

  • 当最后一个正常(非守护)线程结束
  • 调用了System.exit时
  • 或者经过其余特定于平台的方法
7.4.1 关闭钩子

在正常关闭中,JVM首相调用全部已注册的关闭钩子(Shutdown Hook)

关闭钩子是指经过Runtime.addShutdownHook注册的但还没有开始的线程,能够用于实现服务或者应用程序的清理工做。
public void start(){
    Runtime.getRuntime().addShutdownHook(new Thread(){
        public void run(){
            try{
                LogService.this.stop();
            }catch(InterruptedException ingnore){}
        }
    });
}
7.4.2 守护线程

Daemon Thread
除了主线程意外,全部的线程都是守护线程(垃圾回收线程和其余的辅助线程)

普通线程和守护线程的区别在于,当一个线程退出时,JVM会检查其余正在运行的线程,若是这些线程都是守护线程,那么jvm会正常退出操做。
当JVM中止时,全部仍然存在的守护线程将被抛弃,既不会执行finally也不会执行回卷栈,而jvm只是直接退出

7.4.3 终结器

finalize

避免使用终结器

第八章 线程池的使用

8.1 在任务与执行策略之间的隐形耦合

8.1.1 线程饥饿死锁
8.1.2 运行较长时间的任务

8.2 设置线程的大小

Runtime.availableProcessors

观察CPU利用水平来设置线程数

N_cpu = number of CPUs
U_cpu = target CPU utilization, 0 <= U_cpu <= 1
W/C = ratio of wait time to compute time

N_threads = N_cpu * U_cpu *(1+W/C)

CPU并非惟一影响线程大小的资源,还报错内存,文件句柄,套接字句柄和数据库连接等

8.3 配置ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize, 
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<RUnnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectExecutionHandler handler){……}
8.3.1 线程的建立与销毁
8.3.2 管理队列任务
8.3.3 饱和策略
  • AbortPloicy
  • CallerRunsPolicy
  • DiscardPolicy
  • DiscardOldestPolicy
8.3.4 线程工厂

每当线程池须要建立一个线程时,都是经过一个县城工厂方法来完成的。
默认的线程工厂方法将建立一个新的,非守护的线程,而且不包含特殊的配置信息。

第十章 避免活跃性危险

10.1.1 锁顺序死锁

若是全部线程以固定的顺序来得到锁,那么在程序中就不会出现锁顺序死锁问题

10.1.2 动态的锁顺序死锁

查看是否存在嵌套的锁获取操做

10.1.3 在协做对象之间发生死锁
10.1.4 开放调用

在调用某个方法是不须要持有锁,那么这种调用被称为开放调用

经过开放调用来避免死锁,相似于采用封装机制提供线程安全的方法

10.1.5 资源死锁

10.2 死锁的避免与诊断

10.2.1 支持定时的锁
10.2.2 经过线程转储来分析思索

10.3 其余活跃性问题

10.3.1 饥饿
10.3.2响应性差
10。3.3活锁

活锁一般发生在处理事务消息的应用程序中,若是不能成功地处理某个消息,那么消息处理机制将会回滚整个事务,并将它从新放在队列的开头。会致使重复调用,虽然县城并无阻塞,可是也没法继续进行下去。
一般是因为过分的错误恢复代码形成的,由于错误的将不可修复的错误做为可修复的错误

当多个相互协做的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都没法继续执行时,就发生了活锁。
解决这种问题,能够引入随机性(Raft 中的随机性)

第十一章 性能与可伸缩性

可伸缩性指的是,增长计算资源是,程序的吞吐量或者处理能力响应增长

11.3 线程引入的开销

11.3.1 上下文切换
11.3.2 内存同步
11.3.3 阻塞

11.4 减小锁的竞争

  • 缩小范围
  • 缩小锁的粒度
  • 锁分段
  • 避免热点域
  • 一些替代独占锁的方法

原子变量、读写锁

  • 检测CPU的利用率

第十三章

ReentrantLock
并非一种替代内置加锁的方法,而时当内置加锁机制不适用时,做为一种可选择的高级功能。

13.1 Lock与ReentrantLock

Lock提供了一种无条件的,可轮询的、定时的以及可中断的锁获取操做
Lock的视线中必须提供与内部锁象通的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能方面有所不一样

public interface Lock{
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long timeOut, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
为何要建立一个与内置锁如此类似的加锁机制?
大可能是状况下,内置锁均可以知足工做,可是功能上存在局限性,例如,没法中断一个正在等待获取锁的线程,没法在请求获取一个锁时,无限等待下去。内置锁必须在获取该锁的代码块中释放,这简化了编码工做,而且与异常处理操做实现了很好的交互,但却没法实现非阻塞结构的加锁规则。
Lock lock = new ReentrantLock();
....

lock.lock();
try{
    //doSomethind()
}finally{
    lock.unlock();
}
13.1.1 轮询锁与定时锁

定时锁与轮询所提供了避免死锁发生的另外一种选择

13.1.2 可中断的所
public boolean sendOnSharedLine(String message) throws InterruptedException{
    lock.lockInterruptibly();
    try{
        return cancellableSendOnSharedLine(message);
    }finally{
        lock.unlock();
    }

}

private boolean cancellableSendOnSharedLine(String message) throws InterruptedException{
    ...
}
13.1.3 非块结构的加锁

13.2 性能

13.3 公平性

公平性将因为挂起线程和回复线程时存在的开销极大下降性能。在实际状况中,统计上的公平性保证——确保被阻塞的线程最终得到锁,一般已经够用

13.4 synchrinized和ReentrantLock之间的选择
与显示锁相比,内置锁仍然具备很大的优点。内置锁为许多开发人员锁熟悉,而且简洁紧凑。

在一些内置锁没法知足需求的状况下 ReentrantLcok能够做为一种高级工具。当须要一些高级功能是才应该使用ReentrantLock,这些功能包括可定时的,可轮询的与可中断的锁获取操做,公平队列以及非块结构的锁,不然,仍是应该优先使用synchronized

另外synchronized在线程转储中能给出在那些调用帧中得到了哪些锁,并可以检测和识别发生死锁的线程。

synchronized时JVM的内置属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,经过增长锁的粒度来消除内置锁的同步。

13.5 读写锁

public interface ReadWriteLock{
    Lock readLock();
    Lock writeLock();
}

读写锁中的一些可选实现

  • 释放优先
  • 读线程插队
  • 重入性
  • 降级
  • 升级

第十四章 构建自定义的同步工具

14.1 状态依赖性的管理

14.1.3 条件队列

Object.wait会自动释放锁,并请求操做系统挂起当前线程

@ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V>{
 //条件谓词: not-full(!isFull())
 //条件谓词: not-emptu(!isEmpty())

 public BoundedBuffer(int size){
     super(size);
 }

 public sunchronized void put(V v) throws InterruptedException{
     while(isFull()){
         wait();
     }
     doPut(v);
     notifyAll();
 }

 public synchronized V take() throws InterruptedException{
     while(isEmpte()){
         wait();
     }
     V v = doTake();
     notifyAll();
     return v;
 }
}

14.2 使用条件队列

14.2.1 条件谓词

条件谓词:使某个操做成为状态依赖操做的前提

在有界缓存中,只有当缓存不为空take方法才能执行, 对于take来讲,他的条件谓词就是缓存不为空

在条件等待中存在一种重要的三元关系, 包括加锁,wait方法和一个条件谓词

每一次wait调用都会隐式的与特定的条件谓词关联起来,当调用某个特定的条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,而且这个锁必须保护着构成条件谓词的状态变量
14.2.2 过早唤醒

wait方法的返回并不必定意味着线程正在等待的条件谓词成真了
内置条件队列能够与多个条件谓词一块儿使用。

使用条件等待时 如 Object.wait或者 Condition.wait

- 一般都有一个条件谓词——包括一些对象状态的测试,线程在执行前必须首先经过这些测试
- 在调用wait以前测试条件谓词,而且从wait中返回时再次进行测试
- 在一个循环中调用wait
- 确保使用与条件队列相关的锁来保护构成条件谓词的哥哥状态变量
- 当条用wait、notify、nitofyAll时必定要持有与条件队列相关的锁
- 在检查条件谓词以后以及开始执行响应的操做以前,不要释放锁
14.2.3 丢失的信号

丢失信号是指:线程必须等待一个已经为真的条件,可是在开始等待以前没有检查条件谓词

14.2.4 通知

因为在调用notify或nitofyAll时必须持有条件队列对象的锁,而若是这些等待中的线程此时不能从新得到锁,那么没法从wait返回,所以发出通知的线程应该尽快释放锁,从而确保正在等待的线程尽量快的解除阻塞

只有同时知足如下两个条件时,才能用单一的nofify而不是notifyAll
- 全部等待线程的类型都象通
- 单进单出

14.3 显式的Condition对象

Condition是一种广义的内置条件队列

public interface Condition{
    void await() throws InterruptedException;
    boolean await(long timeOut, TimeUnit unit) throws InterruptedException;
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    void awaitUniterruptibly();
    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();
    void signalAll();

}

Lock.newCondition

@TreadSafe
public class ConditionBoundedBuffer<T>{
    protected final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    @GuardBy("lock")
    private final T[] items =(T[]) new Object[BUFFER_SIZE];
    @GuardBy("lock")
    private int tail, head, count;

    public void put(T x) throw InterruptedException{
     lock.lock();
     try{
         while (count == items.length){
             notFull.await();
         }
         items[tail] = x;
         if(++tail == items.length){
             tail = 0;
         }
         ++count;
         notEmpty.signal();
     }finally{
         lock.unlock();
     }
    }

    public T take() throws InterruptedException{
        lock.lock();
        try{
            while(count == 0){
                notEmpty.await();
            }
            T x = items[head]
            items[head] == null;
            if(++head == items.length){
                head = 0;
            }
            --count;
            notFull.signal();
            return x;
        } finally{
            lock.unlock();
        }
    }
}

经过将两个条件谓词分别放到两个等待线程集中

14.4 Synchronizer剖析

实际上 ReentrantLock和 Semaphore的实现都使用了一个共同的基类,即AbstractQueuedSynchronizer(AQS)

AQS是一个用域构建锁和同步器的框架,许多同步器均可以经过AQS构造

//AQS中获取操做和释放操做的基标准形式
boolean acquire() throws InterruptedException{
    while(当前状态不容许获取操做){
        if(须要阻塞获取请求){
            若是当前线程不在队列中,则将其插入队列
            阻塞当前线程
        }
        else
            返回失败
    }
    可能更新同步器的状态
    若是线程位于队列中,则将其移出队列
    返回成功
}

void release(){
    更新同步器的状态
    if(新的状态容许某个被阻塞的线程获取成功)
        解除队列中一个或多个线程的阻塞状态
}

第15章 原子变量与非阻塞同步机制

15.1 锁的劣势

15.2 硬件对并发的支持

15.1 CAS

CAS的主要缺点是, 它将使调用者处理竞争问题,而锁能自动处理竞争问题

15.2.3 JVM对CAS的支持

java.util.concurrent中的大多数类在实现时直接或间接的使用了这些原子变量类

15.3 原子变量类

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean

15.4 非阻塞算法

基于锁的算法可能会发生各类活跃性故障,若是线程在持有锁时因为阻塞I/O,内存也确实或其余延迟致使推迟执行,那么颇有可能其余线程都不能继续执行。
若是在某种算法中,一个线程的失败或挂起不会致使其余线程也失败或挂起,那么这种算法就被称为非阻塞算法。

非阻塞算法一般不会出现死锁和优先级反转的问题,可是可能会出现饥饿和活锁问题。

15.4.1 非阻塞栈
@ThreadSafe

public class ConcurrentStack <E> {
    AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
    public void push(E item){
        Node<E> new Head = new Node<E>(item);
        Node<E> oldHead;
        do{
            oldHead = top.get();
            newHead.next = oleHead;
        } while (!top.compareAndSet(old,newHead));

    }

    public E pop(){
        Node<E> oldHead;
        Node<E> newHead;
        do{
            oldHead = top.get();
            if(oldHead == null){
                return null;
            }
            newHead = oldHead.next;
        } while(!top.compareAndSet(oldHead, newHead));
    }

    private static Node<E>{
        public final E item;
        public Node<E> next;
        public Node(E item){
            this.item = item;

        }
    }
}

第十六章 Java内存模型

Java语言规范要求JVM在线程中维护一种相似串行的语义:只要程序的最终结果与在严格串行的环境中执行结果相同,那么上述全部操做都是容许的。
JMM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操做再合适将对于其余线程可见。

16.1.1 平台的内存模型
16.1.2 重排序
16.1.3 Java内存模型简介

java内存模型时经过各类操做来定义的,包括对变量的读/写操做,监视器的加锁和释放惭怍,以及线程的启动和合并操做,JMM位程序中全部的操做定义了一个偏序关系
称之为Happens-before
当一个变量被多线程读取,而且至少被一个线程写入时,若是在取操做和写操做之间没有依照Happens-before来排序,那么就会产生数据竞争问题。

  • 程序顺序规则
  • 监视器锁规则
  • volatile变量规则
  • 线程启动规则
  • 线程结束规则
  • 终端规则
  • 终结器规则
  • 传递性

见jvm笔记

16.2 发布

除了不可变对象意外,使用被另外一个线程初始化的对象一般都是不安全的,除非对象的发布操做是在使用该对象的线程开始使用以前执行。

16.2.2 安全发布
  • 延迟初始化
  • 提早初始化
  • 双重检查加锁(Double Lock Check)

16.3 初始化过程当中的安全性

初始化安全性将确保对于被正确构造的对象,全部线程都能看到有构造函数位对象给各个final域设置的正确值,而无论采用何种方式来发布对象。并且,对于能够经过被正确构造对象中某个final域到达的任意变量,将一样对于其余线程可见。

对于含有final域的对象,初始化安全性能够防止对象的初始引用被重排序到构造过程以前。当构造函数完成时,构造函数对final域的全部写入操做,以及对经过这些域能够到达的任何变量的写入操做,都将被冻结。而且任何得到该对象引用的线程至少能确保看到被冻结的值。

初始化安全性只能保证经过final域可达的值从构造过程完成时开始的可见性。对于经过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。
相关文章
相关标签/搜索