Java——线程的同步与死锁

目录

1.同步问题的引出

2.同步处理

2.1 synchronized(内键锁)实现同步处理(加锁操作)

2.2 synchronized对象锁

2.3 synchronized实现原理

2.4 JDk1.5提供的Lock锁——Java语言层锁

2.5 synchronized优化

2.6 其他优化

2.7 死锁

3.ThreadLocal详解 

3.1 概念

3.2 ThreadLocal实现 

3.3 ThreadLocalMap详解 

3.4 内存泄漏


1.同步问题的引出

多个窗口共同售票,票数出现负数——不同步操作

/**
 * 引出同步
 * Author: qqy
 */
public class Test {
    public static void main(String[] args) {
        MyThread mt=new MyThread();
        Thread thread=new Thread(mt,"电话");
        Thread thread1=new Thread(mt,"网站");
        Thread thread2=new Thread(mt,"代购");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

//实现Runnable接口——多个窗口共同售票
class MyThread implements Runnable{
    private int ticket=10;

    @Override
    public void run() {
        while(ticket>0){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"剩余"+this.ticket--+"票");
        }
    }
}

2.同步处理

同步——所有的线程按照顺序一个一个进入到方法中执行

  • 2.1 synchronized(内键锁)实现同步处理(加锁操作)

同步代码块、同步方法

  • 同步代码块(同步代码局部):在方法中使用synchronized(对象),一般锁定当前对象this。表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法。

