java基础| 多线程基础六:Condition接口分析和ThreadLocal类解析

  • Condition

JUC是指java并发包,全称是 java.util.concurrent 包

        JUC提供了Lock可以方便的进行锁操作,但是有时候我们也需要对线程进行条件性的阻塞和唤醒,这时我们就需要condition条件变量,它就像是在线程上加了多个开关,可以方便的对持有锁的线程进行阻塞和唤醒Condition主要是为了在J.U.C框架中提供和Java传统的监视器风格的waitnotifynotifyAll方法类似的功能。condition 是依赖于 ReentrantLock 的,不管是调用 await 进入等待还是 signal 唤醒,都必须获取到锁才能进行操作。

        Condition是一个接口,它的接口实现类是AQS(AbstractQueuedSynchronizer )中的内部类ConditionObject。其类原型如下:

public class ConditionObject implements Condition, java.io.Serializable { 
    private static final long serialVersionUID = 1173984872572414699L; 
    
    // 不要管这里的关键字 transient,是不参与序列化的意思 
    private transient Node firstWaiter; // 条件队列的第一个节点 
    private transient Node lastWaiter; // 条件队列的最后一个节点 
    ......
}

         在AQS中有一个内部类Node类,是线程阻塞队列的节点类,代码如下:

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;
 
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;
 
        /**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         *
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         */
        volatile int waitStatus;
 
        /**
         * Link to predecessor node that current node/thread relies on
         * for checking waitStatus. Assigned during enqueuing, and nulled
         * out (for sake of GC) only upon dequeuing.  Also, upon
         * cancellation of a predecessor, we short-circuit while
         * finding a non-cancelled one, which will always exist
         * because the head node is never cancelled: A node becomes
         * head only as a result of successful acquire. A
         * cancelled thread never succeeds in acquiring, and a thread only
         * cancels itself, not any other node.
         */
        volatile Node prev;
 
        /**
         * Link to the successor node that the current node/thread
         * unparks upon release. Assigned during enqueuing, adjusted
         * when bypassing cancelled predecessors, and nulled out (for
         * sake of GC) when dequeued.  The enq operation does not
         * assign next field of a predecessor until after attachment,
         * so seeing a null next field does not necessarily mean that
         * node is at end of queue. However, if a next field appears
         * to be null, we can scan prev's from the tail to
         * double-check.  The next field of cancelled nodes is set to
         * point to the node itself instead of null, to make life
         * easier for isOnSyncQueue.
         */
        volatile Node next;
 
        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;
 
        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;
 
        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
 
        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
 
        Node() {    // Used to establish initial head or SHARED marker
        }
 
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
 
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

        在AQS中有一个阻塞队列,用于保存等待获取锁的线程的队列。同样,也有一个条件队列,是一个单链表结构,它是Condition主要作用的场所。从上述Node类中可以知道Node类主要有以下几个属性:

volatile int waitStatus; // 可取值 0、CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3) 
volatile Node prev;  //阻塞队列的前驱结点
volatile Node next; //阻塞队列的后继结点
volatile Thread thread;  //节点类中包装的线程
Node nextWaiter;  //条件队列中的后继节点
         prev next 用于实现阻塞队列的双向链表, nextWaiter 用于实现条件队列的单向链表。
 

        对于任意一个Java对象,都拥有一组监视器方法(定义在Object类中),主要包括wait,notify,notifyAll方法,这些方法与synchornized关键字相配合,可以实现等待/通知模式。

         Condition接口也提供了类似的Object的监视器方法,但是它是与Lock配合可以实现等待/通知模式。但是这两者在使用方式以及功能上还是有差别的。两者之间的一个对比:

对象自带的监视器方法和Condition的方法之间的对比。

         Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象创建出来的,换句话说,Condition是依赖Lock对象的。Condition中方法定义如下:

Condition类定义的方法
  • await方法 解析
        ReentrantLock 是独占锁,一个线程拿到锁后如果不释放,那么另外一个线程肯定是拿不到锁,所以在lock.lock()和 lock.unlock() 之间可能有一次释放锁的操作(同样也必然还有一次获取锁的操作)。在进入lock.lock() 后唯一可能释放锁的操作就是 await() 了。 也就是说 await()操作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁!方法原型如下:
public final void await() throws InterruptedException { 
    if (Thread.interrupted()) 
        throw new InterruptedException(); 
    Node node = addConditionWaiter(); //构造一个新的等待队列Node加入到队尾 
    int savedState = fullyRelease(node); //释放当前线程的独占锁,不管重入几次,都把state释放为0 
    int interruptMode = 0; //如果当前节点没有在同步队列上,即还没有被signal,则将当前线程阻塞                 
    while (!isOnSyncQueue(node)) { // 线程挂起 
        LockSupport.park(this); 
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //被中断则直 接退出自旋     
            break; 
    }//退出了上面自旋说明当前节点已经在同步队列上,但是当前节点不一定在同步队列队首。acquireQueued将阻塞直到当前节点成为队首,即当前线程获得了锁。然后await()方法就可以退出了,让 线程继续执行await()后的代码。 
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 
        interruptMode = REINTERRUPT; 
    if (node.nextWaiter != null) // clean up if cancelled 
        unlinkCancelledWaiters(); 
    if (interruptMode != 0) 
        reportInterruptAfterWait(interruptMode); 
}

方法过程解析:

  1. 将当前线程创建为节点,加入等待队列;
  2. 释放锁,唤醒同步队列中的后继节点;
  3. while循环判断节点是否放入同步队列:
  •     没有放入,则阻塞,继续 while 循环(如果已经中断了,则退出)
  •     放入,则退出 while 循环,执行后面的判断
  1. 退出 while 说明节点已经在同步队列中,调用 acquireQueued() 方法加入同步状态竞争。
  2. 竞争到锁后从 await() 方法返回,即退出该方法。

在await()方法中主要用到了以下几个方法:

  1. addConditionWaiter(); //构造一个新的等待队列Node加入到队尾 
  2. fullyRelease(node); //释放当前线程的独占锁,不管重入几次,都把state释放为0
  3. isOnSyncQueue(node) 线程挂起(通过while循环),等待进入阻塞队列

    addConditionWaiter()方法

// 将当前线程对应的节点入队,插入队尾 
private Node addConditionWaiter() { 
    Node t = lastWaiter; // 如果条件队列的最后一个节点取消了,将其清除出去 
    if (t != null && t.waitStatus != Node.CONDITION) { // 这个方法会遍历整个条件队列,然后会将已取消的所有节点清除出队列 
        unlinkCancelledWaiters(); 
        t = lastWaiter; 
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION); // 如果队列为空
    //3、完全释放独占锁
    if (t == null) 
        firstWaiter = node; 
    else
        t.nextWaiter = node; 
        lastWaiter = node; 
    return node; 
}

//unlinkCancelledWaiters方法原型:遍历整个条件队列,将已取消的所有节点清除出队列
// 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去 
private void unlinkCancelledWaiters() { 
    Node t = firstWaiter; 
    Node trail = null; 
    while (t != null) { 
        Node next = t.nextWaiter; // 如果节点的状态不是 Node.CONDITION 的话,这个节点就是被取消的 
        if (t.waitStatus != Node.CONDITION) { 
            t.nextWaiter = null; 
            if (trail == null) 
                firstWaiter = next; 
            else
                trail.nextWaiter = next; 
        if (next == null) 
            lastWaiter = trail; 
        }else
            trail = t; t = next; 
    } 
}
构造一个新的等待队列Node加入到队尾

    fullyRelease(node)方法

    该方法用于释放当前线程的独占锁,方法原型如下:

// 我们要先观察到返回值 savedState 代表 release 之前的 state 值 
// 对于最简单的操作:先 lock.lock(),然后 condition1.await()。 
// 那么 state 经过这个方法由 1 变为 0,锁释放,此方法返回 1 
// 相应的,如果 lock 重入了 n 次,savedState == n 
// 如果这个方法失败,会将节点设置为"取消"状态,并抛出异常 IllegalMonitorStateException 
final int fullyRelease(Node node) { 
    boolean failed = true; 
    try {
        int savedState = getState(); 
        // 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0 
        if (release(savedState)) { 
            failed = false; 
            return savedState; 
        } else { 
            throw new IllegalMonitorStateException(); 
        } 
    } finally { 
        if (failed) 
            node.waitStatus = Node.CANCELLED; 
    } 
}

