读书笔记——Java并发线程编程实战

Java并发线程编程实战

第1章 简介 略

第2章 线程安全性

概念

  1. 线程安全类:当多个线程访问某个类时,这个类始终都能表现出正确的行为。
    举例:无状态对象一定是线程安全的。比如servlet
  2. 竞态条件:由于不恰当的执行时序而出现不正确的结果。
    举例:先检查后执行:基于一种可能失效的观察结果来做出判断或者执行某个计算
  3. 复合操作:比如在一个无状态的类中对于一个状态变量进行递增运算进行(读取——修改——写入)操作时必须原子化,例如使用atomic包下的一些原子变量类
  4. 锁保护:对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
  5. 注意:当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。

第3章 对象的共享

可见性

非原子性的64位操作:
volatile类型的64位数值变量doublelong,JVM允许将64位的读操作或写操作分解为两个32位的操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。

加锁的含义:
不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步

正确使用volatile变量的方式:
1. 当变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;
2. 该变量不会与其他状态变量一起纳入不变性条件中;
3. 在访问变量时不需要加锁。

加锁机制和volatile变量的区别:
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

线程封闭

概念:不共享数据
**分类:**Ad-hoc线程封闭、栈封闭、ThreadLocal
栈封闭:指的是局部变量封闭在执行线程中
ThreadLocal类:这个类能使线程中的某个值与保存值的对象关联起来,通常用于防止对可变的单实例变量或全局变量进行共享。

不变性

概念:不可变对象一定是线程安全的
对象不可变必须满足以下条件:
- 对象创建以后其状态就不能修改
- 对象的所有域都是final类型
- 对象时正确创建的(在对象的创建期间,this引用没有逸出)

final域
final域能够确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步

安全发布

安全发布的常用模式:
- 在静态初始化函数中初始化一个对象引用:例如 public static Holder holder = new Holder(42);
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中

对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布
- 事实不可变对象必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来

实用策略

  • 线程封闭。线程封闭的对象只能由一个线程拥有,对象封闭在该线程中,并且只能由这个线程修改。
  • 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  • 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
  • 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

第4章 对象的组合

实例封闭

将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
**举例:**Java平台类库提供了很多线程封闭的实例,就是讲非线程安全的类转化为线程安全的类,比如sychronizedListconcurrentHashMap

Java监视器模式:将对象所有的可变状态封装起来,并由自己的内置锁来保护。比如VectorHashTable

@ThreadSafe
public final class Counter {
    @GuardedBy("this") private long value = 0;
    public synchronized long getValue() {
        return value;
    }
    public synchronized long increament() {
        if (value == Long.MAX_VALUE) {
            throw new IllegalStateException("counter overflow");
        }    
        return ++value;
    }
}

线程安全性委托

如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。

在现有的线程安全类中添加功能

客户端加锁机制:对于使用某个对象X的客户端代码,使用X本身用户保护其状态的锁来保护这段客户代码。

第5章 基础构建模块

同步容器类:VectorHashTable

vector的问题:
比如getLats()deleteLast()法交替执行时会抛出ArrayIndexOutOfBoundsException异常

迭代器的问题:
容器在迭代过程中被修改时,就会抛出ConcurrentModificationException异常

并发容器:concurrentHashMapCopyOnWriteArrayList

同步工具类:阻塞队列,信号量Semaphore,栅栏Barrier,闭锁Latch

概念:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态。

闭锁:闭锁可以用来确保某些活动直到其他活动都完成后才继续执行
信号量:用来控制同时访问某个特定资源的操作数量,或者执行某个指定操作的数量。
栅栏:栅栏与闭锁的关键区别在于,所有线程必须同时的到达栅栏位置,才能继续执行。闭锁用于等待时间,而栅栏用于等待线程。

第6章 任务执行

Executor框架

示例:基于Executor的Web服务器

class TaskExecutionWebServer {
    private static final int NTHREADS = 100;
    private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);
    public static void main (String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while(true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }
}

线程池:
- newFixedThreadPool:返回通用的ThreadPool-Executor实例
- newCachedThreadPool:返回通用的ThreadPool-Executor实例
- newSingleThreadExecutor
- newScheduledThreadPool

ExecutorService的生命周期
- 运行
- 关闭
- 已终止

延迟任务与周期任务:使用ScheduledThreadPoolExecutor来代替Timer
原因:
1. Timer在执行所有定时任务时只会创建一个线程
2. 不会捕获异常,并会终止定时任务

