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);
}
我们看到有一个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
这里跟弱引用是不同的,get不到值,并且垃圾回收的时候会往你的队列中添加元素
NIO里边有一个比较新的Buffer叫做直接内存,是不被JVM管理的,而是被操作系统管理的。那肯定不能被jvm回收了,这时候你就用虚引用,当jvm要去回收对象的时候,而是通知队列,你可以根据队列,手动回收对象。
说不定将来,写netty分配内存的时候,用的是堆外内存,你就可以去检测虚引用的队列,当检测到被回收的时候,就手动清理堆外内存。
堆外内存怎么回收呢?
c语言和c++是通过del和free函数。java也有,回收类叫做Unsafe,jdk1.8是通过反射使用的,有两个方法,分别是allocateMemory和freeMemory 。