被final修饰的变量会返回正确的构造版本,在一定程度上(构造函数)可以解决内存可见性问题,如下代码:
package 多线程;
import org.junit.Test;
/**
* @Author: cxl
* @Date: 2020/3/3 17:05
* @Version 1.0
*/
public class Test01 {
final int x;
int y;
int z;
static Test01 t;
public Test01() {
x = 3;
y = 4;
// z = x; 因为x被final修饰了,所以也可以读到z的正确构造版本
}
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
t = new Test01();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
if (t != null) {
System.out.println(t.x + ">>>" +t.y);
}
}
}).start();
}
}
正儿八经的原子操作问题看下图:
package 多线程;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author: cxl
* @Date: 2020/3/10 17:21
* @Version 1.0
*/
public class Test02 {
public class Counter{
volatile int i = 0;
public void add() {
i ++;
}
}
@Test
public void test1() throws InterruptedException {
final Counter counter = new Counter();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(counter.i);
}
}
看这个代码,结果是100000,可是一运行,结果却不是我们想的那样:
what?我的工具出了问题?多运行几次发现每次的结果都不一样,69696、54815...,猜测可能是计算变量i那块的代码出了问题,i++并不是一个原子操作,通过对Counter类反编译,查看对应的字节码指令:
public void add();
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #3 // Field i:I
5: iconst_1
6: iadd
7: putfield #3 // Field i:I
10: return
LineNumberTable:
line 20: 0
line 21: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this L多线程/Test02$Counter;
}
这个i++操作是由0~10步组合完成的,要搞清楚i++操作是如何出问题的,我们就要知道这几步是什么意思,咱们逐步分析:
首先:
然后:
第三步:
最后一步:
当引入多线程时:
t1和t2都加载了i的值0(t1先,t2后),t1到红线的位置执行完i++操作将1写入堆内存中,这时t2线程在执行iadd操作的,他读取到的i的值仍然为0不是t1回写到的1,所以t2写到堆内存的值还是1,t2加载的值就失效了(没有加载到t1的1值),这四个操作被分割了,不是原子操作,这也是上面的代码结果远少于100000的问题原因。
原子操作:原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该此操作中保持一致,这是原子性的核心特征。
我们这个例子就是典型的i的值被分割了,资源没有保持一致,不是一个原子操作。
一般出现原子性的两种典型场景:
1、判断的某种状态后,这个状态失效了
if(owner == null){
owner = CurrentThread()
}
当多个线程执行这块代码,线程1判空成功进行赋值操作的一瞬间,别的线程在进行判空的时候,owner就失效了
2、加载了一个值,这个值失效了,上面示图的情况
那么,该怎么解决这个问题呢,其实在三种办法:
1、可以在add方法上加锁,add方法一次只能有一个线程执行,原子性得到了保证,:
public synchronized void add() {
i ++;
}
2、原理同1
Lock lock = new ReentrantLock();
public void add() {
lock.lock();
try {
i ++;
} finally {
lock.unlock();
}
}
3、使用原子操作类
AtomicInteger i = new AtomicInteger(0);
public void add() {
i.getAndIncrement();
}
方法1和2做法都是将方法阻塞,多线程变成单线程执行操作,那么AtomicInteger 又有什么不同呢?其实,这里就要引入一个CAS的概念,硬件级别的原语,cpu用于对内存进行修改:
在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)
cas有两个值,旧值和新值,当旧值和内存条中的值一致时(conmpare)才会进行交换(swap)操作,否则失败,不执行交换操作,重新取内存条中的值循环操作,直到执行交换,而内存条又从硬件级别上限制了同一时刻,只有一个线程能对值进行修改(硬件级别的保护)。
package 多线程;
import org.junit.Test;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Author: cxl
* @Date: 2020/3/10 22:09
* @Version 1.0
*/
public class CounterAtomic {
public int i = 0;
private static Unsafe unsafe = null;
private static long valueOffset;
static {
//java不允许我们这样直接使用,会抛出一个安全异常,所以用反射来拿到
// unsafe = Unsafe.getUnsafe();
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
//获取字段i的偏移量
Field iField = CounterAtomic.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(iField);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() throws NoSuchFieldException {
int current;
do {
//拿到原值
current = unsafe.getIntVolatile(this, valueOffset);
//compareAndSwapInt将拿到的原值和内存条中的值做比较,如果不一致,返回false,继续自旋
}while (!unsafe.compareAndSwapInt(this, valueOffset, current, current+1));
}
}
再将Counter类改为CounterAtomic类进行测试,结果正常
package 多线程;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: cxl
* @Date: 2020/3/10 17:21
* @Version 1.0
*/
public class Test02 {
@Test
public void test1() throws InterruptedException {
final CounterAtomic counter = new CounterAtomic();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
try {
counter.add();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
System.out.println("done");
}
}).start();
}
Thread.sleep(1000L);
System.out.println(counter.i);
}
}
![](https://img-blog.csdnimg.cn/20200310230336338.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNzU3NjYy,size_16,color_FFFFFF,t_70)
![](https://img-blog.csdnimg.cn/20200310231057276.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNzU3NjYy,size_16,color_FFFFFF,t_70)