欧美一区二区三区老妇人-欧美做爰猛烈大尺度电-99久久夜色精品国产亚洲a-亚洲福利视频一区二区

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

ThreadLocal全面解析

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

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

  • 了解ThreadLocal的介紹
  • 掌握ThreadLocal的運(yùn)用場景
  • 了解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).
 *
 * <p>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.
 * <pre>
 * 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&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;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();
 *     }
 * }
 * </pre>
 * <p>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<T> {
    ...

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

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

1.2 基本使用

1.2.1 常用方法

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

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

1.2.2 使用案例

我們來看下面這個案例	
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é)果可以看出多個線程在訪問同一個變量的時候出現(xiàn)的異常,線程間的數(shù)據(jù)沒有隔離。下面我們來看下采用 ThreadLocal 的方式來解決這個問題的例子。

public class MyDemo {

    private static ThreadLocal<String> 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é)果來看,這樣很好的解決了多線程之間數(shù)據(jù)隔離的問題,十分方便。

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

1.3.1 synchronized同步方式

? 這里可能有的朋友會覺得在上述例子中我們完全可以通過加鎖來實(shí)現(xiàn)這個功能。我們首先來看一下用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í)可以解決這個問題,但是在這里我們強(qiáng)調(diào)的是線程數(shù)據(jù)隔離的問題,并不是多線程共享數(shù)據(jù)的問題, 在這個案例中使用synchronized關(guān)鍵字是不合適的。

1.3.2 ThreadLocal與synchronized的區(qū)別

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

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

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

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

2.1 轉(zhuǎn)賬案例

2.1.1 場景構(gòu)建

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

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

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

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

-- 使用數(shù)據(jù)庫
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配置文件和工具類

<c3p0-config>
<!-- 使用默認(rèn)的配置讀取連接池對象 -->
<default-config>
 <!--  連接參數(shù) -->
 <property name="driverClass">com.mysql.jdbc.Driver</property>
 <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
 <property name="user">root</property>
 <property name="password">1234</property>
 
 <!-- 連接池參數(shù) -->
 <property name="initialPoolSize">5</property>
 <property name="maxPoolSize">10</property>
 <property name="checkoutTimeout">3000</property>
</default-config>

</c3p0-config>

? (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ù)庫連接池對象屬性
    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)賬涉及兩個DML操作: 一個轉(zhuǎn)出,一個轉(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)賬過程中的異常
            int i = 1/0;
            // 轉(zhuǎn)入
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

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

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

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

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

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

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

