一、什么是原子性
原子性是世界上最小单位,具有不可分割性。比如a=0;(a非long和double类型)这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++;这个操作实际上是a=a+1;是可分割的,所以他不是一个原子操作。
二、原子操作的作用
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们就称它具有原子性。java.util.concurrent.atomic包下提供了一些原子类如下:
三、理解AtomicInteger类
下面是AtomicInteger类中几个常用的方法:
可能很多人和我一样存在着这样一个疑问,在AtomicInteger中是如何实现原子性的呢?有前辈已经对AtomiInteger的源码进行了分析《对 AtomicInteger 源码 的理解》我在这里仅仅是引用一下,做个记录。先来看看源码:
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } }
看这段代码和上面对原子性的分析,从int next = current + 1可以看到,这里很难保证操作的原子性,重点在于compareAndSet(current, next)方法
该函数 只有两个参数,可操作的确实三个值 ,即 value ,expect, update. 他使用了 由硬件保证其原子性的指令 CAS (compare and swap)。
compareAndSet 函数保证了 比较,赋值这两步操作可以通过一个原子操作完成。
然后看整个函数, 所有代码被放到了一个循环里面, 如果compareAndSet()执行失败,则说明 在int current = get(); 以后,其他线程对value进行了更新, 于是就循环一次,重新获取当前值,直到compareAndSet()执行成功为止。
综上,getAndIncrement() 方法并不是原子操作。 只是保证了他和其他函数对 value 值得更新都是有效的。他所利用的是基于冲突检测的乐观并发策略。 可以想象,这种乐观在线程数目非常多的情况下,失败的概率会指数型增加。
四、理解volatile修饰符
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。
一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。以下例子展现了volatile的作用:
package com.codeing.snail.test; public class TestAtomic extends Thread { private volatile boolean pleaseStop; public void run() { while (!pleaseStop) { // do some stuff... } } public void tellMeToStop() { pleaseStop = true; } }
假如pleaseStop没有被声明为volatile,线程执行run的时候检查的是自己的副本,就不能及时得知其他线程已经调用tellMeToStop()修改了pleaseStop的值。
Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。
volatile关键字用于声明简单类型变量,如int、float、 boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:
package com.codeing.snail.test; public class TestAtomic extends Thread { public static volatile int n = 0; public void run() { for (int i = 0; i < 10; i++) try { n = n + 1; sleep(3); // 为了使运行结果更随机,延迟3毫秒 } catch (Exception e) { } } public static void main(String[] args) throws Exception { Thread threads[] = new Thread[100]; for (int i = 0; i < threads.length; i++) // 建立100个线程 threads[i] = new TestAtomic(); for (int i = 0; i < threads.length; i++) // 运行刚才建立的100个线程 threads[i].start(); for (int i = 0; i < threads.length; i++) // 100个线程都执行完后继续 threads[i].join(); System.out.println(" n= " + TestAtomic.n); } }
如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用。
五、锁和Atomic的使用场景
JDK的文档中说:“设计原子类主要用作各种块,用于实现非阻塞数据结构和相关基础结构类。compareAndSet()方法不是锁定的常规替换方法。仅当对象的重要更新限于单个变量时才应用它”与锁相比,另外Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile
代替 synchronized
来简化代码。然而,使用 volatile
的代码往往比使用锁的代码更加容易出错。