首页 > 编程语言 >《Effective Java》阅读笔记-第十一章

《Effective Java》阅读笔记-第十一章

时间:2024-02-21 16:25:32浏览次数:35  
标签:初始化 同步 Java Effective 第十一章 field 缓存 线程 volatile

Effective Java 阅读笔记

第十一章 并发

第 78 条 同步访问共享的可变数据

多线程访问变量时,需要进行同步,否则就会产生并发问题。
同步代码块、加锁等

或者直接不共享变量,也就是将可变数据限制在单个线程内。
ThreadLocal这种

第 79 条 避免过度同步

为了避免活性失败和安全性失败,在一个同步区域内,不要放弃对调用者的控制。换句话来说,就是同步区域内不应该调用应该被重写的方法,或者调用者传过来的函数。

To avoid liveness and safety failures, never cede control to the client within a synchronized method or block. In other words, inside a synchronized region, do not invoke a method that is designed to be overridden, or one provided by a client in the form of a function object.

从包含同步区域的类来看,这样的方法是外来的,当前类不知道这个方法会做什么事情,也无法控制它,在同步区域中调用这种方法很容易造成死锁或者数据损坏。

通常来说,在同步区域内的工作应该尽可能少。过度同步也会影响到性能。

如果正在编写一个可变的类,有两种选择:第一种是放弃所有同步,如果想并发使用,需要调用者从类外部控制同步;第二种是在内的内部进行同步,使这个类变成线程安全的。

Java 平台早期,很多类使用的都是第二种方法,比如StringBufferVector等,即在类的内部进行同步,但是很显然,第一种方式能获得更好的性能,并且在绝大多数情况下,这些类都是使用在单线程之中,因此逐渐StringBufferStringBuilder代替。

第 80 条 executor、task 和 stream 优先于线程

就是使用 ExecutorService 线程池来代替手动创建线程。

第 81 条 并发工具类优先于 wait 和 notify

Java 5 中添加了很多并发工具类,已经没有理由继续使用 wait 和 notify 了。

这些工具类分为三类:Executor 框架(Executor Framework)、支持并发的集合类(Concurrent Collection)、同步器(Synchronizer)。

并发集合在内部进行了状态同步,比如 Map 接口下有实现类ConcurrentHashMap,List 接口下有实现类CopyOnWriteArrayList,并且应该优先使用这种内部控制的并发集合类,而不是使用Collections.synchronizedMap()对集合类进行同步。

有些集合接口通过阻塞进行了扩展,比如BlockingQueue,在从队列中取值时,如果没有数据,就会阻塞当前线程。

同步器是能让一个线程等待另一个线程的对象,最常见的是CountDownLatchSemaphore(信号量)。

CountDownLatch 是一次性的,可以进行计数,调用countDownLatch.countDown()将计数 -1,在调用countDownLatch.await()时如果计数不为 0,就会阻塞当前线程,可以用来多线程协同处理。

JDK 官方示例

这是个类,其中一组工作线程使用两个倒计时锁存器:

  • 第一个是启动信号,阻止任何工人继续前进,直到司机准备好让他们继续前进;
  • 第二个是完成信号,允许驱动程序等待所有工作人员完成。
import java.util.concurrent.CountDownLatch;

class Driver {
    // ...

    void main() throws InterruptedException {
        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(N);

        for (int i = 0; i < N; ++i) // 创建并启动线程
            new Thread(new Worker(startSignal, doneSignal)).start();

        doSomethingElse(); // 先不让这些线程工作
        startSignal.countDown(); // 让所有线程开始工作
        doSomethingElse();
        doneSignal.await(); // 等待所有线程结束
    }
}

class Worker implements Runnable {
    private final CountDownLatch startSignal;
    private final CountDownLatch doneSignal;

    Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
        this.startSignal = startSignal;
        this.doneSignal = doneSignal;
    }

    public void run() {
        try {
            startSignal.await();
            doWork();
            doneSignal.countDown();
        } catch (InterruptedException ex) {
            // return;
        }
    }

    void doWork() {
        // ...
    }
}

需要注意的是执行countDown方法的线程一定要比 CountDownLatch 的数量多,否则线程就会无限等待,也就是线程饿死。

Semaphore 类似令牌,设置一个数量,semaphore.acquire()会阻塞直到获得许可,semaphore.release()都会释放一个许可。

如果只是用来计时,应该用System.nanoTime()而不是System.currentTimeMillis(),因为前者更精确,并且和系统时间无关(nanoTime 基准点也不确定,但是启动时固定)。

第 82 条 线程安全的文档化

