真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

ThreadLocal的介紹與運(yùn)用

ThreadLocal全面解析

學(xué)習(xí)目標(biāo)

公司主營(yíng)業(yè)務(wù):成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、移動(dòng)網(wǎng)站開(kāi)發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實(shí)現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競(jìng)爭(zhēng)能力。成都創(chuàng)新互聯(lián)公司是一支青春激揚(yáng)、勤奮敬業(yè)、活力青春激揚(yáng)、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊(duì)。公司秉承以“開(kāi)放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對(duì)我們的高要求,感謝他們從不同領(lǐng)域給我們帶來(lái)的挑戰(zhàn),讓我們激情的團(tuán)隊(duì)有機(jī)會(huì)用頭腦與智慧不斷的給客戶帶來(lái)驚喜。成都創(chuàng)新互聯(lián)公司推出大寧免費(fèi)做網(wǎng)站回饋大家。

  • 了解ThreadLocal的介紹
  • 掌握ThreadLocal的運(yùn)用場(chǎng)景
  • 了解ThreadLocal的內(nèi)部結(jié)構(gòu)
  • 了解ThreadLocal的核心方法源碼
  • 了解ThreadLocalMap的源碼

1. ThreadLocal介紹

1.1 官方介紹

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *
 * 

For example, the class below generates unique identifiers local to each * thread. * A thread's id is assigned the first time it invokes {@code ThreadId.get()} * and remains unchanged on subsequent calls. *

 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * 
*

Each thread holds an implicit reference to its copy of a thread-local * variable as long as the thread is alive and the {@code ThreadLocal} * instance is accessible; after a thread goes away, all of its copies of * thread-local instances are subject to garbage collection (unless other * references to these copies exist). * * @author Josh Bloch and Doug Lea * @since 1.2 */ public class ThreadLocal { ...

? 從Java官方文檔中的描述:ThreadLocal類用來(lái)提供線程內(nèi)部的局部變量。這種變量在多線程環(huán)境下訪問(wèn)(通過(guò)get和set方法訪問(wèn))時(shí)能保證各個(gè)線程的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量。ThreadLocal實(shí)例通常來(lái)說(shuō)都是private static類型的,用于關(guān)聯(lián)線程和線程上下文。

我們可以得知 ThreadLocal 的作用是:提供線程內(nèi)的局部變量,不同的線程之間不會(huì)相互干擾,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或組件之間一些公共變量傳遞的復(fù)雜度。
總結(jié):
1. 線程并發(fā): 在多線程并發(fā)的場(chǎng)景下
2. 傳遞數(shù)據(jù): 我們可以通過(guò)ThreadLocal在同一線程,不同組件中傳遞公共變量
3. 線程隔離: 每個(gè)線程的變量都是獨(dú)立的,不會(huì)相互影響

1.2 基本使用

1.2.1 常用方法

? 在使用之前,我們先來(lái)認(rèn)識(shí)幾個(gè)ThreadLocal的常用方法

方法聲明 描述
ThreadLocal() 創(chuàng)建ThreadLocal對(duì)象
public void set( T value) 設(shè)置當(dāng)前線程綁定的局部變量
public T get() 獲取當(dāng)前線程綁定的局部變量
public void remove() 移除當(dāng)前線程綁定的局部變量

1.2.2 使用案例

我們來(lái)看下面這個(gè)案例	
public class MyDemo {
    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                    System.out.println("-----------------------");
             		System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

打印結(jié)果:

? 從結(jié)果可以看出多個(gè)線程在訪問(wèn)同一個(gè)變量的時(shí)候出現(xiàn)的異常,線程間的數(shù)據(jù)沒(méi)有隔離。下面我們來(lái)看下采用 ThreadLocal 的方式來(lái)解決這個(gè)問(wèn)題的例子。

public class MyDemo {

    private static ThreadLocal tl = new ThreadLocal<>();

    private String content;

    private String getContent() {
        return tl.get();
    }

    private void setContent(String content) {
         tl.set(content);
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                    System.out.println("-----------------------");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("線程" + i);
            thread.start();
        }
    }
}

打印結(jié)果:

?

從結(jié)果來(lái)看,這樣很好的解決了多線程之間數(shù)據(jù)隔離的問(wèn)題,十分方便。

1.3 ThreadLocal類與synchronized關(guān)鍵字

1.3.1 synchronized同步方式

? 這里可能有的朋友會(huì)覺(jué)得在上述例子中我們完全可以通過(guò)加鎖來(lái)實(shí)現(xiàn)這個(gè)功能。我們首先來(lái)看一下用synchronized代碼塊實(shí)現(xiàn)的效果:

public class Demo02 {
    
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Demo02 demo02 = new Demo02();
        
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(){
                @Override
                public void run() {
                    synchronized (Demo02.class){
                        demo02.setContent(Thread.currentThread().getName() + "的數(shù)據(jù)");
                        System.out.println("-------------------------------------");
                        String content = demo02.getContent();
                        System.out.println(Thread.currentThread().getName() + "--->" + content);
                    }
                }
            };
            t.setName("線程" + i);
            t.start();
        }
    }
}