方法总体比较简单,首先获取到state值,通过getState()方法得到。然后依赖release()方法进行锁的释放。release()方法如下:

public final boolean release(int arg) { 
    //先将state释放为0 
    if (tryRelease(arg)) { 
        //取到阻塞队列的头节点 
        Node h = head; 
        if (h != null && h.waitStatus != 0) 
            //唤醒头节点,则第一个等待的节点会继续获取锁 
            unparkSuccessor(h); 
        return true; 
    }
    return false; 
}

private void unparkSuccessor(Node node) { 
    int ws = node.waitStatus; 
    if (ws < 0) 
        compareAndSetWaitStatus(node, ws, 0); 
    //从后面开始往前找,找到第一个状态为-1的节点 
    Node s = node.next; 
    if (s == null || s.waitStatus > 0) { 
        s = null; 
        for (Node t = tail; t != null && t != node; t = t.prev) 
            if (t.waitStatus <= 0) 
                s = t; 
    }
    if (s != null) 
        //唤醒第一个状态为-1的节点,则该节点会继续获取锁         
        LockSupport.unpark(s.thread); 
}

    release()方法通过CAS机制来释放当前锁并唤醒第一个状态为-1的节点。

isOnSyncQueue(node)

    await()方法中通过while循环和isOnSyncQueue(node)方法等待进入阻塞队列。即:

int interruptMode = 0; 
while (!isOnSyncQueue(node)) { 
    // 线程挂起 
    LockSupport.park(this); 
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 
        break; 
}

        isOnSyncQueue(Node node) 用于判断节点是否已经转移到阻塞队列了,然后通过工具类LockSupport对线程进行挂起。isOnSyncQueue(Node node)方法原型如下:

final boolean isOnSyncQueue(Node node) { 
    //如果当前节点状态是CONDITION或node.prev是null,则证明当前节点在等待队列上而不是同步队 列上。之所以可以用node.prev来判断,
//是因为一个节点如果要加入同步队列,在加入前就会设置好prev字 段。 
    if (node.waitStatus == Node.CONDITION || node.prev == null) 
        return false; 
    //如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的 
    if (node.next != null) // If has successor, it must be on queue
        return true;
    return findNodeFromTail(node); //前面的两个判断没有返回的话,就从同步队列队尾遍历一个 一个看是不是当前节点。 
}

// 从同步队列的队尾往前遍历,如果找到,返回 true 
private boolean findNodeFromTail(Node node) { 
    Node t = tail; 
    for (;;) { 
        if (t == node) 
            return true; 
        if (t == null) 
            return false; 
        t = t.prev; 
    } 
}

        到此,就完成了await()方法的线程挂起全流程。

  • signal()方法解析

    与await()线程挂起方法相对应的就是signal()线程唤醒方法。该方法用于唤醒线程,转移到阻塞队列

    唤醒操作通常由另一个线程来操作,就像生产者-消费者模式中,如果线程因为等待消费而挂起,那么当生产者生产了一个东西后,会调用 signal 唤醒正在等待的线程来消费。signal()方法原型如下:

// 唤醒等待了最久的线程 
// 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列 
public final void signal() { 
    // 调用 signal 方法的线程必须持有当前的独占锁 
    if (!isHeldExclusively()) 
        throw new IllegalMonitorStateException(); 
    Node first = firstWaiter; 
    if (first != null) 
        doSignal(first); 
}

// 从条件队列队头往后遍历,找出第一个需要转移的 node 
// 因为前面我们说过,有些线程会取消排队,但是还在队列中
private void doSignal(Node first) { 
    do { 
        // 将 firstWaiter 指向 first 节点后面的第一个 
        // 如果将队头移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null 
        if ( (firstWaiter = first.nextWaiter) == null) 
            lastWaiter = null; 
        // 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉 
        first.nextWaiter = null; 
    } while (!transferForSignal(first) && (first = firstWaiter) != null); 
    // 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移, 依此类推 
}