一个类是否可被多个线程安全使用,应该在文档中说明它所支持的线程安全级别:

  • 不可变的(immutable):这个类的实例是不变的,不需要外部的同步。例如StringLongBigInteger等。
  • 无条件的线程安全(unconditionlly thread-safe):这个类是可变的,但是内部有足够的同步,可以被并发使用,且无需外部同步处理。例如AtomicLongConcurrentHashMap等。
  • 有条件的线程安全(unconditionlly thread-safe):除了有些方法需要外部同步之外,其他的和无条件线程安全一致。比如Collections.synchronized包装返回的集合,它要求对迭代器进行同步。
  • 非线程安全(not thread-safe):这些类是可变的,如果要并发使用,需要调用者手动进行同步控制。例如ArrayListHashMap等。
  • 线程对立的(thread-hostile):这种类不能安全的被多个线程使用,即使外围进行了同步。这种类一般根源在于修改静态数据时没有进行同步,这种类一般会得到修正,或者被标注为不再建议使用。

有条件的线程安全中,应该举出例子必须获得哪个锁才能线程安全。

如果使用一个对象作为锁,这个对象应该声明为 final,并且作用域应对最小:

public class Demo {
    private final Object lock = new Object();

    public void doSomething() {
        synchronized (lock) {
            // ...
        }
    }
}

第 83 条 慎用延迟初始化

大多数时候,正常的初始化优先于延迟初始化。

// 普通方式初始化一个字段
private final FieldType field = computeFieldType();

如果想延迟初始化来进行优化,那就使用同步方法,这是最简单、最清楚的一种方式:

private final FieldType field;

private synchronized FieldType getField() {
    if (field == null) {
        field = computeFieldType();
    }
    return field;
}

这两种方式对静态字段也同样适用(正常初始化和同步方法)。

如果出于性能考虑需要对静态字段延迟初始化,可以使用延迟初始化持有者模式(lazy initialization holder):

private static class FieldHolder {
    static final FieldType field = computeFieldType();
}

private static FieldType getField() {
    return FieldHolder.field;
}

getField方法第一次被调用的时候,它第一次读取FieldHolder.field,会导致FieldHolder类被初始化。这种方法没有增加任何访问成本(访问方法没有同步,性能更好)。
现代虚拟机会延迟加载FieldHolder,在加载时对其进行初始化。

学到了!

如果出于性能考虑需要对实例字段进行延迟初始化,就用双重检查模式:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized (this) {
            if (field == null) { // Second check (with locking)
                field = result = computeFieldType();
            } else {
                result = field; // 或者直接返回 field
            }
        }
    }
    return result;
}

这段代码不是很好理解,尤其是局部变量 result。

最外面返回 result 主要是想要避免 volatile 变量读取时的缓存行失效,这样可以提升性能(着实是没什么用处的提升)。如果 DCL 最外层检测失败,或者修改后代码没进入 else ,也可以确保最少限度地访问 field (因为每次访问 volitale 都会使缓存行失效从而从主内存加载最新变量副本到工作缓存)

volatile关键字作用如下:

可见性(Visibility):当一个线程修改了 volatile 变量的值,这个新值对于其他线程是立即可见的。这是因为 volatile 会告诉编译器不要将该变量的值缓存到线程的本地存储中,而是直接从主存中读取和写入该变量。
禁止指令重排序:volatile 还会禁止虚拟机对指令进行重排序,确保 volatile 变量的读取和写入操作按照程序中的顺序执行。

使用volatile修饰的字段访问时可能会使缓存行失效:

缓存行(Cache Line)是计算机系统中的一小块内存,通常大小为 64 字节。多个处理器核心(或线程)共享同一块缓存行。当一个线程修改缓存行中的某个变量时,其他线程也可能会受到影响,因为它们可能缓存了相同的缓存行。
在使用 volatile 关键字的情况下,当一个线程写入 volatile 变量时,它会强制将该变量的值刷新到主内存中,而其他线程在读取该变量时会从主内存中获取最新的值。这确保了变量的可见性,即一个线程对变量的修改对其他线程是可见的。
然而,与缓存行失效相关的问题通常是指当一个线程修改了 volatile 变量时,这个变量所在的缓存行可能会失效,导致其他线程的缓存无效,从而需要重新从主内存中加载该缓存行。这可能引起性能问题,因为缓存行的失效和重新加载需要一些开销。

注意,原书第三版中这段代码是错误的,少了同步代码块中的 else 分支,这样会导致取到的值是 null

还有一种情况是初始化一个可以接受重复初始化的实例字段,这种情况主需要检查一次即可:

// 同样需要使用 volatile 修饰
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null) {
        field = result = computeFieldType();
    }
    return result;
}

这就是单检查模式。

总结:大多数字段应该正常初始化,如果为了性能,或者为了解决循环问题,必须延迟初始化,非静态字段可以使用双重检查方式,静态字段可以使用延迟加载方式。