  • 同步方法(同步代码全部):在方法声明上加synchronized,表示此时只有一个线程进入同步方法。

代码示例:

同步代码块

/**
 * 同步代码块
 * Author: qqy
 */
public class Test1 {
    public static void main(String[] args) {
        MyThread1 mt=new MyThread1();
        Thread thread=new Thread(mt,"电话");
        Thread thread1=new Thread(mt,"网站");
        Thread thread2=new Thread(mt,"代购");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

//实现Runnable接口——多个窗口共同售票
class MyThread1 implements Runnable{
    //票数是共享资源
    private int ticket=100;

    @Override
    public void run() {
        //同一时刻可以由多个线程进入方法,但只能有一个线程进入同步代码块
        for(int i=0;i<100;i++){
            //同步代码块
            //this->mt
            synchronized(this){
                // ---------------------------------
                if(ticket>0){
                    try {
                        //模拟网络延迟
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"  剩余"+this.ticket--+"张票");
                }
                // ---------------------------------
           }
        }
    }
}

同步方法

/**
 * 同步方法
 * Author: qqy
 */
public class Test2 {
    public static void main(String[] args) {
        MyThread2 mt=new MyThread2();
        Thread thread=new Thread(mt,"电话");
        Thread thread1=new Thread(mt,"网站");
        Thread thread2=new Thread(mt,"代购");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

class MyThread2 implements Runnable{
    //票数是共享资源
    private int ticket=100;

    @Override
    public void run() {
       for(int i=0;i<100;i++){
           this.sale(ticket);
       }
    }

    //同步方法
    public synchronized void sale(int ticket){
        if(ticket>0){
            try {
                //模拟网络延迟
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"  剩余"+this.ticket--+"张票");
        }
    }
}
  • 2.2 synchronized对象锁

synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。 

代码示例:

/**
 * synchronized对象锁
 * 仍是同时开始,同时结束,并没有锁住
 * Author: qqy
 */
public class Test4 {
    public static void main(String[] args) {
        MyThread3 mt=new MyThread3();
        for(int i=0;i<3;i++){
            //每次start()->run()都会创建一个新的Sync对象
            //∴是三个不同的线程,无法锁住
            new Thread(mt,"线程"+i+"  ").start();
        }
    }
}

class MyThread3 implements Runnable{

    @Override
    public void run() {
        Sync sync=new Sync();
        //多个对象
        //sync1,线程0
        //sync2,线程1
        //sync3,线程2
        sync.test();
    }
}

class Sync{
    //锁住的是当前对象this
    public synchronized void test(){
        System.out.println(Thread.currentThread().getName()+"test开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"test结束");
    }
}
  • 锁住代码

    1.锁住同一个对象——synchronized(this/obj)

/**
 * synchronized对象锁
 * 只有一个对象,成功锁住
 * Author: qqy
 */

public class Test5 {
    public static void main(String[] args) {
        Sync1 sync=new Sync1();
        MyThread4 mt=new MyThread4(sync);
        for(int i=0;i<3;i++){
            new Thread(mt,"线程"+i+"  ").start();
        }
    }
}

class MyThread4 implements Runnable{
    //只有一个对象
    private Sync1 sync;

    public MyThread4(Sync1 sync){
        this.sync=sync;
    }

    @Override
    public void run() {
        this.sync.test();
    }
}

class Sync1{
    //锁住的是当前对象this
    public synchronized void test(){
        System.out.println(Thread.currentThread().getName()+"test开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"test结束");
    }
}

     2.全局锁——synchronized(class)

        ①使用类的静态同步方法

synchronized和static一起使用,此时锁住的是当前使用的类

/**
 * synchronized对象锁
 * 全局锁
 * Author: qqy
 */
public class Test6 {
    public static void main(String[] args) {
        MyThread5 mt=new MyThread5();
        for(int i=0;i<3;i++){
            new Thread(mt,"线程"+i+"  ").start();
        }
    }
}

class MyThread5 implements Runnable{

    @Override
    public void run() {
        Sync2 sync=new Sync2();
        sync.test();
    }
}

class Sync2{
    //静态方法,锁住的是下方代码段
    public static synchronized void test(){
        System.out.println(Thread.currentThread().getName()+"test开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"test结束");
    }
}

         ②在代码块中锁住当前class对象

synchronized(类名称.class){}

class Sync2{
    //在代码块中锁住当前class对象
    public void test() {
        synchronized (Sync2.class) {
            System.out.println(Thread.currentThread().getName() + "test开始");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "test结束");
        }
    }
}
  • 线程1进入了同步方法A,线程2能否同时进入同步方法B?

       用monitor机制解释:

当testA被synchronized 修饰

-> 编译生成的字节码会产生ACC_SYNCHRONIZED标记,表示线程进入此方法首先要执行monitorenter指令

->获取当前指令对象的计数器,计数器为0,JVM会将线程1设置为当前线程,并且将monitor计数器加 1

判断testB能否进入此方法时,仍要先执行monitorenter指令,而此时的计数器不为0(1),且线程2并不是当前线程

∴线程2不能进入同步方法B,等待

/**
 * 线程1进入了同步方法A,线程2能否同时进入同步方法B?
 * 不能!
 * ∵testA锁住了当前对象 Sync3 sync,只有当testA释放锁之后,testB才可进行
 * Author: qqy
 */
public class Test7 {
    public static void main(String[] args) {
        Sync3 sync=new Sync3();
        MyThread6 myThread=new MyThread6(sync);
        Thread threadA=new Thread(myThread,"A");
        Thread threadB=new Thread(myThread,"B");
        threadA.start();
        try {
            //确保A先进行
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadB.start();
    }
}

class MyThread6 implements Runnable{
    private Sync3 sync;

    public MyThread6(Sync3 sync) {
        this.sync = sync;
    }

    @Override
    public void run() {
        this.sync.testA();
        this.sync.testB();
    }
}

class Sync3{
    //线程1进入testA,线程1仍在执行
    public synchronized void testA(){
       if(Thread.currentThread().getName().equals("A")){
           while(true){}
       }
    }
    //线程2能否进入testB   ->不能
    public synchronized void testB(){
        if(Thread.currentThread().getName().equals("B")){
            System.out.println("线程B");
        }
    }
}
  • 2.3 synchronized实现原理

  • 2.3.1 synchronized底层实现——对象锁(monitor)机制

对象锁(monitor)机制是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁,线程的组赛以及唤醒均寻妖操作系统由用户态切换到内核态,开销非常大,∴ 效率很低

  • 同步代码块底层实现:

执行同步代码块首先要执行monitorenter指令,退出时执行monitorexit指令。使用synchronized实现同步,关键点就是要获取对象的监视器monitor对象。当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。同一时刻只有一个线程可以获取到该对象的monitor监视器。

∵JVM要确保锁获取的锁无论在正常执行路径或异常执行路径都能正确解锁。

∴ 通常一个monitorenter指令会同时包含多个monitorexit指令。

  • ② 同步方法底层实现:

当用 synchronized 标记方法时,字节码会出现访问标记 ACC_SYNCHRONIZED。该标记表示在进入该方法时,JVM需要进行 monitorenter 操作。而在退出该方法时,不管是否正常返回,JVM均需要进行 monitorexit 操作。 

当JVM执行 monitorenter 时,如果目标锁对象的计数器为 0,表明此时该对象没有被其他线程所持有。此时, JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器加 1。

在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 JVM可以将其计数器再次加 1(可重入锁),否则需要等待,直至持有线程释放该锁。 

/**
 * 两个同步方法锁定的都是Test8.class这一个对象,当前线程为主线程
 * 当前计数器不为0(同步方法1),且当前锁对象指向主线程
 * ->计数器再次加 1,允许进入test1
 * 值变为2
 * Author: qqy
 */
public class Test8 {
    public static void main(String[] args) {
        test();
        test1();
    }
    //同步方法1
    public static synchronized void test(){
        System.out.println("c'est la vie");
    }
    //同步方法2
    public static synchronized void test1(){
        System.out.println("c'est la vie");
    }
}

当执行 monitorexit 时,JVM需将锁对象的计数器 -1。当计数器减为 0 时,那便代表该锁已经被释放掉,唤醒所有正在等待的线程去竞争该锁。

  • 2.4 JDk1.5提供的Lock锁——Java语言层锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Lock锁
 * Author: qqy
 */
public class Test9 {
    public static void main(String[] args) {
        LockRunnable mt=new LockRunnable();
        Thread threadA=new Thread(mt,"电话");
        Thread threadB=new Thread(mt,"网站");
        Thread threadC=new Thread(mt,"代理");
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

class LockRunnable implements Runnable{
    private Lock lock=new ReentrantLock();
    private int ticket=500;

    @Override
    public void run() {
        for(int i=0;i<500;i++){
            try {
                //显式上锁
                lock.lock();
                Thread.sleep(20);
                if (ticket>0){
                    System.out.println(Thread.currentThread().getName()+"剩余"+this.ticket--+"票");
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //无论是否出现异常,都需解锁
                lock.unlock();
            }
        }
    }
}
  • 2.5 synchronized优化

  • 2.5.1 CAS(Campare and Swap)

线程获取锁(JDK1.6之前内键锁)是一种悲观锁的策略。

  • 悲观锁:假设每一次执行临界区代码(访问共享资源)都会产生冲突,∴当线程获取到-1锁的同时也会阻塞其他未获取到锁的线程。
  • 乐观锁(CAS操作/无锁操作):假设所有线程访问资源时不会出现冲突,由于不会出现冲突,自然就不会阻塞其他线程。因此,线程不会出现阻塞、停顿的状态。出现冲突时,无锁操作使用CAS(比较交换)来鉴别线程是否出现冲突,出现冲突就重试当前操作指导没有冲突为止。

  • CAS操作过程

CAS可以理解为CAS(V、O、N):

    V——当前内存地址实际存放的值

    O——预期值(旧值)

    N——更新的新值

当V=O时,即期望值与内存实际值相等(该值没有被任何其他线程修改过,值O就是目前最新的值),将新值N赋给V。

当V!=O时,表明该值已经被其他线程修改过了,因此O值并不是当前最新值,返回V,无法修改。


       形象描述:

甲乙丙三人抢座位,甲在座位上——V=甲

V=O : 乙知道甲在座位上——O=甲,此时V=O;乙希望自己能坐在座位上——N=乙 ->(甲、甲、乙)->V=乙

V!=O:乙知道甲在座位上——O=甲,趁乙不注意,甲离开,丙在座位上——V=丙;乙希望自己能坐在座位上——N=乙 ->(丙、甲、乙)-> 告知乙,现在在座位上的是丙


当多个线程使用CAS操作时,只有一个线程会成功,其余线程均失败。失败的线程会重新尝试(自旋)或挂起线程(阻塞)

  • 内键锁与优化锁的区别

内键锁在老版本最大的问题在于——在存在线程竞争的情况下会出现线程的阻塞以及唤醒带来的性能问题,这是一种互斥同步(阻塞同步)。而CAS不是将线程挂起,当CAS失败后会进行一定的尝试操作并非耗时的将线程挂起,也叫做非阻塞同步。

  • 2.5.2 CAS的问题
  • ① ABA问题

解决:使用atomic提供的AtomicStampedReference类来解决,添加版本号

  • ② 自旋会浪费大量的CPU资源

与线程阻塞相比,自旋会浪费大量的处理器资源。因为当前线程仍处于运行状态,只不过跑的时无用指令。

解决:自适应自旋(重量级锁的优化)——根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环次数)。如果在自旋时获取到锁,则会稍微增加下一次自旋的时长;否则就稍微减少下一次自旋时长。

  • ③ 公平性

处于自旋状态的线程更容易获得锁(踩刹车比熄火发动快),内键锁无法实现公平机制,而lock体系可以实现公平锁。

  • 2.5.3 Java对象头

Java对象头Mark Word字段存放内容:对象的Hashcode、分代年龄、锁的标记位

  • JDK1.6之后一共四种状态锁(级别由低到高):

根据竞争状态的激烈程度,锁会自动进行升级,锁不能降级——为了提高锁获取与释放的效率

  • 2.5.4 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销降低引入偏向锁。

偏向锁是锁状态中最乐观的一种锁:从始至终只有一个线程请求一把锁

  • 偏向锁的获取:

当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。

当对象头的Mark Word中存储着指向当前线程的偏向锁 / Mark Word中没有存储,但是其中偏向锁的标识已经设置位1,表明线程已获得锁。

当线程访问同步块失败(已经有一个线程占用偏向锁)时,使用CAS竞争锁,并将偏向锁升级为轻量级锁。

  • 偏向锁的撤销(开销较大):

使用一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)。

如果持有线程已经终止,则将锁对象的对象头设置为无锁状态。

   

  • JDK6之后偏向锁默认开启,两个同时存活的线程,在竞争偏向锁的时候会把偏向锁升级为轻量级锁(当有竞争时,马上升级)
  • 2.5.5 轻量级锁(不存在阻塞)

多个线程在不同时间段请求同一把锁,也就是不存在锁竞争的情况。针对这种状况,JVM采用了轻量级锁来避免线程的阻塞与唤醒。

多个线程在同一时刻竞争同一把锁,升级为重量级锁。

  • 2.5.6 重量级锁

重量级锁是JVM中为基础的锁实现。依赖于操作系统的底层实现;其阻塞、唤醒以来操作系统

  • 2.5.7 三种锁的特点
  1.  重量级锁会阻塞、唤醒请求加锁的线程。——针对多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。 
  2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。——针对多个线程在不同时间段申请同一把锁的情况。
  3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程无需加锁操作。——针对锁仅会被同一线程持有的情况。
  • 2.6 其他优化

  • 2.6.1 锁粗化

将多次连接在一起的加锁、解锁操作合并为一次操作。将多个联系的锁扩展为一个范围更大的锁

/**
 * 锁粗化
 * Author: qqy
 */
public class Test10 {
    //sb是类的属性,多个线程共享——存在线程安全问题
    private static StringBuffer sb=new StringBuffer();
    public static void main(String[] args) {
        //每次调用append()都要进行加锁、解锁,调用三次(进三次同步方法)
        //但是,JVM会进行优化——在sb.append("A")时加锁,在sb.append("C")执行完后再解锁
        sb.append("A");
        sb.append("B");
        sb.append("C");
    }
}
  • 2.6.2 锁消除

删除不必要的加锁操作。根据代码逃逸技术,如果判断一段代码中,堆上的数据不会逃逸出当前线程,则认为此代码是线程安全的,无需加锁。

/**
 * 锁消除
 * Author: qqy
 */
public class Test11 {
    public static void main(String[] args) {
        //sb局部变量,每个线程都有自己的sb变量——不存在线程安全问题
        //JVM会把append()上的synchronized取消掉
        StringBuffer sb =new StringBuffer();
        sb.append("A").append("B").append("C");
    }
}
  • 2.7 死锁

一个线程等待另外一个线程执行完毕执行完成后才可以继续执行。但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁。死锁一旦出现之后,整个程序就将中断执行。过多的同步会造成死锁,对于资源的上锁一定不要成"环"

  • 产生原因:对共享资源的加锁成“环”
/**
 * 死锁
 * Author: qqy
 */
public class Test12 {
    private static Pen pen = new Pen();
    private static Book book = new Book();

    public static void main(String[] args) {
        //书线程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (book) {
                    System.out.println(Thread.currentThread().getName() + "书正在用");
                    synchronized (pen) {
                        System.out.println(Thread.currentThread().getName() + "把笔给我");
                    }
                }
            }
        }, "Book");
        
        //笔线程
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (pen) {
                    System.out.println(Thread.currentThread().getName() + "笔正在用");
                    synchronized (book) {
                        System.out.println(Thread.currentThread().getName() + "把书给我");
                    }
                }
            }
        }, "Pen");

        thread1.start();
        thread2.start();
    }
}

class Pen{
    private String name="笔";