// 将节点从条件队列转移到阻塞队列 
// true 代表成功转移 
// false 代表在 signal 之前,节点已经取消了 
final boolean transferForSignal(Node node) { 
    // CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取 消, 
    // 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点 
    // 否则,将 waitStatus 置为 0 
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) 
        return false; 
    // enq(node): 自旋进入阻塞队列的队尾 
    // 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点 
    Node p = enq(node); 
    int ws = p.waitStatus; 
    // ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。唤醒 之后会怎么样,后面再解释 
    // 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用,上篇介绍的时候说过,节点入 队后,需要把前驱节点的状态设为 Node.SIGNAL(-1) 
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 
        // 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程,之后的操作看下一节         
        LockSupport.unpark(node.thread); 
    return true; 
}

        signal()方法本身很简单。首先获取等待了最久的节点firstWaiter,然后通过doSingal()对其进行唤醒。doSingal()方法首先从条件队列中从头到尾方向找到真正需要唤醒的节点(因为有的节点会取消等待,但是仍然会在队列中占坑)。找到节点之后,通过transferForSignal()方法将节点从条件队列转移到阻塞队列。transferForSignal()方法拿到节点后首先通过CAS机制判断节点是否是真正需要转移的,如果节点已经取消,则不需要再进行转移,否则继续往下,通过自旋将节点插入到阻塞队列的队尾,然后再次通过CAS机制利用工具类LockSupport对节点线程进行唤醒。

        正常情况下, ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 这句中, ws <= 0 , 而且 compareAndSetWaitStatus(p, ws, Node.SIGNAL) 会返回 true ,所以一般也不会进去 if 语句块中唤醒 node 对应的线程。然后这个方法返回 true ,也就意味着 signal 方法结束了,节点进入了阻塞队列, 此时 await() 还是挂起状态 , 并没有被唤醒。 我们可以看到, signal方法只是将Node修改了状态,并没有唤醒线程。要将修改状态后的Node唤醒,唤起线程是在unlock()中。这个方法会对阻塞队列里面的线程从头到尾对状态为-1的节点做唤醒操作。unlock()将此线程唤醒后,await()中可以继续执行,此线程被唤醒的时候它的前驱节点肯定是首节点了,因为unlock()方法是从头到尾进行唤醒。假设发生了阻塞队列中的前驱节点取消等待,或者 CAS 失败,只要唤醒线程,让其进到下一步即可。
  • 总结
        Condition 的本质就是等待队列和同步队列的交互:当一个持有锁的线程调用Condition.await() 时,它会执行以下步骤:
  1. 构造一个新的等待队列节点加入到等待队列队尾
  2. 释放锁,也就是将它的同步队列节点从同步队列队首移除
  3. 自旋,直到它在等待队列上的节点移动到了同步队列(通过其他线程调用signal())或被中断
  4. 阻塞当前节点,直到它获取到了锁,也就是它在同步队列上的节点排队排到了队首。
        当一个持有锁的线程调用 Condition.signal() 时,它会执行以下操作:

        从等待队列的队首开始,尝试对队首节点执行唤醒操作;如果节点CANCELLED,就尝试唤醒下一个节

点;如果再CANCELLED则继续迭代。

        对每个节点执行唤醒操作时,首先将节点加入同步队列,此时await()操作的步骤3的解锁条件就已经开

启了。然后分两种情况讨论:

        1. 如果先驱节点的状态为 CANCELLED(>0) 或设置先驱节点的状态为 SIGNAL 失败,那么就立即唤醒当
前节点对应的线程,此时 await() 方法就会完成步骤 3 ,进入步骤 4.
        2. 如果成功把先驱节点的状态设置为了 SIGNAL ,那么就不立即唤醒了。等到先驱节点成为同步队列
首节点并释放了同步状态后,会自动唤醒当前节点对应线程的,这时候 await() 的步骤 3 才执行完
成,而且有很大概率快速完成步骤 4.
 
  • 比较wait()和await()以及notify() 和signal()方法的区别
    wait()和notify()是Object类提供的方法,而 await() 和 signal() 方法是接口 Condition 的方法由实现类ConditionObject类提供具体过程。
 wait() 与 notify() 需要搭配 synchronized 关键字使用。例如:
synchronized(obj){
 obj.wait();//消费方没东西了,等待
}

