前言
关于并发,一直都有所了解,但并没有真正认真深入的学习过。今天开始探索一下Java并发篇,玩一下多线程。了解了一下关于计数器的多线程下的四种情况。
第一种情况
编写一个类Counter里面有一个成员变量count,写一段简单的i++的代码完成计数的功能,为了暴露多线程下的问题,让每次自增之前睡100ms
public class Counter {
private int count;
private static Map systemTimeMillis = new HashMap();
public void add(){
systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());
try {
for(int i=0;i<200;i++){
Thread.sleep(100);
this.count++;
System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));
}
public Map getSystemTimeMillis() {
return systemTimeMillis;
}
public void setSystemTimeMillis(Map systemTimeMillis) {
this.systemTimeMillis = systemTimeMillis;
}
}
开三个线程测试下会出现什么情况
public static void main(String[] args) {
final Counter counter = new Counter();
new Thread(new Runnable() {
@Override
public void run() {
counter.add();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
counter.add();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
counter.add();
}
}).start();
}
根据代码逻辑,我们很清楚的知道count是一个共享变量,最终结果如果是理想情况不出错的情况下,开三个线程,count最终结果应该是600。然而理想情况一般是不存在的,三个线程避免不了打架,运行情况看下面两张图
根据这两张图很明显,中间出现了错误的情况,首先是515线程号12,14输出了两次,而且最终结果是547
这就影响了程序运行的正确性,这就是线程不安全的实例,并发情况出现了。当然了,这种情况解决很简单,应该每个了解过Java多线程的程序员都会知道,一个synchronized关键字就可以解决这样的问题。
synchronized小概念,内部锁,可重入锁。根据JVM计数器实现。
同一个类中相同或不同同步方法,在被调用的时候,调用者是同一个对象,可以重复进入方法体。
第二种情况,我将add()方法代码改为如下:
public synchronized void add(){
systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());
try {
for(int i=0;i<200;i++){
Thread.sleep(100);
this.count++;
System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));
}
继续测试结果:
如图可见,三个线程规规矩矩的依次执行完,那么这样避免了线程不安全的出现,但是就诞生了另外一个问题,大家想一下,这个是不是跟串行差不了太多了,三个线程依次执行,最终结果虽然没有出错,但是效率也近乎增加了三倍,总耗时应该是三次累计,比起刚才慢了三倍。其实没必要同步整个方法,只需要同步count++就可以了,count在多线程术语中被称为竞态条件。
第三种情况,我再修改下add()方法,这次锁count++,当然输出也要锁到,不然虽然count最终不会出错,但输出有可能会出错:
public void add(){
systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());
try {
for(int i=0;i<200;i++){
Thread.sleep(100);
synchronized (this){
this.count++;
System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));
}
看到输出结果,count又被三个线程交错增加,输出结果时间几乎同步,效率不会像刚才那种锁方法的低,而且保证了线程安全,用synchronized还是不要无脑锁方法,会影响效率的,但是如果锁实例的时候就要判断好竞态条件,保证加了锁之后最终是一致的,保证线程安全。
此外还有一种方式,第四种情况,利用AtomicInteger原子类对象自增:
private AtomicInteger count = new AtomicInteger(0);
public void add(){
systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());
try {
for(int i=0;i<200;i++){
Thread.sleep(100);
synchronized (this){
System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count.incrementAndGet());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));
}
测试结果仍然保证了线程安全。
AtomicInteger是Java并发包下面的一个Integer原子操作类。通过命名也可以看得出来,它可以保证原子操作,那么,什么是原子操作?
如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。
在这里我的理解,在这个程序里就是,稳定+1,没有其他操作可以妨碍我+1,在程序执行过程中,我这个+1不管是一个步骤还是多个步骤,该线程总要给我+1之后才会执行其他的部分。
还有一种老生常谈的说法是,原子操作(atomic operation)是不需要synchronized的。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。
通过这些解释,可以说明AtomicInteger是可以保证线程安全的。那么再探索一下AtomicInteger的自增方法实现原理吧…
首先点进AtomicInteger的有参构造,注释的意思是:创建一个AtomicInteger(原子整数),给一个初始的value
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
看下AtomicInteger的成员属性
有一个Unsafe类型的成员变量,上面有一句注释,建立去用Unsafe对象去比较和转换Int为了更新,翻译的有点蹩脚,意思就是利用Unsafe对象去比较转换Int值的变化。其实就是负责与CAS(compare and swap)相关的操作实现。
valueOffset,从命名上来看是一个偏移量,实际上是用来记录内存首地址的偏移量。
value,这个是用来存放int值的。
静态块,是用来给偏移量初始化,在该原子类加载的时候利用字节码对象反射去获取该value在内存上存储的首地址偏移量,意思可以理解为就是该整型变量在内存上存储的位置的一个记录(标识)。
valatile关键字,在这里保证其对其他线程可见。
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
我调用那个自增方法是incrementAndGet,注释:每个当前value原子的增加1
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
点到Unsafe类的方法 getAndAddInt(this, valueOffset, 1) 实现
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {
int i;
do {
i = this.getIntVolatile(paramObject, paramLong);
} while(!this.compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
return i;
}
this.getIntVolatile(paramObject, paramLong);方法是利用内存偏移量获取到当前value的值,然后一直利用CAS比较转换,直到CAS比较转换成功。incrementAndGet对应++i,是返回自增之后的值。同样AtomicInteger类中也包含i++,i–,--i之类对应的方法。
说到CAS就会谈到ABA。是CAS引发了ABA问题,CAS也并非完美。就是说,当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。
ABA问题如何解决?
两种优化思路:
AtomicStampedReference 本质是有一个int 值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(当然加多少或减多少都是可以自己定义的),在zookeeper中保持数据的一致性也是用的这种方式;
AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已;
具体实现见对应源码,本篇就不再解读这两个类的源码了。