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

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 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 extends SoftReference {


    public TestSoftReference(T referent) {
        super(referent);
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }

    public static void main(String[] args) {
        TestSoftReference 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 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 m = new WeakReference<>(new T());
        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
        ThreadLocal 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> {
            /** 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 list = new ArrayList<>();
    // 垃圾回收器回收虚引用的时候,会往这个加T
    private static final ReferenceQueue 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 reference = (Reference) 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中引用以及ThreadLocal原理与源码)