Effective Java 第三版——78. 同步访问共享的可变数据

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java 第三版——78. 同步访问共享的可变数据

并发

线程允许多个活动同时进行。 并发编程比单线程编程更难,因为更多的事情可能会出错,并且失败很难重现。 你无法避免并发。 它是平台中固有的,也是要从多核处理器获得良好性能的要求,现在无处不在。本章包含的建议可帮助你编写清晰,正确,文档完备的并发程序。

  1. 同步访问共享的可变数据

synchronized关键字确保一次只有一个线程可以执行一个方法或代码块。许多程序员认为同步只是一种互斥的方法,以防止一个线程在另一个线程修改对象时看到对象处于不一致的状态。在这个观点中,对象以一致的状态创建(条目 17),并由访问它的方法锁定。这些方法观察状态,并可选地引起状态转换,将对象从一个一致的状态转换为另一个一致的状态。正确使用同步可以保证没有任何方法会观察到处于不一致状态的对象。

这种观点是正确的,但它只说明了一部分意义。如果没有同步,一个线程的更改可能对其他线程不可见。同步不仅阻止线程观察处于不一致状态的对象,而且确保每个进入同步方法或块的线程都能看到由同一锁保护的所有之前修改的效果。

语言规范保证读取或写入变量是原子性(atomic)的,除非变量的类型是long或double [JLS, 17.4, 17.7]。换句话说,读取long或double以外的变量,可以保证返回某个线程存储到该变量中的值,即使多个线程在没有同步的情况下同时修改变量也是如此。

你可能听说过,为了提高性能,在读取或写入原子数据时应该避免同步。这种建议大错特错。虽然语言规范保证线程在读取属性时不会看到任意值,但它不保证由一个线程编写的值对另一个线程可见。 同步是线程之间可靠通信以及互斥所必需的。这是语言规范中称之为内存模型(memory model)的一部分,它规定了一个线程所做的更改何时以及如何对其他线程可见[JLS, 17.4;Goetz06, 16)。

即使数据是原子可读和可写的,未能同步对共享可变数据的访问的后果也是可怕的。 考虑从另一个线程停止一个线程的任务。 Java类库提供了Thread.stop方法,但是这个方法很久以前就被弃用了,因为它本质上是不安全的——它的使用会导致数据损坏。 不要使用Thread.stop。 从另一个线程中停止一个线程的推荐方法是让第一个线程轮询一个最初为false的布尔类型的属性,但是第二个线程可以设置为true以指示第一个线程要自行停止。 因为读取和写入布尔属性是原子的,所以一些程序员在访问属性时不需要同步:

// Broken! - How long would you expect this program to run?

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;
    }
}

你可能希望这个程序运行大约一秒钟,之后主线程将stoprequired设置为true,从而导致后台线程的循环终止。然而,在我的机器上,程序永远不会终止:后台线程永远循环!

问题是在没有同步的情况下,无法确保后台线程何时(如果有的话)看到主线程所做的stopRequested值的变化。 在没有同步的情况下,虚拟机将下面代码:

   while (!stopRequested)
        i++;

转换成这样:

if (!stopRequested)
    while (true)
        i++;

这种优化称为提升(hoisting,它正是OpenJDK Server VM所做的。 结果是活泼失败( liveness failure):程序无法取得进展。 解决问题的一种方法是同步对stopRequested属性的访问。 正如预期的那样,该程序大约一秒钟终止:

// Properly synchronized cooperative thread termination
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();
    }
}

注意,写方法(requestStop)和读方法(stop- required)都是同步的。仅同步写方法是不够的! 除非读和写操作同步,否则不能保证同步工作。有时,只同步写(或读)的程序可能在某些机器上显示有效,但在这种情况下,表面的现象是具有欺骗性的。

