一. JAVA ++操作和volatile关键字
研究ThreadPoolExecutor的时候,发现其中大量使用了volatile变量。
不知为何,因此做了一番查找,研究:其中借鉴了很多网上资料。
在了解volatile变量作用前,先需要明白一些概念:
什么是原子操作?
所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。
很多操作系统都为int类型提供了+-赋值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函数。
关于java中的原子性?
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作。
因为JVM的版本和其它的问题,其它的很多操作就不好说了,例如++操作
另外,Java提供了AtomicInteger等原子类。再就是用原子性来控制并发比较麻烦,也容易出问题。
volatile原理是什么?
Java中volatile关键字原义是“不稳定、变化”的意思
使用volatile和不使用volatile的区别在于 JVM内存主存和线程工作内存的同步之上。volatile保证变量在线程工作内存和主存之间一致。
其实是告诉处理器, 不要将我放入工作内存, 请直接在主存操作我. (解决的是存储上的一致问题)
接下来是测试 :(通过测试能更好的发现和分析问题)
package interview.liao.test;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 测试原子性的同步
* @author polarbear 2009-3-14
*
*/
public class ActomTest{
public static AtomicInteger astom_i = new AtomicInteger();
public static volatile Integer v_integer_i = 0;
public static volatile int v_i = 0;
public static Integer integer_i = 0;
public static int i = 0;
public static int endThread = 0;
public static void main(String[] args) {
new ActomTest().testAtomic();
}
public void testAtomic() {
for(int i=0; i<100; i++) {
new Thread(new IntegerTestThread()).start();
}
try {
for(;;) {
// Thread.sleep(500);
if(ActomTest.endThread == 100) {
System.out.println(">>Execute End:");
System.out.println(">>AtomicInteger astom_i: \t"+ActomTest.astom_i);
System.out.println(">>volatile Integer v_integer_i: \t"+ActomTest.v_integer_i);
System.out.println(">>Integer integer_i: \t"+ActomTest.integer_i);
System.out.println(">>int i: \t"+ActomTest.i);
System.out.println(">>volatile int v_i: \t"+ActomTest.v_i);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class IntegerTestThread implements Runnable {
public void run() {
int x = 0;
while(x<1000) {
ActomTest.astom_i.incrementAndGet();
ActomTest.v_integer_i++;
ActomTest.integer_i++;
ActomTest.i++;
ActomTest.v_i++;
x++;
}
++ActomTest.endThread;
}
}
申明了几种整形的变量,开启100个线程同时对这些变量进行++操作,发现结果差异很大:
>>Execute End:
>>AtomicInteger astom_i: 100000
>>volatile Integer v_integer_i: 87330
>>Integer integer_i: 91696
>>int i: 99299
>>volatile int v_i: 99459
也就是说除了Atomic,其他的都是错误的。
我们通过一些疑问,来解释一下。
1:为什么会产生错误的数据?
多线程引起的,因为对于多线程同时操作一个整型变量在大并发操作的情况下无法做到同步,而Atom提供了很多针对此类线程安全问题的解决方案,因此解决了同时读写操作的问题。
2:为什么会造成同步问题?
Java多线程在对变量进行操作的时候,实际上是每个线程会单独分配一个针对i值的拷贝(独立内存区域),但是申明的i值确是在主内存区域中,当对i值修改完毕后,线程会将自己内存区域块中的i值拷贝到主内存区域中,因此有可能每个线程拿到的i值是不一样的,从而出现了同步问题。
3:为什么使用volatile修饰integer变量后,还是不行?
因为volatile仅仅只是解决了存储的问题,即i值只是保留在了一个内存区域中,但是i++这个操作,涉及到获取i值、修改i值、存储i值(i=i+1),这里的volatile只是解决了存储i值得问题,至于获取和修改i值,确是没有做到同步。
4:既然不能做到同步,那为什么还要用volatile这种修饰符?
主要的一个原因是方便,因为只需添加一个修饰符即可,而无需做对象加锁、解锁这么麻烦的操作。但是本人不推荐使用这种机制,因为比较容易出问题(脏数据),而且也保证不了同步。
5:那到底如何解决这样的问题?
第一种:采用同步synchronized解决,这样虽然解决了问题,但是也降低了系统的性能。
第二种:采用原子性数据Atomic变量,这是从JDK1.5开始才存在的针对原子性的解决方案,这种方案也是目前比较好的解决方案了。
6:Atomic的实现基本原理?
首先Atomic中的变量是申明为了volatile变量的,这样就保证的变量的存储和读取是一致的,都是来自同一个内存块,然后Atomic提供了getAndIncrement方法,该方法对变量的++操作进行了封装,并提供了compareAndSet方法,来完成对单个变量的加锁和解锁操作,方法中用到了一个UnSafe的对象,现在还不知道这个UnSafe的工作原理(似乎没有公开源代码)。Atomic虽然解决了同步的问题,但是性能上面还是会有所损失,不过影响不大,网上有针对这方面的测试,大概50million的操作对比是250ms : 850ms,对于大部分的高性能应用,应该还是够的了。
如何正确的使用volatile参考: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
二. C++中的 ++操作
1.i++分为三个阶段:
内存到寄存器
寄存器自增
写回内存
这三个阶段中间都可以被中断分离开
3. 面试题目:i++在两个线程里边分别执行100次,能得到的最大值和最小值分别是多少?
假设两个线程的执行步骤如下:
1. 线程A执行第一次i++,取出内存中的i,值为0,存放到寄存器后执行加1,此时CPU1的寄存器中值为1,内存中为0;
2. 线程B执行第一次i++,取出内存中的i,值为0,存放到寄存器后执行加1,此时CPU2的寄存器中值为1,内存中为0;
3. 线程A继续执行完成第99次i++,并把值放回内存,此时CPU1中寄存器的值为99,内存中为99;
4. 线程B继续执行第一次i++,将其值放回内存,此时CPU2中的寄存器值为1,内存中为1;
5. 线程A执行第100次i++,将内存中的值取回CPU1的寄存器,并执行加1,此时CPU1的寄存器中的值为2,内存中为1;
6. 线程B执行完所有操作,并将其放回内存,此时CPU2的寄存器值为100,内存中为100;
7. 线程A执行100次操作的最后一部分,将CPU2中的寄存器值放回内存,内存中值为2;
8. 结束!
便可以得出最终结果,最小值为2,最大值为200。