打印結(jié)果:

?

? 從結(jié)果可以發(fā)現(xiàn), 加鎖確實(shí)可以解決這個(gè)問(wèn)題,但是在這里我們強(qiáng)調(diào)的是線程數(shù)據(jù)隔離的問(wèn)題,并不是多線程共享數(shù)據(jù)的問(wèn)題, 在這個(gè)案例中使用synchronized關(guān)鍵字是不合適的。

1.3.2 ThreadLocal與synchronized的區(qū)別

? 雖然ThreadLocal模式與synchronized關(guān)鍵字都用于處理多線程并發(fā)訪問(wèn)變量的問(wèn)題, 不過(guò)兩者處理問(wèn)題的角度和思路不同。

synchronized ThreadLocal
原理 同步機(jī)制采用'以時(shí)間換空間'的方式, 只提供了一份變量,讓不同的線程排隊(duì)訪問(wèn) ThreadLocal采用'以空間換時(shí)間'的方式, 為每一個(gè)線程都提供了一份變量的副本,從而實(shí)現(xiàn)同時(shí)訪問(wèn)而相不干擾
側(cè)重點(diǎn) 多個(gè)線程之間訪問(wèn)資源的同步性 多線程中讓每個(gè)線程之間的數(shù)據(jù)相互隔離
總結(jié): 在剛剛的案例中,雖然使用ThreadLocal和synchronized都能解決問(wèn)題,但是使用ThreadLocal更為合適,因?yàn)檫@樣可以使程序擁有更高的并發(fā)性。

2. 運(yùn)用場(chǎng)景_事務(wù)案例

? 通過(guò)以上的介紹,我們已經(jīng)基本了解ThreadLocal的特點(diǎn)。但是它具體的應(yīng)用是在哪里呢? 現(xiàn)在讓我們一起來(lái)看一個(gè)ThreadLocal的經(jīng)典運(yùn)用場(chǎng)景: 事務(wù)。

2.1 轉(zhuǎn)賬案例

2.1.1 場(chǎng)景構(gòu)建

? 這里我們先構(gòu)建一個(gè)簡(jiǎn)單的轉(zhuǎn)賬場(chǎng)景: 有一個(gè)數(shù)據(jù)表account,里面有兩個(gè)用戶Jack和Rose,用戶Jack 給用戶Rose 轉(zhuǎn)賬。

? 案例的實(shí)現(xiàn)就簡(jiǎn)單的用mysql數(shù)據(jù)庫(kù),JDBC 和 C3P0 框架實(shí)現(xiàn)。以下是詳細(xì)代碼 :

? (1) 項(xiàng)目結(jié)構(gòu)

? (2) 數(shù)據(jù)準(zhǔn)備

-- 使用數(shù)據(jù)庫(kù)
use test;
-- 創(chuàng)建一張賬戶表
create table account(
	id int primary key auto_increment,
	name varchar(20),
	money double
);
-- 初始化數(shù)據(jù)
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 1000);

? (3) C3P0配置文件和工具類




 
 com.mysql.jdbc.Driver
 jdbc:mysql://localhost:3306/test
 root
 1234
 
 
 5
 10
 3000