    public String getName() {
        return name;
    }
}

class Book{
    private String name="书";

    public String getName() {
        return name;
    }
}

3.ThreadLocal详解 

  • 3.1 概念

ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。

  • 同步机制是为了保证多线程环境下数据的一致性ThreadLocal 是保证了多线程环境下数据的独立性
/**
 * ThreadLocal的简单使用
 * staticCommValue主线程修改完子线程再修改,——staticCommValue是共享的
 * threadLocal在子线程中修改的值在主线程看不到
 * Author: qqy
 */
public class Test {
    //多线程共享
    private static String staticCommValue;
    
    //多线程独立
    private static ThreadLocal<String> threadLocal=new ThreadLocal<>();
    
    //withInitial()静态方法,JDK8提供,可以实例化一个对象,初始化值(供给性函数)Hello
    private static ThreadLocal<String> threadLocal1=ThreadLocal.withInitial(()->"Hello");

    //main()是在Thread-main线程执行
    public static void main(String[] args) {
        //主线程中修改staticCommValue
        staticCommValue="main线程修改的staticCommValue";
        //在主线程中修改threadLocal
        threadLocal.set("这是main线程修改的threadLocal");

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //子线程中修改staticCommValue
                staticCommValue="子线程修改的staticCommValue";
                //在子线程中修改threadLocal
                threadLocal.set("这是子线程修改的threadLocal");
                System.out.println("子线程"+threadLocal.get());
            }
        },"子线程");

