读书笔记:Effective Java-第11章 并发Concurrency

11 篇文章 0 订阅
11 篇文章 0 订阅

目录

Item 78: Synchronize access to shared mutable data

Item 79: Avoid excessive synchronization

Item 80: Prefer executors, tasks, and streams to threads

Item 81: Prefer concurrency utilities to wait and notify

Item 82: Document thread safety

Item 83: Use lazy initialization judiciously

Item 84: Don't depend on the thread scheduler


Item 78: Synchronize access to shared mutable data

同步访问共享的可变数据

synchronized关键字可以保证在同一时间,只有一个线程可以执行某个方法或代码块,使操作是互斥的,保证一个线程的修改对另一个线程是可见的。

如果想在一个线程上终止另一个线程,不要使用Thread.stop方法,此方法会导致数据遭破坏,是不安全的。建议做法:在第1个线程上轮询(poll)一个boolean域,第2个线程通过改变该域的值来终止第1个线程。

java语言规范保证读写一个变量是原子的(actomic),除非变量是long或者double类型。但不保证一个线程写入的值对另一个线程时可见的,即不保证同步。这是由于java语言规范中的内存模型(memory model)决定的,它规定了一个线程所做的变化何时以及如何被另一个线程可见。如下错误做法:

// 预期是1秒后停,实际永远不会停
public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested)
                i++;
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
} 

JVM在编译上面代码时,会进行优化提升(hoisting)转变:

while (!stopRequested)
    i++;
转变为:
while (!stopRequested)
    while(true)
        i++;

正确做法是增加同步措施,如下:

// 方法1,加同步
public class StopThread {
    private static boolean stopRequested;

    // 新增写方法,并同步
    private static synchronized void requestStop() {
        stopRequested = true;
    }

    // 新增读方法,也必须同步
    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested())  // 替换为读方法
                i++;
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();  // 替换为写方法
    }
}

// 方法2,给变量加volatile,无需加同步
private static boolean stopRequested;
改成
private static volatile boolean stopRequested;

上述示例中同步的方法即使没有同步也是原子的,增加同步只是为了通讯效果,而非互斥操作。这时可通过给stopRequested变量增加关键字volatile来代替同步设置。

volatile修饰的变量是说此变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值了,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

但使用volatile需要谨慎,容易出错,如下:

private static volatile int nextSerialNumber = 0;

private static int generateSerialNumber() {
    return nextSerialNumber++; 
}

 多线程下会出现意料之外的错误,因为++是非原子操作,包含两个操作:先读取值,然后写回加1后的新值。这属于安全性失败(safety failure),即程序计算出错误的结果。

正确做法是给方法加同步措施,或者使用AtomicLong来代替int/long:

private static final AtomicLong nextSerialNumber = new AtomicLong();

private static int generateSerialNumber() {
    return nextSerialNumber.getAndIncrement(); 
}

AtomicLong在包java.util.concurrent.atomic中,该包提供了免锁定、线程安全的基本类型和操作。 

最佳的做法还是不共享可变数据,要么共享不可变数据,要么压根不共享,将可变数据限制在但线程中。如果做到了这个,就不用担心采用的框架或库是否引入了你不知道的线程。

注意:

  1. 操作必须同时同步,否则无法保证同步起到作用。
  2. 如果执行线程间通讯,而无互斥操作,则可以采用volatile代替同步,但务必小心使用,因为容易错误使用。

Item 79: Avoid excessive synchronization

避免过度同步

术语:

  • recall:回调,类将自己作为参数传递给自己调用的外部函数,如在类A内部调用类B方法b.recall(this)(this是A的,该调用在A内部)。
  • multi-catch:多重捕获,如 cateh (Exception1 | Exception2 e) { ... }
  • reentrant lock:可重入锁
  • open call:开发调用,指在同步区域外被调用的外部方法。

过度使用同步可能导致性能低下、死锁,甚至不确定的行为。

为了避免活性和安全性失败(liveness and safety failure),在一个被同步的方法或代码块中,永远不要放弃对客户端的控制,即不要调用override重写方法或由客户端以函数对象形式提供的方法。

java类库提供了一个并发集合叫CopyOnWriteArrayList,是通过拷贝低层数组的新副本来解决同步问题的(读写分离的并发策略)。但该集合适合较少写、经常读或者遍历的场合,否则大量使用会严重影响性能。

在同步区域内做尽可能少的工作。

过度同步时耗:在多核系统中,过度同步的时间时耗并不是因为获取锁所花费的时间,而是因为失去了并行的机会、确保每个核有一致性的内存视图而导致的延迟、限制虚拟机优化代码的能力。

对于可变mutable类的同步,有两种思路:

  • 外部同步法:在调用的客户端代码处同步;
  • 内部同步法:在类方法实现内进行同步,可获得更高的性能。

Item 80: Prefer executors, tasks, and streams to threads

exector、task、steam优先于线程

Executor Framwork工作示例:

ExecutorService exec = Executors.newSingleThreadExecutor();  // 创造线程executor service
exec.execute(runnable);  // 执行线程
exec.shutdown();  // 如果没关闭,VM不会退出