? (4) 工具類 : JdbcUtils

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    // c3p0 數(shù)據(jù)庫(kù)連接池對(duì)象屬性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 獲取連接
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
    //釋放資源
    public static void release(AutoCloseable... ios){
        for (AutoCloseable io : ios) {
            if(io != null){
                try {
                    io.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    
    public static void commitAndClose(Connection conn) {
        try {
            if(conn != null){
                //提交事務(wù)
                conn.commit();
                //釋放連接
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose(Connection conn) {
        try {
            if(conn != null){
                //回滾事務(wù)
                conn.rollback();
                //釋放連接
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

? (5) dao層代碼 : AccountDao

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

    public void out(String outUser, int money) throws SQLException {
        String sql = "update account set money = money - ? where name = ?";

        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();

        JdbcUtils.release(pstm,conn);
    }

    public void in(String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";

        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();

        JdbcUtils.release(pstm,conn);
    }
}

? (6) service層代碼 : AccountService

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import java.sql.SQLException;

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // 轉(zhuǎn)出
            ad.out(outUser, money);
            // 轉(zhuǎn)入
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

? (7) web層代碼 : AccountWeb

package com.itheima.transfer.web;

import com.itheima.transfer.service.AccountService;

public class AccountWeb {

    public static void main(String[] args) {
        // 模擬數(shù)據(jù) : Jack 給 Rose 轉(zhuǎn)賬 100
        String outUser = "Jack";
        String inUser = "Rose";
        int money = 100;

        AccountService as = new AccountService();
        boolean result = as.transfer(outUser, inUser, money);

        if (result == false) {
            System.out.println("轉(zhuǎn)賬失敗!");
        } else {
            System.out.println("轉(zhuǎn)賬成功!");
        }
    }
}

2.1.2 引入事務(wù)

? 案例中的轉(zhuǎn)賬涉及兩個(gè)DML操作: 一個(gè)轉(zhuǎn)出,一個(gè)轉(zhuǎn)入。這些操作是需要具備原子性的,不可分割。不然就有可能出現(xiàn)數(shù)據(jù)修改異常情況。

public class AccountService {
    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // 轉(zhuǎn)出
            ad.out(outUser, money);
            // 模擬轉(zhuǎn)賬過(guò)程中的異常
            int i = 1/0;
            // 轉(zhuǎn)入
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

? 所以這里就需要操作事務(wù),來(lái)保證轉(zhuǎn)出和轉(zhuǎn)入操作具備原子性,要么同時(shí)成功,要么同時(shí)失敗。

(1) JDBC中關(guān)于事務(wù)的操作的api

Connection接口的方法 作用
void setAutoCommit(false) 禁用事務(wù)自動(dòng)提交(改為手動(dòng))
void commit(); 提交事務(wù)
void rollback(); 回滾事務(wù)

(2) 開(kāi)啟事務(wù)的注意點(diǎn):

  • 為了保證所有的操作在一個(gè)事務(wù)中,案例中使用的連接必須是同一個(gè): service層開(kāi)啟事務(wù)的connection需要跟dao層訪問(wèn)數(shù)據(jù)庫(kù)的connection保持一致

  • 線程并發(fā)情況下, 每個(gè)線程只能操作各自的 connection

2.2 常規(guī)解決方案

2.2.1 常規(guī)方案的實(shí)現(xiàn)

基于上面給出的前提, 大家通常想到的解決方案是 :

  • 從service層將connection對(duì)象向dao層傳遞
  • 加鎖

以下是代碼實(shí)現(xiàn)修改的部分:

? (1 ) AccountService 類

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        //線程并發(fā)情況下,為了保證每個(gè)線程使用各自的connection,故加鎖
        synchronized (AccountService.class) {

            Connection conn = null;
            try {
                conn = JdbcUtils.getConnection();
                //開(kāi)啟事務(wù)
                conn.setAutoCommit(false);
                // 轉(zhuǎn)出
                ad.out(conn, outUser, money);
                // 模擬轉(zhuǎn)賬過(guò)程中的異常
//            int i = 1/0;
                // 轉(zhuǎn)入
                ad.in(conn, inUser, money);
                //事務(wù)提交
                JdbcUtils.commitAndClose(conn);
            } catch (Exception e) {
                e.printStackTrace();
                //事務(wù)回滾
                JdbcUtils.rollbackAndClose(conn);
                return false;
            }
            return true;
        }
    }
}

? (2) AccountDao 類 (這里需要注意的是: connection不能在dao層釋放,要在service層,不然在dao層釋放,service層就無(wú)法使用了)

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

    public void out(Connection conn, String outUser, int money) throws SQLException{
        String sql = "update account set money = money - ? where name = ?";
        //注釋從連接池獲取連接的代碼,使用從service中傳遞過(guò)來(lái)的connection
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //連接不能在這里釋放,service層中還需要使用
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(Connection conn, String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

2.2.2 常規(guī)方案的弊端

上述方式我們看到的確按要求解決了問(wèn)題,但是仔細(xì)觀察,會(huì)發(fā)現(xiàn)這樣實(shí)現(xiàn)的弊端:

  1. 直接從service層傳遞connection到dao層, 造成代碼耦合度提高

  2. 加鎖會(huì)造成線程失去并發(fā)性,程序性能降低

2.3 ThreadLocal解決方案

2.3.1 ThreadLocal方案的實(shí)現(xiàn)

像這種需要在項(xiàng)目中進(jìn)行數(shù)據(jù)傳遞線程隔離的場(chǎng)景,我們不妨用ThreadLocal來(lái)解決:

? (1) 工具類的修改: 加入ThreadLocal

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    //ThreadLocal對(duì)象 : 將connection綁定在當(dāng)前線程中
    private static final ThreadLocal tl = new ThreadLocal();

    // c3p0 數(shù)據(jù)庫(kù)連接池對(duì)象屬性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();

    // 獲取連接
    public static Connection getConnection() throws SQLException {
        //取出當(dāng)前線程綁定的connection對(duì)象
        Connection conn = tl.get();
        if (conn == null) {
            //如果沒(méi)有,則從連接池中取出
            conn = ds.getConnection();
            //再將connection對(duì)象綁定到當(dāng)前線程中
            tl.set(conn);
        }
        return conn;
    }

    //釋放資源
    public static void release(AutoCloseable... ios) {
        for (AutoCloseable io : ios) {
            if (io != null) {
                try {
                    io.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void commitAndClose() {
        try {
            Connection conn = getConnection();
            //提交事務(wù)
            conn.commit();
            //解除綁定
            tl.remove();
            //釋放連接
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose() {
        try {
            Connection conn = getConnection();
            //回滾事務(wù)
            conn.rollback();
            //解除綁定
            tl.remove();
            //釋放連接
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

? (2) AccountService類的修改:不需要傳遞connection對(duì)象

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();

        try {
            Connection conn = JdbcUtils.getConnection();
            //開(kāi)啟事務(wù)
            conn.setAutoCommit(false);
            // 轉(zhuǎn)出 : 這里不需要傳參了 !
            ad.out(outUser, money);
            // 模擬轉(zhuǎn)賬過(guò)程中的異常
//            int i = 1 / 0;
            // 轉(zhuǎn)入
            ad.in(inUser, money);
            //事務(wù)提交
            JdbcUtils.commitAndClose();
        } catch (Exception e) {
            e.printStackTrace();
            //事務(wù)回滾
           JdbcUtils.rollbackAndClose();
            return false;
        }
        return true;
    }
}

? (3) AccountDao類的修改:照常使用

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

    public void out(String outUser, int money) throws SQLException {
        String sql = "update account set money = money - ? where name = ?";
        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //照常使用
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";
        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

2.3.2 ThreadLocal方案的好處

從上述的案例中我們可以看到, 在一些特定場(chǎng)景下,ThreadLocal方案有兩個(gè)突出的優(yōu)勢(shì):

  1. 傳遞數(shù)據(jù) : 保存每個(gè)線程綁定的數(shù)據(jù),在需要的地方可以直接獲取, 避免參數(shù)直接傳遞帶來(lái)的代碼耦合問(wèn)題

  2. 線程隔離 : 各線程之間的數(shù)據(jù)相互隔離卻又具備并發(fā)性,避免同步方式帶來(lái)的性能損失

3. ThreadLocal的內(nèi)部結(jié)構(gòu)

? 通過(guò)以上的學(xué)習(xí),我們對(duì)ThreadLocal的作用有了一定的認(rèn)識(shí)?,F(xiàn)在我們一起來(lái)看一下ThreadLocal的內(nèi)部結(jié)構(gòu),探究它能夠?qū)崿F(xiàn)線程數(shù)據(jù)隔離的原理。

3.1 常見(jiàn)的誤解

? 通常,如果我們不去看源代碼的話,我猜ThreadLocal是這樣子設(shè)計(jì)的:每個(gè)ThreadLocal類都創(chuàng)建一個(gè)Map,然后用線程的ID threadID作為Mapkey,要存儲(chǔ)的局部變量作為Mapvalue,這樣就能達(dá)到各個(gè)線程的局部變量隔離的效果。這是最簡(jiǎn)單的設(shè)計(jì)方法,JDK最早期的ThreadLocal就是這樣設(shè)計(jì)的。

3.2 核心結(jié)構(gòu)

? 但是,JDK后面優(yōu)化了設(shè)計(jì)方案,現(xiàn)時(shí)JDK8 ThreadLocal的設(shè)計(jì)是:每個(gè)Thread維護(hù)一個(gè)ThreadLocalMap哈希表,這個(gè)哈希表的keyThreadLocal實(shí)例本身,value才是真正要存儲(chǔ)的值Object。

? (1) 每個(gè)Thread線程內(nèi)部都有一個(gè)Map (ThreadLocalMap)
? (2) Map里面存儲(chǔ)ThreadLocal對(duì)象(key)和線程的變量副本(value)
? (3)Thread內(nèi)部的Map是由ThreadLocal維護(hù)的,由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置線程的變量值。
? (4)對(duì)于不同的線程,每次獲取副本值時(shí),別的線程并不能獲取到當(dāng)前線程的副本值,形成了副本的隔離,互不干擾。

3.3 這樣設(shè)計(jì)的好處

? 這個(gè)設(shè)計(jì)與我們一開(kāi)始說(shuō)的設(shè)計(jì)剛好相反,這樣設(shè)計(jì)有如下兩個(gè)優(yōu)勢(shì):

(1) 這樣設(shè)計(jì)之后每個(gè)Map存儲(chǔ)的Entry數(shù)量就會(huì)變少,因?yàn)橹暗拇鎯?chǔ)數(shù)量由Thread的數(shù)量決定,現(xiàn)在是由ThreadLocal的數(shù)量決定。

(2) 當(dāng)Thread銷毀之后,對(duì)應(yīng)的ThreadLocalMap也會(huì)隨之銷毀,能減少內(nèi)存的使用。

4. ThreadLocal的核心方法源碼

? 基于ThreadLocal的內(nèi)部結(jié)構(gòu),我們繼續(xù)探究一下ThreadLocal的核心方法源碼,更深入的了解其操作原理。

除了構(gòu)造之外, ThreadLocal對(duì)外暴露的方法有以下4個(gè):

方法聲明 描述
protected T initialValue() 返回當(dāng)前線程局部變量的初始值
public void set( T value) 設(shè)置當(dāng)前線程綁定的局部變量
public T get() 獲取當(dāng)前線程綁定的局部變量
public void remove() 移除當(dāng)前線程綁定的局部變量

其實(shí)get,set和remove邏輯是比較相似的,我們要研究清楚其中一個(gè),其他也就明白了。

4.1 get方法

(1 ) 源碼和對(duì)應(yīng)的中文注釋

    /**
     * 返回當(dāng)前線程中保存ThreadLocal的值
     * 如果當(dāng)前線程沒(méi)有此ThreadLocal變量,
     * 則它會(huì)通過(guò)調(diào)用{@link #initialValue} 方法進(jìn)行初始化值
     *
     * @return 返回當(dāng)前線程對(duì)應(yīng)此ThreadLocal的值
     */
    public T get() {
        // 獲取當(dāng)前線程對(duì)象
        Thread t = Thread.currentThread();
        // 獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以當(dāng)前的ThreadLocal 為 key,調(diào)用getEntry獲取對(duì)應(yīng)的存儲(chǔ)實(shí)體e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到對(duì)應(yīng)的存儲(chǔ)實(shí)體 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 獲取存儲(chǔ)實(shí)體 e 對(duì)應(yīng)的 value值
                // 即為我們想要的當(dāng)前線程對(duì)應(yīng)此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,則證明此線程沒(méi)有維護(hù)的ThreadLocalMap對(duì)象
        // 調(diào)用setInitialValue進(jìn)行初始化
        return setInitialValue();
    }

    /**
     * set的變樣實(shí)現(xiàn),用于初始化值initialValue,
     * 用于代替防止用戶重寫set()方法
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 調(diào)用initialValue獲取初始化的值
        T value = initialValue();
        // 獲取當(dāng)前線程對(duì)象
        Thread t = Thread.currentThread();
        // 獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在則調(diào)用map.set設(shè)置此實(shí)體entry
            map.set(this, value);
        else
            // 1)當(dāng)前線程Thread 不存在ThreadLocalMap對(duì)象
            // 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對(duì)象的初始化
            // 3)并將此實(shí)體entry作為第一個(gè)值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回設(shè)置的值value
        return value;
    }

    /**
     * 獲取當(dāng)前線程Thread對(duì)應(yīng)維護(hù)的ThreadLocalMap 
     * 
     * @param  t the current thread 當(dāng)前線程
     * @return the map 對(duì)應(yīng)維護(hù)的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *創(chuàng)建當(dāng)前線程Thread對(duì)應(yīng)維護(hù)的ThreadLocalMap 
     *
     * @param t 當(dāng)前線程
     * @param firstValue 存放到map中第一個(gè)entry的值
     */
	void createMap(Thread t, T firstValue) {
        //這里的this是調(diào)用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

(2 ) 代碼執(zhí)行流程

? A. 首先獲取當(dāng)前線程

? B. 根據(jù)當(dāng)前線程獲取一個(gè)Map

? C. 如果獲取的Map不為空,則在Map中以ThreadLocal的引用作為key來(lái)在Map中獲取對(duì)應(yīng)的value e,否則轉(zhuǎn)到E

? D. 如果e不為null,則返回e.value,否則轉(zhuǎn)到E

? E. Map為空或者e為空,則通過(guò)initialValue函數(shù)獲取初始值value,然后用ThreadLocal的引用和value作為firstKey和firstValue創(chuàng)建一個(gè)新的Map

總結(jié): 先獲取當(dāng)前線程的 ThreadLocalMap 變量,如果存在則返回值,不存在則創(chuàng)建并返回初始值。

4.2 set方法

(1 ) 源碼和對(duì)應(yīng)的中文注釋

  /**
     * 設(shè)置當(dāng)前線程對(duì)應(yīng)的ThreadLocal的值
     *
     * @param value 將要保存在當(dāng)前線程對(duì)應(yīng)的ThreadLocal的值
     */
    public void set(T value) {
        // 獲取當(dāng)前線程對(duì)象
        Thread t = Thread.currentThread();
        // 獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在則調(diào)用map.set設(shè)置此實(shí)體entry
            map.set(this, value);
        else
            // 1)當(dāng)前線程Thread 不存在ThreadLocalMap對(duì)象
            // 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對(duì)象的初始化
            // 3)并將此實(shí)體entry作為第一個(gè)值存放至ThreadLocalMap中
            createMap(t, value);
    }

(2 ) 代碼執(zhí)行流程

? A. 首先獲取當(dāng)前線程,并根據(jù)當(dāng)前線程獲取一個(gè)Map

? B. 如果獲取的Map不為空,則將參數(shù)設(shè)置到Map中(當(dāng)前ThreadLocal的引用作為key)

? C. 如果Map為空,則給該線程創(chuàng)建 Map,并設(shè)置初始值

4.3 remove方法

(1 ) 源碼和對(duì)應(yīng)的中文注釋

 /**
     * 刪除當(dāng)前線程中保存的ThreadLocal對(duì)應(yīng)的實(shí)體entry
     */
     public void remove() {
        // 獲取當(dāng)前線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在則調(diào)用map.remove
            // 以當(dāng)前ThreadLocal為key刪除對(duì)應(yīng)的實(shí)體entry
             m.remove(this);
     }

(2 ) 代碼執(zhí)行流程

? A. 首先獲取當(dāng)前線程,并根據(jù)當(dāng)前線程獲取一個(gè)Map

? B. 如果獲取的Map不為空,則移除當(dāng)前ThreadLocal對(duì)象對(duì)應(yīng)的entry

4.4 initialValue方法

/**
  * 返回當(dāng)前線程對(duì)應(yīng)的ThreadLocal的初始值
  
  * 此方法的第一次調(diào)用發(fā)生在,當(dāng)線程通過(guò){@link #get}方法訪問(wèn)此線程的ThreadLocal值時(shí)
  * 除非線程先調(diào)用了 {@link #set}方法,在這種情況下,
  * {@code initialValue} 才不會(huì)被這個(gè)線程調(diào)用。
  * 通常情況下,每個(gè)線程最多調(diào)用一次這個(gè)方法。
  *
  * 

這個(gè)方法僅僅簡(jiǎn)單的返回null {@code null}; * 如果程序員想ThreadLocal線程局部變量有一個(gè)除null以外的初始值, * 必須通過(guò)子類繼承{@code ThreadLocal} 的方式去重寫此方法 * 通常, 可以通過(guò)匿名內(nèi)部類的方式實(shí)現(xiàn) * * @return 當(dāng)前ThreadLocal的初始值 */ protected T initialValue() { return null; }

? 此方法的作用是 返回該線程局部變量的初始值。

(1) 這個(gè)方法是一個(gè)延遲調(diào)用方法,從上面的代碼我們得知,在set方法還未調(diào)用而先調(diào)用了get方法時(shí)才執(zhí)行,并且僅執(zhí)行1次。

(2)這個(gè)方法缺省實(shí)現(xiàn)直接返回一個(gè)null

(3)如果想要一個(gè)除null之外的初始值,可以重寫此方法。(備注: 該方法是一個(gè)protected的方法,顯然是為了讓子類覆蓋而設(shè)計(jì)的)

5. ThreadLocalMap源碼分析

5.1 基本結(jié)構(gòu)

? ThreadLocalMap是ThreadLocal的內(nèi)部類,沒(méi)有實(shí)現(xiàn)Map接口,用獨(dú)立的方式實(shí)現(xiàn)了Map的功能,其內(nèi)部的Entry也是獨(dú)立實(shí)現(xiàn)。

(1) 成員變量

    /**
     * 初始容量 —— 必須是2的整次冪
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放數(shù)據(jù)的table,Entry類的定義在下面分析
     * 同樣,數(shù)組長(zhǎng)度必須是2的冥。
     */
    private Entry[] table;

    /**
     * 數(shù)組里面entrys的個(gè)數(shù),可以用于判斷table當(dāng)前使用量是否超過(guò)負(fù)因子。
     */
    private int size = 0;

    /**
     * 進(jìn)行擴(kuò)容的閾值,表使用量大于它的時(shí)候進(jìn)行擴(kuò)容。
     */
    private int threshold; // Default to 0
    
    /**
     * 閾值設(shè)置為長(zhǎng)度的2/3
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

(2) 存儲(chǔ)結(jié)構(gòu) - Entry

// 在ThreadLocalMap中,也是用Entry來(lái)保存K-V結(jié)構(gòu)數(shù)據(jù)的。但是Entry中key只能是ThreadLocal對(duì)象,這點(diǎn)被Entry的構(gòu)造方法已經(jīng)限定死了
// 另外,Entry繼承WeakReference,使用弱引用,可以將ThreadLocal對(duì)象的生命周期和線程生命周期解綁,持有對(duì)ThreadLocal的弱引用,可以使得ThreadLocal在沒(méi)有其他強(qiáng)引用的時(shí)候被回收掉,這樣可以避免因?yàn)榫€程得不到銷毀導(dǎo)致ThreadLocal對(duì)象無(wú)法被回收

static class Entry extends WeakReference {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

5.2 hash沖突的解決

ThreadLocal使用的是自定義的ThreadLocalMap,接下來(lái)我們來(lái)探究一下ThreadLocalMap的hash沖突解決方式。

(1) 先回顧ThreadLocal的set() 方法

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }
  • 代碼很簡(jiǎn)單,獲取當(dāng)前線程,并獲取當(dāng)前線程的ThreadLocalMap實(shí)例(從getMap(Thread t)中很容易看出來(lái))。
  • 如果獲取到的map實(shí)例不為空,調(diào)用map.set()方法,否則調(diào)用構(gòu)造函數(shù) ThreadLocal.ThreadLocalMap(this, firstValue)實(shí)例化map。

可以看出來(lái)線程中的ThreadLocalMap使用的是延遲初始化,在第一次調(diào)用get()或者set()方法的時(shí)候才會(huì)進(jìn)行初始化。

(2) 下面來(lái)看看構(gòu)造函數(shù)ThreadLocalMap(ThreadLocal firstKey, Object firstValue)

 ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //計(jì)算索引
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //設(shè)置值
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
        size = 1;
        //設(shè)置閾值
        setThreshold(INITIAL_CAPACITY);
    }

主要說(shuō)一下計(jì)算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

  • 關(guān)于& (INITIAL_CAPACITY - 1),這是取模的一種方式,對(duì)于2的冪作為模數(shù)取模,用此代替%(2^n),這也就是為啥容量必須為2的冥,在這個(gè)地方也得到了解答。
  • 關(guān)于firstKey.threadLocalHashCode
 private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    private static AtomicInteger nextHashCode =  new AtomicInteger();
            
    private static final int HASH_INCREMENT = 0x61c88647;

? 這里定義了一個(gè)AtomicInteger類型,每次獲取當(dāng)前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,這個(gè)值和斐波那契散列有關(guān)(這是一種乘數(shù)散列法,只不過(guò)這個(gè)乘數(shù)比較特殊,是32位整型上限2^32-1乘以黃金分割比例0.618....的值2654435769,用有符號(hào)整型表示就是-1640531527,去掉符號(hào)后16進(jìn)制表示為0x61c88647),其主要目的就是為了讓哈希碼能均勻的分布在2的n次方的數(shù)組里, 也就是Entry[] table中,這樣做可以盡量避免hash沖突。

(3) ThreadLocalMap中的set()

? ThreadLocalMap使用開(kāi)發(fā)地址-線性探測(cè)法來(lái)解決哈希沖突,線性探測(cè)法的地址增量di = 1, 2, ... 其中,i為探測(cè)次數(shù)。該方法一次探測(cè)下一個(gè)地址,直到有空的地址后插入,若整個(gè)空間都找不到空余的地址,則產(chǎn)生溢出。假設(shè)當(dāng)前table長(zhǎng)度為16,也就是說(shuō)如果計(jì)算出來(lái)key的hash值為14,如果table[14]上已經(jīng)有值,并且其key與當(dāng)前key不一致,那么就發(fā)生了hash沖突,這個(gè)時(shí)候?qū)?4加1得到15,取table[15]進(jìn)行判斷,這個(gè)時(shí)候如果還是沖突會(huì)回到0,取table[0],以此類推,直到可以插入。

按照上面的描述,可以把table看成一個(gè)環(huán)形數(shù)組。

先看一下線性探測(cè)相關(guān)的代碼,從中也可以看出來(lái)table實(shí)際是一個(gè)環(huán):

    /**
     * 獲取環(huán)形數(shù)組的下一個(gè)索引
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * 獲取環(huán)形數(shù)組的上一個(gè)索引
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

ThreadLocalMap的set()代碼如下:

private void set(ThreadLocal key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //計(jì)算索引,上面已經(jīng)有說(shuō)過(guò)。
        int i = key.threadLocalHashCode & (len-1);

        /**
         * 根據(jù)獲取到的索引進(jìn)行循環(huán),如果當(dāng)前索引上的table[i]不為空,在沒(méi)有return的情況下,
         * 就使用nextIndex()獲取下一個(gè)(上面提到到線性探測(cè)法)。
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal k = e.get();
            //table[i]上key不為空,并且和當(dāng)前key相同,更新value
            if (k == key) {
                e.value = value;
                return;
            }
            /**
             * table[i]上的key為空,說(shuō)明被回收了
             * 這個(gè)時(shí)候說(shuō)明改table[i]可以重新使用,用新的key-value將其替換,并刪除其他無(wú)效的entry
             */
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

網(wǎng)站標(biāo)題:ThreadLocal的介紹與運(yùn)用
本文URL:http://weahome.cn/article/dsoiohg.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部