synchronize(obj){ 
    obj.notify();//有东西了,唤醒 消费进程
}

    而await()和signal()方法需要搭配着Lock锁,Condition来控制被阻塞线程使用,Condition 这个接口把 Object 的 wait(), notify(), notifyAll() 分解到了不同的对象中, 搭配上任意一种 Lock 的使用, 使得一个对象可以拥有多个等待集。

例如:

// 消费者
lock.lock();
condition.await();
lock.unlock();


//生产者
lock.lock(); 
condition.signal(); 
lock.unlock();

为什么 wait(), notify() 需要搭配 synchronized 关键字使用 ?
        wait(), notify() 操作的目的是基于某种条件, 协调多个线程间的运行状态, 由于涉及到多个线程间基于共享变量的相互通信, 必然需要引入某种同步机制, 以确保wait(), notify() 操作在线程层面的原子性。

  • 总结

        await(), signal(),signalAll() 的功用和 wait(), notify(), notifyAll() 基本相同, 区别是, 基于 Condition 的 await(), signal(), signalAll() 使得我们可以在同一个锁的代码块内, 优雅地实现基于多个条件的线程间挂起与唤醒操作。

  • 线程类方法join()
        thread.Join 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B 中调用了线程 A Join() 方法,直到线程 A 执行完毕后,才会继续执行线程 B。用法: t.join(); // 调用 join 方法,等待线程 t 执行完毕。 t.join(1000); // 等待 t 线程,等待时间是 1000 毫秒。

 

  • ThreadLocal类
        ThreadLocal是线程的内部存储类,可以 在指定线程内存储数据。只有指定线程可以得到存储数据。每个线程都有一个ThreadLocalMap的实例对象,并且通过ThreadLocal管理ThreadLocalMap。
//Thread.class中的变量
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

        每个新线程都会实例化为一个ThreadLocalMap并且赋值给成员变量ThreadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。使用场合:

  1. 当某些数据是以线程为作用域并且不同线程有不同数据副本时,考虑ThreadLocal。
  2. 无状态,副本变量独立后不影响业务逻辑的高并发场景。
  3. 如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决。
        线程局部变量(ThreadLocal) 其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java 中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。通过ThreadLocal 存取的数据,总是与当前线程相关,也就是说, JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
 
ThreadLocal类方法解析, ThreadLocal 最常见的操作就是 set get remove 三个动作
1 ThreadLocal()
        创建一个线程本地变量。
 
2 void set(T value)
         将此线程局部变量的当前线程副本中的值设置为指定值。
public void set(T value) { 
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); 
    if (map != null) 
        map.set(this, value); 
    else
        createMap(t, value); 
}

        set方法首先取出了当前线程 t,然后调用getMap(t)方法时传入了当前线程。判定如果这个map不为空,那么设置Map中的Key就是this,值就是外部传入的参数。这个 this 就是定义的ThreadLocal对象。getMap(Thread)方法如下:

ThreadLocalMap getMap(Thread t) { 
    return t.threadLocals; 
}

        可以看到ThreadLocalMap其实就是线程里面的一个属性,该属性定义如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
        即 : 每个 Thread 对象都有一个 ThreadLocal.ThreadLocalMap类型 成员变量 ,ThreadLocal.ThreadLocalMap 是一个ThreadLocal 类的静态内部类 ( static class ThreadLocalMap ), 所以 Thread 类可以进行引用 .所以每个线程都会有一个ThreadLocal.ThreadLocalMap对象的引用。 首先获取当前线程的引用 , 然后获取当前线程的 ThreadLocal.ThreadLocalMap 对象 , 如果该对象为空就创 建一个 , 如下所示 :
void createMap(Thread t, T firstValue) { 
    t.threadLocals = new ThreadLocalMap(this, firstValue); 
}
        这个 this 变量就是 ThreadLocal 的引用 , 对于同一个 ThreadLocal 对象每个线程都是相同的 , 但是每个线程各自有一个ThreadLocal.ThreadLocalMap 对象保存着各自 ThreadLocal 引用为 key 的值 , 所以互不影响 , 而且:  如果你新建一个 ThreadLocal 的对象 , 这个对象还是保存在每个线程同一个ThreadLocal.ThreadLocalMap对象之中 , 因为一个线程只有一个 ThreadLocal.ThreadLocalMap 对象 ,
