java并发编程实战阅读笔记(第三章)对象的共享

本文探讨了Java并发编程中对象的共享和可见性问题,包括volatile关键字的作用,对象的发布与逸出,以及如何通过线程封闭、不变性和安全发布来确保线程安全。文章详细阐述了可见性的概念,强调了volatile变量的内存屏障效果,同时介绍了避免同步的策略,如线程封闭和不可变对象的应用。
摘要由CSDN通过智能技术生成

如何共享和发布对象,从而使他们能够安全地由多个线程访问。

可见性

可见性:
  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

  在 Java 中 volatile、synchronized 和 final 实现可见性。

原子性:

  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型,为何要强调非long和double类型?下面有说明) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性:

  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。一个线程安全的可变整数类样例:

public class SynchronizedInteger {
    private int value;
    public synchronized int get(){
        return value;
    }
    public synchronized void set(int value){
        this.value = value;
    }
}

最低安全性:
当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值,这种安全性保证也被称为最低安全性。

非原子的64位操作:
即使不考虑数据失效性,在多线程中使用共享且可变的long 和 double等类型的变量也是不安全的,除非用volatile来声明他们,或者永锁保护起来。因为JVM允许将64位的读操作或写操作分解成两个32位来操作,当读取一个非volatile类型的long变量时,可能会读到某个值的高32位和另一个值的低32位。

volatile变量
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。**当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。**volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

了解一下多线程情况下对变量的操作:
这里写图片描述
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:

  1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。

  2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile 性能:
  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

发布与逸出

一、对象的发布和逸出

发布(publish)对象意味着其作用域之外的代码可以访问操作此对象。例如将对象的引用保存到其他代码可以访问的地方,或者在非私有的方法中返回对象的引用,或者将对象的引用传递给其他类的方法。
为了保证对象的线程安全性,很多时候我们要避免发布对象,但是有时候我们又需要使用同步来安全的发布某些对象。
逸出即为发布了本不该发布的对象。
使用静态变量引用对象是发布对象最直观和最简单的方式。例如以下代码示例,在示例中我们也看到,由于任何代码都可以遍历我们发布persons集合,导致我们间接的发布了Person实例,自然也就造成可以肆意的访问操作集合中的Person元素。

public class ObjectPublish {

    public static HashSet<Person> persons ;

    public void init()
    {
        persons = new HashSet<Person>();
    }

}

在非私有的方法内返回一个私有变量的引用会导致私有变量的逸出,例如以下代码

public class ObjectPublish {    
    private  HashSet<Person> persons=  new HashSet<Person>();
    public HashSet<Person>  getPersons()
    {
        return this.persons;
    }

}

发布一个对象也会导致此对象的所有非私有的字段对象的发布,其中也包括方法调用返回的对象。
在构造函数中使用直接初始化或者调用可改写的实例方法都会导致隐式的this逸出也是经常发生的事情,例如以下代码,在EventListener的实例中也通过this隐含的发布了尚未构造完成的ConstructorEscape实例,可能会造成无法预知的结果。

public class ConstructorEscape {
    public ConstructorEscape(EventSource eventSource)
    {
        eventSource.registerListener(
                    new EventListener(){
                        public void OnEvent(Event e)
                        {
                            doSomeThing(e);
                        }
                    }
                );
    }
}

我们可以使用工厂方法防止隐式的this逸出问题,例如以下代码


public class ConstructorEscape {
    private final EventListener listener;

    private  ConstructorEscape()
    {
        this.listener=    new EventListener(){
            public void OnEvent(Event e)
            {
                doSomeThing(e);
            }
        };        
    }

    public static ConstructorEscape getInstance(EventSource eventSource)
    {
        ConstructorEscape  instance = new ConstructorEscape();
        eventSource.registerListener(instance.listener);
        return instance;
    }    
}
二、避免同步之线程封闭

线程封闭可以使数据的访问限制在单个线程之内,相对锁定同步来说,其实实现线程安全比较简单的方式。
java提供了ThreadLocal类来实现线程封闭,其可以使针对每个线程存有共享状态的独立副本。其通常用于防止对可变的单实例变量和全局变量进行共享,例如每个请求作为一个逻辑事务需要初始化自己的事务上下文,这个事务上下文应该使用ThreadLocal来实现线程封闭。

public class SynchronizedInteger {
    private ThreadLocal<Long> value;

    public Long get(){
        return this.value.get();
    }

    public void set(Long value){
        this.value.set(value);
    }
}

