i++不是一个原子性操作,首先作为结论摆出这句话。
那么什么是原子性操作?简单说就是该操作是不可分割的,或者说编译后的指令就是一句话。
既然说i++不是原子性操作,那么是几步呢?是三步,那么如何证明呢?
写一个最简单的代码来看看:
public class Test
{
public int i = 10;
public void increase(){
i++;
}
}
命令行中javac命令编译生成class文件,javap命令查看class文件的反汇编结果。
结果如下:
public class Test {
public int i;
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field i:I
10: return
public void increase();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
}
Test()的构造方法我们不管,看到下面的increase方法中的内容,嗯,看不懂,解释一下:
aload_0 将this引用推送至栈顶。
dup 复制栈顶值this应用,并将其压入栈顶,即此时操作数栈上有连续相同的this引用。
getfield 弹出栈顶的对象引用,获取其字段race的值并压入栈顶。第一次操作。
iconst_1 将int型(1)推送至栈顶
iadd 弹出栈顶两个元素相加(race+1),并将计算结果压入栈顶。第二次操作。
putfield 从栈顶弹出两个变量(累加值,this引用),将值赋值到this实例字段race上。第三次操作,赋值。
这是汇编层面的对于i++的原子性的理解。那么代码层面呢?
public class Test
{
public int race = 0;
public void increase() {
race++;
}
public static void main(String[] args) {
//创建5个线程,同时对同一个volatileTest实例对象执行累加操作
Test test=new Test();
int threadCount = 10;
Thread[] threads = new Thread[threadCount];//5个线程
for (int i = 0; i < 10; i++) {
//每个线程都执行1000次++操作
threads[i] = new Thread(()->{
for (int j = 0; j < 1000; j++) {
test.increase();
}
});
threads[i].start();
}
//等待所有累加线程都结束
for (int i = 0; i < threadCount; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("累加结果:"+test.race);
}
}
控制台输出结果是:累加结果:9651(并无法保证每一次执行后,累加结果都是小于10000的,多执行几次肯定会有)
我们可以看到,按理说race这个变量自增了10000次,最终累加结果应该是10000,但是实际结果却是小于10000的。这就是race++这行代码带来的结果。
那么如何避免因为race++不保证原子性带来的问题呢?
一个是可以在increase方法上添加synchronized关键字。来保证race++的操作是具有原子性的。
public synchronized void increase() {
race++;
}
或者使用AtomicInteger直接进行原子操作来实现也可以:
AtomicInteger atomicInteger = new AtomicInteger(0);
public void addAtomic() {
atomicInteger.getAndIncrement();
}
顺便说另一个小问题:
在i++的原子性问题的代码中,在输出累加结果之前,判断前面的10个线程是否都已经执行完的,这里使用的是遍历线程,然后调用join方法的。
还有一种写法是:
while(Thread.activeCount()>1)
{
Thread.yield();
}
注意,这里的判断条件与java 的IDE有关,如果在eclipse中的话,while条件中是Thread.activeCount()>1是没有问题的。
但是如果是使用IntelliJ IDEA的话,如果还是Thread.activeCount()>1的话,那么会一直在while循环中无法跳出,要使用Thread.activeCount()>2的话才可以。
使用代码来验证:
public class TheadNameTest {
public static void main(String[] args) {
System.out.println(findAllThreads());
System.out.println(Thread.activeCount());
}
public static String findAllThreads() {
ThreadGroup group =
Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = group;
// 遍历线程组树,获取根线程组
while ( group != null ) {
topGroup = group;
group = group.getParent();
}
// 激活的线程数加倍
int estimatedSize = topGroup.activeCount() * 2;
Thread[] slackList = new Thread[estimatedSize];
//获取根线程组的所有线程
int actualSize = topGroup.enumerate(slackList);
// copy into a list that is the exact size
Thread[] list = new Thread[actualSize];
System.arraycopy(slackList, 0, list, 0, actualSize);
String threadName = "";
for (Thread thread : list) {
threadName += thread.getName()+"#";
}
return threadName;
}
}}
在IDEA中使用run执行的话,结果为:
Reference Handler#Finalizer#Signal Dispatcher#Attach Listener#main#Monitor Ctrl-Break#
2
在IDEA中使用debug,
或eclipse中使用run,
或eclipse中使用debug,
或使用java的命令行执行的话,结果都为:
Reference Handler#Finalizer#Signal Dispatcher#Attach Listener#main#
1
明显看到所有线程的名称中多了一个Monitor Ctrl-Break,而且activeCount活跃线程也是多了一个。这就是为什么在IDEA中要Thread.activeCount()>2的原因。
那么这个Monitor Ctrl-Break线程是干什么的呢?又是怎么出现的呢?
请转到知乎中的回答:
在阅读《深入理解Java虚拟机》一书中,执行代码清单12-1的例子时,疑似发现bug?
中@ETIN和@gavin的回答,可以解答你的疑惑。