sun.misc.Unsafe
类是超越Java的存在,它违反了Java在内存管理上的设计初衷,却又是Java很多重要特性与功能得以实现的基础,它使Java的安全性受到威胁,却又使Java在很多方面的性能得到提升,它是魔鬼与天使的混合体。
Java是一个安全的开发工具,它阻止开发人员犯很低级的错误,而大部分的错误都是基于内存管理的。Unsafe类突破了Java原生的内存管理体制,使用Unsafe类可以在系统内存的任意地址进行读写数据,而这些操作对于普通用户来说是非常危险的,Unsafe的操作粒度不是类,而是数据和地址。
从另一方讲,Java正被广泛应用于游戏服务器和高频率的交易应用。这些之所以能够实现主要归功于Java提供的这个非常便利的类sun.mics.Unsafe
。Unsafe类为了速度,在Java严格的安全标准方法做了一些妥协。
Java在JUC包中提供了对sun.misc.Unsafe
类的封装实现,这就是java.util.concurrent.LockSupport
。
本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
sun.mics.Unsafe
一共提供了106个函数,这些函数涵盖了以下五个方面的功能:
sun.misc.Unsafe
只有一个无参的私有构造函数,要想实例化sun.misc.Unsafe
可以调用getUnsafe()
方法。
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(var0.getClassLoader() != null) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
出于安全考虑,Unsafe类只能被系统类加载器实例化,否则会抛出SecurityException
异常。普通用户如果想实例化sun.misc.Unsafe
类的对象,需要通过类反射机制或者修改Java的安全策略。
// 返回对象中指定静态成员变量的内存偏移量(相对于类存储)
public native long staticFieldOffset(Field f);
// 返回对象中指定成员变量的内存偏移量(相对于对象实例)
public native long objectFieldOffset(Field f);
// 返回对象中指定成员变量
public native Object staticFieldBase(Field f);
sun.misc.Unsafe
的操作对象是内存数据,获取指定成员变量的内存地址是对其进行操作的第一步。
staticFieldOffset
是一个本地函数,返回指定静态field的内存地址偏移量,Unsafe
类的其他方法中这个值是被用作一个访问特定field的一个方式。这个值对于给定的field是唯一的,并且后续对该方法的调用都返回相同的值。
objectFieldOffset
获取到的是内存偏移量,并不是真正的内存指针地址,Unsafe类提供了getAddress
函数将该偏移量转换为真正的内存指针地址,有了该内存指针地址,就可以直接操作内存数据的读写了。
有了objectFieldOffset
获取到的内存偏移量,就可以使用Unsafe类对该内存位置的数据进行读写。Unsafe类提供了对所有Java基本数据类型(byte, short, int, long, float, double)和对象类型的读写,这些方法都是本地函数(另外有一些对本地函数进行封装的读写函数,已经被标识为弃用)。
这些操作可以从另一个层面理解为sun.misc.Unsafe
对序列化和反序列化的支持。
// o: 对象引用
// offset: 内存偏移量,通过objectFieldOffset获取
public native int getInt(Object o, long offset);
// o: 对象引用
// offset: 内存偏移量,通过objectFieldOffset获取
// x: 新的数据值
public native void putInt(Object o, long offset, int x);
public native Object getObject(Object o, long offset);
public native void putObject(Object o, long offset, Object x);
public native boolean getBoolean(Object o, long offset);
public native void putBoolean(Object o, long offset, boolean x);
public native byte getByte(Object o, long offset);
public native void putByte(Object o, long offset, byte x);
public native short getShort(Object o, long offset);
public native void putShort(Object o, long offset, short x);
public native char getChar(Object o, long offset);
public native void putChar(Object o, long offset, char x);
public native long getLong(Object o, long offset);
public native void putLong(Object o, long offset, long x);
public native float getFloat(Object o, long offset);
public native void putFloat(Object o, long offset, float x);
public native double getDouble(Object o, long offset);
public native void putDouble(Object o, long offset, double x);
// 获取obj对象中offset地址对应的object型field的值为指定值。
// getObject(Object, long)的volatile版
public native Object getObjectVolatile(Object o, long offset);
// 设置obj对象中offset偏移地址对应的object型field的值为指定值。
// putObject(Object, long, Object)的volatile版
public native void putObjectVolatile(Object o, long offset, Object x);
public native int getIntVolatile(Object o, long offset);
public native void putIntVolatile(Object o, long offset, int x);
public native boolean getBooleanVolatile(Object o, long offset);
public native void putBooleanVolatile(Object o, long offset, boolean x);
public native byte getByteVolatile(Object o, long offset);
public native void putByteVolatile(Object o, long offset, byte x);
public native short getShortVolatile(Object o, long offset);
public native void putShortVolatile(Object o, long offset, short x);
public native char getCharVolatile(Object o, long offset);
public native void putCharVolatile(Object o, long offset, char x);
public native long getLongVolatile(Object o, long offset);
public native void putLongVolatile(Object o, long offset, long x);
public native float getFloatVolatile(Object o, long offset);
public native void putFloatVolatile(Object o, long offset, float x);
public native double getDoubleVolatile(Object o, long offset);
public native void putDoubleVolatile(Object o, long offset, double x);
// 设置obj对象中offset偏移地址对应的object型field的值为指定值。这是一个有序或者
// 有延迟的<code>putObjectVolatile</cdoe>方法,并且不保证值的改变被其他线程立
// 即看到。只有在field被<code>volatile</code>修饰并且期望被意外修改的时候
// 使用才有用。
// 这个方法在对低延迟代码是很有用的,它能够实现非堵塞的写入,这些写入不会被Java的JIT重新排序指令
// (instruction reordering),这样它使用快速的存储-存储(store-store) barrier, 而不是较慢
// 的存储-加载(store-load) barrier, 后者总是用在volatile的写操作上,这种性能提升是有代价的,
// 虽然便宜,也就是写后结果并不会被其他线程看到,甚至是自己的线程,通常是几纳秒后被其他线程看到,
// 这个时间比较短,所以代价可以忍受。类似Unsafe.putOrderedObject还有unsafe.putOrderedLong
// 等方法,unsafe.putOrderedLong比使用 volatile long要快3倍左右。.
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
objectFieldOffset
获取到的是内存偏移量,并不是真正的内存指针地址,Unsafe类提供了getAddress
函数将该偏移量转换为真正的内存指针地址,有了该内存指针地址,就可以直接操作内存数据的读写了。
// 根据给定的内存偏移量(objectFieldOffset的返回值),获取真正的内存指针地址。
// 如果给定的内存偏移量为0或者并没有指向一个内存块,返回undefined。
// 如果返回的内存指针地址位宽小于64,用无符号整数进行扩展转换为Java long型。
public native long getAddress(long var1);
// 保存一个内存指针地址到给定的内存偏移量。
// 如过给定的内存偏移量为0或者并没有指向一个内存块,返回undefined。
public native void putAddress(long var1, long var3);
// 返回一个内存指针占用的字节数(bytes)
public native int addressSize();
// 返回一个内存页占用的字节数(bytes)
public native int pageSize();
sun.mics.Unsafe
类允许Java程序使用JVM堆外内存,即操作系统内存。BufferBytes
类也可以分配JVM堆外内存,但是只能使用最大2GB的JVM堆外内存空间,而sun.mics.Unsafe
类没有这个限制。
// 分配一块大小为var1字节的JVM堆外内存。
// 新分配的内存空间中的内容处于未初始化状态。
// 新分配的内存空间的指针地址不为0,并对所有的值类型做内存对齐。
public native long allocateMemory(long var1);
// 调整JVM堆外内存空间大小。
// 参数var1是待调整的JVM堆外内存空间的指针地址。
// 参数var3是新的JVM堆外内存空间字节大小。
// 如果新空间大小var1=0,则返回指针地址为0.
public native long reallocateMemory(long var1, long var3);
// 释放指定内存指针地址的内存空间。
public native void freeMemory(long var1);
有了addAddress
函数获取到的内存指针地址,就可以直接操作该内存指针地址处的数据了。Unsafe类提供了对所有Java基础数据类型和对象类型的直接内存操作函数。
下面提供的这些函数,都是按照数据类型对内存数据进行读写。
// var1: 内存指针地址
public native byte getByte(long var1);
// var1: 内存指针地址
// var3: 新的数据值
public native void putByte(long var1, byte var3);
public native short getShort(long var1);
public native void putShort(long var1, short var3);
public native char getChar(long var1);
public native void putChar(long var1, char var3);
public native int getInt(long var1);
public native void putInt(long var1, int var3);
public native long getLong(long var1);
public native void putLong(long var1, long var3);
public native float getFloat(long var1);
public native void putFloat(long var1, float var3);
public native double getDouble(long var1);
public native void putDouble(long var1, double var3);
有了addAddress
函数获取到的内存指针地址,就可以直接操作该内存指针地址处的数据了。Unsafe类提供了直接按照字节为单位对指定的内存指针地址进行数据操作的函数。
public native void setMemory(Object o, long offset, long bytes, byte value);
public void setMemory(long address, long bytes, byte value) {
setMemory(null, address, bytes, value);
}
有了addAddress
函数获取到的内存指针地址,还可以直接将一个内存指针地址对应的数据块拷贝到另一个内存指针地址对应的位置。
public native void copyMemory(Object srcBase, long srcOffset,
Object destBase, long destOffset,
long bytes);
public void copyMemory(long srcAddress, long destAddress, long bytes) {
copyMemory(null, srcAddress, null, destAddress, bytes);
}
Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。
Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。
将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
// 返回给定数组的第一个元素的内存偏移量
public native int arrayBaseOffset(Class arrayClass);
// 返回给定数组的转换因子,也就是数组中元素的增量地址
public native int arrayIndexScale(Class arrayClass);
sun.misc.Unsafe
类提供了CAS原子操作,能够实现高性能的线程安全的无锁数据结构。sun.misc.Unsafe
类的CAS操作是java.util.concurrent
包的基础,LockSupport
,AbstractQueuedSynchronized
,AtomicInteger
等原子变量和锁框架都基于CAS操作实现的。
由于CAS操作在执行时当前线程不会被阻塞,所以通常使用自旋锁循环执行,直到操作成功时,表示获取到锁。
// 当Java对象o的域偏移offset上的值为excepted时,原子地修改为x。
// 如果修改成功,返回true。否则,返回false。
// 操作过程中线程不会阻塞。
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);
// 当Java对象o的域偏移offset上的值为int型的excepted时,原子地修改为x。
// 如果修改成功,返回true。否则,返回false。
// 操作过程中线程不会阻塞。
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
// 当Java对象o的域偏移offset上的值为int型的excepted时,原子地修改为x。
// 如果修改成功,返回true。否则,返回false。
// 操作过程中线程不会阻塞。
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
synchronized
是JVM最早提供的锁,称为监视器锁,也称对象锁。获得锁的过程称为monitorEnter,释放锁的过程称为monitorExit,锁的信息保存在对象头里,同步语句会在编译成字节码后转换成监视器语法(monitorEnter和monitorExit)。sun.misc.Unsafe
类提供了监视器的相关操作。
// 锁住对象
public native void monitorEnter(Object o);
// 尝试锁住对象
public native boolean tryMonitorEnter(Object o);
// 解锁对象
public native void monitorExit(Object o);
在实现java.util.concurrent.AbstractQueued
类,并基于AQS实现整个JUC锁框架的过程中,一方面需要使用sun.misc.Unsafe
类的CAS操作进行锁的获取(标记位state的修改),另一方在获取锁失败时要把当前线程放入等待队列,并阻塞当前线程。阻塞当前的线程的方法也是sun.misc.Unsafe
类提供的。
// 阻塞当前线程。
// 直到通过unpark方法解除阻塞,或者线程被中断,或者指定的超时时间到期
// isAbsolute参数是指明时间是绝对的,还是相对的
// time单位是纳秒,如果为0则表示长期阻塞
public native void park(boolean isAbsolute, long time);
// 解除指定线程的阻塞状态。
public native void unpark(Object thread);
park方法的两个参数里并没有指定要阻塞的线程引用,JVM怎么知道要将哪个线程阻塞?而unpark方法又是如何将一个线程的阻塞状态解除的呢?要真正理解park和unpark的工作原理,需要深入到HotSpot的源码。
简单的讲,park和unpark本质上是通过HotSpot里的一个volatile共享变量(volatile int _counter)来通信的,当park时,这个变量设置为0,当unpark时,这个变量设置为1。
由此,我们发现使用park和unpark来对线程进行同步控制非常灵活,unpark甚至可以在park之前调用。park/unpark模型真正实现了线程之间的同步,Java线程之间不再需要一个Object(synchronized代表的对象锁,用对象头存储锁信息)或者其他变量来存储状态(AQS中的state变量)来存储状态,不再需要关心对方的状态。
对比Java5中提供的wait/notify/notifyAll同步体系。wait/notify机制有个很蛋疼的地方是,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。编程的时候就会很蛋疼。
unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。
在HotSpot的实现里,每个Java线程都有一个Parker实例,Parker类的定义如下:
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}
sun.misc.Unsafe
类还提供了抛出异常的能力。
// 在不通知验证器(verifier)的情况下,抛出异常。
public native void throwException(Throwable ee);
sun.misc.Unsafe
类还提供了一些对类和对象进行操作的函数。通过这些函数,用户可以在绕过虚拟机的情况下进行类的加载、初始化,或者对对象进行实例化。
// 让虚拟机在不进行安全检查的情况下定义一个类。
// 默认情况下,该类的类加载器和保护域来自调用类。
public native Class defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,
ProtectionDomain protectionDomain);
public native Class defineClass(String name, byte[] b, int off, int len);
// 在不调用构造函数的情况下,实例化类Class的一个对象
// 如果累Class还没有加载到JVM,则进行加载
public native Object allocateInstance(Class cls)
throws InstantiationException;
// 定义一个匿名类,该类将不被classloader,或系统目录感知
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
// 确保指定的类已经被初始化(加载到JVM)
public native void ensureClassInitialized(Class c);
本节记录了一些使用sun.misc.Unsafe
的实例,并对这些实例进行分析。
很多类为了封装的需要将构造函数声明成私有的,防止被实例化。在sun.misc.Unsafe
类面前,这中做法不堪一击。allocateInstance
方法可以在不调用构造函数的情况下,直接实例化类的一个对象。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeUser {
public static void main(String[] args) throws Exception {
// 由于安全限制,只有系统class loader才能使用getUnsafe()方法
// 普通用户只能通过反射实例化Unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// 实例化User,不调用构造函数
User user = (User) unsafe.allocateInstance(User.class);
user.setName("liang");
System.out.println(user.getName());
}
}
class User {
private String name;
private User() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
// 执行后输出
liang
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeUser {
public static void main(String[] args) throws Exception {
// 由于安全限制,只有系统classloader才能使用getUnsafe()方法
// 普通用户只能通过反射实例化Unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// 实例化User,不调用构造函数
User user = (User) unsafe.allocateInstance(User.class);
user.setName("liang");
user.setAge(28);
// 输出user对象中各个成员遍历的内存偏移值
for (Field f : user.getClass().getDeclaredFields()) {
System.out.println(f.getName() + " 对应的内存偏移地址: " + unsafe.objectFieldOffset(f));
}
System.out.println("---------------------");
// 获取age内存偏移量
long ageOffset =
unsafe.objectFieldOffset(user.getClass().getDeclaredField("age"));
// // 获取name内存偏移量
long nameOffset =
unsafe.objectFieldOffset(user.getClass().getDeclaredField("name"));
// 修改age值
unsafe.putInt(user, ageOffset, 29);
// 修改name值
unsafe.putObject(user, nameOffset, "zhang liang");
System.out.println("age: " + user.getAge());
System.out.println("name: " + user.getName());
}
}
class User {
private int age;
private String name;
private User() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
// 执行后输出:
age 对应的内存偏移地址: 12
name 对应的内存偏移地址: 16
---------------------
age: 29
name: zhang liang
Java中数组的最大长度为Integer.MAX_VALUE,正常情况下如果想创建一个大于Integer.MAX_VALUE的数组是做不到的,但是Unsafe可以,通过对内存进行直接分配实现。
public class BigArray {
public static void main(String[] arg) throws Exception {
// 由于安全限制,只有系统classloader才能使用getUnsafe()方法
// 普通用户只能通过反射实例化Unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
//只要内存够大,可以把这个调大,大于Integer.MAX_VALUE
long size = (long) Integer.MAX_VALUE * 2;
long addr = unsafe.allocateMemory(size);
System.out.println("unsafe address :" + addr);
for (int i = 0; i < size; i++) {
unsafe.putByte(addr + i, (byte) 6);
if (unsafe.getByte(addr + i) != 6) {
System.out.println("failed at offset");
}
}
}
}
// 运行结果
unsafe address :4754382848
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。
整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class Lock {
public static void main(String[] args) throws Exception {
// 由于安全限制,只有系统classloader才能使用getUnsafe()方法
// 普通用户只能通过反射实例化Unsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
WaitThread waitThread = new WaitThread(unsafe);
waitThread.start();
WorkThread workThread = new WorkThread(unsafe, waitThread);
workThread.start();
workThread.join();
System.out.println("the end.");
}
}
/**
* 工作线程
*/
class WorkThread extends Thread {
private Thread waitThread;
private Unsafe unsafe;
public WorkThread(Unsafe unsafe, Thread waitThread) {
this.waitThread = waitThread;
this.unsafe = unsafe;
}
public void run() {
int i = 0;
while (true) {
if (i == 5) {
System.out.println("WorkThread is now to wake WaitThread");
unsafe.unpark(waitThread);
break;
}
System.out.println("WorkThread is now working for " + (++i) + " s");
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class WaitThread extends Thread {
private Unsafe unsafe;
public WaitThread(Unsafe unsafe) {
this.unsafe = unsafe;
}
public void run() {
System.out.println("Wait Thread is now going to block!");
unsafe.park(false, 0);
System.out.println("WaitThread is now awake");
}
}
// 执行结果
Wait Thread is now going to block!
WorkThread is now working for 1 s
WorkThread is now working for 2 s
WorkThread is now working for 3 s
WorkThread is now working for 4 s
WorkThread is now working for 5 s
WorkThread is now to wake WaitThread
WaitThread is now awake
the end.
参考:
]]>为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读锁本质上是一种共享锁,写锁本质上是一种互斥锁。Java通过ReadWriteLock接口声明了读写锁的相关操作,通过该接口用户可以同时获取一个读锁实例和写锁实例。ReentrantReadWriteLock是ReadWriteLock的唯一实现,该类通过静态内部类的方式实现了ReadLock和WriteLock,并且根据需要提供了公平锁(FairSync)和非公平所(NonfairSync)的实现。
本文重点关注ReadWriteLock接口的设计,具体实现会在ReentrantReadWriteLock类中进行具体分析。
本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
Lock readLock();
Lock writeLock();
返回一个读锁。读锁本质上是一个共享锁。在Java的实现中,共享锁通过计数器实现,区分公平锁和非公平锁。
返回一个写锁。写锁本质上是一个独占锁。在Java的实现中,区分公平锁和非公平锁。
]]>本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
相比同步锁,JUC包中的锁的功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁,只是它的用法更难罢了。
JUC包中的锁,包括:
在JUC包中,Lock接口定义了一个锁应该拥有基本操作。Lock接口的实现类非常多,既有共享锁,也有独占锁,甚至在ConcurrentHashMap等并发集合里的Segment结构本质上也是锁的实现。另外,Lock接口还组合了一个Condition类型的条件变量,用于提供更加灵活、高效的控制操作。
本文重点关注Lock接口的设计,具体实现会在各个实现类中进行具体分析。
本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
// 获取锁
// 如果获取失败,则进入阻塞队列
// 忽略了中断,在成功获取锁之后,再根据中断标识处理中断,即selfInterrupt中断自己
void lock();
// 获取锁
// 如果获取失败,则进入阻塞队列
// 在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断。
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁
// 获取成功,返回true
// 获取失败,返回fasle
// 不阻塞
boolean tryLock();
// 尝试获取锁
// 获取成功,返回true
// 获取失败,返回false
// 该操作必须在time时间内完成
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 创建一个条件变量,用于更加精细地控制同步过程
Condition newCondition();
]]>Condition是一个接口,用于定义条件变量。条件变量的实例化是通过一个Lock对象调用newCondition()方法获取的,这样,条件变量就和一个锁对象绑定起来了。Java中的条件变量只能和锁配合使用,来控制编发程序访问竞争资源的安全。条件变量增强了juc包下基于AQS锁框架的灵活性。对比synchronized代表的监视器锁,条件变量将锁和监视器操作(await, signal, signalAll)分离开来,而且一个锁可以绑定多个条件变量,每个条件变量的实例会维护一个单独的等待队列。条件变量使得锁框架能更加精细控制线程等待与唤醒。在AbstractQueuedSynchronizer和AbstractQueuedLongSynchronizer类中分别有一个实现ConditionObject,为整个AQS框架提供条件变量的相关能力。
本文重点关注Condition接口的设计,具体实现会在AbstractQueuedSynchronizer类中进行具体分析。
本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
signal()和signal()函数的字面意思很好理解,signal()负责唤醒等待队列中的一个线程,signalAll负责唤醒等待队列中的所有线程。
那什么时候用signal()?什么时候用signalAll()?
答案是:避免死锁的情况下,要用signalAll(),其他情况下两者可以通用,甚至signal()的效率要高一些。
参考:
]]>队列同步器AbstractQueuedSynchronizer(以下简称AQS),是用来构建锁或者其他同步组件的基础框架。它使用一个int成员变量来表示同步状态(重入次数,共享状态等),通过CAS操作对同步状态进行修改,确保状态的改变是安全的。通过内置的FIFO(First In First Out)队列来完成资源获取的排队工作。在AQS里有两个队列,分别是维护Sync Queue和Condition Queue,两个队列的节点都是AQS的静态内部类Node。Sync Queue在独占模式和共享模式中均会使用到,本质上是一个存放Node的CLH队列(主要特点是, 队列中总有一个 dummy 节点, 后继节点获取锁的条件由前继节点决定, 前继节点在释放 lock 时会唤醒sleep中的后继节点),维护的是等待获取锁的线程信息。Condition Queue在独占模式中才会用到,当用户使用条件变量进行线程同步时,维护的是等待条件变量的线程信息。
通过AQS实现的锁分独占锁(ReentrantLock,WriteLock,Segment等)和共享锁(ReadLock),使用一个volatile修饰的int类型的变量state来表示当前同步块的状态。state在AQS中功能强大,即可以用来表示同步器的加锁状态,也可以用来表示重入锁的重入次数(tryAcquire),还可以用来标识读锁和写锁的加锁状态。
在AQS的基础上,JUC包实现了如下几类锁:
1,公平锁和非公平所
2,可重入锁
3,独占锁和共享锁
以上三类锁并不是独立的,可以有多种组合。
1,ReentrantLock:可重入锁,公平锁|非公平锁,独占锁。
2,ReentrantReadWriteLock:可重入锁,公平锁|非公平锁,独占锁|共享锁。
另外,除了上面列举的ReentrantLock和ReentrantReadWriteLock外,下面几个类也是依靠AQS实现的。
1,CountDownLatch
2,CyclicBarrier
3,Semaphore
4,Segment
AQS主要包含下面几个特点,是我们理解AQS框架的关键:
1,内部含有两条队列(Sync Queue,Condition Queue)。
2,AQS内部定义获取锁(acquire),释放锁(release)的主逻辑,子类实现相应的模板方法。
3,支持共享和独占两种模式(共享模式时只用Sync Queue,独占时只用Sync Queue,但如果涉及条件变量Condition,则还有Condition Queue)。
4,支持不响应中断获取独占锁(acquire),响应中断获取独占锁(acquireInterruptibly),超时获取独占锁(tryAcquireNanos);不响应中断获取共享锁(acquireShared),响应中断获取共享锁(acquireSharedInterruptibly),超时获取共享锁(tryAcquireSharedNanos);
5,在子类的tryAcquire,tryAcquireShared中实现公平和非公平的区分。
本文重点介绍AbstractQueuedSynchronizer的设计,其实现待到具体的子类再做分析。
本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
Node是AbstractQueuedSynchronizer的静态内部类,文章概述里,我们说在AQS中有两类等待队列(Sync Queue和Condition Queue),Node就是等待队列的节点类。AQS的等待队列是"CLH"锁队列的变种。"CLH"锁是一种自旋锁,在AQS中
// 标识节点是否是 共享的节点(这样的节点只存在于 Sync Queue 里面)
static final Node SHARED = new Node();
// 标识节点是 独占模式
static final Node EXCLUSIVE = null;
// 代表线程已经被取消
static final int CANCELLED = 1;
// 代表后续节点需要唤醒
static final int SIGNAL = -1;
// 代表线程在condition queue中,等待某一条件
static final int CONDITION = -2;
// 代表后续结点会传播唤醒的操作,共享模式下起作用
static final int PROPAGATE = -3;
// 当前节点的状态
volatile int waitStatus;
// 当前节点的上一个节点
volatile Node prev;
// 当前节点的下一个节点
volatile Node next;
// 当前节点代表的线程
volatile Thread thread;
// 这个节点等待的模式(共享模式和独占模式)
Node nextWaiter;
// 空制造函数
Node();
// 构造函数,初始化nextWaiter
// addWaiter使用
Node(Thread thread, Node mode);
// 构造函数,初始化waitStatus
// Condition使用
Node(Thread thread, int waitStatus);
// 如果当前节点的等待模式(nextWaiter)是共享模式,返回true
final boolean isShared();
// 返回当前节点的上一个节点
final Node predecessor() throws NullPointerException;
参考:
]]>在JUC包中实现的同步器锁分为独占锁(如ReentrantLock、WriteLock)和共享锁(ReadLock)。共享锁本质上是通过对volatile修饰的计数器state进行维护而实现的。独占锁则是通过在同步器中设置独占线程来实现的。在JUC包中AbstractOwnableSynchronizer是个抽象类,它维护了一个Thread类型的成员变量,标识当前独占同步器的线程引用。AbstractOwnableSynchronizer的子类是大名鼎鼎的AbstractQueuedSynchronizer和AbstractQueuedLongSynchronizer,这两个子类是实现JUC包下锁框架的基础。
本文重点研究AbstractOwnerSynchronizer抽象类的设计,具体实现会在AbstractQueuedSynchronizer类中进行分析。
本文基于JDK1.7.0_67
java version "1.7.0_67"_
_Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
在AbstractOwnableSynchronizer类中只有一个成员变量exclusiveOwnerThread,该变量记录当前独占同步器的那个线程。
private transient Thread exclusiveOwnerThread;
// 空实现的构造函数,供子类实现
protected AbstractOwnableSynchronizer();
// 设置同步器的独占线程
protected final void setExclusiveOwnerThread(Thread t);
// 获取同步器的独占线程
protected final Thread getExclusiveOwnerThread();
]]>锁作为数据同步工具,Java提供了两种实现:synchronized和AQS,这两种锁的实现根本不同,但是在加锁和解锁的过程中,也有很多共同点。它们在进行加锁/解锁时或多或少的用到自旋锁的设计思想。对于这几种自旋锁设计思想的研究,可以帮助我们更好的理解Java的Lock框架。
Spin锁即自旋锁。自旋锁是采用让当前线程不停地在循环体内检测并设置临界资源的状态
,直到状态满足条件并设置为指定的新状态。检测并设置临界资源
操作必须是原子的,这样即使多个线程在给定时间自旋,也只有一个线程可获得该锁。
自旋锁的优点之一是自旋的线程不会被阻塞,一直处于活跃状态,对于锁保护的临界区较小的情况下,自旋获取锁和释放锁的成本都比较低,时间比较短。
在JAVA中,我们可以使用原子变量和Unsafe类的CAS操作来实现自旋锁:
public class SpinLock {
private AtomicReference<Thread> atomic = new AtomicReference<Thread>();
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则设置当前线程为锁的拥有者。
while(!atomic.compareAndSet(null, currentThread)) {}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的拥有者能释放锁
atomic.compareAndSet(currentThread, null);
}
}
自旋锁在Linux内核中广泛使用。在Linux操作系统中,自旋锁是一个互斥设备,它只有两个值锁定
和解锁
。
由于操作系统和CPU直接打交道,自旋锁又可分为在单核处理器上和多核处理器上。
用在单核处理器上,有可分为两种:
此时自旋锁什么也不做,确实也不需要做什么,因为单核处理器只有一个线程在执行,又不支持内核抢占,因此资源不可能会被其他的线程访问到。
这种情况下,自旋锁加锁仅仅是禁止了内核抢占,解锁则是启用了内核抢占。
在上述两种情况下,在获取自旋锁后可能会发生中断,若中断处理程序去访问自旋锁所保护的资源,则会发生死锁。因此,linux内核又提供了spin_lock_irq()和spin_lock_irqsave(),这两个函数会在获取自旋锁的同时(同时禁止内核抢占),禁止本地外部可屏蔽中断,从而保证自旋锁的原子操作。
多核处理器意味着有多个线程可以同时在不同的处理器上并行执行。
举个例子:
四核处理器,若A处理器上的线程1获取了锁,B、C两个处理器恰好这个时候也要访问这个锁保护的资源,因此他俩CPU就一直自旋忙等待。D并不需要这个资源,因此它可以正常处理其他事情。
自旋锁的几个特点:
1.被自旋锁保护的临界区代码执行时不能睡眠。单核处理器下,获取到锁的线程睡眠,若恰好此时CPU调度的另一个执行线程也需要获取这个锁,则会造成死锁;多核处理器下,若想获取锁的线程在同一个处理器下,同样会造成死锁,若位于另外的处理器,则会长时间占用CPU等待睡眠的线程释放锁,从而浪费CPU资源。
2.被自旋锁保护的临界区代码执行时不能被其他中断打断。
3.被自旋锁保护的临界区代码在执行时,内核不能被抢占。
// 最基本得自旋锁函数,它不失效本地中断。
void spin_lock(spinlock_t *lock);
// 在获得自旋锁之前禁用硬中断(只在本地处理器上),而先前的中断状态保存在flags中
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
// 在获得自旋锁之前禁用硬中断(只在本地处理器上),不保存中断状态
void spin_lockirq(spinlock_t *lock);
// 在获得锁前禁用软中断,保持硬中断打开状态
void spin_lock_bh(spinlock_t *lock);
Ticket锁即排队自旋锁,Ticket锁是为了解决上面自旋锁的公平性问题,类似于现实中海底捞的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮训锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮训。
当前线程释放锁时,将服务号加1,这样下一个线程看到这个变化,就退出自旋,表示获取到锁。
在JAVA中,我们可以使用原子变量和Unsafe类的CAS操作来实现Ticket自旋锁:
public class TicketLock {
private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
private AtomicInteger ticketNum = new AtomicInteger(); // 排队号
public int lock() {
// 首先原子性地获得一个排队号
int myTicketNum = ticketNum.getAndIncrement();
// 只要当前服务号不是自己的就不断轮询
while (serviceNum.get() != myTicketNum) {}
return myTicketNum;
}
public void unlock(int myTicket) {
// 只有当前线程拥有者才能释放锁
int next = myTicket + 1;
serviceNum.compareAndSet(myTicket, next);
}
}
Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum
,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
排队自旋锁(FIFO Ticket Spinlock)是Linux内核2.6.25版本引入的一种新型自旋锁,它解决了传统自旋锁由于无序竞争导致的"公平性"问题。但是由于排队自旋锁在一个共享变量上“自旋”,因此在锁竞争激烈的多核或 NUMA 系统上导致性能低下。
MCS自旋锁是一种基于链表的高性能、可扩展的自旋锁。申请线程之在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
MCS锁的设计目标如下:
在JAVA中,我们可以使用原子变量和Unsafe类的CAS操作来实现MCS自旋锁:
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isBlock = true; // 本地自旋变量,默认是在等待锁
}
volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater UPDATER =
AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) {
predecessor.next = currentThread;// step 2
while (currentThread.isBlock) {// step 3
}
}else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞
currentThread.isBlock = false;
}
}
public void unlock(MCSNode currentThread) {
if (currentThread.isBlock) {// 锁拥有者进行释放锁才有意义
return;
}
if (currentThread.next == null) {// 检查是否有人排在自己后面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isBlock = false;
currentThread.next = null;// for GC
}
}
目前 Linux 内核尚未使用 MCS Spinlock。根据上节的算法描述,我们可以很容易地实现 MCS Spinlock。本文的实现针对x86 体系结构(包括 IA32 和 x86_64)。原子交换、比较-交换操作可以使用带 LOCK 前缀的 xchg(q),cmpxchg(q)[3] 指令实现。
CLH(Craig, Landin, and Hagersten)锁也是基于链表的可扩展、高性能、公平的自旋锁,申请线程旨在本地变量上自旋,它不断轮训前驱的状态,如果发现前驱释放了锁就结束自旋。
在Java中CLH的应用非常广泛,比如JUC包下的锁框架AbstractQueuedSynchronized就是基于CLH实现的,并进而实现了整个Lock框架体系。
在JAVA中,我们可以使用原子变量和Unsafe类的CAS操作来实现CLH自旋锁:
public class CLHLock {
public static class CLHNode {
private volatile boolean isLocked = true; // 默认是在等待锁
}
@SuppressWarnings("unused" )
private volatile CLHNode tail ;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode .class , "tail" );
public void lock(CLHNode currentThread) {
CLHNode preNode = UPDATER.getAndSet( this, currentThread);
if(preNode != null) {//已有线程占用了锁,进入自旋
while(preNode.isLocked ) {
}
}
}
public void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER .compareAndSet(this, currentThread, null)) {
// 还有后续线程
currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋
}
}
}
下图是经典的CLH锁和MCS锁队列图示:
差异:
注意:这里实现的锁都是独占的,且不能重入的。
参考:
]]>