作为在java开发领域摸爬滚打的童鞋们,多线程编程问题可以说是令大家头痛的一个问题。但是不怕,看完这篇文章之后,大家可能对于多线程编程中的原子性问题的理解可能更加的深刻。
1.多线程中的原子性概念
我们都知道,线程安全问题表现为三个方面: 原子性、可见性、有序性,本次我我们主要谈一谈多线程中的原子性问题。
何为多线程中的原子性?
原子性(Atomic)就是不可分割的意思,是指在进行一系列操作的时候这些操作要么全部执行要么全部不执行,不存在只执行一部分的情况。
原子操作的不可分割有两层含义:
- 访问(读、写)某个共享变量的操作从其他线程来看,该操作要么已经执行要么尚未发生,即其他线程看不到当前操作中的中间结果
- 访问同一组共享变量的原子操作时不能相交错的。如现实生活中从ATM机取款。
java有两种方式实现原子性:一种是使用锁,另一种是使用处理器的CAS。
锁具有排他性,保证共享变量在某一时刻只能被一个线程访问。
CAS指令直接在硬件(处理器层次)上实现。(硬件锁)
2.为何存在原子性问题?
我们以自增操作++为例进行讲解:
在我们的正常理解看来,对于一个变量i的自增操作 i++就是很轻松的在i的原有基础上进行了加1 的操作,但是在操作系统或者说是java的底层实现过程中非也! 在操作系统实现i++的过程其实是经历了三个阶段:
++自增操作实现的步骤:
- 读取num的值
- num自增
- 把自增之后的值再赋值给变量
而这三步的操作在多线程的情况操作的过程中,任何一个步骤线程都有可能被阻塞或者是挂起,这样的情况下当其他线程操作被上一线程挂起后留下的变量数据的时候就会早还曾数据的不一致。或者是多加,或者是原本加一操作没有加一成功。
我们看以下程序的运行:(你们可以阅读程序,并猜测他的运行结果)
package Concurrent_KeCheng.threadsafe;
/**
* @author MaZhiCheng
* @date 2020/11/10 - 21:07
*
* 测试线程的原子性
*/
public class Thread01 {
public static void main(String[] args) {
MyInt myInt = new MyInt();
for(int i = 0;i < 2;i++){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName() + "->" + myInt.getNum());
try{
Thread.sleep(110);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}
}
static class MyInt{
int num;
public int getNum(){
return num++;
/*
++自增操作实现的步骤:
读取num的值
num自增
把自增之后的值再赋值给变量
*/
}
}
}
这段代码的作用是创建了两个线程去读取num加一后的值进行显示。
你们猜测的运行结果是不是这样的?
但是!他还有可能是这样的!,甚至任何一个加一后的结果都有可能被两个线程同时持有!
这是为何?
因为++操作在底层的实现不是原子性的!是分为三步进行的!
3.java中原子性问题的解决方法
看到多线程的安全性问题,我相信很多同学的第一回答一定是:用锁啊,用synchronize,就这?就这?能不能来点高大上的?动不动就synchronize?
的确,使用synchronize确实可以解决线程安全中的原子性问题。但是synchronize是独占锁,没有获取内部锁的线程会被阻塞掉,大大降低了程序的并发性。那有没有更好的解决方法呢?当然有!java中提供了一个线程安全的AtomicInteger类,保证了操作的原子性
我们对以上的代码进行简单修改,再观察其运行结果!
package Concurrent_KeCheng.threadsafe;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author MaZhiCheng
* @date 2020/11/10 - 21:07
*
* 测试线程的原子性
*/
public class Thread01 {
public static void main(String[] args) {
MyInt1 myInt = new MyInt1();
for(int i = 0;i < 2;i++){
new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName() + "->" + myInt.getNum());
try{
Thread.sleep(110);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}
}
//java中提供了一个线程安全的AtomicInteger类,保证了操作的原子性
static class MyInt1{
AtomicInteger num = new AtomicInteger();
public int getNum(){
return num.getAndIncrement();
}
}
}
当我们创建num变量为AtomicInteger 类型的时候,我们在对num操作进行操作的时候就可以保证整个操作是原子性的。
通过对于AtomicInteger源码的阅读我们可以知道:
AtomicInteger的实现内部使用的是非阻塞的CAS算法,
完美的解决了并发操作下的原子性问题和性能之间的问题。
到这里多线程下的原子性问题的基本原理和解决方案的讲解就告一段落了,不知道大家是否有所收获,有问题记得在评论中进行提问吗,我们一起讨论学习呀!