        thread.start();
        //等待子线程运行完后,主线程继续
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(staticCommValue);
        System.out.println(threadLocal.get());
    }
}
  • 3.2 ThreadLocal实现 

ThreadLocal 如何保证其值在各个线程中是独立?

  •  set(T value)方法
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap,ThreadLocalMap 相当于一个 HashMap,是真正保存值的地方。
        ThreadLocalMap map = getMap(t);
        //若不为空,则将 value 保存到 ThreadLocalMap 中,并用当前 ThreadLocal 作为 key;
        //否则创建一个 ThreadLocalMap 并给到当前线程,然后保存 value。
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  •  get()方法 
    public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //若不为null,则把获取 key 为当前 ThreadLocal 的值;
        //否则调用 setInitialValue() 方法返回初始值,并保存到新创建的 ThreadLocalMap 中。
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  •  remove()方法 
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
  • 3.3 ThreadLocalMap详解 

使用 ThreadLocal 类型变量进行相关操作时,会通过当前线程获取到 ThreadLocalMap 来完成操作。每个线程的 ThreadLocalMap 是属于线程自己的——ThreadLocal 类型的变量在每个线程中是独立的。

  • 3.3.1构造方法
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化容量为16的数组
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //取下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //更改阈值
        setThreshold(INITIAL_CAPACITY);
    }
  • 3.3.2 存储结构
    // 初始容量,必须是 2 的幂
    private static final int INITIAL_CAPACITY = 16;

    // 存储数据的哈希表
    private Entry[] table;

    // table 中已存储的条目数 
    private int size = 0;

    // 表示一个阈值,当 table 中存储的对象达到该值时就会扩容 
    private int threshold;

    // 设置 threshold 的值——长度的2/3
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
  • 3.3.3 存储对象Entry
    //Entry继承了一个弱引用(垃圾回收时,优先回收)
    static class Entry extends WeakReference<ThreadLocal<?>> {
        //value = ThreadLocal中存放的变量的值
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            //k在WeakReference中定义
            super(k);
            value = v;
        }
    }
  • 3.3.4 保存键值对
    private void set(ThreadLocal<?> key, Object value) {

        //将表格赋给临时变量
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        //取出长度
        int len = tab.length;
        //计算索引位置
        int i = key.threadLocalHashCode & (len-1);

        // 循环判断要存放的索引位置是否已经存在 Entry,若存在,进入循环体,替换k
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            // 若索引位置的 Entry 的 key 和要保存的 key 相等,则更新该 Entry 的值
            if (k == key) {
                e.value = value;
                return;
            }

            // 若key已经被回收了,表示该位置的 Entry 已经无效,用要保存的键值替换该位置上的 Entry
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // 要存放的索引位置没有 Entry,将当前键值作为一个 Entry 保存在该位置
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
        // 增加 table 存储的条目数
        int sz = ++size;
        //清除一些无效的条目并判断 table 中的条目数是否已经超出阈值
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();// 调整 table 的容量,并重新摆放 table 中的 Entry 
    }

    //ThreadLocal 对象实例化后就生成HashCode
    private final int threadLocalHashCode = nextHashCode();

    //使用 AtomicInteger(原子变量) 保证多线程环境下的同步
    private static AtomicInteger nextHashCode =
            new AtomicInteger();

    // 每次创建 ThreadLocal 对象是 HashCode 的增量
    private static final int HASH_INCREMENT = 0x61c88647;

    // 计算 ThreadLocal 对象的 HashCode
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
  • AtomicInteger