其实用枚举单例也可以

第 84 条 不要依赖于线程调度器

当有多个线程可以运行时,由线程调度器决定运行哪个线程。

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。

线程不应该一直处于 busy-wait 状态,即反复检查一个共享对象:

while (true) {
    synchronized (this) {
        // do something
    }
}

这是做法会增加 CPU 负担。

如果一个线程因为没有足够的 CPU 时间导致无法工作,不要通过在其他线程内调用Thread.yield()来解决。这个方法在不同的虚拟机上实现可能不同,Thread.yield没有可测试语义。

还有就是通过调整线程优先级,线程优先级是 Java 平台上最不可移植的特性,不要通过修改有限即来控制。

标签:初始化,同步,Java,Effective,第十一章,field,缓存,线程,volatile
From: https://www.cnblogs.com/code-blog/p/18025539

相关文章

  • 《Effective Java》阅读笔记-第十章
    EffectiveJava阅读笔记第十章异常第69条只针对异常的情况才使用异常说白了就是不要吧你的业务逻辑用异常来写。举个反例比如用抛出异常来遍历一个数组:try{inti=0;while(true){range[i++].doSomething();}}catch(ArrayIndexOutOfBoun......
  • 《Effective Java》阅读笔记-第十二章
    EffectiveJava阅读笔记第十二章序列化第85条其他方法优先于Java本身的序列化Java本身的序列化漏洞过多,很容易被攻击。避免被序列化攻击的最好方式就是不要反序列化任何字节流,并且新的系统中没有任何理由使用Java本身的序列化。JSON和Protobuf是两种优秀的序列化......
  • java类初始化及代码块加载顺序连根拔起
    说明相信很多人对于java中父子继承关系中,子类实例化调用过程中,代码块的执行顺序都容易忘记或搞混,尤其是java初级笔试题或面试题最容易出这类题目,让人恨得牙痒痒!!!本文就一次性将其连根铲除,看完你就不会有这个烦恼了,哈哈。先引用一下骨灰级大作《Java编程思想》的复用章节Java......
  • 一键脚本破解最新版 idea 步骤,开启学习java 之旅,好好好
    效果:步骤1、idea安装:直接在官网下载最新idea-2022.2.3.dwg(:官网地址,然后根据安装引导一步一步完成安装即可,完成后打开idea看到如下效果表示idea安装成功!如图发现idea需要注册!这里我们先不管,直接关闭idea准备激活!步骤2、下载最新的破解包https://pan.baidu.com/s/1iQby9......
  • Hutool - 简化Java编程的法宝,让工作更高效
    一、在项目的pom.xml的dependencies中加入以下内容:<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.0.7</version></dependency>非Maven项目的话就自己百度下载一下jar包,导入即可。二、StrUtil看这里名字也应该明白了,......
  • 承前启后,Java对象内存布局和对象头
    承前启后,Java对象内存布局和对象头大家好,我是小高先生。在我之前的一篇文章《并发编程防御装-锁(基础版)》中,我简要介绍了锁的基础知识,并解释了为什么Java中的任何对象都可以作为锁。在那里,我提到了对象头中有一个指向ObjectMonitor的指针,但没有深入探讨Java对象的内存结构。本文将......
  • Java的配置
    环境变量配置找到配置的位置右击此电脑-->属性-->高级系统设置-->环境变量-->系统变量配置Path环境变量(必须配置的)(目的:为了可以在任意目录下找到javac和java命令)方式1:直接在Path变量中添加jdk的bin目录的完整路径系统变量-->Path-->新建-->D:\soft\java\jdk\bin方式2:(推荐......
  • java练习2(四位数字进行加密)
    packagecom.shujia.zuoye;importjava.util.Scanner;/*某个公司采用公用电话传递数据,数据是四位的整数,在传递过程中是加密的,加密规则如下:每位数字都加上5,然后用和除以10的余数代替该数字,再将第一位和第四位交换,第二位和第三位交换。结果如图所示。*/publicclass加密......
  • java练习1(求圆的周长与面积)
    packagecom.shujia.zuoye;importjava.util.Scanner;/*输入圆形半径,求圆形的周长和圆形的面积,并将结果输出。/publicclass求圆的面积{publicstaticvoidmain(String[]args){Scannersc=newScanner(System.in);System.out.println("请输入圆的半径:");doubler=s......
  • 在阿里云部署javaspringboot项目
    记住自己服务器的账号密码配置安全组  用xshell连接服务器(xftp同理) 到官网去下载jdk的Linux版本,官网地址:https://www.oracle.com/technetwork/java/javase/downloads 安装JDK我自己用的是jdk21,下载完毕后用xftp传到服务器上(解压一下)#tar-zxvf压缩包.tar.......