2.2 常規(guī)解決方案

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

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

  • 從service層將connection對象向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ā)情況下,為了保證每個線程使用各自的connection,故加鎖
        synchronized (AccountService.class) {

            Connection conn = null;
            try {
                conn = JdbcUtils.getConnection();
                //開啟事務(wù)
                conn.setAutoCommit(false);
                // 轉(zhuǎn)出
                ad.out(conn, outUser, money);
                // 模擬轉(zhuǎn)賬過程中的異常
//            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層就無法使用了)

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中傳遞過來的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ī)方案的弊端

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

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

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

2.3 ThreadLocal解決方案

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

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

? (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對象 : 將connection綁定在當(dāng)前線程中
    private static final ThreadLocal<Connection> tl = new ThreadLocal();

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

    // 獲取連接
    public static Connection getConnection() throws SQLException {
        //取出當(dāng)前線程綁定的connection對象
        Connection conn = tl.get();
        if (conn == null) {
            //如果沒有,則從連接池中取出
            conn = ds.getConnection();
            //再將connection對象綁定到當(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對象

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();
            //開啟事務(wù)
            conn.setAutoCommit(false);
            // 轉(zhuǎn)出 : 這里不需要傳參了 !
            ad.out(outUser, money);
            // 模擬轉(zhuǎn)賬過程中的異常
//            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方案的好處

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

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

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

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

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

3.1 常見的誤解

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

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

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

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

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

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

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

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

4. ThreadLocal的核心方法源碼

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

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

方法聲明 描述
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邏輯是比較相似的,我們要研究清楚其中一個,其他也就明白了。

4.1 get方法

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

    /**
     * 返回當(dāng)前線程中保存ThreadLocal的值
     * 如果當(dāng)前線程沒有此ThreadLocal變量,
     * 則它會通過調(diào)用{@link #initialValue} 方法進(jìn)行初始化值
     *
     * @return 返回當(dāng)前線程對應(yīng)此ThreadLocal的值
     */
    public T get() {
        // 獲取當(dāng)前線程對象
        Thread t = Thread.currentThread();
        // 獲取此線程對象中維護(hù)的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以當(dāng)前的ThreadLocal 為 key,調(diào)用getEntry獲取對應(yīng)的存儲實(shí)體e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到對應(yīng)的存儲實(shí)體 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 獲取存儲實(shí)體 e 對應(yīng)的 value值
                // 即為我們想要的當(dāng)前線程對應(yīng)此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,則證明此線程沒有維護(hù)的ThreadLocalMap對象
        // 調(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)前線程對象
        Thread t = Thread.currentThread();
        // 獲取此線程對象中維護(hù)的ThreadLocalMap對象
        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對象
            // 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對象的初始化
            // 3)并將此實(shí)體entry作為第一個值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回設(shè)置的值value
        return value;
    }

    /**
     * 獲取當(dāng)前線程Thread對應(yīng)維護(hù)的ThreadLocalMap 
     * 
     * @param  t the current thread 當(dāng)前線程
     * @return the map 對應(yīng)維護(hù)的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/**
     *創(chuàng)建當(dāng)前線程Thread對應(yīng)維護(hù)的ThreadLocalMap 
     *
     * @param t 當(dāng)前線程
     * @param firstValue 存放到map中第一個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)前線程獲取一個Map

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

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

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

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

4.2 set方法

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

  /**
     * 設(shè)置當(dāng)前線程對應(yīng)的ThreadLocal的值
     *
     * @param value 將要保存在當(dāng)前線程對應(yīng)的ThreadLocal的值
     */
    public void set(T value) {
        // 獲取當(dāng)前線程對象
        Thread t = Thread.currentThread();
        // 獲取此線程對象中維護(hù)的ThreadLocalMap對象
        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對象
            // 2)則調(diào)用createMap進(jìn)行ThreadLocalMap對象的初始化
            // 3)并將此實(shí)體entry作為第一個值存放至ThreadLocalMap中
            createMap(t, value);
    }

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

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

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

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

4.3 remove方法

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

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

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

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

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

4.4 initialValue方法

/**
  * 返回當(dāng)前線程對應(yīng)的ThreadLocal的初始值
  
  * 此方法的第一次調(diào)用發(fā)生在,當(dāng)線程通過{@link #get}方法訪問此線程的ThreadLocal值時
  * 除非線程先調(diào)用了 {@link #set}方法,在這種情況下,
  * {@code initialValue} 才不會被這個線程調(diào)用。
  * 通常情況下,每個線程最多調(diào)用一次這個方法。
  *
  * <p>這個方法僅僅簡單的返回null {@code null};
  * 如果程序員想ThreadLocal線程局部變量有一個除null以外的初始值,
  * 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
  * 通常, 可以通過匿名內(nèi)部類的方式實(shí)現(xiàn)
  *
  * @return 當(dāng)前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

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

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

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

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

5. ThreadLocalMap源碼分析

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

? ThreadLocalMap是ThreadLocal的內(nè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ù)組長度必須是2的冥。
     */
    private Entry[] table;

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

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

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

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

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

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

5.2 hash沖突的解決

ThreadLocal使用的是自定義的ThreadLocalMap,接下來我們來探究一下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);
    }
  • 代碼很簡單,獲取當(dāng)前線程,并獲取當(dāng)前線程的ThreadLocalMap實(shí)例(從getMap(Thread t)中很容易看出來)。
  • 如果獲取到的map實(shí)例不為空,調(diào)用map.set()方法,否則調(diào)用構(gòu)造函數(shù) ThreadLocal.ThreadLocalMap(this, firstValue)實(shí)例化map。

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

(2) 下面來看看構(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);
    }

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

  • 關(guān)于& (INITIAL_CAPACITY - 1),這是取模的一種方式,對于2的冪作為模數(shù)取模,用此代替%(2^n),這也就是為啥容量必須為2的冥,在這個地方也得到了解答。
  • 關(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;

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

(3) ThreadLocalMap中的set()

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

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

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

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

    /**
     * 獲取環(huán)形數(shù)組的上一個索引
     */
    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)有說過。
        int i = key.threadLocalHashCode & (len-1);

        /**
         * 根據(jù)獲取到的索引進(jìn)行循環(huán),如果當(dāng)前索引上的table[i]不為空,在沒有return的情況下,
         * 就使用nextIndex()獲取下一個(上面提到到線性探測法)。
         */
        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為空,說明被回收了
             * 這個時候說明改table[i]可以重新使用,用新的key-value將其替換,并刪除其他無效的entry
             */
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

網(wǎng)站標(biāo)題:ThreadLocal的介紹與運(yùn)用
當(dāng)前URL:http://chinadenli.net/article46/dsoiohg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供Google、網(wǎng)站設(shè)計(jì)、服務(wù)器托管定制網(wǎng)站、電子商務(wù)微信公眾號

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站托管運(yùn)營