概述
本来以为自己对于java Volatile关键字比较熟悉的了,结局了可见性和重排序,但是不能保证的原子性。然而最近面试的时候,有个面试官手写了部分代码,问关于多线程自增的原子性问题。然后才发现自己其实也没那么熟悉,以及理解的不透彻。那就肯定得开篇文章出来说一下这个问题了,顺便巩固记忆!
问题
有代码如下:
public class A {
public volatile int i;
public static void main(String[] args) {
A a = new A();
for (int k = 0; k < 3; k++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
a.add();
}
a.print();
}).start();
}
}
public void add() {
i++;
}
public void print() {
System.out.println(i);
}
}
问:输出的最大的值是否为300?
当时我看到volatile关键字以为确实是300,其实后面一想,并且关联上单例的一些多线程问题,发现确实不一定是300,至于为什么?下面直接看字节码来解答一下即可。
问题原因
首先先了解一下原子性是什么:
那既然非原子性的操作,多线程的情况下会有问题,那如果会产生上面的问题的话,直接看一下代码是否是原子性就好。
其实直接看我们编写的代码的话,其实就一行,那这样看起来确实是原子性的
但是为什么又会出现当前问题呢,这个只能看class文件里面的内容了
首先我是使用idea的,可以直接用插件 jclasslib ByteCode viewer
来看当前类的字节码文件
然后看到 i++的操作其实是下面这样的,那其实很明显,获取值 -> 相加 -> 赋值这操作,其实不是原子性的,多线程的情况下有可能出现混乱的问题
如果正常来说,两个线程的流程是这样的:
这样一看,线程相加也没问题
但是由于没保证到原子性,那出现问题的流程又会是怎样的呢:
根据字节码以及两张流程图,其实就可以发现问题的所在,以及多线程原子性的问题。那我们该怎么解决这些问题呢?
解决方案
当时面试官说的解决方案是有多个,但是目前我只想到了两个办法,一个是加锁,如synchronized和Lock,另外一个是cas(概念没记错是线程不争抢所,大家都去操作,只有得到的值是预期值的时候,才返回相对应的值),但是cas如何实现却不太知道,所以就有点知其然不知其所以然了。
synchrnoized关键字加锁
当然除了加在方法中,也可以再 for循环那边进行加锁。
加锁的操作的话,除了synchronized的话,也可以使用Lock对象
Lock对象加锁
cas操作
cas操作的话只懂概念,但是不会怎么做,然后发现如果是多线程自增的话,其实是有对象可以直接使用的,如:AtomicInteger
此对象就是专门实现了 cas编程的一个interger类,我们可以先看示例代码
但是为什么使用这个类就可以实现原子性的自增呢?网上说是内部实现了cas,那只能直接从方法上看到底是实现了什么了:
然后这个时候就没办法了。只能知道这个对象利用c++实现了一些cas的编程。
不过那既然是native方法,那此时其实就可以去下载 hotspot然后去下载吧:http://hg.openjdk.java.net/
省略下载步骤以及寻找步骤
主要是找unsafe这个文件名的文件就好,然后往里面找方法 compareAndSetInt,然后搜到的方法为
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
懂c++的大神可以自己去研究一下,由于自身确实不怎么懂c++,所以也只能到这里告终了。
但是其实还是遗留了问题的,如果是自己实现cas的话,应该如何实现呢?这个确实还是只知道概念,不太清楚如何实现。如果多线程自增的话,就可以按照上面两个方法实现了,一个是加锁,一个是使用原子性的Number类