Java编程思想——多线程的三大核心源码层解密

 

欢迎大家关注,欢迎评论

对于Java并发编程,一般来说有以下的关注点:

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

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

3.性能

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

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

1.信号量 2.信号 3.管道 4.共享内存 5.消息队列 6.socket

java中线程通信主要使用共享内存的方式。共享内存的通信方式首先要关注的就是可见性有序性。而原子性操作一般都是必要的,所以主要关注这三个问题。

1、原子性(Atomicity)

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

JMM只是保证了基本的原子性,但类似于i++之类的操作,看似是原子操作,其实里面涉及到:

获取 i 的值。

自增。

再赋值给 i。

这三步操作,所以想要实现i++这样的原子操作就需要用到synchronize或者是lock进行加锁处理。

如果是基础类的自增操作可以使用AtomicInteger这样的原子类来实现(其本质是利用了CPU级别的 的CAS指令来完成的)。

其中用的最多的方法就是:incrementAndGet()以原子的方式自增。 源码如下:

Java编程思想——多线程的三大核心源码层解密

首先是获得当前的值,然后自增 +1。接着则是最核心的compareAndSet()来进行原子更新。

Java编程思想——多线程的三大核心源码层解密

其逻辑就是判断当前的值是否被更新过,是否等于current,如果等于就说明没有更新过然后将当前的值更新为next,如果不等于则返回false进入循环,直到更新成功为止。

还有其中的get()方法也很关键,返回的是当前的值,当前值用了volatile关键词修饰,保证了内存可见性。

private volatile int value;

2、可见性

可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

现代计算机中,由于CPU直接从主内存中读取数据的效率不高,所以都会对应的CPU高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。

Java编程思想——多线程的三大核心源码层解密

如上图所示。

volatile关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。

使用volatile关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。

synchronize和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和volatile相比开销较大。

3、顺序性

Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。

Java编程思想——多线程的三大核心源码层解密

正常情况下的执行顺序应该是1>>2>>3。但是有时JVM为了提高整体的效率会进行指令重排导致执行的顺序可能是2>>1>>3。但是JVM也不能是什么都进行重排,是在保证最终结果和代码顺序执行结果一致的情况下才可能进行重排。

重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。

Java 中可以使用volatile来保证顺序性, 和 lock也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。

除了通过volatile关键字显式的保证顺序之外,JVM还通过happen-before原则来隐式的保证顺序性。

其中有一条就是适用于volatile关键字的,针对于volatile关键字的写操作肯定是在读操作之前,也就是说读取的值肯定是最新的。

volatile 的应用

双重检查锁的单例模式

可以用volatile实现一个双重检查锁的单例模式:

Java编程思想——多线程的三大核心源码层解密

这里的volatile关键字主要是为了防止指令重排。 如果不用volatile,singleton = new Singleton();,这段代码其实是分为三步:

分配内存空间。(1)

初始化对象。(2)

将singleton对象指向分配的内存地址。(3)

加上volatile是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

控制停止线程的标记

Java编程思想——多线程的三大核心源码层解密

这里如果没有用 volatile 来修饰 flag ,就有可能其中一个线程调用了stop()方法修改了 flag 的值并不会立即刷新到主内存中,导致这个循环并不会立即停止。

这里主要利用的是volatile的内存可见性。

总结一下:

volatile关键字只能保证可见性,顺序性,不能保证原子性。

 

欢迎工作一到五年的Java工程师朋友们加入Java架构开发:468947140

点击链接加入群聊【Java-BATJ企业级资深架构】:https://jq.qq.com/?_wv=1027&k=5zMN6JB

本群提供免费的学习指导 架构资料 以及免费的解答

不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导

如果觉得本文还可以,欢迎大家《关注》,算是对笔者的支持

上一篇:Linux curl使用简单介绍


下一篇:思考:.Net中Web控件与自定义控件的区别?