volatile, CAS 非阻塞算法,Treiber 非阻塞算法,Unsafe
Volatile
volatile 保证变量在多个线程的可见性。如何确保可见性的呢?是通过Java线程内存模型确保的。
CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相同,就使用一个新值替换当前变量的值。
下面来看一个示例:
class MyLock {
private boolean locked = false;
public boolean lock() {
if(!locked) {
locked = true;
return true;
}
return false;
}
}
上面这段代码在多线程环境下不能正常工作。为了在一个多线程程序中良好的工作,"check then act" 操作必须是原子的。
下面的示例代码,把之前的lock() 方法用 synchronized 关键字重构成一个原子块。
class MyLock {
private boolean locked = false;
public synchronized boolean lock() {
if(!locked) {
locked = true;
return true;
}
return false;
}
现在lock()方法是同步的,所以,在某一时刻只能有一个线程在同一个MyLock实例上执行它。
原子的lock方法实际上是一个”compare and swap“的例子。
CAS用作原子操作
现在CPU内部已经执行原子的CAS操作,java5以来,可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能。
下面使用一个AtomicBoolean来实现lock()方法的例子:
public static class MyLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public boolean lock() {
return locked.compareAndSet(false, true);
}
}
locked 变量不再是boolean类型而是AtomicBoolean。这个类中有一个compareAndSet() 方法,它使用一个期望值和AtomicBoolean实例的值比较,和两者相等,则使用一个新值替换原来的值。在这个例子中,它比较locked的值和false,如果locked的值为false,则把修改为true。如果值被替换了,compareAndSet()返回true,否则,返回false。
使用Java5+提供的CAS特性而不是使用自己实现的的好处是Java5+中内置的CAS特性可以让你利用底层的你的程序所运行机器的CPU的CAS特性。这会使还有CAS的代码运行更快。
Treiber
多线程环境下各种数据结构的丝线有了很大的变化,每当我们更新某个数据的时候,我们都要考虑其它线程是否对其进行了修改。最简单的一种方法就是加锁,不过加锁会导致性能低下,而且可能阻塞其它线程。因此,我们引入了非阻塞(non-blocking)的算法--通过CAS操作保证操作的原子性,同时我们还引入了lock-free的概念,它指的是一个线程出现问题(如阻塞,失败)但不影响其它线程(从总体看程序仍然是运行的)。这里就来看一个Nonblocking stack的一个实现 -- Treiber stack.
简化版java实现:
public class ConcurrentStack<E> {
private AtomicReference<Node<E>> top = new AtomicReference<>();
public void push(E elem) {
Node<E> newHead = new Node<>(elem);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null)
return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
private static class Node<E> {
public final E item;
public Node<E> next;
public Node(E item) {
this.item = item;
}
}
}
我们使用了AtomicReference来实现 TreiberStack。每当我们push进去一个元素的时候,我们首先根据要添加的元素创建一个Node,然后获取原栈顶结点,并将新结点的下一个结点指向原栈顶结点。此时我们使用CAS操作来更改栈顶结点,如果此时的栈顶和之前的相同,代表CAS操作成功,那么就把新插入的元素设为栈顶;如果此时的栈顶和之前的不同(即其他线程改变了栈顶结点),CAS操作失败,那么需要重复上述操作(更新当前的栈顶元素并且重设为next),直到成功。pop操作的原理也相似。
Unsafe
Unsafe类说明
从这个类的名字Unsafe上来说这个类就是一个不安全的类,也是不开放给用户直接使用的(当然我们还是可以通过其他一些方法用到)。
这个类在jdk源码中多个类中用到,主要作用是任意内存地址位置处读写数据,外加一下CAS操作。它的大部分操作都是绕过JVM通过JNI完成的,因此它所分配的内存需要手动free,所以是非常危险的。但是Unsafe中很多(但不是所有)方法都很有用,且有些情况下,除了使用JNI,没有其他方法弄够完成同样的事情。
至于研究它的起因,是因为我最近在看jdk8的ConcurrentHashMap,这个版本的主要函数就是用过Unsafe来完成的。
Unsafe类的调用
Unsafe类是一个单例,调用的方法为getUnsafe,如下。可以看到,虽然是可以调用,但是会有一步判断,判断是不是内部会检查该CallerClass是不是由系统类加载器BootstrapClassLoader加载。由系统类加载器加载的类调用getClassLoader()会返回null,所以要检查类是否为bootstrap加载器加载只需要检查该方法是不是返回null。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
那我们怎么能调用到它呢,有以下2中方法。
- 通过JVM参数-Xbootclasspath指定要使用的类为启动类;
- 在Unsafe类中有一个成员变量theUnsafe,因此我们可以通过反射将private单例实例的accessible设置为true,然后通过Field的get方法获取,如下。
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
不调用构造方法生成对象
import java.lang.reflect.Field;
public class UnsafeTest {
private String name = "";
private int flag = 0;
public UnsafeTest() {
this.name = "beijing";
this.flag = 1;
}
@Override
public String toString() {
return "UnsafeTest{" +
"name='" + name + '\'' +
", flag=" + flag +
'}';
}
public static void main(String[] args) throws Exception {
// 通过反射得到theUnsafe对应的Field对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该field为可访问
field.setAccessible(true);
// 通过filed得到该Field对应的具体对象,传入null是因为该Field为static的
Unsafe unsafe = (Unsafe) field.get(null);
UnsafeTest test = (UnsafeTest) unsafe.allocateInstance(UnsafeTest.class);
System.out.println(test); // dont invoke constructer
UnsafeTest another = new UnsafeTest();
System.out.println(another);
}
}
内存修改
我们看一下下面的代码,在正常的情况下sizeValidate始终范围false,但是通过计算MAX_SIZE的位移,将其进行修改之后,就会返回true。
import java.lang.reflect.Field;
public class Validation {
private int MAX_SIZE = 10;
public boolean sizeValidate() {
return 20 < MAX_SIZE;
}
public static void main(String[] args) throws Exception {
// 通过反射得到theUnsafe对应的Field对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Validation v = new Validation();
System.out.println(v.sizeValidate()); // false
Field f = v.getClass().getDeclaredField("MAX_SIZE");
unsafe.putInt(v, unsafe.objectFieldOffset(f), 100); // memory corruption
System.out.println(v.sizeValidate());
}
}
计算Java对象大小
有两种计算java对象大小的方式。
- 通过java.lang.instrument.Instrumentation 的 getObjectSize(obj) 直接获取对象的大小;
- 通过sun.misc.Unsafe对象和objectFieldOffSet(field)等方法结合反射来计算对象的大小。
通过Unsafe获取Java对象大小的基本思路如下:
- 通过反射获得一个类的Field;
- 通过Unsafe的objectFieldOffset()获得每个Field的off。Set;
- 对Field进行遍历,取得最大的offset,然后加上这个field的长度,再加上Padding对齐。
这边不写代码了,想要了解的同学可以看一下这个
Java并发中的应用
在Java并发中会用到CAS操作,对应于Unsafe类中的compareAndSwapInt, compareAndSwapLong等。下面的例子就是使用Unsafe实现的无锁数据结构。
class LongValue {
private volatile long counter = 0;
private Unsafe unsafe;
private long offset;
public LongValue() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(LongValue.class.getDeclaredField("counter"));
}
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
public long getCounter() {
return counter;
}
}
看一下Java中AtomicLong的实现,下面摘出来一部分。可以看到该类在加载的时候将value的偏移位置计算出来,然后在compareAndSet等方法中使用Unsafe中的CAS操作进行替换,这样的无锁操作可以大大提高效率。
...
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;
// setup to use Unsafe.compareAndSwapLong for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
...
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile long value;
...
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
...
其他使用方式
- 通过Unsafe的defineClass可以动态加载Class;
- 通过Unsafe的copyMemory、freeMemory等可以实现内存的复制与释放,如果我们知道了对象的大小,利用arrayBaseOffset和copyMemory可以完成对象的浅拷贝。
Unsafe还有其他很多的用途,但是要记得这是一个非常危险的类,在使用的过程中需要万分小心,不到万不得已的情况不要使用。
网友评论