Java并发基础

作者: 横渡 | 来源:发表于2018-06-21 15:06 被阅读20次

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中方法。

  1. 通过JVM参数-Xbootclasspath指定要使用的类为启动类;
  2. 在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对象大小的方式。

  1. 通过java.lang.instrument.Instrumentation 的 getObjectSize(obj) 直接获取对象的大小;
  2. 通过sun.misc.Unsafe对象和objectFieldOffSet(field)等方法结合反射来计算对象的大小。

通过Unsafe获取Java对象大小的基本思路如下:

  1. 通过反射获得一个类的Field;
  2. 通过Unsafe的objectFieldOffset()获得每个Field的off。Set;
  3. 对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);
    }
    ...

其他使用方式

  1. 通过Unsafe的defineClass可以动态加载Class;
  2. 通过Unsafe的copyMemory、freeMemory等可以实现内存的复制与释放,如果我们知道了对象的大小,利用arrayBaseOffset和copyMemory可以完成对象的浅拷贝。

Unsafe还有其他很多的用途,但是要记得这是一个非常危险的类,在使用的过程中需要万分小心,不到万不得已的情况不要使用。

相关文章

网友评论

    本文标题:Java并发基础

    本文链接:https://www.haomeiwen.com/subject/nxaqyftx.html