面试官:你对多线程熟悉吗,谈谈线程安全中的原子性,有序性和可见性?

对于Java并发编程,通常来讲有如下的关注点:java

  1. 线程安全性,正确性。web

  2. 线程的活跃性(死锁,活锁)编程

  3. 性能缓存

其中线程的安全性问题是首要解决的问题,线程不安全,运行出来的结果和预期不一致,那就连基本要求都没达到了。安全

保证线程的安全性问题,本质上就是保证线程同步,实际上就是线程之间的通讯问题。咱们知道,在操做系统中线程通讯有如下几种方式:多线程

  1. 信号量并发

  2. 信号app

  3. 管道jvm

  4. 共享内存socket

  5. 消息队列

  6. socket

java中线程通讯主要使用共享内存的方式。共享内存的通讯方式首先要关注的就是可见性和有序性。而原子性操做通常都是必要的,因此主要关注这三个问题。

1.原子性

原子性是指操做是不可分的。其表如今于对于共享变量的某些操做,应该是不可分的,必须连续完成。例如a++,对于共享变量a的操做,实际上会执行三个步骤:

  1. 读取变量a的值

  2. a的值+1

  3. 将值赋予变量a 。

这三个操做中任何一个操做过程当中,a的值被人篡改,那么都会出现咱们不但愿出现的结果。因此咱们必须保证这是原子性的。Java中的锁的机制解决了原子性的问题。

2.可见性

可见性是值一个线程对共享变量的修改,对于另外一个线程来讲是不是能够看到的。

为何会出现这种问题呢?

咱们知道,java线程通讯是经过共享内存的方式进行通讯的,而咱们又知道,为了加快执行的速度,线程通常是不会直接操做内存的,而是操做缓存。

java线程内存模型:

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

实际上,线程操做的是本身的工做内存,而不会直接操做主内存。若是线程对变量的操做没有刷写会主内存的话,仅仅改变了本身的工做内存的变量的副本,那么对于其余线程来讲是不可见的。而若是另外一个变量没有读取主内存中的新的值,而是使用旧的值的话,一样的也能够列为不可见。

对于jvm来讲,主内存是全部线程共享的java堆,而工做内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。

那么咱们怎么知道何时工做内存的变量会刷写到主内存当中呢?

这就涉及到java的happens-before关系了。

在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必须存在happens-before关系。

这我的的博客写的不错:http://ifeve.com/easy-happens-before/。

简单来讲,只要知足了happens-before关系,那么他们就是可见的。

例如:

线程A中执行i=1,线程B中执行j=i。若是线程A的操做和线程B的操做知足happens-before关系,那么j就必定等于1,不然j的值就是不肯定的。

happens-before关系以下:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做;

  2. 锁定规则:一个unLock操做先行发生于后面对同一个锁额lock操做;

  3. volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做;

  4. 传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C;

  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做;

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

  7. 线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

从上面的happens-before规则,显然,通常只须要使用volatile关键字,或者使用锁的机制,就能实现内存的可见性了。

3.有序性

有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

为何会出现不一致的状况呢?

这是因为重排序的缘故。

在Java内存模型中,容许编译器和处理器对指令进行重排序,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

举个例子:

线程A:

context = loadContext();    
inited = true;    

线程B:

while(!inited ){
 sleep
}
doSomethingwithconfig(context);

若是线程A发生了重排序:

inited = true;    
context = loadContext(); 

那么线程B就会拿到一个未初始化的content去配置,从而引发错误。

由于这个重排序对于线程A来讲是不会影响线程A的正确性的,而若是loadContext()方法被阻塞了,为了增长Cpu的利用率,这个重排序是可能的。

若是要防止重排序,须要使用volatile关键字,volatile关键字能够保证变量的操做是不会被重排序的。

相关文章
相关标签/搜索