即使没有同步,StopThread中同步方法的操作也是原子性的。换句话说,这些方法上的同步仅用于其通信效果,而不是互斥。虽然在循环的每个迭代上同步的成本很小,但是有一种正确的替代方法,它不那么冗长,而且性能可能更好。如果stoprequest声明为volatile,则可以省略StopThread的第二个版本中的锁定。虽然volatile修饰符不执行互斥,但它保证任何读取属性的线程都会看到最近写入的值:

// Cooperative thread termination with a volatile field
public class StopThread {
    private static volatile 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;
    }
}

在使用volatile时一定要小心。考虑下面的方法,该方法应该生成序列号:

// Broken - requires synchronization!

private static volatile int nextSerialNumber = 0;

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

该方法的目的是保证每次调用都返回一个唯一值(只要调用次数不超过232次)。 方法的状态由单个可原子访问的属性nextSerialNumber组成,该属性的所有可能值都是合法的。 因此,不需要同步来保护其不变量。 但是,如果没有同步,该方法将无法正常工作。

问题是增量运算符(++)不是原子的。 它对nextSerialNumber属性执行两个操作:首先它读取值,然后它写回一个新值,等于旧值加1。 如果第二个线程在线程读取旧值并写回新值之间读取属性,则第二个线程将看到与第一个线程相同的值并返回相同的序列号。 这是安全性失败(safety failure):程序计算错误的结果。

修复generateSerialNumber的一种方法是将synchronized修饰符添加到其声明中。 这确保了多个调用不会交叉读取,并且每次调用该方法都会看到所有先前调用的效果。 完成后,可以并且应该从nextSerialNumber中删除volatile修饰符。 要保护该方法,请使用long而不是int,或者在nextSerialNumber即将包装时抛出异常。

更好的是,遵循条目 59条中建议并使用AtomicLong类,它是java.util.concurrent.atomic包下的一部分。 这个包为单个变量提供了无锁,线程安全编程的基本类型。 虽然volatile只提供同步的通信效果,但这个包还提供了原子性。 这正是我们想要的generateSerialNumber,它可能强于同步版本的代码:

// Lock-free synchronization with java.util.concurrent.atomic
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

避免此条目中讨论的问题的最佳方法是不共享可变数据。 共享不可变数据(条目 17)或根本不共享。 换句话说, 将可变数据限制在单个线程中。 如果采用此策略,则必须对其进行文档记录,以便在程序发展改进时维护此策略。 深入了解正在使用的框架和类库也很重要,因为它们可能会引入你不知道的线程。

一个线程可以修改一个数据对象一段时间后,然后与其他线程共享它,只同步共享对象引用的操作。然后,其他线程可以在不进一步同步的情况下读取对象,只要不再次修改该对象。这些对象被认为是有效不可变的( effectively immutable)[Goetz06, 3.5.4]。将这样的对象引用从一个线程转移到其他线程称为安全发布(safe publication )[Goetz06, 3.5.3]。安全地发布对象引用的方法有很多:可以将它保存在静态属性中,作为类初始化的一部分;也可以将其保存在volatile属性、final属性或使用正常锁定访问的属性中;或者可以将其放入并发集合中(条目 81)。

总之, 当多个线程共享可变数据时,每个读取或写入数据的线程都必须执行同步。 在没有同步的情况下,无法保证一个线程的更改对另一个线程可见。 未能同步共享可变数据的代价是活性失败和安全性失败。 这些失败是最难调试的。 它们可以是间歇性的和时间相关的,并且程序行为可能在不同VM之间发生根本的变化。如果只需要线程间通信,而不需要互斥,那么volatile修饰符是一种可接受的同步形式,但是正确使用它可能会比较棘手。

