线程进阶:多任务处理(17)——Java中的锁(Unsafe基础)

原创 2017年06月14日 18:41:06

1. 概述

本专题在之前的文章中详细介绍了Java中最常使用的一种锁机制——同步锁。但是同步锁肯定是不适合在所有应用场景中使用的。所以从本文开始,笔者将试图通过两到三篇文章的篇幅向读者介绍Java中锁的分类、原理和底层实现。以便大家在实际工作中根据应用场景进行使用。本篇文章我们先介绍Java中关于锁的底层实现的基础类sun.misc.Unsafe。

2. Unsafe原子操作

在介绍Java中除同步锁以外的其它锁机制前,我们要介绍一个Java中基于操作系统级别的原子操作类sun.misc.Unsafe,它是Java中对大多数锁机制实现的最基础类。请注意,JDK 1.8和之前JDK版本的中sun.misc.Unsafe类可提供的方法有较大的变化,而本文的讲解主要是基于JDK 1.8进行的。sun.misc.Unsafe类提供的原子操作基于操作系统直接对CPU进行操作,而以下这些方法又是sun.misc.Unsafe类中经常被使用的:

2-1. 在你的代码中使用Unsafe

Java语言出于封装性和安全性的考虑,它不允许技术人员直接使用Unsafe类和其中的各种方法,特别是不能直接使用“new”的方式对sun.misc.Unsafe类进行实例化(否则会报告“java.lang.SecurityException: Unsafe”错误)。但是你可以通过Java提供的反射机制获取到Unsafe类的实例:

……
Field f = null;
sun.misc.Unsafe unsafe = null;
try {
    f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    // 得到Unsafe类的实例
    unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}
……

2-2. Unsafe典型方法

2-2-1. Unsafe.objectFieldOffset(Field) 和类似方法

Unsafe中除了objectFieldOffset(Field) 这个方法外,还有一个类似的方法staticFieldOffset(Field)。这两个方法用于返回类定义中某个属性在主存中设定的偏移量。请看以下代码:

……
// 注意本代码中的unsafe对象就是根据之前代码获取到的

// 开始使用unsafe对象,分别找到UserPojo对象中child属性和name属性的内存地址偏移量
// 首先是UserPojo类中的child属性,在内存中设定的偏移位置
Field field = UserPojo.class.getDeclaredField("child");
// 这就是一旦这个类实例化后,该属性在内存中的偏移位置
long offset = unsafe.objectFieldOffset(field);
System.out.println("child offset = " + offset);

// 然后是UserPojo类中的name属性,在内存中设定的偏移位置
Field fieldName = UserPojo.class.getDeclaredField("name");
long nameOffset = unsafe.objectFieldOffset(fieldName);
System.out.println("name offset = " + nameOffset);
……

这两个方法相比后面要介绍的各种Unsafe方法来说,显得更为重要。因为Unsafe对象中属性的操作方式都是直接通过内存偏移量的方式找到操作目标。

2-2-2. Unsafe.compareAndSwapObject(Object, long, Object, Object)和类似方法

Unsafe中除了compareAndSwapObject 这个方法外,还有两个类似的方法:unsafe.compareAndSwapInt和unsafe.compareAndSwapLong。这些方法的作用就是对属性进行比较并替换(俗称的CAS过程——Compare And Swap)。当给定的对象中,指定属性的值符合预期,则将这个值替换成一个新的值并且返回true;否则就忽略这个替换操作并且返回false。

请注意CAS过程是sun.misc.Unsafe类中除了获取内存偏移量以外,提供的最重要的功能了——因为Java中很多基于“无同步锁”方式的功能实现原理都是基于CAS过程。请看如下示例代码:

……
UserPojo user = new UserPojo();
user.setName("yinwenjie");
user.setSex(11);
user.setUserId("userid");

// 获得sex属性的内存地址偏移量 
Field field = UserPojo.class.getDeclaredField("sex");
long sexOffset = unsafe.objectFieldOffset(field);

/* 
 * 比较并修改值
 * 1、需要修改的对象
 * 2、要更改的属性的内存偏移量
 * 3、预期的值
 * 4、设置的新值
 * */
// 为什么是Object而不是int呢?因为sex属性的类型是Integer不是int嘛
if(unsafe.compareAndSwapObject(user, sexOffset, 11, 13)) {
    System.out.println("更改成功!");
} else {
    System.out.println("更改失败!");
}
……

首先创建一个UserPojo类的实例对象,这个实例对象有三个属性name、sex和userId。接着我们找到sex属性在主存中设定的偏移量sexOffset,并进行CAS操作。请注意compareAndSwapObject方法的四个值:第一个值表示要进行操作的对象user,第二个参数通过之前获取的主存偏移量sexOffset告诉方法将要比较的是user对象中的哪个属性,第三个参数为技术人员所预想的该属性的当前值,第四个参数为将要替换成的新值。

那么将方法套用到以上的compareAndSwapObject执行过程中:如果当前user对象中sex属性为11,则将这个sex属性的值替换为13,并返回true;否则不替换sex属性的值,并且返回false。

2-2-3. Unsafe.getAndAddInt(Object, long, int)和类似方法

类似的方法还有getAndAddLong(Object, long, long),它们的作用是利用Unsafe的原子操作性,向调用者返回某个属性当前的值,并且紧接着将这个属性增加一个新的值。在java.util.concurrent.atomic代码包中,有一个类AtomicInteger,这个类用于进行基于原子操作的线程安全的计数操作,且这个类在JDK1.8+的版本中进行了较大的修改。以下代码示例了该类的getAndIncrement()方法中的实现片段:

……
public class AtomicInteger extends Number implements java.io.Serializable {
    ……
    private volatile int value;
    ……
    private static final long valueOffset;
    ……
    // 获取到value属性的内存偏移量valueOffset
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    ……
    /**
     * 这是JDK1.8中的实现
     * Atomically increments by one the current value.
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    ……
}

通过以上代码的演示可以看到,AtomicInteger类中定义了一个value属性,并通过unsafe.objectFieldOffset方法获取到了这个属性在主存中设定的偏移量valueOffset。接着就可以在getAndIncrement方法中直接使用unsafe.getAndAddInt的方式,通过偏移量valueOffset将value属性的值加“1”。但是该方法的实现在JDK1.8之前的版本中,实现代码却是这样的:

// 获取偏移量valueOffset的代码类似,这里就不再展示了
……
public final int getAndIncrement() {
    // 一直循环,直到
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
……
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
……

代码中采用的就是基于Unsafe的“乐观锁”进行实现的,后文中我们还会讲解java的自旋锁以及自旋锁的一种具体实现方式“乐观锁”,还会分析“乐观锁”适合使用的场景。在以上代码中,getAndIncrement方法内部会不停的循环,直到unsafe.compareAndSwapInt方法执行成功。但多数情况下,循环只会执行一次,因为多线程强占同一对象属性的情况并不是随时都会出现。

2-2-4. 其它典型方法

在JDK1.8中Unsafe还有一些其它实用的原子操作方法:

  • PutXXXXX(Object, long, short)

    类似的方法包括:putInt(Object, long, int)、putBoolean(Object, long, boolean)、putShort(Object, long, short)、putChar(Object, long, char)、putDouble(Object, long, double)等,这些都是针对指定对象中在偏移量上的属性值,进行直接设定。这些操作发生在CPU一级缓存(L1) 或者二级缓存(L2)中,但是这些方法并不保证工作在其它内核上的线程“立即看到”最新的属性值。

  • putXXXXXVolatile(Object, long, byte)

    类似的方法包括:putByteVolatile(Object, long, byte)、putShortVolatile(Object, long, short)、putFloatVolatile(Object, long, float)、putDoubleVolatile(Object, long, double)等等,这些方法的主要作用虽然也是直接针对偏移量改变指定对象中的属性值,但是这些方法保证工作在其它内核上的线程能“立即看到”最新的属性值——也就是说这些方法满足volatile语义(后续文章会详细介绍volatile的详细工作原理)

===============(接下文)

版权声明:欢迎转载,但是看在我辛勤劳动的份上,请注明来源:http://blog.csdn.net/yinwenjie(未经允许严禁用于商业用途!)

架构设计:负载均衡层设计方案(9)——负载均衡层总结下篇

很明显通过前面的八篇文章的介绍,并不能覆盖负载均衡层的所有技术,但是可以作为一个引子,告诉各位读者一个学习和使用负载均衡技术的思路。虽然后面我们将转向“业务层”和“业务通信”层的介绍,但是对负载均衡层...

架构设计:系统存储(1)——块存储方案(1)

在“系统存储”专题中,我们将按照“从上至下”的顺序向读者介绍整个“系统存储”体系。在这个专题中我们将至少介绍机械硬盘的主要结构、硬盘阵列的分类、操作系统的EXT文件系统、NAS文件共享存储方案、分布式...

线程基础:多任务处理(12)——Fork/Join框架(基本使用)

ForkJoinPool由Java大师Doug Lea主持编写,它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。本文中对Fork/Join框架的讲解...

线程基础:多任务处理(13)——Fork/Join框架(解决排序问题)

之前文章讲解Fork/Join框架的基本使用时,所举的的例子是使用Fork/Join框架完成1-1000的整数累加。这个示例如果只是演示Fork/Join框架的使用,那还行,但这种例子和实际工作中所面...

线程基础:多任务处理(16)——Fork/Join框架(排序算法性能补充)

本篇文章从单线程快速排序到多线程归并排序、再到多线程桶排序的方式,依次分析它的执行性能。本篇文章并不侧重于算法详细过程的讲解,关于快速排序、桶排序、归并排序等排序算法的详细过程请读者参见其它专门介绍算...

线程基础:多任务处理(15)——Fork/Join框架(要点2)

Fork/Join Pool采用优良的设计、代码实现和硬件原子操作机制等多种思路保证其执行性能。其中包括(但不限于):计算资源共享、高性能队列、避免伪共享、工作窃取机制等。本文(以及后续文章)试图和读...

线程基础:多任务处理(14)——Fork/Join框架(要点1)

Fork/Join Pool采用优良的设计、代码实现和硬件原子操作机制等多种思路保证其执行性能。其中包括(但不限于):计算资源共享、高性能队列、避免伪共享、工作窃取机制等。本文(以及后续文章)试图和读...

java线程进阶

一、线程的生命周期       这里所说的线程的生命周期,也是根据Thread类里面的方法来定义的。JDK API 1.6里和生命周期有关的方法有一些几个:       1、interrupt()...

疯狂Java学习笔记(63)-----------线程进阶

Java线程各个阶段的状态:       那么我们开始一点点研究吧:   启动线程   一、定义线程   1、扩展java.lang.Thread类。   此类中有个run()方法,应该注意其用法:...

Java学习笔记(63)-----------线程进阶

Java线程各个阶段的状态:       那么我们开始一点点研究吧:   启动线程   一、定义线程   1、扩展java.lang...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:线程进阶:多任务处理(17)——Java中的锁(Unsafe基础)
举报原因:
原因补充:

(最多只允许输入30个字)