尽量不要自己编写工作队列,也不要直接使用线程。当直接使用线程时(直接通过new Thread或其子类创建),线程既是工作单元(称为task)有时执行机制。应该使用Executor Framwork,它将二者分开,executor是执行机制,任务类型有两种类型:Runable、Callable(有返回值和能抛出异常)。

java 7中,Executor Framwork支持fork-join任务。

并发的strream是基于fork join池上编写的。

更多并发的知识参考《Java Concurrency in Practice》

Item 81: Prefer concurrency utilities to wait and notify

并发工具优于wait和notify

术语:

  • thread starvation deadlock:线程饥饿死锁

不推荐使用wait和nitify的原因是很难去正确使用它们,因此推荐使用更高级的并发工具来代替。

java.util.concurrent包中的并发工具可以分为3类:Eexcutor Framework、并发集合concurrent collection、同步器synchronizer。

concurrent collection有ConcurrentHashMap、BlockingQueue(含阻塞操作的集合)等。

synchronizer同步器是使线程能够等待另一个线程的对象,允许它们协调活动。有CountDownLatch倒计数锁存器、Semaphore、Phaser、CyclicBarrier等。

对于间歇式的定时,应优先使用System.nanoTime,而不是System.currentTomeMillis。前者更精确,且不受系统时间调整的影响。

真的需要使用wait的时候(如维护旧代码,必须在synchronized区域内调用wait方法,且应该使用wait循环模式来调用wait方法(避免被意外或恶意唤醒),即

synchronized (obj) {
    while (跳出条件不满足) {
        obj.wait();  // 释放锁,等待被唤醒
    }
    ...// 执行唤醒后的其他操作
}

从保守角度看,建议使用notifyAll,从优化角度看,建议使用notify方法。但还是建议使用notifyAll,可防止不相关线程意外或恶意的等待。

Item 82: Document thread safety

用文档描述线程安全性

private lock object:私有锁对象

如果未对并发时的线程安全性进行描述,使用者容易出错,如缺乏同步或者过度同步。

类文档中应该清楚地说明它所支持的线程安全性级别。常见的级别有:

  • 不可变的(immutable):类的实例是不可变的。
  • 无条件的线程安全(unconditionally thread-safe):类实例是可变的,但有充分的内部同步措施。
  • 有条件的线程安全(conditionally thread-safe):部分方法需要增加外部同步措施,且一般会说明使用哪种类型的锁。
  • 非线程安全(not thread-safe):所有方法都要求增加外部同步措施。
  • 线程对立的(thread-hostile):即使增加外部同步措施,也不能保证并发时线程安全。原因在于没有从内部同步修改静态static域数据,如以下方法(同Item 78示例):
private static volatile int nextSerialNumber = 0;

private static int generateSerialNumber() {
    return nextSerialNumber++; 
}

一般在类文档注释说明类的线程安全性;有特殊线程安全属性的方法则在对应的方法文档注释中写明;静态工厂必须说明线程安全性(除非返回类型非常明显),如Collections.synchronizedMap的注释文档:

    /**
     * Returns a synchronized (thread-safe) map backed by the specified
     * map.  In order to guarantee serial access, it is critical that
     * <strong>all</strong> access to the backing map is accomplished
     * through the returned map.<p>
     *
     * It is imperative that the user manually synchronize on the returned
     * map when traversing any of its collection views via {@link Iterator},
     * {@link Spliterator} or {@link Stream}:
     * <pre>
     *  Map m = Collections.synchronizedMap(new HashMap());
     *      ...
     *  Set s = m.keySet();  // Needn't be in synchronized block
     *      ...
     *  synchronized (m) {  // Synchronizing on m, not s!
     *      Iterator i = s.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * </pre>
     * Failure to follow this advice may result in non-deterministic behavior.
     *
     * <p>The returned map will be serializable if the specified map is
     * serializable.
     *
     * @param <K> the class of the map keys
     * @param <V> the class of the map values
     * @param  m the map to be "wrapped" in a synchronized map.
     * @return a synchronized view of the specified map.
     */
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
        return new SynchronizedMap<>(m);
    }

优先使用私有锁对象(private lock object),而不是公共可访问的锁对象(影响性能):

private final Object lock = new Object();  // 私有锁对象

public void foo() {
    synchronized(lock) {
        ...
    } 
}

锁lock域必须声明为final。

私有锁对象只能用于无条件的线程安全类。 有条件的线程安全类必须要用文档来说明获取锁的类型。

私有锁对象特别适用于面向继承的类。在继承中如果采用的是其他锁(如对象锁),子类容易无意中妨碍父类的操作(举例? 子类实例对象锁与父类实例对象锁的关系 - 亮仔的程序园 - 博客园 、 Java多线程(2):synchronized 锁重入、锁释放、锁不具有继承性_保暖大裤衩LeoLee的博客-CSDN博客)。

Item 83: Use lazy initialization judiciously

慎用延迟初始化

术语

lazy initialization:延迟初始化,一种优化技术,指将域的初始化操作延迟到需要用到域值的时候再进行。