携带结果的任务CallableFuture
- 与Runable的区别:可以返回一个值或者一个受检查的异常
- Future表示一个任务的生命周期

示例:利用FutureCompletionService实现页面渲染器

为任务设置时限:
如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务

示例:旅行预订门户网站

第7章 取消与关闭

任务取消

中断:

//Thread中的中断方法
public class Thread {
    //中断目标线程
    public void interrupt(){}
    //返回目标线程的中断状态
    public boolean isInterrupt(){}
    //清楚当前线程的中断状态,返回它之前的值
    public static boolean interrupted(){}
}

对中断操作的正确理解:它并不会真正地中断一个正在运行的线程,而是发出中断请求,然后由线程在下一个合适的时刻中断自己。

响应中断:
处理InterruptedException
- 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断的阻塞方法
- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理

通过Future来实现取消:

public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
    Future<?> task = taskExec.submit(r);
    try {
        task.get(timeout, unit);
    } catch (TimeoutException e) {
        //接下来任务将被取消
    } catch (ExecutionException e) {
        //如果在任务中抛出了异常,那么重新抛出该异常
        throws launderThrowable(e.getCause());
    } finally {
        //如果任务已经结束,那么执行取消操作也不会带来任何影响
        task.cancel; //如果任务正在运行,那么将被中断
    }
}

停止基于线程的服务

毒丸对象:是指一个放在队列上的对象,其含义是当得到这个对象时,立即停止。在FIFO队列中,毒丸对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交毒丸对象之前的提交的所有工作都会被处理,而生产者在提交了毒丸对象之后,将不会再提交任何工作。

public class IndexingService {
    private static final File POISON = new File("");
    private final IndexingThread consumer = new IndexerThread();
    private final CrawlerThread producer = new CrawlerThread();
    private final BlockingQueue<File> queue;
    private final FileFilter fileFilter;
    private final File root;

class CrawlerThread extends Thread {}
class IndexerThread extends Thread{}

    public void start() {
        producer.start();
        consumer.start();
    }
    public void stop() {
        producer.interrupt();
    }
    public void awaitTermination() throws InterruptException {
        consumer.join();
    }
}

处理非正常的线程终止

未捕获异常:
当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.err

JVM关闭

关闭钩子:
在正常关闭中,JVM首先调用所有已注册的关闭钩子,指的是通过Runtime.addShutdownHook注册的但尚未开始的线程。

注意:对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之间出现竞态条件或死锁等问题。

守护线程:执行一些辅助性工作,但又不阻碍JVM的关闭的线程。
与普通线程的区别:仅在于退出时发生的操作,当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。

注意:守护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移除逾期的数据。

第8章 线程池的使用

配置ThreadPoolExecutor

通用构造函数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}

管理队列任务:
- 有界队列
- 无界队列
- 同步移交

饱和策略:ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。
- AbortPolicy
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy

ThreadFactory:每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的,即newThread()方法。

第9章 图形用户界面应用程序 略

第10章 避免活跃性危险

死锁

锁顺序死锁:两个线程试图以不同的顺序来获得同样的锁
动态锁顺序死锁:如果两个线程同时调用thransferMoney,其中一个线程从X向Y转账,另一个线程从Y向X转账,那么就会发生死锁
在协作对象之间发生的死锁
开放调用:调用某个方法时不需要持有锁

死锁的避免与诊断

  • 支持定时的锁
  • 通过线程转储信息来分析死锁

其他活跃性危险

饥饿:当线程由于无法访问它所需要的资源而不能继续执行时。
糟糕的响应性
活锁:线程不断重复执行相同的操作,而且总会失败
例子:活锁通常发生在处理事务消息的应用程序中,如果不能成功地处理某个消息,那么消息处理机制将会回滚整个事务,并将它重新放回队列的开头。
解决方法:在重试机制中引入随机性

第11章 性能与可伸缩性

Amdahl定律:

定律:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中并行组件与串行组件所占的比重

线程引入的开销

上下文切换:如果可运行的线程大于cpu的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用cpu,这将导致一次上下文切换,在这个过程中将保存当前运行线程的执行上下文,并将重新调度进来的线程的执行上下文设置为当前上下文。

内存同步:
阻塞:可以通过自旋等待或者被挂起

