i++ 是线程安全的吗?(是否具有原子性)不是!(经典的内存不可见问题)

 

https://www.jb51.net/article/179905.htm

 

本文参考https://mp.weixin.qq.com/s/7H9n2DLZOaANTch72ln5ww

本文参考https://www.jianshu.com/p/0be2689550e7

"原子操作(atomic operation)是不需要synchronized",
答案是否定的,i++和++i都不具有原子性。
i++:先赋值再自加。
++i:先自加再赋值。
i++和++i的线程安全分为两种情况:
1、如果i是局部变量(在方法里定义的),那么是线程安全的。因为局部变量是线程私有的,别的线程访问不到,其实也可以说没有线程安不安全之说,因为别的线程对他造不成影响。
2、如果i是全局变量,则同一进程的不同线程都可能访问到该变量,因而是线程不安全的,
会产生脏读。

i++和++i:

        {
            int i = 1;
            int j1 = i++;
            System.out.println("j1=" + j1); // 输出 j1=1
            System.out.println("i=" + i); // 输出 i=2
        }
        {
            int i = 1;
            int j2 = ++i;
            System.out.println("j2=" + j2); // 输出 j2=2
            System.out.println("i=" + i); // 输出 i=2
        }

i++: 
        读值,+1,写值。在这三步任何之间都可能会有CPU调度产生,造成i的值被修改,造成脏读脏写。
        1 如果是方法里定义的,一定是线程安全的,因为每个方法栈是线程私有的。


        2 如果是类的静态成员变量,i++则不是线程安全的,因为i++会被编译成几句字节码语句执行,而每个线程都有自己的工作内存,每个线程需要对共享变量操作的时候必须先把共享变量从主内存load到自己的工作内存,登完成对共享变量的操作时再保存到主内存。如果一个线程运算完成后还没刷到主内存,此时这个共享变量的值被另一个线程从主内存读取到了,这个时候读取的数据就是脏数据了,他会覆盖其他线曾程计算的值。可以通过synchronize块来提供同步。

    volatile:
        每次修改volatile变量都会同步到主存中。
        每次读取volatile变量的值都强制从主存读取最新的值(强制JVM不可优化volatile变量,如JVM优化后变量读取会使用cpu缓存而不从主存中读取)、
        加了volatile和没加volatile都无法解决非原子操作的线程同步问题:
            多个线程同时从主内存中读取某个值。
            r1(线程), r3读到的值都是 0,两个线程都将 +1 写入 i, 最后 i等于 1,但是却进行了两次自增操作。
            解决:使用循环CAS,实现i++原子操作(cas,会将操作的值与原值比较,相同才将新值写入主内存。)使用支持原子性操作的类,如 java.util.concurrent.atomic.AtomicInteger,它使用的是 CAS 算法


 i++, ++i 和 i+++++i 以及 i+++i++ 吗?

public static void main(String[] args) {
   int i = 1;
   System.out.println(i+++i++);
   System.out.println(i);
}

 i++

int i = 1;
System.out.println(i++);

这两行代码的部分汇编指令如下,

ICONST_1 //把常量 1 加载到栈顶
ISTORE 1 //把栈顶的元素弹出,并赋值给局部变量表中位置为“1”的变量,此时指变量i。这两句就相当于 int i = 1;

//接下来执行第二行代码
ILOAD 1  //把局部变量表中位置为“1”的变量加载到栈顶,即把i的值加载到栈顶
IINC 1 1  //直接把局部变量表中位置为“1”的变量加1,即把 i 加1。注意,这条指令并没有修改操作数栈就把 i 加1了。
INVOKEVIRTUAL java/io/PrintStream.println (I)V  //把栈顶的元素打印出来,此时栈顶的元素是 1。所以打印的是 1

所以,此时打印的是1。

1、执行  ICONST_1,常量 1 进栈

2、执行 ISTORE 1,栈顶元素出栈存到位置“1”

3、执行  ILOAD 1,把位置“1”的变量值存到栈顶

4、执行 IINC 1 1 ,直接把局部变量表中位置为“1”的变量加 1

5、执行 INVOKEVIRTUAL java/io/PrintStream.println (I)V  ,把栈顶的元素打印出来,此时栈顶的元素是 1.

所以虽然i已经等于2了,但此时栈顶的元素却是i之前的值 1 ,所以打印的是1。

这下关于 i ++ 的懂了吧?

++ i 

int i = 1;
System.out.println(++i);

对应的部分重点汇编指令如下:

//和上面i++差不多,不过IINC 1 1 和ILOAD 1这两句的顺序调换了。
ICONST_1
ISTORE 1
IINC 1 1 //直接把局部变量表中位置为“1”的变量加1
ILOAD 1  //把位置“1”的变量压到栈顶,此时栈顶的元素是 2
INVOKEVIRTUAL java/io/PrintStream.println (I)V //所以打印的是2

1、执行了ICONST_1 和ISTORE 1这两句过后的局部变量和栈的情况如下

2、执行 IINC 1 1。注意,执行这条指令,操作数栈不会发生变化。

3、执行 ILOAD 1,把位置“1”的变量值压入栈顶

4、执行 INVOKEVIRTUAL java/io/PrintStream.println (I)V  ,把栈顶的元素打印出来,此时栈顶的元素是 2

i+++i++

int i = 1;
System.out.println(i+++i++);
System.out.println(i);

按照运算符号的优先顺序,i+++i++等价于 (i++) + (i++)。

对应的部分汇编指令如下:

//第一行
ICONST_1
ISTORE 1
//第二行
ILOAD 1
IINC 1 1
ILOAD 1
IINC 1 1
IADD  //把栈顶的两个元素弹出相加之后在把结果放回栈顶
INVOKEVIRTUAL java/io/PrintStream.println (I)V
//第三行
ILOAD 1
INVOKEVIRTUAL java/io/PrintStream.println (I)V

1、执行了 ICONST_1 和ISTORE 1之后的状态如下

2、执行 ILOAD 1

3、执行 IINC 1 1

4、执行  ILOAD 1

5、执行 IINC  1 1。

此时实际上 i 的值已经是 3 了,只是栈顶放的都是 i 的旧值。

6、执行 IADD ,把栈顶两个元素出栈相加后再把结果入栈

7、执行INVOKEVIRTUAL java/io/PrintStream.println (I)V,此时栈顶元素为3,所以打印的是3

8、执行  ILOAD 1,把局部变量表加载到栈顶

9、执行INVOKEVIRTUAL java/io/PrintStream.println (I)V,此时栈顶元素为3,所以打印的是3