(2)Java并发编程基础篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/JavaMrZhang/article/details/87712493

什么是多线程并发编程

并发指在同一时间段内多个任务执行,而并行指单位时间内多个任务执行。同一时间段内由多个单位时间组成,所以并发的多个任务不一定在单位时间内同时执行。并发强调的是同一时间段。

为什么要进行多线程并发编程

多核CPU时代的到来有效解决了单核CPU的困扰,线程各自使用自己的CPU运行,减少了线程上下文切换的资源开销,但随着大数据时代的来临,系统逐渐对性能和吞吐的要求提高,需要对高并发编程有这强烈的需求。

什么是线程安全问题

指多个线程去读写共享资源时,出现脏数据或者不一致的情况。例如拿计数例子来说,t1时刻线程1从主存中读取count=0,t2 时刻 将count进行计数累加得到1,此时线程2从主存读取count=0(此处不考虑内存可见性的问题)进行计数累加得到1,t3 时刻 线程1和线程2 分别将各自计数后的count值 写入主存。明明是两次计数会何结果却是1呢?其实这就是共享资源出现线程安全的问题,Java中通过synchroized 关键字来进行避免。

共享变量内存可见性问题

Java内存模型规定,当线程获取共享变量时,会从主存中复制共享变量值到自己的工作内存中,线程在读写变量时操作的是自己的工作内存。实际线程运作的模型图如下:
在这里插入图片描述
多核CPU中,每核(线程运行时)都会拥有自己独立的 控制器 、运算器、cache。控制器包含了一组寄存器和操作控制器,运算器主要用于算术逻辑的运算,cache 为每个核的一级缓存,是相互隔离的。在一些CPU架构中还存在二级cache用于多核之间共享。
那内存不可见指的是什么呢?还是那计数案例来说,看以下分析

  • 线程A 首先从两级缓存中读取共享变量count,发现没有命中,从主存中读取共享变量count=0 并复制到两级缓存中去,然后对count进行计数操作后得到count=1,线程执行完毕后,将count=1刷新到两级缓存和主存中。(没问题)

  • 线程B 首先从一级缓存中读取共享变量count,发现没有命中,进而从二级缓存中读取共享变量count,发现命中。将count=1 复制到自己的一级缓存中,进行计数操作后得到2,线程执行完毕后,将count=2刷新到两级缓存中和主存中。(没问题)

  • 线程A 再次进行计数操作,首先从自己的一级缓存中获取count,发现命中,然后获取count=1,进行计数操作 count=2(出现问题)
    线程A进行再次计数操作时,本该获取count的值是线程B计数之后的值,但获取的却是自身上次计数后的值。那么java是通过什么来规避这种问题的呢? 用volatile和synchronized关键字!

synchronized的内存语义

用synchroized包含的代码块,线程执行时会清除自己的工作内存,强制从主内存中读取变量值,执行完毕后会将值刷新到主内存。但synchronized关键字往往会带来线程上下文切换开销的性能问题。

volatile关键字

用volatile关键字修饰的共享变量,线程读取的时候会强制从主存中读取,写入的时候,会强制将值刷新到主存中,并不会等到线程执行完毕。

原子性操作

所谓的原子性操作是指在执行一系列的系统指令时,这些指令要么全部执行、要么全部不执行,不存在只执行一部分的情况。(指令的连续执行过程中,不能插入其他命令)
如下面的代码就是线程不安全:

public class ThreadNoSafe{
private Long value;
public Long getCount(){
	return value;
	}
public void incr(){
	 ++value
	}
}

通过java -c 命令查看该类的汇编代码,可知++value这个自增操作由多个字节码指令来完成 读-改-写这三个操作的,因此它并不满足原子性。那如何保证上述的原子性呢?就是用synchronized关键字。由于synchronized关键字是独占锁,对于没有获得锁的线程就会阻塞挂起,这时就避免不了线程上下文切换的资源开销了。那还有没有其他的方法来实现这个原子性操作呢?答案是有的即CAS。

CAS操作

Java提供了非阻塞性volatile的关键字来保证内存变量的可见性,一定程度上减少了锁资源开销带来的问题,但是它并不能保证原子性操作。CAS 即Compare And Swap(比较和交换),这是计算机底层硬件的一个原子性操作指令,实现比较和更新。 CAS 核心是 通过预期值与内存的值相比,如果相等则更新给定的新值,反之更新失败。

关于CAS操作存在ABA的一个问题:
假如线程1要求修改值为A的变量X,首先会从内存中读取变量X(值为A),然后通过CAS操作进行比较 将值更新为B。至此,难道线程A更新的数据就是对的么?未必,因为有可能在线程1还未更新完之前,线程B也对变量X进行了CAS操作将变量从 A->B->A 了。这时线程A并不知道变量X的值是线程B已经更新过的。那Java中是如何来规避这中问题呢?使用AtomicStampedReference类,它为每一个变量配备了一个时间戳来避免ABA问题的出现。

Java中对于CAS操作封装的工具包都处于rt.jar中的Unsafe类中.

伪共享

为了解决主存与CPU运行速度相差的问题,会在主存与CPU之间添加一级或二级的缓存器。如下图:在这里插入图片描述
一级或二级的缓存器一般都被集成到CPU硬件当中。其中缓存器与主存进行数据交互的时候是按行进行存储的。
在这里插入图片描述
当CPU访问变量的时候,会从两级的缓存器中寻找,如果命中则进行使用,未命中从主存中访问变量,同时会将变量所在大小的cache行(可能包含其他的变量)缓存至两级缓存中。多个线程同时访问这个缓存块与多个线程访问一个缓存块中只有单个变量相比性能会有所降低 ,这就是伪共享。
很显然,伪共享这种情况java当中是会发生的,让该如何进行避免呢?

如何避免伪共享

  1. 通过字节填充的方式来实现,保证字节填充后的大小等于缓存行的大小
  2. JDK8 提供了 @sun.misc.Contended(“tlr”) 注解用来解决伪共享的问题。例如Thread类中就有几个属性标注了此注解。需要注意的是此注解只能作用于java核心包,如果用户类路径下的类需要用此注解,则要添加JVM参数:-XX:RestrictContended 填充的默认宽度是128byte

猜你喜欢

转载自blog.csdn.net/JavaMrZhang/article/details/87712493