Volatile适合使用场景
a)对变量的写入操作不依赖其当前值
不满足: number++. count=count*5等
满足: boolean变量、直接赋值的变量等
b)该变量没有包含在具有其他变量的不变式中
不满足:不变式low<up
总结:变量真正独立于其他变量和自己以前的值,在单独使用的时候,适合用volatile
synchronized和volatile比较
a) volatile不需要加锁,比synchronized更轻便,不会阻塞线程
b) synchronized既能保证可见性,又能保证原子性.而volatile只能保证可见性,无法保证原子性
与锁相比, Volatile 变量是一种非常简单但同时又非常脆弱的同步机制 ,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循volatile 的使用条件(变量真正独立于其他变量和自己以前的值)在某些情况下可以使用volatile代替synchronized来优化代码提升效率。
J.U.C之CAS
J.U.C即java.util.concurrent ,是JISR 166标准规范的一个实现: JSR 166以及J.U.C包的作者是Doug Lea。
J.U.C框架是Java 5中引入的.而我们最熟悉的线程池机制就在这个包, J.U.C框架包含的内容有:
●AbstractQueuedSynchronizer ( AQS框架) , J.U.C中实现锁和同步机制的基础;
●Locks & Condition (锁和条件变量) ,比synchronized, walt、 notify 更细粒度的锁机制;
●Executor框架(线程池、Callable、 Future) ,任务的执行和调度框架;
●Synchronizers (同步器) ,主要用于协助线程同步,有CountDownLatch、CyclicBarrier、Semaphore、Exchanger ;
●Atomic Variables (原子变量) , 方便程序员在多线程环境下,无锁的进行原子操作,核心操作是CAS原子操作,所谓的CAS操作,即compare and swap ,指的是将预期值与当前变量的值比较(compare) ,如果相等则使用新值替换(swap)当前变量,否则不作操作;
●BlockingQueue (阻塞队列) , 阻塞队列提供了可阻塞的入队和出队操作,如果队列满了,入队操作将阻塞直到有空间可用,如果队列空了,出队操作将阻塞直到有元素可用;
●Concurrent Collections (并发容器) ,说到并发容器,不得不提同步容器。在JDK1.5之前,为了线程安全,我们一般都是使用同步容器,同步容器主要的缺点是:对所有容器状态的访问都串行化,严重降低了并发性;某些复合操作,仍然需要加锁来保护;迭代期间,若其它线程并发修改该容器,会抛出ConcurrentModificationException异常。即快速失败机制;
●Fork/Join并行计算框架.这块内容是在JDK1.7中引入的.可以方便利用多核平台的计算能力,简化并行程序的编写,开发人员仅需关注如何划分任务和组合中间结果;
CAS介绍
CAS , Compare And Swap ,即比较并交换。同步组件中大量使用CAS技术实现了Java多线程的并发操作。整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
Lock 同步器 阻塞队列 执行器 并发容器
↑↑↑ ↑↑↑ ↑↑↑
AQS 非阻塞数据结构 原子变量类
↑↑↑ ↑↑↑
volatile变量的读/写 CAS
CAS原理剖析
再次测试之前Volatile的例子,把循环的次数调整为一亿(保证在一秒之内不能遍历完成,从而测试三种原子操作的性能) , 我们发现, AtomicInteger原子操作性能最高,他是用的就是CAS
synchronized同步分析
注意,本小节是解释synchronized性能低效的原因,只要能理解synchronized同步过程其实还需要做很多事,这些逻辑的执行都需要占用资源,从而导致性能较低,是为了对比CAS的高效。这部分分析过于深入MM底层原理,不适合初级甚至中级程序员学习。
我们之前讲过, synchronized的同步操作主要是monitorenter和monitorexit这两个jvm指令实现的,我们先写一段简单的代码:
public class Demo2Synchronized {
public void test2() {
synchronized (this) {
}
}
CAS原理
在上一部分,我们介绍了synchronized底层做了大量的工作,才实现同步,而同步保证了原子操作。但是不可避免的是性能较低。CAS是如何提高性能的呢?
CAS的思想很简单:三个参数, -一个当前内存值V、旧的预期值A、即将更新的值B ,当且仅当旧的预期值A和的存值V相同时,将内存值修改为B并返回true ,否则什么都不做,并返回false.如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
CAS在先比较后修改这个CAS过程中,根本没有获取锁,释放锁的操作,是硬件层面的原子操作,跟JMM內存模型没有关系。大家可以理解为直接使用其他的语言,在JVM虚拟机之外直接操作计算机硬件,正因为如此,对比synchronized的同步, 少了很多的逻辑步骤,使得性能大为提高。
jUC下的atomic类都是通过CAS来实现的,下面就是一个AtomicInteger原子操作类的例子 ,在其中使用了Unsafe unsafe = Unsafe.getUnsafe()。Unsafe 是CAS的核心类,它提供了硬件级别的原子操作。
private static final Unsafe unsafe = Unsafe. getUnsafe();
private static final long value0ffset;
static {
try {
value0ffset = unsafe . objectFieldoffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//操作的值也进行了volatile修饰,保证内存可见性
private volatile int valuey;
继续查看AtomicInteger的addAndGet()方法:
pub1ic final int addAndGet(int delta) {
return unsafe. get AndAddInt(this, value0ffset, delta) + delta;
}
public final int getAndAddInt (object var1, long var2,int var4) {
int var5;
do {
var5 = this. getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1,var2,var5,var5 + var4));
return var5;
}
native关键词
前面提到了sun.misc.Unsafe这个类,里面的方法使用native关键词声明本地方法.为什么要用native ?
Java无法直接访问底层操作系统,但有能力调用其他语言编写的函数or方法,是通过JNI(Java Native
Interfface)实现。使用时,通过native关键字告诉JVM这个方法是在外部定义的。但JVM也不知道去哪找这个原生方法,此时需要通过javah命令生成.h文件。
示例步骤(c语言为例) :
1.javac生成.class文件.比如javac NativePeer.java
2.javah生成h文件,比如javah NativePeer
3.编写c语言文件,在其中include上进一步生成的.h文件,然后实现其中声明而未实现的函数
4.生成dll共享库,然后Java程序load库,调用即可
native可以和任何除abstract外的关键字连用。这也说明了这些方法是有实体的.并且能够和其他Java方法一样,拥有各种Java的特性。
native方法有效地扩充了jvm ,实际上我们所用的很多代码已经涉及到这种方法了,通过非常 简洁的接口帮我们实现Java以外的工作。
native优势:
1.很多层次上用Java去实现是很麻烦的.而且Java解释执行的效率也差了c语言啥的很多,纯Java实现可能会导致效率不达标,或者可读性奇差。
2. Java毕竟不是一个完整的系统 ,它经常需要一些底层的支持 ,通过JNI和native method我们就可以实现jre与底层的交互,得到强大的底层操作系统的支持,使用一些ava本身没有封装的操作系统的特性。
多CPU的CAS处理
CAS可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。
CPU提供了两种方法来实现多处理器的原子操作,总线加锁或者缓存加锁。
●总线加锁:总线加锁就是就是使用处理器提供的-一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。
●缓存加锁:其实针对于上面那种情况我们只需要保证在同-一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。 缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改缓存行中的时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。