上一讲我们讲述了线程池整个的过程,这一讲我们来先底层的3个组件,synchronized,Unsafe以及LockSupport
Unsafe
常用api
/**
* 获取对象指定Field对应的内存地址偏移量,可以理解为跟C++中的指针一样,获取到了属性的地址,在一个对象中
* 属性的偏移地址是固定的,不会发生变化
* @param var1
* @return
*/
public native long objectFieldOffset(Field var1);
/**
* 可以修改对象属性值
* @param var1 对象实例
* @param var2 属性偏移量
* @param var4 要修改的值
*/
public native void putInt(Object var1, long var2, int var4);
/**
* 获取对象实例
* @param var1
* @return
* @throws InstantiationException
*/
public native Object allocateInstance(Class<?> var1) throws InstantiationException;
/**
* CAS
* @param var1
* @param var2
* @param var4
* @param var5
* @return
*/
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);Unsafe实例的获取
首先从名字就可以看出来这个类是不安全的,比较敏感,所以jdk底层也没有开放使用这个类,该类提供了单利模式获取Unsafe对象的方法:
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
//类加载器必须是root类加载器,否则不允许使用该类
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
所以我们在应用中要获取该类的实例,就得通过反射获取了,反射获取静态私有属性的方式:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);api实例使用
对象属性偏移量获取
public class UnsafeTest {
private Integer abc = 10;
public UnsafeTest(){
abc = 10;
}
public Integer getAbc() {
return abc;
}
public static void main(String[] args) {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);
Class<UnsafeTest> unsafeTestClass = UnsafeTest.class;
Field[] declaredFields = unsafeTestClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField.getName() + ",偏移地址:" + unsafe.objectFieldOffset(declaredField));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}修改对象属性值
@Test
public void updateObjectField()throws Exception{
Unsafe unsafe = getUnsafe();
UnsafeTest unsafeTest = new UnsafeTest();
Field abc = unsafeTest.getClass().getDeclaredField("abc");
System.out.println(unsafeTest.getAbc());
unsafe.putObject(unsafeTest, unsafe.objectFieldOffset(abc), 40);
System.out.println(unsafeTest.getAbc());
}这里需要注意下,修改对象的属性值,如果是基本属性,有对应的方法,其他包装类型或者对象的属性,需要用unsafe.putObject();
获取对象实例
@Test
public void allocateInstanceTest()throws Exception{
A a = new A();//age=10
A o = (A)getUnsafe().allocateInstance(A.class);
}CAS
@Test
public void CasTest()throws Exception{
Unsafe unsafe = getUnsafe();
UnsafeTest unsafeTest = new UnsafeTest();
Field abc = unsafeTest.getClass().getDeclaredField("abc");
unsafe.compareAndSwapObject(unsafeTest, unsafe.objectFieldOffset(abc),10, 40);
System.out.println(unsafeTest.getAbc());
}该操作是个原子的,也就是先对比,再进行修改。那是怎么保证这个原子性的呢?这是个本地方法,所以其实现是通过c++来实现的:
compareAndSwap (volatile jint *addr, jint old, jint new_val)
{
jboolean result = false;
spinlock lock;//关键代码
if ((result = (*addr == old)))
*addr = new_val;
return result;
}总线锁定
缓存行锁定
我么可以看到,通过这两种机制,处理器会保证对一个变量的读-改-写保证原子操作,回到Unsafe.compareAndSwapObject()这个方法上来,那这个方法的执行是可以保证原子性的。
Atomic系列原子类
我们常用的Atomic*系列原子类,底层也就是通过Unsafe来操作的,可以举个例子,AtomicInteger,看下这个类的几个api
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}缺点
- ABA问题,可以类比我们常用的数据库的操作,我们在更新一些敏感字段的时候,如果是多线程操作,不加分布式锁或者其他处理的话,会存在丢失更新的问题,我们平常的处理都会加乐观锁,也可以通过分布式锁去处理,那其实在jdk也为我们提供了这样类似的方式:AtomicStampedReference
- 循环问题,如果一直不成功的话,会极大的占用cpu资源.看下unsafe中的实现:
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;
}3. 只能操作一个共享变量,不能操作多个。如果要操作多个,可以通过设置多维度变量,比如有两个共享变量t=2,k=p,合并一下tk=2p,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
synchronized
同步原理
- 同步普通方法,锁的是实例对象
- 同步静态方法,锁的是class对象
- 同步代码块,锁的是synchronized里面的对象
而对于代码块的同步,我们可以通过反编译代码看出来,是通过monitorenter和monitorexit来实现的,而对于同步方法,是通过在方法声明处标识ACC_SYNCHRONIZED
public class SynchronizedTest {
private Object lock = new Object();
public void execute(){
synchronized (lock){
System.out.println("asd");
}
}
}对象头
| mark word | 锁标志位 | 代表锁状态 |
|---|---|---|
| 对象哈希码、对象分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
| 空,不需要记录信息 | 11 | GC标记 |
| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
偏向锁
轻量级锁
获取锁
线程在获取锁之前,会在其栈帧创建一份存储锁记录的空间lock record,如下图:
释放锁
LockSupport.park()/unpark()
demo使用
LockSupport.park()/unpark()可以实…
@Test
public void parkTest(){
final Thread thread = Thread.currentThread();
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println( "子线程:许可释放成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(thread);
}
}).start();
System.out.println("主线程:等待许可中...");
LockSupport.park();// 获取许可,阻塞
System.out.println("主线程:获取许可成功");
}主线程:等待许可中...
子线程:许可释放成功
主线程:获取许可成功 @Test
public void parkUnParkTest(){
final Thread mainThread = Thread.currentThread();
mainThread.setName("主线程");
LockSupport.unpark(mainThread);//释放许可
LockSupport.unpark(mainThread);//释放许可
LockSupport.park(mainThread);
System.out.println("主线程:获取许可成功");
LockSupport.park(mainThread);//会阻塞
}LockSupport还提供了一个带参数的park()方法:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}wait/notify比较
- 在使用wait/notify的时候,首先我们同构notify没法保证要让哪个线程去竞争锁,因为这个方法只会在等待的队列中随机选一个线程去唤醒,所以可能我们更多的都是通过notifyAll来唤醒所有的线程,所有的线程都唤醒了,无疑又增大了锁的竞争,又会出现大批线程的状态的切换,而通过LockSupport.unpark(Thread t) 我们可以有针对的去指定要让哪个线程唤醒,这让我们的应用更具有可控性,同时也避免了大量线程同时去竞争锁的资源消耗。
- notify/notifyAll必须在wait之后调用,如果在wait之前调用了,可能会出现线程一直被阻塞,无法唤醒的状态。而unpark/park 无需关心次序,调用一次unpark()就是给了一次许可,调用park就是获取了这个许可,所以在使用上park/unpark 更方便些。
- 机制不同。可以看下这篇文章的讲解LockSupport源码,里面针对park/unpark的jvm源码进行了讲解,这里就简单提下,从源码中可以看到底层是用了Posix的mutex,condition来实现的。而在wait方法上我们可以看到有这么一句注释: 那调用wait导致线程阻塞的原因其实是在等待synchronized锁,也就是说如果调用wait导致线程阻塞或者获取synchronized导致线程阻塞,通过LockSupport.unpark()是唤不醒的。
@Test
public void parkUnParkTest2(){
final Thread mainThread = Thread.currentThread();
final Object lock = new Object();
mainThread.setName("主线程");
new Thread(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("子线程");
try {
TimeUnit.SECONDS.sleep(5);
LockSupport.unpark(mainThread);
System.out.println(Thread.currentThread().getName() + " invoke unpark");
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " invoke notify");
synchronized (lock){
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
synchronized (lock) {
try {
System.out.println("主线程正在等待");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("主线程执行结束");
}主线程正在等待
子线程 invoke unpark
子线程 invoke notify
主线程执行结束LockSupport源码分析
LockSupport用到的成员变量不多,就两个,其他的都是内部的UNFAFE要用的:
// Hotspot implementation via intrinsics API
private static final sun.misc.Unsafe UNSAFE;
private static final long parkBlockerOffset;parkBlockerOffset这个变量看着就熟悉了,刚刚我们看到的可以记录线程被谁阻塞的,给监控工具和线程堆栈提供可用的分析信息,我们可以通过api来获取这个blocker
/**
* Returns the blocker object supplied to the most recent
* invocation of a park method that has not yet unblocked, or null
* if not blocked. The value returned is just a momentary
* snapshot -- the thread may have since unblocked or blocked on a
* different blocker object.
*
* @param t the thread
* @return the blocker
* @throws NullPointerException if argument is null
* @since 1.6
*/
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}而这个parkBlockerOffset对应的属性其实是在Thread源码里面定义的parkBlocker成员变量
/**
* The argument supplied to the current call to
* java.util.concurrent.locks.LockSupport.park.
* Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
* Accessed using java.util.concurrent.locks.LockSupport.getBlocker
*/
volatile Object parkBlocker;即我们调用LockSupport的park/unpark方法的时候,也会在线程里面通过setBlocker来设置这个parkBlocker
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}小结
我们已经分别介绍了在J.U.C中常用的并发工具类的使用以及原理,这些都是我们在并发中常用的一些操作,同时为后面锁的分析打入一些基础,下篇文章我们来看下Lock