Original: https://www.cnblogs.com/IcanFixIt/p/10633368.html
Author: 林本托
Title: Effective Java 第三版——78. 同步访问共享的可变数据

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/537080/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • c 的陷阱

    c语言算是非常古老了,像瑞士军刀灵活却也很容易伤到自己,即使是多年的老杆子,以致于市面上都有一本经典的C的书叫《C陷阱与缺陷》的书。 这个文章总结下c中常见的陷阱,可能在日常工作或…

    Java 2023年5月29日
    089
  • Python基本语法学习

    CSN Python学习作业 Python的变量不需要声明,但每个变量在使用前都必须赋值。在Python中,变量就是变量,它没有所谓的”类型”一说 Pyth…

    Java 2023年6月7日
    057
  • 16.服务端、客户端解决粘包问题,服务端加入退出指令线程

    客户端: DataHeader.hpp EasyTcpClient.hpp main.cpp 服务端: 对客户端的管理要进行升级,每个客户端要有自己的消息缓冲区。 对客户端的管理使…

    Java 2023年5月29日
    0122
  • 并查集优化

    并查集及其优化 并查集可以动态地连通两个点,可以非常快速判断两个点是否连通。假设存在 n 个节点,我们先将所有结点的 leader 标为自身;每次连接节点 i 和 j 时,我们可以…

    Java 2023年6月8日
    080
  • 线程从2022-06-23 18.21 等待到2022-06-24 11:00

    "task-scheduler-4" #412 prio=5 os_prio=0 tid=0x00007fc5c0017800 nid=0x55d4 in Ob…

    Java 2023年6月9日
    099
  • Spring AOP实现接口调用异常时重试

    调用某个接口时,可能因为数据同步延迟等原因导致抛异常,很希望程序可以重试指定次数后再结束运行。 注意:接口需配合事务,当抛异常时,进行回滚,以撤销异常之前对数据库的操作。 @Asp…

    Java 2023年6月8日
    084
  • 信管知识梳理(六)信息化发展与应用、信息系统规划和服务管理、首席信息官

    一、信息化发展与应用 1.1 信息化发展与应用的新特点 我国在”十三五”规划纲要中,将培育人工智能、移动智能终端、5G、先进传感器等作为新一代技术产业创新重…

    Java 2023年6月6日
    073
  • Spring常用注解(SpirngBoot方面讲的更加详细)

    使用注解须知: 基本方向 1. bean @Component 2. 属性如何注入 @Component public class User{ public String name…

    Java 2023年6月14日
    090
  • Effective Java 第三版—— 85. 其他替代方式优于Java本身序列化

    Tips书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code注意,书中的有些代码里方法是基于Java 9…

    Java 2023年5月29日
    094
  • kotlin学习一:kotlin简介

    kotlin是JetBrains公司出品的基于JVM的语言,和其他JVM语言一样,目的在于提供比JAVA更加简介的语法, 同时提供函数式编程,不需要再像JAVA一样所有的一切都要依…

    Java 2023年6月16日
    082
  • 数据库连接查询总结

    建表SQL create table account ( account_id bigint PRIMARY KEY AUTO_INCREMENT, name varchar(64…

    Java 2023年6月6日
    085
  • Spring JDBC操作

    1、在JAVA创建数据库表对象 2、创建DAO接口及其对应实现类 3、创建sevice 4、编辑XML配置 5、批量操作 Original: https://www.cnblogs…

    Java 2023年6月7日
    077
  • ucore操作系统学习(二) ucore lab2物理内存管理分析

    一、lab2物理内存管理介绍 操作系统的一个主要职责是管理硬件资源,并向应用程序提供具有良好抽象的接口来使用这些资源。 而内存作为重要的计算机硬件资源,也必然需要被操作系统统一的管…

    Java 2023年6月8日
    0115
  • MySQL性能优化的5个维度

    面试官如果问你:你会从哪些维度进行MySQL性能优化?你会怎么回答? 所谓的性能优化,一般针对的是MySQL查询的优化。既然是优化查询,我们自然要先知道查询操作要经过哪些环节,然后…

    Java 2023年6月7日
    077
  • 【软件构造】抽象数据类型ADT

    【软件构造】抽象数据类型ADT 1.前言 在Lab2中,涉及到了大量的ADT、RI、AF的设计,仅以此篇博客做一个小小的总结。🥰 2.ADT定义 除了java等编程语言自带的数据类…

    Java 2023年6月5日
    0132
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球