读书笔记 | Java并发编程实战

读书笔记 | Java并发编程实战

一、基础知识

1. 线程安全性

线程安全的代码,核心在于对状态访问操作的管理特别是共享和可变状态的管理

  • 对象的状态:存储在状态变量(如实例或静态域)中的数据
  • 共享意味着变量可以由多个线程同时访问
  • 可变意味着变量的值会发生改变

当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问.
Java常见的同步机制:
* synchronized
* volatile
* 显示锁(Explicit Lock)
* 原子变量

2. 什么是线程的安全性

线程的安全性就是当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就是线程安全的.

3. 非原子的64位操作

  • Java内存模型要求,变量的读取和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分为两个32位的操作.当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读到某个值的高32位和另一个值得低32位.因此,即使不考虑失效数据问题,在多线程中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来
  • 如何复现问题,64位系统上也会有这种问题吗
    • 64位系统中没有这个问题

4. volatile

  • volatile变量用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方.因此在读取volatile变量时,总是返回最新写入的值.
  • volatile变量对可见性的影响:
    • 当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前所有对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的.
  • volatile变量的正确使用方式包括
    • 确保它们自身状态的可见性
    • 确保它们所引用对象的状态的可见性
    • 作为一些事件的开关.

5. 发布与逸出

  • 发布:
    • 将一个指向该对象的引用保存到其他代码能访问的地方
    • 或者在一个非私有的方法中返回该引用
    • 或者将一个引用传递到其他类的方法中
  • 逸出:
    • 当某个不该发布的对象被发布时,这种情况就是逸出

6. 并发容器

6-1. ConcurrentHashMap

  • 内部结构
  • add
  • get
  • size

6-2. CopyOnWriteArrayList

  • 用途:在一些读操作远大于写操作的情况下,才可以使用写入时复制容器
    • 在事件通知系统中,在分发通知时,需要迭代已注册的监听器链表,在大多数情况下,注册和注销事件监听器的操作远小于接收事件的操作.

6-3. 阻塞队列和生产者消费者模式

  • 阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法.

6-4. 同步工具类

6-4-1. 闭锁

  • 闭锁是一种同步工具类,可以延迟线程的进度知道其达到终止状态.
  • 闭锁可以用来确保某些活动直到其他活动都完成后才继续执行
    • 确保某个计算所需要的资源都初始化后才开始执行(资源初始化)
    • 确保某个服务所依赖的其他服务都启动后才启动(服务依赖)
    • 确保某个操作的所有操作者都就绪后再继续执行
  • CountDownLatch
    • CountDownLatch(int)
    • await():void
    • await(long,TimeUnit):boolean
    • countDown():void
// 在计时测试中使用CountDownLatch来启动和停止线程
public long timeTasks(int nThreads, Runnable task) throws InterruptedException {
    final CountDownLatch startGate = new CountDownLatch(1);
    final CountDownLatch endGate = new CountDownLatch(nThreads);
    for (int i = 0; i < nThreads; i++) {
        Thread t = new Thread(() -> {
            try {
                //线程启动后都在这里等待startGate变为0
                startGate.wait(); 
                try {
                    task.run();
                } finally {
                    //任务运行完,endGate减一
                    endGate.countDown(); 
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
    }
    long start = System.nanoTime();
    startGate.countDown(); //所有线程开始任务
    endGate.wait();  //等待所有线程执行完成
    long end = System.nanoTime();
    return end - start;
}

6-4-2. FutureTask

  • FutureTask实现了Future语义,表示一种抽象的可生成结果的计算.FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于一下三种状态:等待运行,正在运行,运行完成.运行完成表示计算的所有可能结束方式,包括正常结束,由于取消而结束和由于异常而结束等.当FutureTask进入完成状态后,它会永远停止在这个状态上.
  • FutureTask的用途:
    • 在Executor框架中表示异步任务
    • 还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动.通过提前启动计算,可以减少等待结果时需要的时间.
  • FutureTask的问题
    • Callable表示的任务可以抛出受检查的或不受检查的异常,这些异常被封装到ExecutionException中,并在Future.get中被重新抛出,这将使得调用get的代码变得复杂,因为它要对不同的异常进行不同的处理.
// 使用FutureTask来提前加载稍后需要的数据
public class N5_5_12Proloader {
    private final FutureTask<ProductInfo> future = new FutureTask<>(ProductInfo::new);
    private final Thread thread = new Thread(future);
    public void start() {
        thread.start();
    }
    public ProductInfo get() throws InterruptedException {
        try {
            return future.get();
        } catch (ExecutionException e) {
            System.out.println("初始化ProductionInfo发生错误");
            return null;
        }
    }
}
class ProductInfo {
}

6-4-3. 信号量Semaphore

  • 计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个操作的数量.计数信号量还可以用来实现某种资源池,或者对容器施加边界.
  • Semaphore
    • public Semaphore(int permits)
    • public Semaphore(int permits, boolean fair)
    • public void acquire() throws InterruptedException
    • public void release()

6-4-4. 栅栏CyclicBarrier

  • CyclicBarrier
    • public CyclicBarrier(int parties)
    • public CyclicBarrier(int parties, Runnable barrierAction)
    • public int await()
  • 等到设定的n个线程都到达了指定位置后在再同时继续往下执行,CyclicBarrier在初始化时还可以设置Runnable的action,最后一个到达指定位置的线程会去运行这个action

6-5. 构建高效且可伸缩的结果缓存

  • 场景:假设有个函数 value = fun(key),这个计算过程需要消耗一定的时间和资源,现在想要将计算的结果缓存下来,下次再计算同一个key时可以从缓存中直接获取value.
  • 思路:可以使用map类将key和value缓存起来,每次计算key的值时,先看map中有没有这个key对应的value,如果有,直接返回,如果没有,计算结果并存入map中.
  • 其中的坑:
    • 涉及到多线程,要使用ConcurrentHashMap,确保get和set时的线程安全
    • 因为计算需要消耗一定时间,如果一个线程在计算key的时候,另一个线程也来请求计算key,这个时候因为第一个线程的计算结果没出来,所以map中是空的,这时候第二个线程会再去计算.
  • 解决办法:map中不保存key和value的键值对,而是保存key和Future,其中Future中在计算value的值,通过future.get()方法,如果计算完成了直接返回value的值,如果计算还没结束,会阻塞一直等到它计算完成并返回.还需要注意的是,需要使用map.putIfAbsent(key,future)方法存入key和future,因为判断key是否存在和放入key不是原子操作.

二、结构化并发引应用程序

1. 任务执行

posted on 2020-03-15 22:17  Serenity1994  阅读(169)  评论(0编辑  收藏  举报