个对象是在第一个 ThreadLocal 第一次设值的时候进行创建 , 如上所述的 createMap 方法。
        一个线程中只有一个 ThreadLocal.ThreadLocalMap对象,但是ThreadLocalMap对象是一个map对象,可以根据不同的key设置多个value值。
         ThreadLocal 的原理简单来讲,就是每个 Thread 里面有一个 ThreadLocal.ThreadLocalMap threadLocals 作为私有的变量而存在,所以是线程安全的。 ThreadLocal 通过 Thread.currentThread() 获取当前的线程就能得到这个 Map 对象,同时将自身 ThreadLocal 对象)作为 Key 发起写入和读取,由于将自身作为 Key ,所以一个 ThreadLocal 对象就能 存放一个线程中对应的 Java 对象,通过 get 也自然能找到这个对象。
 
3 T get()和remove()
        get()返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
public T get() { 
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); 
    if (map != null) { 
        ThreadLocalMap.Entry e = map.getEntry(this); 
        if (e != null) { 
            @SuppressWarnings("unchecked") T result = (T)e.value; 
            return result; 
        } 
    }
    return setInitialValue(); 
}

public void remove() { 
    ThreadLocalMap m = getMap(Thread.currentThread()); 
    if (m != null) m.remove(this); 
}
        第一句是取得当前线程,然后通过 getMap(t) 方法获取到一个 map map 的类型为ThreadLocalMap。然后接着下面获取到 <key,value> 键值对,注意这里获取键值对传进去的是 this ,而不是当前线程t 。 如果获取成功,则返回value 值。如果map 为空,则调用 setInitialValue 方法返回 value。 setInitialValue() 只有在线程第一次使用 get() 方法访问变量的时候调用 。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
protected T initialValue() { return null; }
        该方法定义为 protected 级别且返回为 null ,很明显是要子类实现它的,所以我们在使用 ThreadLocal 的时候一般都应该覆盖该方法, 创建匿名内部类重写此方法。该方法不能显示调用,只有在第一次调用  get() 或者 set()  方法时才会被执行,并且仅执行 1 次。
 
对于 ThreadLocal 需要注意的有两点:
1 ThreadLocal 实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得 key
2 、是 ThreadLocal 包含在 Thread 中,而不是 Thread 包含在 ThreadLocal 中,有些小伙伴会弄错他们的
关系。
 
  • ThreadLocal为什么会内存泄漏

         首先看到ThreadLocalMap类中的Entry内部类:

static class Entry extends WeakReference<ThreadLocal<?>> { 
    /** The value associated with this ThreadLocal. */ 
    Object value; 
    Entry(ThreadLocal<?> k, Object v) { 
        super(k); 
        value = v; 
    } 
}
        Entry 继承了 WeakReference ,该 map key 为一个弱引用,弱引用有利于GC回收。 ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key 如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 会被回收,这样一来, ThreadLocalMap 中就会出现 key null Entry ,就没有办法访问这些 key null Entry value ,如果当前线程再迟 迟不结束的话,这些 key null Entry value 就会一直存在一条强引用链: Thread Ref ->Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。其实 ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施: ThreadLocal
get() , set() , remove() 的时候都会清除线程 ThreadLocalMap 里所有 key null value 。但是这些被动的预防措施并不能保证不会内存泄漏:
  1. 使用 static ThreadLocal ,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏。
  2. 分配使用了 ThreadLocal 又不再调用 get() , set() , remove() 方法,那么就会导致内存泄漏。

强弱引用的区别:

key 使用强引用 :引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有ThreadLocal 的强引用,如果没有手动删除, ThreadLocal 不会被回收,导致 Entry 内存泄漏。
key 使用弱引用 :引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有ThreadLocal 的弱引用,即使没有手动删除, ThreadLocal 也会被回收。 value 在下一次ThreadLocalMap 调用 set , get remove 的时候会被清除。
怎么避免内存泄漏?
  1. 每次使用完 ThreadLocal ,都调用它的 remove() 方法,清除数据。
  2. 在使用线程池的情况下,没有及时清理 ThreadLocal ,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值