详解java安全编码指南之可见性和原子性

不可变对象的可见性

不可变对象就是初始化之后不能够被修改的对象,那么是不是类中引入了不可变对象,所有对不可变对象的修改都立马对所有线程可见呢?

实际上,不可变对象只能保证在多线程环境中,对象使用的安全性,并不能够保证对象的可见性。

先来讨论一下可变性,我们考虑下面的一个例子:

public final class ImmutableObject {
    private final int age;
    public ImmutableObject(int age){
        this.age=age;
    }
}

我们定义了一个ImmutableObject对象,class是final的,并且里面的唯一字段也是final的。所以这个ImmutableObject初始化之后就不能够改变。

然后我们定义一个类来get和set这个ImmutableObject:

public class ObjectWithNothing {
    private ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
        return refObject;
    }
    public void setImmutableObject(int age){
        this.refObject=new ImmutableObject(age);
    }
}

上面的例子中,我们定义了一个对不可变对象的引用refObject,然后定义了get和set方法。

注意,虽然ImmutableObject这个类本身是不可变的,但是我们对该对象的引用refObject是可变的。这就意味着我们可以调用多次setImmutableObject方法。

再来讨论一下可见性。

上面的例子中,在多线程环境中,是不是每次setImmutableObject都会导致getImmutableObject返回一个新的值呢?

答案是否定的。

当把源码编译之后,在编译器中生成的指令的顺序跟源码的顺序并不是完全一致的。处理器可能采用乱序或者并行的方式来执行指令(在JVM中只要程序的最终执行结果和在严格串行环境中执行结果一致,这种重排序是允许的)。并且处理器还有本地缓存,当将结果存储在本地缓存中,其他线程是无法看到结果的。除此之外缓存提交到主内存的顺序也肯能会变化。

怎么解决呢?

最简单的解决可见性的办法就是加上volatile关键字,volatile关键字可以使用java内存模型的happens-before规则,从而保证volatile的变量修改对所有线程可见。

public class ObjectWithVolatile {
    private volatile ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
        return refObject;
    }
    public void setImmutableObject(int age){
        this.refObject=new ImmutableObject(age);
    }
}

另外,使用锁机制,也可以达到同样的效果:

public class ObjectWithSync {
    private  ImmutableObject refObject;
    public synchronized ImmutableObject getImmutableObject(){
        return refObject;
    }
    public synchronized void setImmutableObject(int age){
        this.refObject=new ImmutableObject(age);
    }
}

最后,我们还可以使用原子类来达到同样的效果:

public class ObjectWithAtomic {
    private final AtomicReference<ImmutableObject> refObject= new AtomicReference<>();
    public ImmutableObject getImmutableObject(){
        return refObject.get();
    }
    public void setImmutableObject(int age){
        refObject.set(new ImmutableObject(age));
    }
}

保证共享变量的复合操作的原子性

如果是共享对象,那么我们就需要考虑在多线程环境中的原子性。如果是对共享变量的复合操作,比如:++, -- *=, /=, %=, +=, -=, <<=, >>=, >>>=, ^= 等,看起来是一个语句,但实际上是多个语句的集合。

我们需要考虑多线程下面的安全性。

考虑下面的例子:

public class CompoundOper1 {
    private int i=0;
    public int increase(){
        i++;
        return i;
    }
}

例子中我们对int i进行累加操作。但是++实际上是由三个操作组成的:

1.从内存中读取i的值,并写入CPU寄存器中。

2.CPU寄存器中将i值+1

3.将值写回内存中的i中。

如果在单线程环境中,是没有问题的,但是在多线程环境中,因为不是原子操作,就可能会发生问题。

解决办法有很多种,第一种就是使用synchronized关键字

public synchronized int increaseSync(){
    i++;
    return i;
}

第二种就是使用lock:

private final ReentrantLock reentrantLock=new ReentrantLock();

public int increaseWithLock(){
    try{
        reentrantLock.lock();
        i++;
        return i;
    }finally {
        reentrantLock.unlock();
    }
}

第三种就是使用Atomic原子类:

private AtomicInteger atomicInteger=new AtomicInteger(0);

public int increaseWithAtomic(){
    return atomicInteger.incrementAndGet();
}

保证多个Atomic原子类操作的原子性

