zoukankan      html  css  js  c++  java
  • 读书笔记 | 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. 任务执行

  • 相关阅读:
    [Swift]LeetCode1035.不相交的线 | Uncrossed Lines
    [Swift]LeetCode1034.边框着色 | Coloring A Border
    [Swift]LeetCode1033. 移动石子直到连续 | Moving Stones Until Consecutive
    [Swift]美人征婚问题
    [Swift]动态变化顶部状态栏(statusBar)的颜色
    [Swift-2019力扣杯春季决赛]4. 有效子数组的数目
    [Swift-2019力扣杯春季决赛]3. 最长重复子串
    [Swift-2019力扣杯春季决赛]2. 按字典序排列最小的等效字符串
    转 ORA-12638: 身份证明检索失败
    转 构建镜像
  • 原文地址:https://www.cnblogs.com/Serenity1994/p/12500525.html
Copyright © 2011-2022 走看看