当前位置:

首页 > 编程开发 > 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 安全的共享对象

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

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

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

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

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

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
谷歌浏览器Mac版入口
谷歌浏览器Mac版入口

谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

Chrome浏览器JS脚本不运行怎么办
Chrome浏览器JS脚本不运行怎么办

Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

IE浏览器怀旧版在线网址
IE浏览器怀旧版在线网址

IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

火狐浏览器有哪些设置功能
火狐浏览器有哪些设置功能

火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

chrome搜索免验证入口
chrome搜索免验证入口

Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

java heap space 选型思路:使用场景与区别整理
java heap space 选型思路:使用场景与区别整理

Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

java heap space 使用中遇到的问题怎么解决
java heap space 使用中遇到的问题怎么解决

Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

java xml 选型思路:使用场景与区别整理
java xml 选型思路:使用场景与区别整理

XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

查看更多
精品专题 更多
装机必备
装机必备

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

Windows
Windows

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

macOS软件
macOS软件

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

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。