展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > Java并发编程之对象的共享怎么实现

Java并发编程之对象的共享怎么实现

1.可见性通常,我们无法保证执行读操作的线程能看到其他线程写入的值,因为每个线程都由自己的缓存机制。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。publicclassNoVisibility{privatestaticbooleanready;privatestaticintnumber;privatestaticclassReaderThreadextendsThread{publicvoidrun(){while(!ready)Thread.yield();System.out.pr

本文目录

    1.可见性

    通常,我们无法保证执行读操作的线程能看到其他线程写入的值,因为每个线程都由自己的缓存机制。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

    public class NoVisibility {
        private static boolean ready;
        private static int number;
    
        private static class ReaderThread extends Thread {
            public void run() {
                while (!ready)
                    Thread.yield();
                System.out.println(number);
            }
        }
    
        public static void main(String[] args) {
            new ReaderThread().start();
            number = 42;
            ready = true;
        }
    }

    以上代码,看起来会输出42,但事实上很可能根本无法终止,因为读线程永远看不到ready的值;很有可能输出0,因为读线程看到了写入ready的值,却没有看到之后写入number的值,这种现象称为“重排序”。在没有同步的情况下,编译器、处理器、运行时等都有可能对操作的执行顺序进行一些意想不到的调整。

    所以,只要有数据在多个线程之间共享时,就应该使用正确的同步。

    1.1 失效数据

    除非使用同步,否则很可能获得变量的失效值。失效值可能不会同时出现,一个线程可能获得一个变量的最新值,而获得另一个变量的失效值。失效数据还可能导致一些令人困惑的故障,如:意料之外的异常、被破坏的数据结构、不精确的计算、无限循环等等。

    1.2 非原子的64位操作

    对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。所以,很可能会读取到最新值的高32位和失效值的低32值,造成读取到是一个随机值。除非用关键字volatile来声明它们,或者用锁保护起来。

    1.3 加锁和可见性

    当某线程执行由锁保护的同步代码块时,可以看到其他线程之前在同一同步代码块中的所有操作结果。如果没有同步,将无法实现上述保证。加锁的含义不仅仅局限于互斥行为,还包括可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。

    1.4 volatile变量

    当把变量声明为volatile类型后,编译器和运行时都不会将该变量上的操作也其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

    当且仅当满足以下所有条件时,才应该使用volatile变量:

    • 对变量的写入操作不依赖变量的当前值,或者能确保只用单个线程更新变量的值。

    • 该变量不会与其他状态变量一起纳入不变性条件中。

    • 在访问变量时不需要加锁。

    2. 发布与泄露

    发布一个对象是指,是对象能够在当前作用域之外的代码中使用。发布对象的方式包括:非私用变量的引用、方法调用返回的引用、发布内部类对象隐含外部类的引用等等。当某个不应该发布的对象被发布是,就被称为泄露。

    public class ThisEscape {
       private int status;
       public ThisEscape(EventSource source) {
          source.registerListener(new EventListener() {
             public void onEvent(Event e) {
                doSomething(e);
             }
          });
          status = 1;
       }
    
       void doSomething(Event e) {
          status = e.getStatus();
       }
    
       interface EventSource {
          void registerListener(EventListener e);
       }
    
       interface EventListener {
          void onEvent(Event e);
       }
    
       interface Event {
          int getStatus();
       }
    }

    由于内部类的实例包含了对外部类实例的隐含引用,当ThisEscape发布EventListener时,也隐含发布了ThisEscape实例本身。但在此时,变量status还没有被初始化,造成了this引用在构造函数中泄露。可以使用一个私有的构造函数和一个公共的工厂方法,避免不正确的构造过程:

    public class SafeListener {
        private int status;
        private final EventListener listener;
        private SafeListener() {
            listener = new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            };
            status = 1;
        }
        public static SafeListener newInstance(EventSource source) {
            SafeListener safe = new SafeListener();
            source.registerListener(safe.listener);
            return safe;
        }
    
        void doSomething(Event e) {
            status = e.getStatus();
        }
    
        interface EventSource {
            void registerListener(EventListener e);
        }
    
        interface EventListener {
            void onEvent(Event e);
        }
    
        interface Event {
            int getStatus();
        }
    }

    3. 线程封闭

    一种避免使用同步的方式就是不共享。如果仅在单线程内访问数据,就不需要同步,这就被称为线程封闭。线程封闭是程序设计中的考虑因素,必须在程序中实现。Java也提供了一些机制帮助维护线程封闭,比如局部变量和ThreadLocal。

    3.1 Ad-hoc线程封闭

    Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。使用volatile变量是实现Ad-hoc线程封闭的一种方式,只要能保证只有单个线程对共享volatile变量执行写入操作,那么就可以安全低在这些变量上进行“读取-修改-写入”操作,volatile变量的可见性又保证了其他线程能够看到最新的值。

    Ad-hoc线程封闭是非常脆弱的,因此在程序中尽量少使用。在可能的情况下,使用其他线程封闭技术,比如:栈封闭、ThreadLocal。

    3.2 栈封闭

    在栈封闭中,只能通过局部变量才能访问对象。它们位于执行线程的栈中,其他线程无法访问到。即使这些对象是非线程安全的对象,它们仍然是线程安全的。然而,值得注意的是,只要编写代码的人才知道哪些对象是栈封闭的。如果没有明确的说明,后续的维护人员很容易错误的泄露这些对象。

    3.3 ThreadLocal类

    使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

    public class ConnectionDispenser {
        static String DB_URL = "jdbc:mysql://localhost/mydatabase";
    
        private ThreadLocal connectionHolder
            = new ThreadLocal() {
                public Connection initialValue() {
                    try {
                        return DriverManager.getConnection(DB_URL);
                    } catch (SQLException e) {
                        throw new RuntimeException("Unable to acquire Connection, e");
                    }
            };
        };
    
        public Connection getConnection() {
            return connectionHolder.get();
        }
    }

    从概念上看,你可以将ThreadLocal视为包含了Map对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。

    4. 不变性

    如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:

    • 对象创建以后其状态就不能改变

    • 对象的所有域都是final类型

    • 对象是正确创建的,在对象创建期间,this引用没有泄露

    public final class ThreeStooges {
        private final Set stooges = new HashSet();
    
        public ThreeStooges() {
            stooges.add("Moe");
            stooges.add("Larry");
            stooges.add("Curly");
        }
    
        public boolean isStooge(String name) {
            return stooges.contains(name);
        }
    }

    上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。

    4.1 final域

    final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和“除非需要更高的可见性,否则应将所有的域都声明为私用域”一样,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。

    4.2 使用volatile类型来发布不可变对象

    因式分解Sevlet将执行两个原子操作:

    • 更新缓存

    • 通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果

    每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:

    public class OneValueCache {
        private final BigInteger lastNumber;
        private final BigInteger[] lastFactors;
    
        public OneValueCache(BigInteger i, BigInteger[] factors) {
            lastNumber = i;
            lastFactors = Arrays.copyOf(factors, factors.length);
        }
    
        public BigInteger[] getFactors(BigInteger i) {
            if (lastNumber == null || !lastNumber.equals(i))
                return null;
            else
                return Arrays.copyOf(lastFactors, lastFactors.length);
        }
    }

    当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:

    public class VolatileCachedFactorizer implements Servlet {
        private volatile OneValueCache cache = new OneValueCache(null, null);
    
        public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = cache.getFactors(i);
            if (factors == null) {
                factors = factor(i);
                cache = new OneValueCache(i, factors);
            }
            encodeIntoResponse(resp, factors);
        }
    }

    5 安全发布

    5.1 不正确的发布

    像这样将对象引用保存到公有域中就是不安全的:

    public Holder holder;
    public void initialize(){
        holder = new Holder(42);
    }

    由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。

    public class Holder {
        private int n;
    
        public Holder(int n) {
            this.n = n;
        }
    
        public void assertSanity() {
            if (n != n)
                throw new AssertionError("This statement is false.");
        }
    }

    上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。

    5.2 不可变对象与初始化安全性

    即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

    5.3 安全发布的常用模式

    要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:

    • 在静态初始化函数里初始化一个对象引用。

    • 将对象的引用保存到volatile类型的域或者AtomicReference对象中。

    • 将对象的引用保存到某个正确构造对象的final类型域中。

    • 将对象的引用保存到一个由锁保护的域中。

    线程安全库中的容器类提供了以下的安全发布保证:

    • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。

    • 通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。

    • 通过将某个对象放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该对象安全地发布到任何从这些队列中访问该对象的线程。

    5.4 事实不可变对象

    如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为事实不可变对象。在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。例如维护一个Map对象,其中保存了每位用户的最新登录时间:

    public Map lastLogin =
    Collections.synchronizedMap(new HashMap如果Date对象的值在被放入Map后就不会改变,那么synchronizedMap中的同步机制就足以使Date值被安全地发布,并且在访问这些Date值时不需要额外的同步。

    5.5 可变对象

    对于可变对象,不仅在发布对象是需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。对象的发布需求取决于它的可变性:

    • 不可变对象可以通过任意机制来发布。

    • 事实不可变对象必须通过安全方式来发布。

    • 可变对象必须通过安全方式来发布,而且必须是线程安全的或者用某个锁保护起来。

    5.6 安全的共享对象

    在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

    • 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。

    • 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。

    • 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公共接口来进行访问而不需要进一步的同步。

    • 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    Postman macOS版
    Postman macOS版

    Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。

    Docker Desktop macOS版
    Docker Desktop macOS版

    Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。

    Android Studio macOS版
    Android Studio macOS版

    Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。

    Xcode macOS版
    Xcode macOS版

    Xcode 是苹果面向 Mac 提供的官方集成开发环境,将代码编辑、项目构建、SwiftUI 实时预览、设备模拟、自动化测试、断点调试、性能分析和应用分发整合在同一套工作流中,适合开发与维护 iPhone、iPad、Mac、Apple Wa

    JetBrains WebStorm macOS版
    JetBrains WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    PhpStorm macOS版
    PhpStorm macOS版

    PhpStorm 是 JetBrains 推出的专业 PHP 集成开发环境,可在 Mac 上完成代码编写、智能检查、重构、调试、测试和数据库管理。它支持主流 PHP 框架、Composer、Git、Docker 与远程解释器。

    JetBrains PyCharm macOS版
    JetBrains PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    JetBrains IntelliJ IDEA macOS版
    JetBrains IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    Visual Studio Code macOS版
    Visual Studio Code macOS版

    Visual Studio Code 是微软提供的免费代码编辑器,支持 macOS,并为 Intel 与 Apple 芯片 Mac 提供适配版本。软件集代码编辑、调试、Git 源代码管理、集成终端和扩展市场于一体。

    REAPER macOS版
    REAPER macOS版

    REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。

    Adobe Audition macOS版
    Adobe Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Pro Tools macOS版
    Pro Tools macOS版

    Pro Tools 是 Avid 推出的专业数字音频工作站,覆盖多轨录音、无损剪辑、MIDI 编曲、插件处理、自动化混音及声音与画面同步等工作。它拥有 Intro、Artist、Studio 和 Ultimate 等方案。

    WINDOWS 更多
    Postman Windows版
    Postman Windows版

    Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等

    Docker Desktop Windows版
    Docker Desktop Windows版

    Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。

    Android Studio Windows版
    Android Studio Windows版

    Android Studio 是 Google 面向 Android 应用开发提供的官方集成开发环境,以 IntelliJ IDEA 为基础,集成智能代码编辑、Gradle 构建、设备模拟、界面预览、调试测试及性能分析工具,

    JetBrains WebStorm Windows版
    JetBrains WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    JetBrains PhpStorm Windows版
    JetBrains PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    JetBrains PyCharm Windows版
    JetBrains PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    JetBrains IntelliJ IDEA Windows版
    JetBrains IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    Visual Studio Code Windows版
    Visual Studio Code Windows版

    Visual Studio Code 是微软推出的免费代码编辑器,Windows 版集成智能补全、代码导航、调试、终端和版本控制,并可通过扩展添加语言、格式化及远程开发能力。它启动较快、配置灵活,

    REAPER Windows版
    REAPER Windows版

    REAPER 是 Cockos 推出的数字音频工作站,适用于录音、MIDI 编曲、音频编辑、混音、母带制作及声音设计。软件安装包小巧,支持多轨录制、灵活路由、自动化、第三方插件和脚本扩展,并提供完整功能的 60 天免费评估期。

    Adobe Audition Windows版
    Adobe Audition Windows版

    Adobe Audition 是面向播客、影视后期、广播与音乐制作的专业音频工作站,提供波形编辑、多轨混音、频谱分析、录音、降噪修复和效果处理等功能。Windows 版通过 Adobe Creative Cloud 安装和管理,

    Pro Tools Windows版
    Pro Tools Windows版

    Pro Tools 是 Avid 推出的专业数字音频工作站,提供多轨录音、音频剪辑、MIDI编曲、插件处理、自动化混音及影视声音制作能力。它适合音乐人、录音师、制作人和音频后期团队使用,并提供免费入门版、订阅方案及永久授权选择。

    Cubase Windows版
    Cubase Windows版

    Cubase 是 Steinberg 推出的数字音频工作站,提供 MIDI 编曲、多轨录音、音频编辑、虚拟乐器、效果处理、自动化、混音和母带输出工具。其 Pro、Artist 与 Elements 版本面向不同预算和制作规模,