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();
}
运行结果:
如上图所示,每台机器的执行可能略有差距,但大多数情况下并不能给我们想要的真实值 20000。
3.原理分析
“++” 操作在多线程下引发混乱的原因:因为 ++ 操作对于底层操作系统来说,并不是一条 CPU 操作指令,而是三条 CPU 操作指令——取值、累加、存储,因此无法保证原子性,就会出现上面代码执行后的误差。详细解释,我们看一下JMM模型
4.内存模型相关概念
计算机在执行程序时,每条指令都是在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并发包的内容将会在后续展开。