何谓原子性操作,即为最小的操作单元,比如i=1,就是一个原子性操作,这个过程只涉及一个赋值操作。又如i++就不是一个原子操作,它相当于语句i=i+1;这里包括读取i,i+1,结果写入内存三个操作单元。因此如果操作不符合原子性操作,那么整个语句的执行就会出现混乱,导致出现错误的结果,从而导致线程安全问题。
因此,在多线程中需要保证线程安全问题,就应该保证操作的原子性,那么如何保证操作的原子性呢?
其一当然是加锁,这可以保证线程的原子性,比如使用synchronized代码块保证线程的同步,从而保证多线程的原子性。但是加锁的话,就会使开销比较大。
另外,可以使用J.U.C下的atomic来实现原子操作。接下来我们就通过对比来说明atomic实现原子操作的功能。
一般情况下如果我们想避免原子性问题的时都会选择加锁,但是我们都知道加锁和解锁是有消耗的。并且只要有加锁、解锁就会伴随着线程阻塞、线程的唤醒,这样线程的切换也是消耗性能的。
从JDK1.5起就提供了原子类,能无锁的避免原子性问题,所以在简单的情况下,而且是只有就竞争一个共享变量的情况下,可以使用Java原子类,如果是多个共享变量的话基本上只能加锁了,原子类就不太好使了!
Java原子类可以分为五大类:原子更新基本类型、原子更新数组、原子更新引用类型、原子更新属性(类的字段)、原子累加器。
它们是怎么做到没加锁实现原子性的呢?没什么大秘密就是硬件的支持!我们知道CPU指令肯定是原子性的,为了解决并发的问题CPU提供了一条指令——CAS(compare and swap 即比较并交换)。
CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C。只有当内存中地址A的值等于B,才能把地址A的值变成C。
也就是只有预期值B等于内存地址A中的值时,表明共享变量没有被其他线程修改过,所以才允许更新新值,这样就保证了原子性!
CAS还会有ABA问题,就是当你比较的时候,可能你的值被一个线程改了之后,另一个线程又改了回来,然后你比较的时候发现和预期值一样,其实是被改过的。就类似你走在路上,你手机放口袋里,小偷偷走了你的手机,但是一抬头看到了头上的监控,又默默的放回你的口袋。你根本不知道发生了这件事,然后悠然的拿起手机,刷了波朋友圈,又默默的塞进口袋。。。
大部分情况下不需要关心这个,例如基本类型等,但是原子更新引用类型,因为可能比较的时候线程但是里面的某个属性已经变了。可以添加一个版本号来避免这个问题。
通常利用CAS来解决并发问题都通过自旋手段,这里的自旋的其实循环尝试,再说白一点就是while循环。举个例子来看一下AtomicInteger源码,声明了 private volatile int value; 通过volatile 关键字保证可见性。
原子类都是调用sun.misc.Unsafe来的实现的,就比如上面的代码就是调用unsafe.getAndAddInt()。
do while就是自旋操作了,compareAndSwapInt是native方法。
所有的原子类实现基本上都是这个思路!讲白了就是调用硬件提供CAS这种操作,Java并发包帮我们封装了一下,使得我们更容易的调用!
1.非原子操作
首先,了解一下若不保证原子操作会出现什么样的情况。代码如下:
package concurrent;
import java.util.concurrent.*;
public class GeneralTest {
private static final ExecutorService es=Executors.newFixedThreadPool(10);
private static int count=0;
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<20;i++)
{
Thread t1=new Thread(){
public void run(){
count+=1;
}
};
es.execute(t1);
}
for(int i=0;i<5;i++)
System.out.println("累加20次,得到结果是:"+count);
}
}
以上就是创建20个线程进行对count这个类变量自增一次,预期的值应该是20,但是运行上述程序出现如下结果:
累加20次,得到结果是:16
累加20次,得到结果是:19
累加20次,得到结果是:19
累加20次,得到结果是:19
累加20次,得到结果是:19
从以上结果可以看出,输出五次的结果都是没有达到20,可能出现的问题便是count+=1包含了三个操作,可能这个线程读取count的时候,上一个线程还没把更新的count值写入内存,这就是因无法保证操作的原子性而导致的线程安全问题。
2.通过synchronized加锁
synchronized称为互斥锁,它的工作原理就是一旦一个线程抢占了得到锁,其它线程便进入等待状态,只有当该线程释放锁,其它线程才有获取锁的机会。因此使用synchronized对count加锁,那么实现的操作便是原子性操作,因为只有当一个线程完成所有操作,然后释放锁,其它线程才会抢占锁。以下还是自增问题,当一个线程在实现++count时,只有当该线程将数据存入内存,释放锁之后,其它内存才会获取锁,读取数据,因此不会出现干涉问题,从而避免线程安全问题。
package concurrent;
import java.util.concurrent.*;
public class GeneralTest {
private static final ExecutorService es=Executors.newFixedThreadPool(20);
private static Integer count=0;
public static void main(String[] args) {
// TODO Auto-generated method stub
long start=System.currentTimeMillis();
for(int i=0;i<1000;i++)
{
Thread t1=new Thread(){
public void run(){
//count+=1;
synchronized(count)
{
System.out.println(Thread.currentThread().getName()+"实现了自增一次,结果为:"+(++count));
}
}
};
es.execute(t1);
}
try
{
Thread.sleep(5000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println("计算过程的耗时为:"+(System.currentTimeMillis()-start-5000));
System.out.println("累加1000次,得到结果"+count);
}
}
运行以上程序,得到如下结果,由于累加1000次太多,所以不把每次加的结果显示出来,只显示一部分过程以及最终的结果。
pool-1-thread-8实现了自增一次,结果为:999
pool-1-thread-8实现了自增一次,结果为:1000
pool-1-thread-2实现了自增一次,结果为:679
pool-1-thread-14实现了自增一次,结果为:678
pool-1-thread-3实现了自增一次,结果为:677
pool-1-thread-1实现了自增一次,结果为:675
pool-1-thread-19实现了自增一次,结果为:674
pool-1-thread-13实现了自增一次,结果为:673
pool-1-thread-10实现了自增一次,结果为:882
pool-1-thread-7实现了自增一次,结果为:881
pool-1-thread-17实现了自增一次,结果为:715
pool-1-thread-9实现了自增一次,结果为:714
pool-1-thread-5实现了自增一次,结果为:713
pool-1-thread-11实现了自增一次,结果为:712
pool-1-thread-20实现了自增一次,结果为:706
pool-1-thread-18实现了自增一次,结果为:698
pool-1-thread-4实现了自增一次,结果为:697
pool-1-thread-16实现了自增一次,结果为:696
pool-1-thread-15实现了自增一次,结果为:695
pool-1-thread-6实现了自增一次,结果为:694
pool-1-thread-12实现了自增一次,结果为:693
计算过程的耗时为:13
累加1000次,得到结果1000
从上面可以看出,结果和预期一样,并且计算过程花费了13ms。
3.atomic实现
atomic系列的类在是J.U.C包下的一系列类。它主要包括四类:基本类型,数组类型,属性原子修改器类型,引用类型。
基本类型的类主要包括AtomicInteger、AtomicLong、AtomicBoolean等;数组类型主要包括AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;属性原子修改器类型主要包括AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater ;引用类型主要包括AtomicReference、AtomicStampedRerence、AtomicMarkableReference。
1.基本类型的实现:
package concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
private static final AtomicInteger at=new AtomicInteger();
private static final ExecutorService es=Executors.newFixedThreadPool(20);
public static void main(String[] args) {
// TODO Auto-generated method stub
long start=System.currentTimeMillis();
for(int i=0;i<1000;i++)
{
Thread t1=new Thread() {
public void run()
{
System.out.println(Thread.currentThread().getName()+“实现了一次自增原子操作,结果为:”+at.incrementAndGet());
}
};
es.execute(t1);
}
try
{
Thread.sleep(5000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println(“计算过程的耗时为:”+(System.currentTimeMillis()-start-5000));
System.out.println(“累加1000次,得到结果”+at);
}
}
运行结果如下:
pool-1-thread-13实现了一次自增原子操作,结果为:984
pool-1-thread-8实现了一次自增原子操作,结果为:983
pool-1-thread-14实现了一次自增原子操作,结果为:982
pool-1-thread-20实现了一次自增原子操作,结果为:981
pool-1-thread-10实现了一次自增原子操作,结果为:980
pool-1-thread-12实现了一次自增原子操作,结果为:979
pool-1-thread-3实现了一次自增原子操作,结果为:978
pool-1-thread-7实现了一次自增原子操作,结果为:977
pool-1-thread-4实现了一次自增原子操作,结果为:976
pool-1-thread-18实现了一次自增原子操作,结果为:1000
pool-1-thread-9实现了一次自增原子操作,结果为:999
pool-1-thread-17实现了一次自增原子操作,结果为:998
pool-1-thread-6实现了一次自增原子操作,结果为:997
pool-1-thread-5实现了一次自增原子操作,结果为:996
pool-1-thread-2实现了一次自增原子操作,结果为:995
计算过程的耗时为:9
累加1000次,得到结果1000
由上面可知使用基本类型的原子操作类进行数字的自增,不仅可以保证操作操作的原子性,而且相对来说花费的时间代价比使用synchronized加锁的时间代价要小。
2.数组类型
以下以AtomicIntegerArray为例通过对一个数组内的每个元素进行自增计算,从而来介绍数组类型的原子类的运用。
package concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicTest {
private static final AtomicIntegerArray at=new AtomicIntegerArray(new int[5]);
private static final ExecutorService es=Executors.newFixedThreadPool(20);
public static void main(String[] args) {
// TODO Auto-generated method stub
long start=System.currentTimeMillis();
for(int i=0;i<1000;i++)
{
Thread t1=new Thread() {
public void run()
{
for(int i=0;i<5;i++)
System.out.println(Thread.currentThread().getName()+“实现了对第”+(i+1)+“个元素一次自增原子操作,结果为:”+at.incrementAndGet(i));
}
};
es.execute(t1);
}
try
{
Thread.sleep(5000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println(“计算过程的耗时为:”+(System.currentTimeMillis()-start-5000));
for(int i=0;i<5;i++)
System.out.println(“第”+(i+1)+“个元素累加1000次,得到结果”+at.get(i));
}
}
运行程序结果如下,其中运算过程部分给出:
pool-1-thread-3实现了对第5个元素一次自增原子操作,结果为:980
pool-1-thread-20实现了对第1个元素一次自增原子操作,结果为:989
pool-1-thread-20实现了对第2个元素一次自增原子操作,结果为:1000
pool-1-thread-20实现了对第3个元素一次自增原子操作,结果为:1000
pool-1-thread-20实现了对第4个元素一次自增原子操作,结果为:1000
pool-1-thread-20实现了对第5个元素一次自增原子操作,结果为:1000
pool-1-thread-15实现了对第5个元素一次自增原子操作,结果为:998
计算过程的耗时为:9
第1个元素累加1000次,得到结果1000
第2个元素累加1000次,得到结果1000
第3个元素累加1000次,得到结果1000
第4个元素累加1000次,得到结果1000
第5个元素累加1000次,得到结果1000
从结果可以看出,数组类型的原子类的使用,可以保证每个元素的自增等操作都满足原子性。
3.属性原子修改器
以AtomicIntegerFieldUpdater类为例,该类有一个静态方法newUpdater(Class<U> tclass,String fieldName),则这个表示为tclass类的fieldName属性创建一个属性原子修改器,如果需要修改该属性,则只需要调用原子修改器的方法,比如addAndGet(tclass obj,int delta):该方法表示将该修改器对应的属性增加delta。以下通过代码来了解属性原子修改器的作用。
package concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
class Count{
public volatile int number;
}
public class AtomicTest {
private static final AtomicIntegerFieldUpdater aifu=AtomicIntegerFieldUpdater.newUpdater(Count.class, “number”);
private static final ExecutorService es=Executors.newFixedThreadPool(20);
public static void main(String[] args) {
// TODO Auto-generated method stub
final Count count=new Count();
long start=System.currentTimeMillis();
for(int i=0;i<1000;i++)
{
Thread t1=new Thread() {
public void run()
{
System.out.println(Thread.currentThread().getName()+"实现了一次自增原子操作,结果为:"+aifu.addAndGet(count, 1));
}
};
es.execute(t1);
}
try
{
Thread.sleep(5000);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
System.out.println("计算过程的耗时为:"+(System.currentTimeMillis()-start-5000));
for(int i=0;i<5;i++)
System.out.println("第"+(i+1)+"个元素累加1000次,得到结果"+count.number);
}
}
运行以上程序,得到如下结果:
pool-1-thread-17实现了一次自增原子操作,结果为:993
pool-1-thread-5实现了一次自增原子操作,结果为:992
pool-1-thread-19实现了一次自增原子操作,结果为:991
pool-1-thread-3实现了一次自增原子操作,结果为:990
pool-1-thread-15实现了一次自增原子操作,结果为:989
pool-1-thread-20实现了一次自增原子操作,结果为:988
pool-1-thread-18实现了一次自增原子操作,结果为:987
pool-1-thread-6实现了一次自增原子操作,结果为:986
pool-1-thread-7实现了一次自增原子操作,结果为:985
pool-1-thread-9实现了一次自增原子操作,结果为:984
计算过程的耗时为:10
第1个元素累加1000次,得到结果1000
第2个元素累加1000次,得到结果1000
第3个元素累加1000次,得到结果1000
第4个元素累加1000次,得到结果1000
第5个元素累加1000次,得到结果1000
从上面结果可以看出,属性原子修改器的使用也能达到原子性操作的目的。
4.引用类型
以AtomicReference类为例,通过AtomicReference<Count> ar=new AtomicReference<>();ar可以调用set()方法设置初始值,调用compareAndSet(expect,update):这个方法就是将存在ar中的值与expect值作比较,如果两者相等,则更新该值为update;代码如下:
package concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
class Count{
public int count;
public Count(int count)
{
this.count=count;
}
public String toString()
{
return “这个对象的位置是:”+count;
}
}
public class AtomicTest {
private static final AtomicReference ar=new AtomicReference();
public static void main(String[] args) {
// TODO Auto-generated method stub
Count count=new Count(1001);
long start=System.currentTimeMillis();
ar.set(count);
System.out.println("你好,"+ar.get());
Count count1=new Count(1002);
ar.compareAndSet(count, count1);//内存值与count是一样的,所以值更新为count1
System.out.println("我发生了改变:"+ar.get());
Count count2=new Count(1003);
ar.compareAndSet(count, count2);//此时内存智为count1,与count不一致,所以无法更新,因此内存值依然为count1
System.out.println("我发生了改变:"+ar.get());
}
}
运行程序,结果如下:
你好,这个对象的位置是:1001
我发生了改变:这个对象的位置是:1002
我发生了改变:这个对象的位置是:1002
扩展阅读
原子更新基本类型
相关实现类:AtomicBoolean、AtomicInteger、AtomicLong。主要就是用来基本类型的操作
原子更新数组类型
相关实现类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。可以原子化地更新数组里面的每一个元素,和原子更新基本类型差别就是基本上方法都多了个数组的下标。
原子更新引用类型
相关实现类:AtomicReference、AtomicStampedReference 、AtomicMarkableReference。这个就要重点关注ABA,但是Java已经关注到了所以AtomicStampedReference 和 AtomicMarkableReference 就能避免ABA问题了,就是用了版本号!
原子更新字段类型
相关实现类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
它们可以原子化地更新对象的属性,注意更新类的字段(属性)必须使用public volatile修饰符,这样才能保证可见性。
原子累加器
相关实现类:LongAccumulator、LongAdder、DoubleAccumulator、DoubleAdder。这几个类只能用来进行累加操作,现对于原子更新基本类型它们的性能更好些,所以如果只有累加操作可以用这几个类!