1. 初始化为1

2. getAndAdd(3)  返回1,当前值为4

3.getAndAdd(5)   返回4,当前值为9


    private void rehash() {
        //清除无效Entry
        expungeStaleEntries();

        // size >= 长度的一半,扩容
        if (size >= threshold - threshold / 4)
            resize();
    }
  • 3.3.5  获取Entry对象 
    private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
        // 使用指定的 key 的 HashCode 计算索引位置
        int i = key.threadLocalHashCode & (table.length - 1);
        // 获取当前位置的 Entry
        ThreadLocal.ThreadLocalMap.Entry e = table[i];
        // 如果 Entry 不为 null 且 Entry 的 key 和 指定的 key 相等,则返回该 Entry    
        // 否则调用 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 方法 ——解决哈希冲突
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

    private ThreadLocal.ThreadLocalMap.Entry 
    getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            // 如果 Entry 的 key 和指定的 key 相等,则返回该 Entry
            if (k == key)
                return e;
            // 如果 key 已经被回收了,清除无效的 Entry        
            // 否则获取下一个位置的 Entry,循环判断
            if (k == null)
                expungeStaleEntry(i);
            else
                //取得新的下标
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

    private static int nextIndex(int i, int len) {
        //下标+1 < 长度(没有越界),新下标=旧的+1,否则新下标取0
        return ((i + 1 < len) ? i + 1 : 0);
    }
  • 3.3.6 移除Entry 
private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        // 使用指定的 key 的 HashCode 计算索引位置
        int i = key.threadLocalHashCode & (len-1);
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                // 清除 Entry 的 key 的引用
                e.clear();
                // 清除无效的 Entry
                expungeStaleEntry(i);
                return;
            }
        }
    }
  • 3.4 内存泄漏

Entry 中的 key 使用了弱引用的方式,这样做是为了降低内存泄漏发生的概率。

ThreadLocalMap 的 set(),get() 和 remove() 方法中,都有清除无效 Entry 的操作——为了降低内存泄漏发生的可能。

在使用 ThreadLocal 的时候,每次用完 ThreadLocal 都调用 remove() 方法,清除数据,防止内存泄漏。

class MyRunnable implements Runnable{
    private ThreadLocal<String> threadLocal=new ThreadLocal<>();

    @Override
    public void run() {
        String name =Thread.currentThread().getName();
        threadLocal.set(name);
        System.out.println(name+"获取ThreadLocal的值"+threadLocal.get());
        //防止内存泄漏
        threadLocal.remove();
    }
}

猜你喜欢

转载自blog.csdn.net/qq_42142477/article/details/86211836