• 005-多线程-锁-JUC锁-LockSupport【使用、Unsafe、对比Object的wait、底层源码】


    一、概述

      在Java多线程中,当需要阻塞或者唤醒一个线程时,都会使用LockSupport工具类来完成相应的工作。LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也因此成为了构建同步组件的基础工具。

      LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。 

      LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。

      因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。

      实现的阻塞和解除阻塞是基于”许可(permit)”作为关联,permit相当于一个信号量(0,1),默认是0. 线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态.

      在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。

    1.1、支持的函数

    // 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。
    static Object getBlocker(Thread t)
    // 为了线程调度,禁用当前线程,除非许可可用。
    static void park()
    // 为了线程调度,在许可可用之前禁用当前线程。
    static void park(Object blocker)
    // 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。
    static void parkNanos(long nanos)
    // 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。
    static void parkNanos(Object blocker, long nanos)
    // 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
    static void parkUntil(long deadline)
    // 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
    static void parkUntil(Object blocker, long deadline)
    // 如果给定线程的许可尚不可用,则使其可用。
    static void unpark(Thread thread)

      在Java 6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker, long nanos)、parkUntil(Object blocker, long deadline)这3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。下面的示例中,将对比parkNanos(long nanos)和parkNanos(Object blocker, long nanos)方法来展示阻塞对象blocker的用处。

      采用parkNanos(long nanos)阻塞线程:

    public class LockSupportTest {
        public static void main(String[] args) {
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(20));
        }
    }

      可以通过命令:jstack -l pid查看dump

    "main" #1 prio=5 os_prio=31 tid=0x00007fae82000000 nid=0x2603 waiting on condition [0x000070000eccb000]
       java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:338)
        at com.github.bjlhx15.common.thread.juc.collection.LockSupportTest.main(LockSupportTest.java:8)

      采用parkNanos(Object blocker, long nanos)阻塞线程:

    public class LockSupportTest1 {
        public static void main(String[] args) {
            LockSupport.parkNanos(new Object(), TimeUnit.SECONDS.toNanos(20));
        }
    }
    可以通过命令:jstack -l pid查看dump
    "main" #1 prio=5 os_prio=31 tid=0x00007fb3b5813000 nid=0x2603 waiting on condition [0x000070000861d000]
       java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076acfb350> (a java.lang.Object)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at com.github.bjlhx15.common.thread.juc.collection.LockSupportTest1.main(LockSupportTest1.java:8)

      这两段代码都是 阻塞当前线程20秒,从上面的dump结果可以看出,有阻塞对象的parkNanos方法能够传递给开发人员更多的现场信息。这是由于在Java 5之前,当线程使用synchronized关键字阻塞在一个对象上时,通过线程dump能够看到该线程的阻塞对象,而Java 5推出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息。因此,在Java 6中,LockSupport新增了上述3个含有阻塞对象的方法,用以替代原有的park方法。

      通过源码可以发现,LockSupport的park和unpark方法都是通过sun.misc.Unsafe类的park和unpark方法实现的,那下面我们对sun.misc.Unsafe类的源码进行进一步解析。

    1.2、LockSupport源码分析

    从源码中得到初步信息:

      1、不能被实例化(构造函数是私有的)

      2、方法都是静态方法

      3、核心方法实现基于Unsafe类中的park和unpark方法

    1.2.1、UNSAFE

      JDK内部用的工具类, 可以直接操控内存,被JDK广泛用于自己的包中.它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。该类不应该在JDK核心类库之外使用。

      Unsafe类就和它的名字一样,是一个比较危险的类,它主要用于执行低级别、不安全的方法。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。如果我们要使用Unsafe类,首先需要获取Unsafe类的对象,但是它的构造函数是private的:

    private Unsafe() {}  

      只能通过Unsafe的getUnsafe()方法获取该类的对象:

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

      Unsafe类是比较危险的,它只有在授信代码中才会返回theUnsafe对象,否则,抛出SecurityException异常,那什么是授信的代码呢?我们看一看getClassLoader()方法:

    @CallerSensitive
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }

      该方法返回加载该类的类加载器,如果是被Bootstrap ClassLoader加载的类,则cl为null,然后我们再看VM.isSystemDomainLoader(ClassLoader)方法:

    public static boolean isSystemDomainLoader(ClassLoader loader) {
        return loader == null;
    }

      若类加载器为null,则返回true,即该代码为授信代码。所以,只要代码是被Bootstrap ClassLoader类加载器加载的类就是授信代码了。

      我们知道Bootstrap ClassLoader类加载器会加载-Xbootclasspath参数所指定的路径中的类,所以,我们可以修改-Xbootclasspath参数,将我们的代码所在的路径添加进去,那我们的代码就可以使用Unsafe类了;或者也可以使用反射从 Unsafe类上得到它私有的Unsafe实例。如下所示:

    public class UnsafeTest {
        public static void main(String[] args) throws Exception {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
        }
    }

      先看一下Unsafe的park方法和unpark方法:

    public native void park(boolean isAbsolute, long time);
    public native void unpark(Object thread);

      这两个类都是声明native的,其具体实现要去看Java虚拟机的源代码,接下来看一看Unsafe类的其他方法,看一看Unsafe还能做什么危险操作。

    1.2.2、Unsafe修改内存

      Unsafe类的putInt()和getInt()方法可以直接修改内存,如下面这一段代码:

        public static void main(String[] args) throws Exception {
            unsafePutGetInt();
        }
        private static Unsafe getUnsafe(){
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                Unsafe unsafe = (Unsafe) field.get(null);
                return unsafe;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static void unsafePutGetInt() throws Exception {
            Unsafe unsafe = getUnsafe();
    
            class Student {
                private int age = 5;
    
                public int getAge() {
                    return age;
                }
            }
    
            Student student = new Student();
            System.out.println(student.getAge());
    
            Field field = student.getClass().getDeclaredField("age");
    
            unsafe.putInt(student, unsafe.objectFieldOffset(field), 10);
    
            System.out.println(student.getAge());
        }
    }

    输出

    5
    10

      通过Unsafe直接修改了类的private变量值。类似的还有getBoolean()、putBoolean()、getChar()、putChar()等方法。

    1.2.3、Unsafe在非Java堆中分配内存 

      使用new关键字分配的内存会在堆中,并且对象的生命周期内,会被垃圾回收器管理。Unsafe类通过allocateMemory(long)方法分配的内存,不受Integer.MAX_VALUE的限制,并且分配在非堆内存,使用它时,需要非常谨慎,该部分内存需要手动回收,否则会产生内存泄露;非法的地址访问时,会导致Java虚拟机崩溃。在需要分配大的连续区域、实时编程时,可以使用该方式,java的nio使用了这一方法。

    public class UnsafeTest3 {
        public static void main(String[] args) throws Exception {
            unsafeAllocateMemory();
        }
        private static Unsafe getUnsafe(){
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                Unsafe unsafe = (Unsafe) field.get(null);
                return unsafe;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        public static void unsafeAllocateMemory() throws Exception {
            Unsafe unsafe = getUnsafe();
            int BYTE = 1;
    
            long address = unsafe.allocateMemory(BYTE);
            unsafe.putByte(address, (byte) 10);
            byte num = unsafe.getByte(address);
    
            System.out.println(num);
    
            unsafe.freeMemory(address);
        }
    }

    输出

    10

    1.2.4、Unsafe提供CAS原子操作

      Unsafe类中提供了compareAndSwapObject()、compareAndSwapInt()和compareAndSwapLong()这三个方法用来实现对应的CAS原子操作。在Java的并发编程中用到的CAS操作都是调用的Unsafe类的相关方法。我们以Unsafe实现一个自定义原子类:

    public class UnsafeTest4 {
        public static void main(String[] args) throws Exception {
            unsafeCAS();
    
        }
    
        private static Unsafe getUnsafe() {
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                Unsafe unsafe = (Unsafe) field.get(null);
                return unsafe;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    
        public static void unsafeCAS() throws Exception {
            Unsafe unsafe = getUnsafe();
            class MyAutomicInteger {
                private volatile int value = 0;
                private Unsafe unsafe;
                private long offset;
    
                public MyAutomicInteger(Unsafe unsafe) throws Exception {
                    this.unsafe = unsafe;
                    this.offset = unsafe.objectFieldOffset(MyAutomicInteger.class.getDeclaredField("value"));
                }
    
                public void increment() {
                    int oldValue = value;
    
                    for (;;) {
                        if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue + 1)) {
                            break;
                        }
    
                        oldValue = value;
                    }
                }
    
                public int getAndIncrement() {
                    int oldValue = value;
    
                    for (;;) {
                        if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue + 1)) {
                            return oldValue;
                        }
    
                        oldValue = value;
                    }
                }
    
                public int getValue() {
                    return value;
                }
            }
    
            MyAutomicInteger myAutomicInteger = new MyAutomicInteger(unsafe);
            myAutomicInteger.increment();
            System.out.println(myAutomicInteger.getValue());
    
            for (int i = 0; i < 5; i++) {
                System.out.println(myAutomicInteger.getAndIncrement());
            }
    
            System.out.println(myAutomicInteger.getValue());
        }
    }

    结果

    1
    1
    2
    3
    4
    5
    6

    可以看到,通过Unsafe类可以实现很多有趣的功能,这些方法都是比较底层的方法,而且效率比较高,但是使用起来却比较危险,因为Unsafe类中的方法与我们通常的用法相悖,比如,通过Unsafe类直接修改其他类的parivate变量,直接分配堆外内存等等,这很像c语言的malloc()方法。在平常的开发当中,并不建议直接使用Unsafe类。

    1.3、LockSupport的park(),unPark()与Thread的wait(),notify()区别

      1.面向的主体不一样。LockSuport主要是针对Thread进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程。

      2.实现机制不同。虽然LockSuport可以指定monitor的object对象,但和object.wait(),两者的阻塞队列并不交叉。

      3.实现机制和wait/notify有所不同,面向的是线程

      4.LockSupport不需要依赖监视器

      5.与wait/notify没有交集

      6.LockSupport使用起来方便灵活

      LockSupport比Object的wait/notify有两大优势

        ①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。

        ②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

    1.4、示例

    1.4.1、使用Object的wait和notify

        public static void objWaitNotify() throws InterruptedException {
            final Object obj = new Object();
            Thread A = new Thread(new Runnable() {
                @Override
                public void run() {
                    int sum = 0;
                    for (int i = 0; i < 10; i++) {
                        sum += i;
                    }
                    try {
                        obj.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(sum);
                }
            });
            A.start();
            //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
            Thread.sleep(1000);
            obj.notify();
    
        }

    出现错误

    java.lang.IllegalMonitorStateException
        at java.lang.Object.wait(Native Method)
        at java.lang.Object.wait(Object.java:502)
        at com.github.bjlhx15.datastructure.algorithm.thread.LockSupportDemo$1.run(LockSupportDemo.java:18)
        at java.lang.Thread.run(Thread.java:748)
    45
    Exception in thread "main" java.lang.IllegalMonitorStateException
        at java.lang.Object.notify(Native Method)
        at com.github.bjlhx15.datastructure.algorithm.thread.LockSupportDemo.objWaitNotify(LockSupportDemo.java:28)
        at com.github.bjlhx15.datastructure.algorithm.thread.LockSupportDemo.main(LockSupportDemo.java:5)

    原因,wait和notify/notifyAll方法只能在同步代码块里用。所以将代码修改为如下就可正常运行了:

        public static void objWaitNotifySynchronized() throws InterruptedException {
            final Object obj = new Object();
            Thread A = new Thread(new Runnable() {
                @Override
                public void run() {
                    int sum = 0;
                    for (int i = 0; i < 10; i++) {
                        sum += i;
                    }
                    try {
                        synchronized (obj) {
                            obj.wait();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(sum);
                }
            });
            A.start();
            //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
            Thread.sleep(1000);
            synchronized (obj) {
                obj.notify();
            }
        }

    1.4.2、使用LockSupport

        public static void lockSupport() throws InterruptedException {
            Thread A = new Thread(new Runnable() {
                @Override
                public void run() {
                    int sum = 0;
                    for(int i=0;i<10;i++){
                        sum+=i;
                    }
                    LockSupport.park();
                    System.out.println(sum);
                }
            });
            A.start();
            //睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
            Thread.sleep(1000);
            LockSupport.unpark(A);
        }

    二、应用

    2.1、ThreadPoolExecutor中使用

        public static  void testFeture() throws ExecutionException, InterruptedException {
            ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(1000);
            ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1000, TimeUnit.SECONDS,queue);
    
            Future<String> future = poolExecutor.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    TimeUnit.SECONDS.sleep(5);
                    return "hello";
                }
            });
            String result = future.get();
            System.out.println(result);
        }

      代码中我们向线程池中扔了一个任务,然后调用Future的get方法,同步阻塞等待线程池的执行结果。    

      问题1、get方法是如何组塞住当前线程?线程池执行完任务后又是如何唤醒线程的呢?

    等待

      查看submit方法

        public <T> Future<T> submit(Callable<T> task) {
            if (task == null) throw new NullPointerException();
            RunnableFuture<T> ftask = newTaskFor(task);
            execute(ftask);
            return ftask;
        }

      在submit方法里,线程池将我们提交的基于Callable实现的任务,封装为基于RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。

      进入newTaskFor方法,就一句话:return new FutureTask<T>(callable);

      所以,咱们主线程调用future的get方法就是FutureTask的get方法,线程池执行的任务对象也是FutureTask的实例。

      接下来看看FutureTask的get方法的实现:

        public V get() throws InterruptedException, ExecutionException {
            int s = state;
            if (s <= COMPLETING)
                s = awaitDone(false, 0L);
            return report(s);
        }

      判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。

        private int awaitDone(boolean timed, long nanos)
            throws InterruptedException {
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            WaitNode q = null;
            boolean queued = false;
            for (;;) {
                if (Thread.interrupted()) {
                    removeWaiter(q);
                    throw new InterruptedException();
                }
    
                int s = state;
                if (s > COMPLETING) {
                    if (q != null)
                        q.thread = null;
                    return s;
                }
                else if (s == COMPLETING) // cannot time out yet
                    Thread.yield();
                else if (q == null)
                    q = new WaitNode();
                else if (!queued)
                    queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                         q.next = waiters, q);
                else if (timed) {
                    nanos = deadline - System.nanoTime();
                    if (nanos <= 0L) {
                        removeWaiter(q);
                        return state;
                    }
                    LockSupport.parkNanos(this, nanos);
                }
                else
                    LockSupport.park(this);
            }
        }

      awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程。

    唤醒

      提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:

        public void run() {
            if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                             null, Thread.currentThread()))
                return;
            try {
                Callable<V> c = callable;
                if (c != null && state == NEW) {
                    V result;
                    boolean ran;
                    try {
                        result = c.call();
                        ran = true;
                    } catch (Throwable ex) {
                        result = null;
                        ran = false;
                        setException(ex);
                    }
                    if (ran)
                        set(result);
                }
            } finally {
                // runner must be non-null until state is settled to
                // prevent concurrent calls to run()
                runner = null;
                // state must be re-read after nulling runner to prevent
                // leaked interrupts
                int s = state;
                if (s >= INTERRUPTING)
                    handlePossibleCancellationInterrupt(s);
            }
        }

      c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,唤醒线程的工作就在这,

        private void finishCompletion() {
            // assert state > COMPLETING;
            for (WaitNode q; (q = waiters) != null;) {
                if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                    for (;;) {
                        Thread t = q.thread;
                        if (t != null) {
                            q.thread = null;
                            LockSupport.unpark(t);
                        }
                        WaitNode next = q.next;
                        if (next == null)
                            break;
                        q.next = null; // unlink to help gc
                        q = next;
                    }
                    break;
                }
            }
    
            done();
    
            callable = null;        // to reduce footprint
        }

      先是通过cas操作将所有等待的线程拿出来,然后便使用LockSupport的unpark唤醒每个线程。

    三、实现原理

    3.1、park

      进入LockSupport的park方法,可以发现它是调用了Unsafe的park方法,这是一个本地native方法,只能通过openjdk的源码看看其本地实现了。

        

      它调用了线程的Parker类型对象的park方法,如下是Parker类的定义:

        

      类中定义了一个int类型的_counter变量,咱们上文中讲灵活性的那一节说,可以先执行unpark后执行park,就是通过这个变量实现,看park方法的实现代码

        

      park方法会调用Atomic::xchg方法,这个方法会原子性的将_counter赋值为0,并返回赋值前的值。如果调用park方法前,_counter大于0,则说明之前调用过unpark方法,所以park方法直接返回。接着往下看:

        

      实际上Parker类用Posix的mutex,condition来实现的阻塞唤醒。如果对mutex和condition不熟,可以简单理解为mutex就是Java里的synchronized,condition就是Object里的wait/notify操作。

      park方法里调用pthread_mutex_trylock方法,就相当于Java线程进入Java的同步代码块,然后再次判断_counter是否大于零,如果大于零则将_counter设置为零。最后调用pthread_mutex_unlock解锁,相当于Java执行完退出同步代码块。如果_counter不大于零,则继续往下执行pthread_cond_wait方法,实现当前线程的阻塞。

       最后再看看unpark方法的实现吧,这块就简单多了,直接上代码:

        

      图中的1和4就相当于Java的进入synchronized和退出synchronized的加锁解锁操作,代码2将_counter设置为1,同时判断先前_counter的值是否小于1,即这段代码:if(s<1)。如果不小于1,则就不会有线程被park,所以方法直接执行完毕,否则就会执行代码3,来唤醒被阻塞的线程。

      通过阅读LockSupport的本地实现,我们不难发现这么个问题:多次调用unpark方法和调用一次unpark方法效果一样,因为都是直接将_counter赋值为1,而不是加1。简单说就是:线程A连续调用两次LockSupport.unpark(B)方法唤醒线程B,然后线程B调用两次LockSupport.park()方法, 线程B依旧会被阻塞。因为两次unpark调用效果跟一次调用一样,只能让线程B的第一次调用park方法不被阻塞,第二次调用依旧会阻塞。

  • 相关阅读:
    RabbitMQ学习总结(5)——发布和订阅实例详解
    RabbitMQ学习总结(4)——分发任务在多个工作者之间实例教程
    RabbitMQ学习总结(4)——分发任务在多个工作者之间实例教程
    RabbitMQ学习总结(3)——入门实例教程详解
    RabbitMQ学习总结(3)——入门实例教程详解
    RabbitMQ学习总结(2)——安装、配置与监控
    RabbitMQ学习总结(2)——安装、配置与监控
    ActiveMQ学习总结(4)——业界消息队列简介
    ActiveMQ学习总结(4)——业界消息队列简介
    华为云ModelArts图深度学习,学习知识还能考取微认证
  • 原文地址:https://www.cnblogs.com/bjlhx/p/11058086.html
Copyright © 2020-2023  润新知