Effective Java 读书笔记(九):并发

本文探讨了EffectiveJava中关于并发编程的最佳实践,包括同步访问共享数据、避免过度同步、使用线程池、并发工具的选择及线程安全性文档化等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Effective Java 读书笔记(九):并发

同步访问共享的可变数据

  1. 当共享可变数据时,不进行同步有两个坏处:一是可能导致数据处于不一致的状态,二是一个线程的修改无法马上对其他线程可见。
  2. 同步带来两个效果:原子性、可见性。Synchronized 既能保证原子性,也能保证可见性。volatile 则只能保证可见性。
  3. 增量操作符 ++ 不是原子的。
  4. 共享不可变的数据是安全的,不需要加锁同步。
  5. 对于共享对象,不去修改其内容,只同步其引用,这种对象可以称为事实上不可变的。这种对象引用从一个线程传递到其他线程,被称为安全发布(safe publication)。

避免过度同步

为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,同时要尽量限制同步区域内部的工作量。

使用线程池管理线程,不要手动 new 线程

从 Java5 开始引入了 Executor Framework,这是一个灵活的基于接口的任务执行工具,执行的是 Runnable 或 Callable(后者有返回值)。

在实际做项目时,不推荐使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式。为什么呢?Executors 返回的线程池有一些缺点:
1. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

代码不受约束是很可怕的事情。

从本质上将,Executor Framework 所做的工作是执行,犹如 Collections Framework 所做的工作是聚集一样。

并发工具优先于 wait 和 notify

wait 和 notify 有什么缺点呢?
1. 只有一个资源,但是可能用 notify all 唤醒了所有等待线程。
2. 在没有通知的情况下,等待线程也可能苏醒,被称为虚假唤醒 spurious wakeup。

wait 的标准用法如下:

synchronized (obj) {
    while (condition does not hold) {
        obj.wait(); // 释放锁,等待被唤醒
    }
    // 执行其他操作
}

始终使用 wait 循环模式调用 wait 方法,循环会在等待之前和之后测试条件。

在有现成的并发工具时,不要自己用 wait、notify 去控制并发,比如用 ConcurrentMap,好过自己控制并发。ConcurrentMap 相对于 Collections.synchronizedMap 性能更好。

线程安全性的文档化

当一个类的实例或静态方法被并发使用的时候,这个类的行为如何,是该类与其客户端程序建立的约定的重要组成部分。如果没有在文档中描述类的并发性情况,使用这个类的程序员将不得不做出某些假设。如果假设错误,可能导致缺少同步,或过度同步。

线程安全有多种级别:
1. 不可变的 immutable:String、Long、BigInteger。
2. 无条件的线程安全:无需任何外部同步,比如 Random、ConcurrentHashMap。
3. 有条件的线程安全:部分方法需要外部同步,比如 Collections.synchronizedMap 的迭代器。
4. 非线程安全:比如 ArrayList 和 HashMap。
5. 线程对立的:外部同步也白搭的,不能线程安全地被多个线程并发使用。

在无条件的线程安全中,需要使用私有锁对象,防止外部客户端访问锁,影响对象的同步。私有锁对象特别适合于那些为继承而设计的类,如果使用自身实例作为锁,子类很容易在无意中妨碍基类的操作,反之亦然。出于不同的目的而使用相同的锁,子类和基类可能会互相绊住对方的脚。

有条件的线程安全,需要在文档中说明:在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。

慎用延迟初始化

是否要用延迟初始化,要看具体情况。延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销(会影响初次访问的性能)。若非必要,就不要使用延迟初始化

一切都是权衡,很多服务器系统会在起到后,提前初始化一些大对象后,或者进行一定程度的预热后,再对外提供服务。

如果出于性能的考虑要对静态域使用延迟初始化,推荐使用 lazy initialization holder class 模式:

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
static FieldType getInstance() {
    return FieldHolder.field;
}

如果出于性能考虑,要对实例域进行初始化,就使用双重检查模式 double check idiom:锁 + volatile。

private volatile FieldType field;
FieldType getInstance() {
    FieldType result = field;
    if (result == null) {
        synchronized (this) {
            result = field;
            if (result == null) {
                field = result = computeFieldValue();
            }
        }
    }
    return result;
}

局部变量 result 的使用,能够提升 25% 的性能。

如果可以接受实例域的重复初始化,可以使用单重检查模式 single check idiom:

private volatile FieldType field;
FieldType getInstance() {
    FieldType result = field;
    if (result == null) {
        field = result = computeFieldValue();
    }
    return result;
}

去掉 volatile 可以加快某些架构上的访问,代价是增加了额外的初始化,这种模式叫 racy single check idiom,在 String 的散列码字段 hash 的初始化中用到。

    private int hash; // Default to 0
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

不要依赖于线程调度器

任何依赖于线程调度器来达到正确运行或者性能要求的程序,很可能都是不可移植的。要编写健壮的、响应良好的、可移植的应用程序,最好的办法是设置合理的线程数目,减少无意义的工作(比如 busy-wait)。

不要依赖 Thread.yield 或线程优先级,它们不具有可移植性,也就是说在不同 JVM 上表现不同。

避免使用线程组 ThreadGroup

线程组的实现由很多问题,使用线程池 executor 即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值