美文网首页
java多线程与高并发(六)AQS源码阅读与强软弱虚4中引用以及

java多线程与高并发(六)AQS源码阅读与强软弱虚4中引用以及

作者: 小偷阿辉 | 来源:发表于2021-04-26 11:08 被阅读0次

    1.AQS源码分析

    AQS核心是什么,就是一个state,这个代表的意思由其子类决定,我们先来讲一下ReentranLock,刚才state为0,当你获得锁之后,它就变成了1,释放的时候变成0,这个state值得基础之上,它的下面还带有一个队列,是AQS自己内部所维护的队列,队列所维护的是一个node节点,node节点是AQS的内部类,最重要的是,它保留了一个Thread一个线程,所以这个队列是线程队列,而且还有prev和next分别指向前后两个节点,所以是一个双向列表
    所以,AQS的核心就是state和监控这个state双向链表,哪个节点的线程得到了state,哪个线程要等待,都要进入到队列里边。当我们acquire(1)上来以后看到这个state是0,那就直接拿到state这把锁,如果是非公平上来抢,抢不到就进入队列里acquiredQueued(),先拿到当前线程,然后再获取state的值,如果为0,那么就compareAndSetState()尝试把state改为1,假如改成了,那么继续使用setExclusiveOwnerState()把当前线程设置为独占state这把锁的状态,说明这把锁是互斥的,因为别的线程进来,state的值就为1,如果是可重入的,state继续加1 就行了。

    1.1.通过ReentrantLock来解读AQS源码

    package com.learn.thread.five;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class TestReentranLock {
        private static volatile int i = 0;
    
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();
            reentrantLock.lock();
            System.out.println("sssasdas");
            reentrantLock.unlock();
        }
    }
    
            final void lock() {
                // 如果cas设置state为1 ,说明上锁成功
                if (compareAndSetState(0, 1))
                   setExclusiveOwnerThread(Thread.currentThread());
                else
                // 否则等待
                    acquire(1);
            }
    
      public final void acquire(int arg) {
            // 判断是否需要等待
            // tryAcquire 返回false说明没有拿到锁,进入acquireQueued等待
            if (!tryAcquire(arg) &&
                // 以排他的形式丢到队列里去,排他锁
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    
           protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
    
            // 返回true说明拿到锁,返回false没有拿到锁
           final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                // 判断当前线程是否是拥有线程的锁
                else if (current == getExclusiveOwnerThread()) {
                    // 如果是就重入
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    
      // 这个方法就干了一件事情,把最后一个节点丢到双向链表末尾
      private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // Try the fast path of enq; backup to full enq on failure
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                // 注意这里用了cas操作,操作对象是原来的tail末尾节点,原来的值是pred,要改成你新增的节点node
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            // 如果原来的cas操作失败了,就进入死循环,反正就是要把新增的节点保证插入到链表末尾 
            enq(node);
            return node;
        }
        
          private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) { // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

    1.2.想想为什么要用cas操作,而不是直接赋值?

    因为当很多线程往链表添加尾巴的时候,存在线程安全问题,如果使用锁,你就要锁整个链表,代价太大了。所以采用cas操作,只需要观察原来的tail末节点,观察它是不是预期值tail,如果是,就修改成新值,不是,继续循环,直到设置成功。

    1.3.为什么是双向链表?

    因为你要看前置节点的状态,所以必须是双向的

        // node是你新增的节点,arg常量1
        @ReservedStackAccess
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    // 拿到新增节点前面一个节点
                    final Node p = node.predecessor();
                    // 如果前置节点是头结点,并且是获取锁成功了,
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    // 如果不是头结点,跟头结点竞争拿锁,成功了,
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    上面这个方法的意思是,在链表里尝试获得锁。for循环获取node的前置节点,判断前置节点是头结点,并且调用tryAcquired尝试获取锁,获得了头结点,你设置的节点就是第二个,第二个节点要跟头结点竞争锁,如果头结点释放了锁,你设置的节点拿到了这把锁,拿到以后你设置的node节点就成为了前置节点,如果没有拿到锁,当前节点就会阻塞selfInterrupt,等着前置节点叫醒,如果你设置的节点不是第二个,那就等着,直到你设置的节点前置节点是头结点,才去竞争锁。

    2.ThreadLocal

    先来看一个小程序

    package com.learn.thread.five;
    
    public class TestThreadLock {
        volatile static Person person = new Person();
    
        public static void main(String[] args) {
            // 第一个线程睡眠两秒后打印值
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                    System.out.println(person.name);
                } catch (Exception ex) {
    
                }
            }).start();
            // 第二个线程立马改变值
            new Thread(() -> {
                person.name = "zhangliasd";
            }).start();
        }
    }
    class Person {
        String name = "zlx";
    }
    
    
    共享变量

    可以发现,person这个实例被线程共享了。
    有没有办法隔离线程共享的实例?有,就是ThreadLocal
    再来看一个小程序

    package com.learn.thread.five;
    
    public class TestThreadLocal2 {
        static ThreadLocal<Person2> t1 = new ThreadLocal<>();
    
        public static void main(String[] args) {
            // 这个线程打印的为null
            new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(t1.get());
            }).start();
            new Thread(() -> {
                t1.set(new Person2());
            }).start();
        }
    }
    class Person2 {
        String name = "zlx";
    }
    

    2.1.ThreadLocal源码

       public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
    ThreadLocal.png

    我们看到有一个ThreadLocalMap,是一个key/value对,key就是当前对象this,value就是你设置的那个值。

        // 发现这个map就是在Thread中,所以做到了线程隔离
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    

    2.2.为什么要用ThreadLocal

    我们用一个Spring事物管理来解析,如果我们写的方法1,方法2,方法3都需要事物。那么假设方法1从配置文件拿到Connection,方法2也是这样,那么这样完全不能形成一个完整的事物,所以方法1拿到Connection的时候放入ThreadLocal中,后续方法都是从ThreadLocal拿的,而不是从线程池拿。
    ThreadLocal应用场景:用来解决数据库连接、Session 管理等。

    3.Java的四种引用:强软弱虚

    java中分别有强软弱虚,首先明白什么是引用?
    Object object = new Object() 就是一个引用,object变量指向new出来的新对象,并且这是一个强引用。

    3.1.强引用

    我们来看一个方法finalize,这是垃圾回收器回收对象的时候会调用这个方法,所以我们重写类的这个方法,你就能知道对象是什么时候被回收了

    package com.learn.thread.five;
    
    import com.learn.thread.one.T;
    
    import java.io.IOException;
    
    public class TestStrongReference {
    
        @Override
        public void finalize() {
            System.out.println("finalize");
        }
    
        public static void main(String[] args) throws IOException {
            TestStrongReference testStrongReference = new TestStrongReference();
            // 如果不设置为null,垃圾回收机器不会去回收,也就不会去调用finalize
            testStrongReference = null;
            // 注意java是另外一个线程去回收垃圾的
            System.gc();
            // 所以这里要阻塞主程序的运行
            System.in.read();
        }
    }
    
    

    3.2.软引用

    SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null

    要去设置jvm的堆内存-Xmx20m

    package com.learn.thread.five;
    
    import lombok.Data;
    
    import java.lang.ref.SoftReference;
    
    public class TestSoftReference<T> extends SoftReference<T> {
    
    
        public TestSoftReference(T referent) {
            super(referent);
        }
    
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalize");
        }
    
        public static void main(String[] args) {
            TestSoftReference<byte []> softReference = new TestSoftReference<>(new byte[1024*1024*10]);
            System.out.println(softReference.get());
            System.gc();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(softReference.get());
            byte[] bytes = new byte[1024 * 1024 * 15];
            System.out.println(softReference.get());
    
        }
    }
    
    软引用

    举个例子,你要从内存中取一个大图片,你用完了就没什么用了,你可以放在缓存或者内存中,要的时候从内存中拿,但是如果这个图片比较大,那就下次别人用的时候把它干掉,这时候就用到了软引用。

    3.3.弱引用

    弱引用就是只要遇到gc就会被回收,刚才我们说的软引用的概念,垃圾回收器不一定回收,只要空间不够就去回收。只要垃圾回收看到这个引用是一个比较弱引用指向的时候,就把它给干掉。
    我们来看看WeakReference m = new WeakReference<>(new m()),这里我们New了一个对象,m是指向一个弱引用,这个弱引用里边还有一个引用,弱弱的指向了另外一个M对象,然后通过m.get()获取,如果调用System.gc(),如果他没有被回收,你接下来还能get拿到,反之不能。

    package com.learn.thread.five;
    
    import com.learn.thread.one.T;
    
    import java.lang.ref.WeakReference;
    
    public class TestWeakReference {
        public static void main(String[] args) {
            WeakReference<T> m = new WeakReference<>(new T());
            System.out.println(m.get());
            System.gc();
            System.out.println(m.get());
        }
    }
    
    
    弱引用

    可以看到gc之后,打印了null值,m本来指向弱引用对象,这个对象又有一个弱弱的引用指向一个T对象,这个T对象垃圾回收就干掉,那他创建出来的意义在哪里?
    意义在于有另外一个强引用指向了这个弱引用后,强引用消失了,这个弱引用也一定被回收了,这种设计一般用到容器里

    我们再来看ThreadLocal的源码,这里面其实是用到了弱引用

    package com.learn.thread.five;
    
    import com.learn.thread.one.T;
    
    import java.lang.ref.WeakReference;
    
    public class TestWeakReference {
        public static void main(String[] args) {
            WeakReference<T> m = new WeakReference<>(new T());
            System.out.println(m.get());
            System.gc();
            System.out.println(m.get());
            ThreadLocal<T> t = new ThreadLocal<>();
            t.set(new T());
            t.remove();
        }
    }
    
    

    我们逐步分析


    弱引用

    来看看ThreadLocal的源码分析,t1线程new了一个ThreadLocal对象,这是一个强引用,然后往ThreadLocal存放了一个对象,这个对象其实是存在当前线程的threadLocals变量里边,指向的是一个Map,注意看这个map的key是ThreadLocal,value就是我们存进去的对象(其实是一个Entry),Entry是集成WeakReference的

        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
            private void set(ThreadLocal<?> key, Object value) {
    
                // We don't use a fast path as with get() because it is at
                // least as common to use set() to create new entries as
                // it is to replace existing ones, in which case, a fast
                // path would fail more often than not.
    
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
    
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
          static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    // 注意,这里把ThreadLocal设置成了弱引用
                    super(k);
                    value = v;
                }
            }
    

    实线表示强引用,虚线表示弱引用

    如果说这个map的key指向的ThreadLocal是一个强引用的话,t1线程结束的时候,ThreadLocal也就结束了,但是注意ThreadLocal还被一个Map当成Key指向引用的,所以不会消失,并且如果这个线程是长期存在的,不断的运行,map就会长期存在,所以就存在内存泄漏的风险
    Entry的出现把ThreadLocal作为key对象设置成了弱引用,这样t1强引用消失了,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
    Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
    因此,ThreadLocal的对象不用的时候建议remove掉,不然Value也会造成内存溢出

    3.4.虚引用

    虚引用是用于管理堆外内存的,构造方法都是两个参数,第二个参数还是一个队列,我们来看下面一个程序

    package com.learn.thread.five;
    
    import com.learn.thread.one.T;
    
    import java.lang.ref.PhantomReference;
    import java.lang.ref.Reference;
    import java.lang.ref.ReferenceQueue;
    import java.util.ArrayList;
    import java.util.List;
    
    public class TestPhantomReference {
        private static final List<Object> list = new ArrayList<>();
        // 垃圾回收器回收虚引用的时候,会往这个加T
        private static final ReferenceQueue<T> queue = new ReferenceQueue<>();
    
        public static void main(String[] args) {
            PhantomReference phantomReference = new PhantomReference(new T(), queue);
            new Thread(() -> {
                while (true) {
                    list.add(new Byte[1024 * 1024]);
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        Thread.currentThread().interrupt();
                    }
                    System.out.println(phantomReference.get());
                }
            }).start();
            new Thread(() -> {
                while (true) {
                    Reference<T> reference = (Reference<T>) queue.poll();
                    if (reference != null) {
                        System.out.println("虚引用被jvm回收了");
                    }
                }
            }).start();
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    这里跟弱引用是不同的,get不到值,并且垃圾回收的时候会往你的队列中添加元素

    NIO里边有一个比较新的Buffer叫做直接内存,是不被JVM管理的,而是被操作系统管理的。那肯定不能被jvm回收了,这时候你就用虚引用,当jvm要去回收对象的时候,而是通知队列,你可以根据队列,手动回收对象。
    说不定将来,写netty分配内存的时候,用的是堆外内存,你就可以去检测虚引用的队列,当检测到被回收的时候,就手动清理堆外内存。
    堆外内存怎么回收呢?
    c语言和c++是通过del和free函数。java也有,回收类叫做Unsafe,jdk1.8是通过反射使用的,有两个方法,分别是allocateMemory和freeMemory 。

    相关文章

      网友评论

          本文标题:java多线程与高并发(六)AQS源码阅读与强软弱虚4中引用以及

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