Java多线程——线程安全性

java内存模型

    java虚拟机规范中试图定义了一种java内存模型、用于屏蔽掉各种硬件和操作系统的内存访问之间的差异,以实现让java程序能在各个平台上达到一致的内存访问效果。java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节。java内存模型主要如下图所示(模型来源:深入理解jvm虚拟机)

其中

  1. 主内存:所有变量(实例字段、静态字段和构成数组对象的元素,不包含局部变量和方法参数、因为后者是线程私有的)都存储在主内存中。
  2. java线程工作内存:每个线程有自己的工作内存,里面保存了被该线程使用到的变量的主内存副本拷贝(虚拟机不会把整个使用到的对象拷贝到工作内存中、而是可能把使用到的对象引用、字段拷贝过来)。

关于如何将一个变量从主内存中拷贝到工作内存中,再从工作内存同步回主内存,期间有具体的交互协议。java内存模型定义了一下八种操作来完成,这八种操作都是原子的,不可再分的。

  1. lock:作用于主内存变量,它把一个变量标识为一条线程独占状态。
  2. unlock:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定。
  3. read:作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面load读取。
  4. load:作用于工作内存变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use:作用于工作内存,它把工作内存中的一个变量传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令的时候就会执行这个过程。
  6. assin:作用于工作内存的变量,它把执行引擎接收到的值赋给工作内存的变量。每当虚拟机遇到一个需要变量赋值的字节码指令的时候会执行这个操作。
  7. store:作用于工作内存变量,它将工作内存中的变量的值传递给主内存,以便后面write使用。
  8. write:作用于主内存变量,它把store操作从工作内存中得到的变量值放回到主内存变量中。

这8中操作还需要满足其他的一些规则,这里不再详细叙述。

原子性

    原子性指的是不能被线程调度机制中断的操作,一旦操作开始,那么在下一次“上下文切换”(切换到其他线程)之前,这个操作肯定是执行完成的。原子性可以引用于除long/double之外的所有基本类型之上的“简单操作”(注意java中i++不是一个原子操作),由于long和double是64为的,虚拟机将会把它们分离成两个32位的操作来执行,所以long和double的基本操作不是原子的,如果使用volatile修饰long/double,那么对long/double的赋值的返回操作也会变成原子的。

重排序:

    在java内存模型中,如果两个操作之间没有happens-before的关系,那么jvm可以对他们进行任意的重排序入以下代码(代码来源Java并发编程实战):

public class PossibleReordering{
    static int x = 0,y = 0;
    static int a = 0,b = 0;
    public static void main(String[] args)
            throws InterruptedException{
        Thread one =  new Thread(new Runnable(){
            public void run(){
                a = 1;
                x = b;
            }
        });

        Thread two =  new Thread(new Runnable(){
            public void run(){
                b = 1;
                y = a;
            }
        });

        one.start();
        two.start();
        one.join();
        two.jion();
        System.out.println("x:"+x+",y:"+y);
    }
}

    其中,由于再线程one中,a=1和x=b之间不存在任何联系,那么这两行代码可能会被jvm进行指令重排序,即实际可能会先执行x=b在执行a=1,对于线程two来说同样如此,所以打印的结果可能是(x=1,y=0),(x=0,y=1),(x=1,y=1),

(x=0,y=0)。从计算机硬件结构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元处理。

线程安全性

   前面介绍了一些预备的基础知识、其实都在预示着在使用多线程的时候,对共享变量的访问,如果不加以合适的调度机制,那么程序的执行就可能会产生一些预想不结果。线程安全最核心的定义是正确性,如果说当多个线程访问某个类的时候、不管运行环境采用何种调度方式或者这些线程如何交替的执行,并且在主调度代码中,不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

解决线程安全性的一些常见措施

     针对共享变量的处理方式:

  1. 如果一个类没有共享变量,那么这个类肯定是线程安全的、所以也不存在线程安全性问题。
  2. 如果多个线程共享某个变量,但是这个共享变量是不可变的(final),那么这个类也是线程安全的。
  3. 线程封闭,如果将共享变量限制在某个线程内使用,那么也可以解决线程安全性问题,比如使用ThreadLocal。

    针对同步的处理方式

  1. 使用volatile修饰变量,volatile是一种轻量级锁,提供了线程可见性,避免了指令重排序,但是注意volatile的原则:a、运算结果并不依赖变量的当前值,除非你能确保只有单一线程修改变量的值;b、变量不需要与其他的状态变量共同参与约束。所以可以说volatile只能确保可见性、而不能保证原子性。
  2. 使用同步/加锁,悲观锁保证了原子操作的机制。

 

                                                     

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值