延迟初始化是把双刃剑,除非需要,否则不要去用。虽然降低了初始化类或者创建实例的开销,但增加了访问被延迟初始化的域的开销。

在大多数情况下,正常初始化要优于延迟初始化。

延迟初始化示例:

// 正常初始化
private final FieldType field = computerFieldValue();

// 延迟初始化
private FieldType field;
private synchronized FieldType getField() {
    if (field == null) {
        field = computerFieldValue();
    }
    return field;
}

如果出于性能考虑需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。

// 用于static域的lazy initialization holder class模式
private static class FieldHolder {
    static final FieldType field = computerFieldValue();
}

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

getField()方法不需要进行同步,因为VM初始化静态类时会同步域的访问。

如果出于性能考虑需实例域使用延迟初始化,就使用双重检查(double-check)模式。

// 用于实例域的双重检查模式
private volatile FieldType field;  // 因为当域被初始化后没有锁lock,需要将此域声明为volatile

private FieldType getField() {
    FieldType result = filed;
    if (result == null) {  // 第1次检查
        synchronized (this) {
            if (result == null) {  // 第2次检查
                field = result = computerFieldValue(); 
            }   
        }
    }
    return result;
}

需要对代码中的局部变量result的必须性进行解释, 它的作用时确保field域在已经初始化的情况下只被读取一次。虽然严格意义上这不是必须的,但可以提升性能,并提供一个标准,使初级并发编程更加优雅(作者实验表明,使用了局部变量后,程序性能提升了1.4倍)。

如果不介意重复初始化域,可删去第2次检查,这种变形称为单重检查(single-check)模式:

// 双重检查变形1:用于实例域单重检查模式,会导致重复初始化现象(不介意的话)
private volatile FieldType field;  // 因为域被初始化后没有锁lock,需要将此域声明为volatile

private FieldType getField() {
    FieldType result = filed;
    if (result == null) {  // 第1次检查
        field = result = computerFieldValue();   
    }
    return result;
}

// 双重检查变形2:当域的类型为基本类型时,可将volatile去掉,该模式称为racy singel-check

更进一步,如果域的类型为基本类型,不是long、double,可去掉域的volatile修饰符,这种变形体称为racy single-check模式。可加速域的访问速度,但增加额外初始化的开销。且这种变形不常用。

对于基本类型的域或者对象引用域,用null判断,对于数值基本类型域,用0判断。

Item 84: Don't depend on the thread scheduler

不要依赖于线程调度器

线程调度器(thread scheduler):当多个线程可运行时,由线程调度器决定哪些线程将会执行以及执行多久。

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

为了编写出健壮、响应良好、可移植的程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量(如何合理设置线程池大小_lsz冲呀的博客-CSDN博客_线程池大小设置)。

可运行(runnable)的线程数不等于线程总数,还有一部分线程是处于等待状态。

应适当规定线程池大小,并使任务适当小,也不能太小,否则分配的开销会影响整体性能。

线程不应该一直处于忙-等(busy-wait)的状态,即反复检查一个共享状态,等待某些状态的改变(通过类似让出cpu-唤醒方式代替busy-wait?)。

不要通过调用Thread.yield来修正程序(yield的作用是让出线程自己的cpu执行时间),它没有可测试的语义(testable semantic),不可移植,执行效果有不确定性(因为让出cup后可能又被自己抢到)。更好的办法是减少并发运行的线程数。

不要通过调整线程优先级来修正程序(但可用来提高一个已经正常工作的程序的服务质量)。线程优先级是java平台上最不可移植的特征。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
我想将frontend 也是用volumes,将其映射到/app/frontend目录,在/app/frontend下install以及build,如何实现 docker-compose.yml文件: version: '3' services: frontend: build: context: ./frontend dockerfile: Dockerfile ports: - 8010:80 restart: always backend: build: context: ./backend dockerfile: Dockerfile volumes: - /app/backend:/app environment: - CELERY_BROKER_URL=redis://redis:6379/0 command: python manage.py runserver 0.0.0.0:8000 ports: - 8011:8000 restart: always celery-worker: build: context: ./backend dockerfile: Dockerfile volumes: - /app/backend:/app environment: - CELERY_BROKER_URL=redis://redis:6379/0 command: celery -A server worker -l info --pool=solo --concurrency=1 depends_on: - redis - backend restart: always celery-beat: build: context: ./backend dockerfile: Dockerfile volumes: - /app/backend:/app environment: - CELERY_BROKER_URL=redis://redis:6379/0 command: celery -A server beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler depends_on: - redis - backend restart: always redis: image: redis:latest ports: - 6379:6379 restart: always mysql: image: mysql:latest environment: - MYSQL_ROOT_PASSWORD=sacfxSql258147@ ports: - 8016:3306 volumes: - ./mysql:/var/lib/mysql restart: always frontend:dockerfile文件 FROM node:16.18.1 WORKDIR /app/frontend COPY package*.json ./ RUN npm install COPY . . RUN npm run build:prod FROM nginx:latest COPY --from=0 /app/frontend/dist/ /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
最新发布
07-14

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值