减少锁的竞争

  • 缩小锁的范围(快进快出)
  • 减小锁的粒度
  • 锁分段:比如concurrentHashMap
  • 避免热点域:缓存一些反复计算的结果
  • 一些替代独占锁的方法:使用并发容器、读写锁、不可变对象和原子变量
  • 监测cpu的利用率

第12章 并发程序的测试(略)

第13章 显式锁

Lock和ReentrantLock

  • 轮询锁与定时锁:通过tryLock方法实现,避免死锁的发生
  • 可中断的锁等待:能在可取消的操作中使用加锁
  • 公平队列
  • 非块结构的加锁

性能考虑因素

在Java5.0中,ReentrantLock能提供更高的吞吐量,但在Java6.0中,二者的吞吐量非常接近。

公平性

大多数情况下,非公平锁的性能要高于公平锁的性能。

读-写锁

ReentrantReadWriteLock:
- 可以选择非公平还是公平锁
- 如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他线程都不能获得读取锁
- 写线程降级为读线程是可以的,但从读线程升级为写线程是不可以的,防止死锁

性能:
分别用ReentrantLockReadWriteLock来封装ArrayList的吞吐量,在线程数量大于4的时候,后者的吞吐量大约是前者的3倍左右。

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

条件队列

条件谓词:put方法的条件谓词是“缓存不满”,take方法的条件谓词是“缓存不为空”
过早唤醒:wait方法的返回并不意味着线程正在等待的条件谓词已经变成真的了
当使用条件等待时(例如Object.waitCondition.await):
- 通常都有一个条件谓词——包括一些对象状态的测试,线程在执行前必须首先通过这些测试
- 在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试
- 在一个循环中调用wait
- 确保使用与条件队列相关的锁来保护构成条件谓词的各个状态变量
- 当调用waitnotifynotifyAll等方法时,一定要持有与队列相关的锁
- 在检查条件谓词之后以及开始执行相应的操作之前,不要释放锁

通知:优先选择notifyAll
同时满足以下两个条件,才能用单一的notify而是notifyAll
- 所有的等待线程的类型都相同
- 单进单出

显示的Condition对象

使用显式条件变量的有界缓存:

@ThreadSafe
public class ConditionBoundedBuffer<T> {
    protected final Lock lock = new ReentrantLock();
    //条件谓词:notFull (count < items.length)
    private final Condition notFull = lock.newCondition();
    //条件谓词:notEmpty (count > 0)
    private final Condition notEmpty = lock.newCondition();
    @GuardedBy("lock")
    private final T[] items = (T[]) new Object[BUFFER_SIZE];
    @GuardedBy("lock") private int tail, head, count;

    //阻塞并直到: notFull
    public void put(T x) throws 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();
        }
    }

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

AbstractQueuedSynchronizer

概念:是许多同步类的基类,例如ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLockSynchronousQueueFutureTask

java.util.concurrent同步器类中的AQS

  • ReentrantLock同步状态用于保存锁获取操作的次数
  • Semaphore同步状态用于保存当前可用许可的数量
  • FutureTask同步状态用来保存任务的状态
  • ReentrantReadWriteLock使用一个16位的状态来表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数

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

原子变量类

  • 标量类:AtomicIntegerAtomicLongAtomicBooleanAtomicReference
  • 更新器类
  • 数组类
  • 复合变量类

性能比较:在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能更有效地避免竞争。

非阻塞算法

概念:如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。
实现的技巧:将执行原子修改的范围缩小到单个变量上。

第16章 Java内存模型

**简介:**Java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作,JMM为程序中所有的操作定义了一个偏序关系,称之为Happen-Before。

发布

不安全的发布:当缺少Happen-Before关系时,就可能出现重排序问题,导致在没有充分同步的情况下发布一个对象会导致另一个线程看一个只被部分构造的对象。

线程安全的延迟初始化:

@ThreadSafe
public class SafeLazyInitialization {
    private static Resource resource;

    public synchronized static Resource getInstance() {
        if (resource == null) 
            resource = new Resource();
        return resource;

    }
}

双重检查加锁:不要这么做!!!

@NotThreadSafe
public class DoubleCheckLocking {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null) {
            synchronized (DoubleCheckLocking.class) {
                if (resource == null) {
                    resource =  new Resource();
                }
            }
        }
        return resource;
    }
}

猜你喜欢

转载自blog.csdn.net/github_38383183/article/details/80952616