栈封闭是线程封闭的特例,即数据作为局部变量封闭在执行线程中,对于值类型的局部变量不存在逸出的问题,如果是引用类型的局部变量,开发人员需要确保其不要作为返回值或者其他的关联引用等而被逸出。

三、避免同步之不变性

某个对象创建之后就不能修改其状态,那么我们就说这个对象是不可变对象。
由于多线程操作可变状态会导致原子性、可见性一系列问题,所以线程安全性是不可变对象与生俱来的特性。
不可变对象由构造函数初始化状态,并可以安全的传递给任何不可信代码使用。
所有字段标记为final的对象,由于引用字段的对象可能可以直接修改,所以其并不一定是不可变对象,其需要满足以下条件
1)对象的所有字段都用final标记
2)对象创建之后任何状态都不能修改
3)对象不存在this隐式构造函数逸出

public class ThreeStooges {

    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges(){
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Carry");
    }

    public boolean isStooge(String name){
        return stooges.contains(name);
    }
}
四、对象的安全发布

很多时候我们是希望在多线程之间共享数据的,此时我们就必须确保安全的发布共享对象。
要安全的发布一个对象,对象的引用以及对象的状态对其他线程都是可见的,一个正确构造的对象可以通过以下方式安全的发布
1)在静态构造函数中初始化对象引用
2)使用volatile和AtomicReferance限定对象引用
3)使用final限定对象引用
4)将对象引用保存到有锁保护的字段中

以下是对提供的参考资料的总结,按照要求结构化多个要点分条输出: 4G/5G无线网络优化与网规案例分析: NSA站点下终端掉4G问题:部分用户反馈NSA终端频繁掉4G,主要因终端主动发起SCGfail导致。分析显示,在信号较好的环境下,终端可能因节能、过热保护等原因主动释放连接。解决方案建议终端侧进行分析处理,尝试关闭节电开关等。 RSSI算法识别天馈遮挡:通过计算RSSI平均及差识别天馈遮挡,差大于3dB则认定有遮挡。不同设备分组规则不同,如64T和32T。此方法可有效帮助现场人员识别因环境变化引起的网络问题。 5G 160M组网小区CA不生效:某5G站点开启100M+60M CA功能后,测试发现UE无法正常使用CA功能。问题原因在于CA频点集标识配置错误,修正后测试正常。 5G网络优化与策略: CCE映射方式优化:针对诺基亚站点覆盖农村区域,通过优化CCE资源映射方式(交织、非交织),提升RRC连接建立成功率和无线接通率。非交织方式相比交织方式有显著提升。 5G AAU两扇区组网:与三扇区组网相比,AAU两扇区组网在RSRP、SINR、下载速率和上传速率上表现不同,需根据具体场景选择适合的组网方式。 5G语音解决方案:包括沿用4G语音解决方案、EPS Fallback方案和VoNR方案。不同方案适用于不同的5G组网策略,如NSA和SA,并影响语音连续性和网络覆盖。 4G网络优化与资源利用: 4G室分设备利旧:面对4G网络投资压减与资源需求矛盾,提出利旧多维度调优策略,包括资源整合、统筹调配既有资源,以满足新增需求和提质增效。 宏站RRU设备1托N射灯:针对5G深度覆盖需求,研究使用宏站AAU结合1托N射灯方案,快速便捷地开通5G站点,提升深度覆盖能力。 基站与流程管理: 爱立信LTE基站邻区添加流程:未提供具体内容,但通常涉及邻区规划、参数配置、测试验证等步骤,以确保基站间顺畅切换和覆盖连续性。 网络规划与策略: 新高铁跨海大桥覆盖方案试点:虽未提供详细内容,但可推测涉及高铁跨海大桥区域的4G/5G网络覆盖规划,需考虑信号穿透、移动性管理、网络容量等因素。 总结: 提供的参考资料涵盖了4G/5G无线网络优化、网规案例分析、网络优化策略、资源利用、基站管理等多个方面。 通过具体案例分析,展示了无线网络优化中的常见问题及解决方案,如NSA终端掉4G、RSSI识别天馈遮挡、CA不生效等。 强调了5G网络优化与策略的重要性,包括CCE映射方式优化、5G语音解决方案、AAU扇区组网选择等。 提出了4G网络优化与资源利用的策略,如室分设备利旧、宏站RRU设备1托N射灯等。 基站与流程管理方面,提到了爱立信LTE基站邻区添加流程,但未给出具体细节。 新高铁跨海大桥覆盖方案试点展示了特殊场景下的网络规划需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值