如果一个方法使用了多个原子类的操作,虽然单个原子操作是原子性的,但是组合起来就不一定了。

我们看一个例子:

public class CompoundAtomic {
    private AtomicInteger atomicInteger1=new AtomicInteger(0);
    private AtomicInteger atomicInteger2=new AtomicInteger(0);

    public void update(){
        atomicInteger1.set(20);
        atomicInteger2.set(10);
    }

    public int get() {
        return atomicInteger1.get()+atomicInteger2.get();
    }
}

上面的例子中,我们定义了两个AtomicInteger,并且分别在update和get操作中对两个AtomicInteger进行操作。

虽然AtomicInteger是原子性的,但是两个不同的AtomicInteger合并起来就不是了。在多线程操作的过程中可能会遇到问题。

同样的,我们可以使用同步机制或者锁来保证数据的一致性。

保证方法调用链的原子性

如果我们要创建一个对象的实例,而这个对象的实例是通过链式调用来创建的。那么我们需要保证链式调用的原子性。

考虑下面的一个例子:

public class ChainedMethod {
    private int age=0;
    private String name="";
    private String adress="";

    public ChainedMethod setAdress(String adress) {
        this.adress = adress;
        return this;
    }

    public ChainedMethod setAge(int age) {
        this.age = age;
        return this;
    }

    public ChainedMethod setName(String name) {
        this.name = name;
        return this;
    }
}

很简单的一个对象,我们定义了三个属性,每次set都会返回对this的引用。

我们看下在多线程环境下面怎么调用:

ChainedMethod chainedMethod= new ChainedMethod();
Thread t1 = new Thread(() -> chainedMethod.setAge(1).setAdress("www.flydean.com1").setName("name1"));
t1.start();

Thread t2 = new Thread(() -> chainedMethod.setAge(2).setAdress("www.flydean.com2").setName("name2"));
t2.start();

因为在多线程环境下,上面的set方法可能会出现混乱的情况。

怎么解决呢?我们可以先创建一个本地的副本,这个副本因为是本地访问的,所以是线程安全的,最后将副本拷贝给新创建的实例对象。

主要的代码是下面样子的:

public class ChainedMethodWithBuilder {
    private int age=0;
    private String name="";
    private String adress="";

    public ChainedMethodWithBuilder(Builder builder){
        this.adress=builder.adress;
        this.age=builder.age;
        this.name=builder.name;
    }

    public static class Builder{
        private int age=0;
        private String name="";
        private String adress="";

        public static Builder newInstance(){
            return new Builder();
        }
        private Builder() {}

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setAge(int age) {
            this.age = age;
            return this;
        }

        public Builder setAdress(String adress) {
            this.adress = adress;
            return this;
        }

        public ChainedMethodWithBuilder build(){
            return new ChainedMethodWithBuilder(this);
        }
    }

我们看下怎么调用:

final ChainedMethodWithBuilder[] builder = new ChainedMethodWithBuilder[1];
Thread t1 = new Thread(() -> {
    builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
        .setAge(1).setAdress("www.flydean.com1").setName("name1")
        .build();});
t1.start();

Thread t2 = new Thread(() ->{
    builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
        .setAge(1).setAdress("www.flydean.com1").setName("name1")
        .build();});
t2.start();

因为lambda表达式中使用的变量必须是final或者final等效的,所以我们需要构建一个final的数组。

读写64bits的值

在java中,64bits的long和double是被当成两个32bits来对待的。

所以一个64bits的操作被分成了两个32bits的操作。从而导致了原子性问题。

考虑下面的代码:

public class LongUsage {
    private long i =0;

    public void setLong(long i){
        this.i=i;
    }
    public void printLong(){
        System.out.println("i="+i);
    }
}

因为long的读写是分成两部分进行的,如果在多线程的环境中多次调用setLong和printLong的方法,就有可能会出现问题。

解决办法本简单,将long或者double变量定义为volatile即可。

private volatile long i = 0;

以上就是详解java安全编码指南之可见性和原子性的详细内容,更多关于java安全编码指南之可见性和原子性的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java 并发编程的可见性、有序性和原子性

