1.简单介绍

++i 先自加再赋值;
i++ 先赋值再自加;
不再赘述。。。

2. ++/-- 线程安全问题

先看一个例子:

static int count = 1;

public static void main(String[] args){

    new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + (++count));
         }
     }).start();
     new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + (++count));
         }
     }).start();
}

运行结果:

Java int 变成线程安全 java中++线程安全吗_System


如上图所示,每台机器的执行可能略有差距,但大多数情况下并不能给我们想要的真实值 20000。

3.原理分析

“++” 操作在多线程下引发混乱的原因:因为 ++ 操作对于底层操作系统来说,并不是一条 CPU 操作指令,而是三条 CPU 操作指令——取值、累加、存储,因此无法保证原子性,就会出现上面代码执行后的误差。详细解释,我们看一下JMM模型

4.内存模型相关概念

Java int 变成线程安全 java中++线程安全吗_Java int 变成线程安全_02


计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

i = i + 1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

5.解决方案

1 .++/-- 操作放在同步块 synchronized 中

static int count = 0;

public static void main(String[] args){

    new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            incr();
            System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + count);
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            incr();
            System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + (count));
        }
    }).start();
}

    static synchronized void incr(){
        count++;
    }

2. 自己申明锁

static int count = 0;

public static void main(String[] args){

    new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            incr();
            System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + count);
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            incr();
            System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + (count));
        }
    }).start();
}

    static void incr(){
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            count++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

3. 使用 AtomicInteger 类
使用 AtomicInteger 类型替代 int 类型,它使用的是 CAS 算法,效率优于方法一。关于java并发包,后续会展开介绍。

static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args){

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + (count.getAndIncrement()));
                }
            }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println("thread:" + Thread.currentThread().getName() + ",count=" + (count.getAndIncrement()));
                }
            }).start();
     }

6.后续

多线程、锁、Java并发包的内容将会在后续展开。