    并发编程无论在哪门语言里,都属于高级篇,面试中也尝尝会被问到.想要深入理解并发编程机制确实不是一件容易的事,因为它涉及到计算机底层和操作系统的相关知识,如果对这部分知识不是很清楚可能会导致理解困难. 在这个专栏里,王子会尽量以白话和图片的方式剖析并发编程本质,希望可以让大家更容易理解. 今天我们就来谈一谈可见性.有序性和原子性都是什么东西. 并发编程的幕后 进入主题之前,我们先来了解一下并发编程的幕后. 随着CPU.内存和I/O设备的不断升级,它们之间一直存在着一个矛盾,就是速度不一致问题.CP

  • Java内存模型可见性问题相关解析

    这篇文章主要介绍了Java内存模型可见性问题相关解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 之前的文章中讲到,JMM是内存模型规范在Java语言中的体现.JMM保证了在多核CPU多线程编程环境下,对共享变量读写的原子性.可见性和有序性. 本文就具体来讲讲JMM是如何保证共享变量访问的可见性的. 什么是可见性问题 我们从一段简单的代码来看看到底什么是可见性问题. public class VolatileDemo { boolean

  • Java内存模型原子性原理及实例解析

    这篇文章主要介绍了Java内存模型原子性原理及实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文就具体来讲讲JMM是如何保证共享变量访问的原子性的. 原子性问题 原子性是指:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行. 下面就是一段会出现原子性问题的代码: public class AtomicProblem { private static Logger logger = LoggerFactory.

  • 了解Java多线程的可见性与有序性

    多线程的可见性 一个线程对共享变量值的修改,能够及时的被其他线程看到. 共享变量 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量. Java内存模型 JMM(Java Memory Model,简称JMM)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节.它遵循四个原则: 所有的变量都存储在主内存中 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝

  • java 并发中的原子性与可视性实例详解

    java 并发中的原子性与可视性实例详解 并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开.这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同工作).做过java Web开发的人都知道,Java Web中的Servlet程序在Servlet容器的支持下采用单实例多线程的工作模式,Servlet容器为你处理了并发问题. 原子性 原子是世界上的最小单位,具有不可分割性.比如 a=0:(a非long和double类型) 这个操作是不

  • Java并发编程-volatile可见性详解

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

  • 为什么Java volatile++不是原子性的详解

    问题 在讨论原子性操作时,我们经常会听到一个说法:任意单个volatile变量的读写具有原子性,但是volatile++这种操作除外. 所以问题就是:为什么volatile++不是原子性的? 答案 因为它实际上是三个操作组成的一个符合操作. 首先获取volatile变量的值 将该变量的值加1 将该volatile变量的值写会到对应的主存地址 一个很简单的例子: 如果两个线程在volatile读阶段都拿到的是a=1,那么后续在线程对应的CPU核心上进行自增当然都得到的是a=2,最后两个写操作不管怎

  • Java并发volatile可见性的验证实现

    普通读 无法及时获得 主内存变量 public class volatileTest { static boolean flag = false;//非volatile变量 public static void main(String[] args) throws Exception { new Thread(new Runnable() { @Override public void run() { while(!flag){ }; } }).start(); Thread.sleep(100

  • 详解java安全编码指南之可见性和原子性

    不可变对象的可见性 不可变对象就是初始化之后不能够被修改的对象,那么是不是类中引入了不可变对象,所有对不可变对象的修改都立马对所有线程可见呢? 实际上,不可变对象只能保证在多线程环境中,对象使用的安全性,并不能够保证对象的可见性. 先来讨论一下可变性,我们考虑下面的一个例子: public final class ImmutableObject { private final int age; public ImmutableObject(int age){ this.age=age; } }

  • java安全编码指南之:Mutability可变性详解

    简介 mutable(可变)和immutable(不可变)对象是我们在java程序编写的过程中经常会使用到的. 可变类型对象就是说,对象在创建之后,其内部的数据可能会被修改.所以它的安全性没有保证. 而不可变类型对象就是说,对象一旦创建之后,其内部的数据就不能够被修改,我们可以完全相信这个对象. 虽然mutable对象安全性不够,但是因为其可以被修改,所以会有效的减少对该对象的拷贝. 而immutable对象因为不可改变,所以尝试对该对象的修改都会导致对象的拷贝,从而生成新的对象. 我们最常使用

  • java安全编码指南之:Number操作详解

    简介 java中可以被称为Number的有byte,short,int,long,float,double和char,我们在使用这些Nubmer的过程中,需要注意些什么内容呢?一起来看看吧. Number的范围 每种Number类型都有它的范围,我们看下java中Number类型的范围: 考虑到我们最常用的int操作,虽然int的范围够大,但是如果我们在做一些int操作的时候还是可能超出int的范围. 超出了int范围会发送什么事情呢?看下面的例子: public void testIntege

  • 详解Java如何进行Base64的编码(Encode)与解码(Decode)

    关于base64编码Encode和Decode编码的几种方式 Base64是一种能将任意Binary资料用64种字元组合成字串的方法,而这个Binary资料和字串资料彼此之间是可以互相转换的,十分方便.在实际应用上,Base64除了能将Binary资料可视化之外,也常用来表示字串加密过后的内容.如果要使用Java 程式语言来实作Base64的编码与解码功能,可以参考本篇文章的作法. 早期作法 早期在Java上做Base64的编码与解码,会使用到JDK里sun.misc套件下的BASE64Enco

  • 一文详解Java etcd的应用场景及编码实战

    目录 一.白话etcd与zookeeper 二.etcd的4个核心机制 三.Leader选举与客户端交互 四.etcd的应用场景 4.1. kubernetes大脑 4.2. 服务注册与发现 4.3. 健康检查与状态变更通知 4.4.分布式锁 4.5.实现消息队列(纯扯淡) 五.etcd安装 六.jetcd的编码实现配置管理 本文首先用大白话给大家介绍一下etcd是什么?这部分内容网上已经有很多了. etcd有哪些应用场景?这些应用场景的核心原理是什么? 最后不能光动嘴不动手.先搭建一个etcd

  • 一文详解Java如何创建和销毁对象

    目录 一.介绍 二.实例构造(Instance Construction) 2.1 隐式(implicitly)构造器 2.2 无参构造器(Constructors without Arguments) 2.3 有参构造器(Constructors with Arguments) 2.4 初始化块(Initialization Blocks) 2.5 构造保障(Construction guarantee) 2.6 可见性(Visibility) 2.7 垃圾回收(Garbage collect

  • 详解Java 本地接口 JNI 使用方法

    详解Java 本地接口 JNI 使用方法 对于Java程序员来说,Java语言的好处和优点,我想不用我说了,大家自然会说出很多一套套的.但虽然我们作为java程序员,但我们不得不承认java语言也有一些它本身的缺点.比如在性能.和底层打交道方面都有它的缺点.所以java就提供了一些本地接口,他主要的作用就是提供一个标准的方式让java程序通过虚拟机与原生代码进行交互,这也就是我们平常常说的java本地接口(JNI--java native Interface).它使得在 Java 虚拟机 (VM

  • 详解Java面试官最爱问的volatile关键字

    本文向大家分享的主要内容是Java面试中一个常见的知识点:volatile关键字.本文详细介绍了volatile关键字的方方面面,希望大家在阅读过本文之后,能完美解决volatile关键字的相关问题.  在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(JMM),Java并发编程的一些特性都牵扯出来,深入地话还可以考察JVM底层实现以及操作系统的相关知识. 下面我们以一次假想的面试过

  • 详解Java 10 var关键字和示例教程

    关键要点 Java 10引入了一个闪亮的新功能:局部变量类型推断.对于局部变量,现在可以使用特殊的保留类型名称"var"代替实际类型. 提供这个特性是为了增强Java语言,并将类型推断扩展到局部变量的声明上.这样可以减少板代码,同时仍然保留Java的编译时类型检查. 由于编译器需要通过检查赋值等式右侧(RHS)来推断var的实际类型,因此在某些情况下,这个特性具有局限性,例如在初始化Array和Stream的时候. 如何使用新的"var"来减少样板代码. 在本文中,

  • 详解Java高阶语法Volatile

    背景:听说Volatile Java高阶语法亦是挺进BAT的必经之路. Volatile: volatile同步机制又涉及Java内存模型中的可见性.原子性和有序性,恶补基础一波. 可见性: 可见性简单的说是线程之间的可见性,一个线程修改的状态对另一个线程是可见对,也就是一个线程的修改结果另一个线程可以马上看到:但通常,我们无法确保执行读操作的线程能够时时的看到其他线程写入的值,So为了确保多个线程之间对内存写入操作可见性,必须使用同步机制:如用volatile修饰的变量就具有可见性,volat

随机推荐