阿里雲ECS服務器部署HADOOP集群(一):Hadoop完全分佈式集群環境搭建,阿里雲ECS服務器部署HADOOP集群(二):HBase完全分佈式集群搭建(使用外置ZooKeeper),阿里雲ECS服務器部署HADOOP集群(三):ZooKeeper 完全分佈式集群搭建,阿里雲ECS服務器部署HADOOP集群(四):Hive本地模式的安裝,阿里雲ECS服務器部署HADOOP集群(六):Flume 安裝,阿里雲ECS服務器部署HADOOP集群(七):Sqoop 安裝

準備:

兩台配置CentOS 7.3的阿里雲ECS服務器;

hadoop-2.7.3.tar.gz安裝包;

jdk-8u77-linux-x64.tar.gz安裝包;

hostname及IP的配置:

更改主機名:

由於系統為CentOS 7,可以直接使用‘hostnamectl set-hostname 主機名’來修改,修改完畢后重新shell登錄或者重啟服務器即可。

1 hostnamectl set-hostname master
2 exit
3 ssh root@master
1 hostnamectl set-hostname slave1
2 exit 3 ssh root@slave1

設置本地域名:

設置本地域名這一步非常關鍵,ip的本地域名信息配置不好,即有可能造成Hadoop啟動出現問題,又有可能造成在使用Hadoop的MapReduce進行計算時報錯。在ECS上搭建Hadoop集群環境需參考以下兩篇文章:

總結一下那就是,在“/etc/hosts”文件中進行域名配置時要遵從2個原則:

  •  新加域名在前面: 將新添加的Master、Slave服務器ip域名(例如“test7972”),放置在ECS服務器原有本地域名(例如“iZuf67wb***************”)的前面。但是注意ECS服務器原有本地      域名(例如“iZuf67wb***************”)不能被刪除,因為操作系統別的地方還會使用到。
  •  IP本機內網,其它外網: 在本機上的操作,都要設置成內網ip;其它機器上的操作,要設置成外網ip。

master

slave1

此處摘自 

配置好后需要在各個節點上執行如下命令,測試是否相互 ping 得通,如果 ping 不通,後面就無法順利配置成功:

1 ping master -c 3
2 ping slave1 -c 3

例如我在 master 節點上 ping slave1 ,ping 通的話會显示 time,显示的結果如下圖所示:

各節點角色分配

master: NameNode  ResourceManager

slave1: DataNode NodeManager

免密碼登錄配置

分別在 master 和 slave1 上做如下操作

1 ssh-keygen -t rsa
2 ssh-copy-id master 3 ssh-copy-id slave1

驗證

ssh master date;ssh slave1 date

配置JDK

解壓JDK安裝包到/usr/local/下

tar -zxvf jdk-8u77-linux-x64.tar.gz -C /usr/local/

將解壓目錄改為 jdk1.8

mv jdk1.8.0_77/ jdk1.8/

設置JAVA_HOME到系統環境變量

vim /etc/profile

在最後加入以下兩行代碼

export JAVA_HOME=/usr/local/jdk1.8
export PATH=$PATH:$JAVA_HOME/bin

重新加載環境

source /etc/profile

這樣 master 的jdk就配置好了,可以用命令 java -version 測試下。

java -version

下面只需將 master 上配置好的文件分發到 slave1 上即可。

將/usr/local/jdk1.8分發到 slave1 的/usr/local/下(建議壓縮后再分發)

scp -r /usr/local/jdk1.8/ slave1:/usr/local/

將/etc/profile分發到 slave1 的/etc/下

scp /etc/profile slave1:/etc/

  然後重新加載 slave1 環境便完成了 slave1 的jdk配置

source /etc/profile

hadoop集群配置

1 cd ~
2 tar -zxvf hadoop-2.7.3.tar.gz -C /usr/local # 解壓到/usr/local中
3 cd /usr/local/
4 mv ./hadoop-2.7.3/ ./hadoop            # 將文件夾名改為hadoop

輸入如下命令來檢查 Hadoop 是否可用,成功則會显示 Hadoop 版本信息:

1 cd /usr/local/hadoop
2 ./bin/hadoop version

添加 HADOOP_HOME 到系統環境變量

vim /etc/profile

在後面添加如下兩行

1 export HADOOP_HOME=/usr/local/hadoop
2 export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin

重新加載環境,並輸出變量 HADOOP_HOME 驗證

進入/user/local/hadoop/etc/hadoop/可以看到如下配置文件

集群/分佈式模式需要修改 /usr/local/hadoop/etc/hadoop 中的6個配置文件,更多設置項可點擊查看官方說明,這裏僅設置了我完成課堂作業所必須的設置項:hadoop-env.sh, slaves,  ,  ,  ,   。

1.首先來配置 hadoop-env.sh ,只需要設置一下JAVA_HOME即可

注:之前在配置jdk中配置的是基於系統的JAVA_HOME變量,這裏需要配置基於Hadoop集群的JAVA_HOME變量。

hadoop-env.sh 是Hadoop的環境變量配置腳本。

所以應做以下修改 vim hadoop-env.sh

export JAVA_HOME=/usr/local/jdk1.8

2.配置 slave , 指定 slave 節點

sudo vi slaves

刪去原有的 localhost , 添加將作為 slave 節點的 slave1

3.配置 core-site.xml 

 1 <configuration>
 2 
 3     <property>
 4         <name>fs.defaultFS</name>
 5         <value>hdfs://master:9000</value>
 6         <description>The name of the default file system.</description>
 7     </property> 
 8 # 設置訪問hdfs的默認名,9000是默認端口
 9 
10     <property>
11         <name>hadoop.tmp.dir</name>
12         <value>/usr/local/hadoop/tmp</value>
13         <description>Abase for other temporary directories.</description>
14     </property>
15 # 在hdfs格式化的時候會自動創建相應的目錄 'tmp/' 16 17 <property> 18 <name>fs.trash.interval</name> 19 <value>4320</value> 20 <description>Number of minutes after which the checkpoint gets deleted.</description> 21 </property> 22 # 設置回收站里的文件保留時間(單位:秒) 23 24 </configuration>

4.配置 hdfs-site.xml 

 1 <configuration>
 2 
 3     <property>
 4         <name>dfs.namenode.name.dir</name>
 5         <value>/usr/local/hadoop/tmp/dfs/name</value>
 6     </property>
 7 
 8     <property>
 9         <name>dfs.datanode.data.dir</name>
10         <value>/usr/local/hadoop/tmp/dfs/data</value>
11     </property>
12 
13     <property>
14         <name>dfs.replication</name>
15         <value>1</value>
16     </property>
17 # 副本,因為有一個 slave 節點這裏設置為1(一般偽分佈模式設1個,三個或三個以上節點設3個)
18 
19     <property>
20         <name>dfs.permissions.enabled</name>
21         <value>false</value>
22         <description>If "true", enable permission checking in HDFS. If "false", permission checking is turned off, but all other behavior is unchanged. Switching from one parameter value to the other does not change the mode, owner or group of files or directories.</description>
23     </property>
24 
25 </configuration>    

5.配置 mapred-site.xml (這個文件沒有直接提供,而是提供了模版文件,需將模版文件轉換為配置文件) 

1 sudo mv mapred-site.xml.template mapred-site.xml
2 sudo vi mapred-site.xml
 1 <configuration>
 2 
 3     <property>
 4         <name>mapreduce.framework.name</name>
 5         <value>yarn</value>
 6         <description>The runtime framework for executing MapReduce jobs.Can be one of local, classic or yarn.</description>
 7     </property>
 8     <property>
 9         <name>mapreduce.jobtracker.http.address</name>
10         <value>master:50030</value>
11     </property>
12     <property>
13         <name>mapreduce.jobhisotry.address</name>
14         <value>master:10020</value>
15     </property>
16     <property>
17         <name>mapreduce.jobhistory.webapp.address</name>
18         <value>master:19888</value>
19     </property>
20     <property>
21         <name>mapreduce.jobhistory.done-dir</name>
22         <value>/jobhistory/done</value>
23     </property>
24     <property>
25         <name>mapreduce.jobhistory.intermediate-done-dir</name>
26         <value>/jobhisotry/done_intermediate</value>
27     </property>
28     <property>
29         <name>mapreduce.job.ubertask.enable</name>
30         <value>true</value>
31         <description>Whether to enable the small-jobs "ubertask" optimization,which runs "sufficiently small" jobs sequentially within a single JVM."Small" is defined by the following maxmaps, maxreduces, and maxbytes settings. Note that configurations for application masters also affect the "Small" definition - yarn.app.mapreduce.am.resource.mb must be larger than both mapreduce.map.memory.mb and mapreduce.reduce.memory.mb, and yarn.app.mapreduce.am.resource.cpu-vcores must be larger than both mapreduce.map.cpu.vcores and mapreduce.reduce.cpu.vcores to enable ubertask. Users may override this value.</description>
32     </property>
33 
34 </configuration>

6.配置 yarn-site.xml

 1 <configuration>
 2 
 3     <property>
 4         <name>yarn.resourcemanager.hostname</name>
 5         <value>master</value>
 6     </property>
 7     <property>
 8         <name>yarn.nodemanager.aux-services</name>
 9         <value>mapreduce_shuffle</value>
10         <description>A comma separated list of services where service name should only contain a-zA-Z0-9_ and can not start with numbers</description>
11     </property>
12     <property>
13         <name>yarn.resourcemanager.address</name>
14         <value>master:18040</value>
15     </property>
16     <property>
17         <name>yarn.resourcemanager.scheduler.address</name>
18         <value>master:18030</value>
19     </property>
20     <property>
21         <name>yarn.resourcemanager.resource-tracker.address</name>
22         <value>master:18025</value>
23     </property>
24     <property>
25         <name>yarn.resourcemanager.admin.address</name>
26         <value>master:18141</value>
27     </property>
28     <property>
29         <name>yarn.resourcemanager.webapp.address</name>
30         <value>master:18088</value>
31     </property>
32     <property>
33         <name>yarn.log-aggregation-enable</name>
34         <value>true</value>
35     </property>
36     <property>
37         <name>yarn.log-aggregation.retain-seconds</name>
38         <value>86400</value>
39     </property>
40     <property>
41         <name>yarn.log-aggregation.retain-check-interval-seconds</name>
42         <value>86400</value>
43     </property>
44     <property>
45         <name>yarn.nodemanager.remote-app-log-dir</name>
46         <value>/tmp/logs</value>
47     </property>
48     <property>
49         <name>yarn.nodemanager.remote-app-log-dir-suffix</name>
50         <value>logs</value>
51     </property>
52 
53 </configuration>

 到這裏 master 就已經配置好了,下面將該服務器的配置分發到 slave1 上去(建議壓縮后再分發),在此使用壓縮後分發的方法

在 master 節點上執行

1 cd /usr/local
2 tar -zcvf ~/hadoop.master.tar.gz ./hadoop 3 cd ~ 4 scp ./hadoop.master.tar.gz slave1:/root/ 5 scp /etc/profile slave1:/etc/

在 slave1 節點上執行

tar -zxvf ~/hadoop.master.tar.gz -C /usr/local

在 slave1 上重新加載環境並檢查驗證

source /etc/profile
echo $HADOOP_HOME

HDFS NameNode 格式化(只要在 master 上執行即可)

$HADOOP_HOME/bin/hdfs namenode -format

看到下面的輸出,表明hdfs格式化成功

INFO common.Storage: Storage directory /usr/local/hadoop/tmp/dfs/name has been successfully formatted.

啟動前檢查防火牆狀態

systemctl status firewalld

我這裡是已經關閉的,若未關閉,可以參考下圖(來自)

阿里雲服務器還需要在服務器安全組裡配置防火牆,需將配置文件里的相關端口全部添加,否則會出現 web 頁面打不開,以及 DataNode 啟動但 Live datenode 為 0 等問題

啟動 Hadoop 集群

$HADOOP_HOME/sbin/start-all.sh

 

  • 第一次啟動 hadoop 時會出現 ssh 提示,提示是否要連入 0.0.0.0 節點,輸入 yes 即可
  • 若出現 hadoop 啟動時 datanode 沒有啟動,可以參考來解決

啟動 job history server

在 master 上執行

$HADOOP_HOME/sbin/mr-jobhistory-daemon.sh start historyserver

成功后在兩個節點上驗證

在 master 上 執行 

jps

可以看到 ResourceManager、SecondaryNameNode、NameNode、JobHistoryServer 四個進程全部啟動

在 slave1 上執行

jps

可以看到 NodeManager、DataNode 兩個進程全部啟動

缺少任一進程都表示出錯。另外還需要在 Master 節點上通過命令 hdfs dfsadmin -report 查看 DataNode 是否正常啟動,如果 Live datanodes 不為 0 ,則說明集群啟動成功。例如我這邊一共有 1 個 Datanodes:

全部配置完成之後查看 web 頁面

hdfs: http://master:50070/

hdfs Datanode 節點信息

hdfs 的情況

hdfs 的文件情況

 yarn:http://master:18088/

 

阿里雲ECS服務器部署HADOOP集群系列:

 

 

 

 

 

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

Vue項目使用CSS變量實現主題化

主題化管理經常能在網站上看到,一般的思路都是將主題相關的CSS樣式獨立出來,在用戶選擇主題的時候加載相應的CSS樣式文件。現在大部分瀏覽器都能很好的兼容,主題化樣式更容易管理了。最近,使用CSS變量在Vue項目中做了一個主題化實踐,下面來看看整個過程。

可行性測試

為了檢驗方法的可行性,在public文件夾下新建一個themes文件夾,並在themes文件夾新建一個default.css文件:

:root {
  --color: red;
}

在public文件夾的index.html文件中引入外部樣式theme.css,如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>vue-skin-peeler-demo</title>
    <!-- 引入themes文件夾下的default.css -->
    <link rel="stylesheet" type="text/css" href="src/themes/default.css" rel="external nofollow">
  </head>
  <body>
    <noscript>
      <strong>We're sorry but vue-skin-peeler-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

然後,在Home.vue中使用CSS變量:

<template>
  <div class="home">
    <div :class="$style.demo">變紅色</div>
  </div>
</template>

<script>
export default {
  name: 'home'
}
</script>

<style module lang="scss">
  .demo {
    color: var(--color);
  }
</style>

然後,運行項目並在瀏覽器中打開頁面,頁面显示效果正常。

注意:@vue/cli使用link標籤引入css樣式可能報錯“We’re sorry but vue-skin-peeler-demo doesn’t work properly without JavaScript enabled. Please enable it to continue.”。這是因為@vue/cli將src目錄下的文件都通過webpack打包所引起,所以,靜態文件資源要放在public(如果是@vue/cli 2.x版本放在static)文件夾下。

實現主題切換

這裏主題切換的思路是替換link標籤的href屬性,因此,需要寫一個替換函數,在src目錄下新建themes.js文件,代碼如下:

// themes.js
const createLink = (() => {
  let $link = null
  return () => {
    if ($link) {
      return $link
    }
    $link = document.createElement('link')
    $link.rel = 'stylesheet'
    $link.type = 'text/css'
    document.querySelector('head').appendChild($link)
    return $link
  }
})()

/**
 * 主題切換函數
 * @param {string} theme - 主題名稱, 默認default
 * @return {string} 主題名稱
 */
const toggleTheme = (theme = 'default') => {
  const $link = createLink()
  $link.href = `./themes/${theme}.css`
  return theme
}

export default toggleTheme

然後,在themes文件下創建default.css和dark.css兩個主題文件。創建CSS變量,實現主題化。CSS變量實現主題切換請參考另一篇文章

兼容性

IE瀏覽器以及一些舊版瀏覽器不支持CSS變量,因此,需要使用,是一個,可在舊版和現代瀏覽器中為CSS自定義屬性(也稱為“ CSS變量”)提供客戶端支持。由於要開啟watch監聽,所以還有安裝。

安裝:

npm install css-vars-ponyfill mutationobserver-shim --save

然後,在themes.js文件中引入並使用:

// themes.js
import 'mutationobserver-shim'
import cssVars from 'css-vars-ponyfill'

cssVars({
  watch: true
})

const createLink = (() => {
  let $link = null
  return () => {
    if ($link) {
      return $link
    }
    $link = document.createElement('link')
    $link.rel = 'stylesheet'
    $link.type = 'text/css'
    document.querySelector('head').appendChild($link)
    return $link
  }
})()

/**
 * 主題切換函數
 * @param {string} theme - 主題名稱, 默認default
 * @return {string} 主題名稱
 */
const toggleTheme = (theme = 'default') => {
  const $link = createLink()
  $link.href = `./themes/${theme}.css`
  return theme
}

export default toggleTheme

開啟watch后,在IE 11瀏覽器點擊切換主題開關不起作用。因此,每次切換主題時都重新執行cssVars(),還是無法切換主題,原因是開啟watch后重新執行cssVars()是無效的。最後,只能先關閉watch再重新開啟。成功切換主題的themes.js代碼如下:

// themes.js
import 'mutationobserver-shim'
import cssVars from 'css-vars-ponyfill'

const createLink = (() => {
  let $link = null
  return () => {
    if ($link) {
      return $link
    }
    $link = document.createElement('link')
    $link.rel = 'stylesheet'
    $link.type = 'text/css'
    document.querySelector('head').appendChild($link)
    return $link
  }
})()

/**
 * 主題切換函數
 * @param {string} theme - 主題名稱, 默認default
 * @return {string} 主題名稱
 */
const toggleTheme = (theme = 'default') => {
  const $link = createLink()
  $link.href = `./themes/${theme}.css`
  cssVars({
    watch: false
  })
  setTimeout(function () {
    cssVars({
      watch: true
    })
  }, 0)
  return theme
}

export default toggleTheme

查看所有代碼,請移步。

記住主題

實現記住主題這個功能,一是可以向服務器保存主題,一是使用本地存儲主題。為了方便,這裏主要使用本地存儲主題的方式,即使用localStorage存儲主題。具體實現請移步。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

Tesla計畫於上海建電動車廠,關稅考量為主因

電動車製造商特斯拉(Tesla)在Model X 的銷售和Model 3 的產能上都面臨著巨大挑戰,在全球最大的電動車市場──中國,Tesla 則看到了電動車需求持續成長帶來的機會,並希望能夠透過投資建廠、本土化製造等方式在中國電動車市場分一杯羹,據悉Tesla 將在上海建造海外第一座電動車製造工廠,未來可能會用於Model 3 電動車的生產。

據知情人士透露,特斯拉已經與上海市政府達成合作協議,將首次在中國生產製造電動車,此次合作將有助於特斯拉進一步提升在中國市場的銷售,目前中國是全球最大的電動車市場,政府對於電動車的銷售和生產有許多優惠政策和補貼。

特斯拉的製造工廠將建在上海臨港開發區,細節正在確認中,將在本月晚些時候對外公開,據悉上海市政府要求該製造工廠必須由特斯拉和上海的合作夥伴共同投資建造,但是否持有控股權還不得而知。市場諮詢公司Dunne Automotive 總裁Michael Dune 表示,特斯拉有機會佔據中國電動車市場的領先地位,許多有實力和知名度的品牌公司都會選擇在上海建造生產基地。

特斯拉選擇在上海生產電動車,有助於直接與中國汽車廠商的產品競爭,這比在美國生產再進口到中國市場銷售,至少能夠降低25% 的進口關稅,正是由於關稅的成本,Tesla Model S 和Model X 電動車在中國市場的價格比美國市場高一倍。

中國政府已經將電動車產業做為戰略性的新興產業,目標是在未來10 年內將混合式和全電動車的銷量提升10 倍,2016 年中國市場電動車的銷量約為28.3 萬台,佔比全球銷量的41%,Tesla 2016 年營收大約為70 億美元,其中15% 來自中國市場。目前大約有200 家公司宣布在中國製造電動車的計畫,其中北汽汽車和比亞迪公司的電動車佔總銷量的89%。

特斯拉CEO 伊隆·馬斯克(Elon Musk)早在3 年前就表示希望能夠在中國建設製造工廠,自2014 年以後每次到訪中國都會與大量政府官員見面。2017 年6 月初特斯拉宣布2017 年下半年在中國超級充電站建設計畫。目前特斯拉在中國大約有117 個超級充電站。

(合作媒體:。圖片出處:public domain CC0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

市場電動車需求上升,Nissan將對現有車款EV化

日經新聞報導,日產汽車(Nissan)將大舉擴充電動車(EV)產品陣容,日產社長兼CEO西川廣人27日於橫濱市舉行的定期股東會上表示,「今年度將推出新型『Leaf』,且中期來看,將推動現行已進行量產販售的車款EV化」。

因北美、中國加強環保規範,帶動EV有望進一步普及。日產目前的EV車款僅有「Leaf」等少數幾款,而之後計畫將SUV、輕型汽車以及商用車進行EV化。

另外,日產會長Carlos Ghosn也在股東會上表示,「日產在EV界居領導位置。日產EV累計銷售量超過60萬台、為美國特斯拉(Tesla)的2倍」。

日本市調機構富士經濟(Fuji Keizai)6月22日公布銷售動向報告指出,EV在2025年以後需求將急速增加,預估2030年時EV年銷售量將增至407萬台、超越油電混合車(HV、2030年銷售量預估為391萬台),且之後雙方的差距將持續擴大。在中國需求增加加持下,2035年EV全球銷售量將擴大至630萬台、將達2016年的13.4倍(較2016年增加12.4倍)。

(本文內容由授權使用。圖片出處:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

ThreadLocal原理分析與代碼驗證

ThreadLocal提供了線程安全的數據存儲和訪問方式,利用不帶key的get和set方法,居然能做到線程之間隔離,非常神奇。

比如

ThreadLocal<String> threadLocal = new ThreadLocal<>();

in thread 1

//in thread1
treadLocal.set("value1");
.....
//value的值是value1
String value = threadLocal.get();

in thread 2

//in thread2
treadLocal.set("value2");
.....
//value的值是value2
String value = threadLocal.get();

不論thread1和thread2是不是同時執行,都不會有線程安全問題,我們來測試一下。

線程安全測試

開10個線程,每個線程內都對同一個ThreadLocal對象set不同的值,會發現ThreadLocal在每個線程內部get出來的值,只會是自己線程內set進去的值,不會被別的線程影響。

static void testUsage() throws InterruptedException {
    Utils.println("-------------testUsage-------------------");
    ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    AtomicBoolean threadSafe = new AtomicBoolean(true);
    int count = 10;
    CountDownLatch countDownLatch = new CountDownLatch(count);
    Random random = new Random(736832);
    for (int i = 0; i < count; i ++){
        new Thread(() -> {
            try {
                //生成一個隨機數
                Long value = System.nanoTime() + random.nextInt();
                threadLocal.set(value);
                Thread.sleep(1000);

                Long value2 = threadLocal.get();
                if (!value.equals(value2)) {
                    //get和set的value不一致,說明被別的線程修改了,但這是不可能出現的
                    threadSafe.set(false);
                    Utils.println("thread unsafe, this could not be happen!");
                }
            } catch (InterruptedException e) {

            }finally {
                countDownLatch.countDown();
            }

        }).start();
    }

    countDownLatch.await();

    Utils.println("all thread done, and threadSafe is " + threadSafe.get());
    Utils.println("------------------------------------------");
}

輸出:

-------------testUsage------------------
all thread done, and threadSafe is true
-----------------------------------------

原理淺析

翻開ThreadLocal的源碼,會發現ThreadLocal只是一個空殼子,它並不存儲具體的value,而是利用當前線程(Thread.currentThread())的threadLocalMap來存儲value,key就是這個threadLocal對象本身。

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

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

Thread的threadLocals字段是ThreadLocalMap類型(你可以簡單理解為一個key value的Map),key是ThreadLocal對象,value是我們在外層設置的值

  • 當我們調用threadLocal.set(value)方法的時候,會找到當前線程的threadLocals這個map,然後以this作為key去set key value
  • 當我們調用threadLocal.get()方法的時候,會找到當前線程的threadLocals這個map,然後以this作為key去get value
  • 當我們調用threadLocal.remove()方法的時候,會找到當前線程的threadLocals這個map,然後以this作為key去remove

這就相當於:

Thread.currentThread().threadLocals.set(threadLocal1, "value1");
.....
//value的值是value1
String value = Thread.currentThread().threadLocals.get(threadLocal1);

因為每個Thread都是不同的對象,所以他們的threadLocals也是不同的map,threadLocal在不同的線程里工作時,實際上是從不同的map里get/set,這也就是線程安全的原因了,了解到這一點就差不多了。

再深入一些,ThreadLocalMap的結構

如果繼續翻ThreadLocalMap的源碼,會發現它有個字段table,是Entry類型的數組。

我們不妨寫段代碼,把ThreadLocalMap的結構輸出出來。

由於Thread.threadLocals和ThreadLocalMap類不是public的,我們只有通過反射來獲取它的值。反射的代碼如下(如果嫌長可以不看,直接看輸出):

static Object getThreadLocalMap(Thread thread) throws NoSuchFieldException, IllegalAccessException {        
    //get thread.threadLocals
    Field threadLocals = Thread.class.getDeclaredField("threadLocals");
    threadLocals.setAccessible(true);
    return threadLocals.get(thread);
}

static void printThreadLocalMap(Object threadLocalMap) throws NoSuchFieldException, IllegalAccessException {
    String threadName = Thread.currentThread().getName();
    
    if(threadLocalMap == null){
        Utils.println("threadMap is null, threadName:" + threadName);
        return;
    }

    Utils.println(threadName);

    //get threadLocalMap.table
    Field tableField = threadLocalMap.getClass().getDeclaredField("table");
    tableField.setAccessible(true);
    Object[] table = (Object[])tableField.get(threadLocalMap);
    Utils.println("----threadLocals (ThreadLocalMap), table.length = " + table.length);

    for (int i = 0; i < table.length; i ++){
        WeakReference<ThreadLocal<?>> entry = (WeakReference<ThreadLocal<?>>)table[i];
        printEntry(entry, i);
    }
}
static void printEntry(WeakReference<ThreadLocal<?>> entry, int i) throws NoSuchFieldException, IllegalAccessException {
    if(entry == null){
        Utils.println("--------table[" + i + "] -> null");
        return;
    }
    ThreadLocal key = entry.get();
    //get entry.value
    Field valueField = entry.getClass().getDeclaredField("value");
    valueField.setAccessible(true);
    Object value = valueField.get(entry);

    Utils.println("--------table[" + i + "] -> entry key = " + key + ", value = " + value);
}

測試代碼:

static void testStructure() throws InterruptedException {
    Utils.println("-------------testStructure----------------");
    ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

    Thread thread1 = new Thread(() -> {
        threadLocal1.set("threadLocal1-value");
        threadLocal2.set("threadLocal2-value");

        try {
            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            printThreadLocalMap(threadLocalMap);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread1");

    thread1.start();

    //wait thread1 done
    thread1.join();

    Thread thread2 = new Thread(() -> {
        threadLocal1.set("threadLocal1-value");
        try {
            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            printThreadLocalMap(threadLocalMap);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread2");

    thread2.start();
    thread2.join();
    Utils.println("------------------------------------------");
}

我們在創建了兩個ThreadLocal的對象threadLocal1和threadLocal2,在線程1里為這兩個對象設置值,在線程2里只為threadLocal1設置值。然後分別打印出這兩個線程的threadLocalMap。

輸出結果為:

-------------testStructure----------------
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = java.lang.ThreadLocal@33baa315, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@4d42db5c, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
thread2
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> null
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@4d42db5c, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
------------------------------------------

從結果上可以看出:

  • 線程1和線程2的threadLocalMap對象的table字段,是個數組,長度都是16
  • 由於線程1里給兩個threadLocal對象設置了值,所以線程1的ThreadLocalMap里有兩個entry,數組下標分別是1和10,其餘的是null(如果你自己寫代碼驗證,下標不一定是1和10,不需要糾結這個問題,只要前後對的上就行)
  • 由於線程2里只給一個threadLocal對象設置了值,所以線程1的ThreadLocalMap里只有一個entry,數組下標是10,其餘的是null
  • threadLocal1這個對象在兩個線程里都設置了值,所以當它作為key加入二者的threadLocalMap時,key是一樣的,都是java.lang.ThreadLocal@4d42db5c;下標也是一樣的,都是10。

為什麼是WeakReference

查看Entry的源碼,會發現Entry繼承自WeakReference:

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

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

構造函數里把key傳給了super,也就是說,ThreadLocalMap中對key的引用,是WeakReference的。

Weak reference objects, which do not prevent their referents from being
made finalizable, finalized, and then reclaimed. Weak references are most
often used to implement canonicalizing mappings.

通俗點解釋:

當一個對象僅僅被weak reference(弱引用), 而沒有任何其他strong reference(強引用)的時候, 不論當前的內存空間是否足夠,當GC運行的時候, 這個對象就會被回收。

看不明白沒關係,還是寫代碼測試一下什麼是WeakReference吧…

static void testWeakReference(){
    Object obj1 = new Object();
    Object obj2 = new Object();
    WeakReference<Object> obj1WeakRef = new WeakReference<>(obj1);
    WeakReference<Object> obj2WeakRf = new WeakReference<>(obj2);
    //obj32StrongRef是強引用
    Object obj2StrongRef = obj2;
    Utils.println("before gc: obj1WeakRef = " + obj1WeakRef.get() + ", obj2WeakRef = " + obj2WeakRf.get() + ", obj2StrongRef = " + obj2StrongRef);

    //把obj1和obj2設為null
    obj1 = null;
    obj2 = null;
    //強制gc
    forceGC();

    Utils.println("after gc: obj1WeakRef = " + obj1WeakRef.get() + ", obj2WeakRef = " + obj2WeakRf.get() + ", obj2StrongRef = " + obj2StrongRef);
}

結果輸出:

before gc: obj1WeakRef = java.lang.Object@4554617c, obj2WeakRef = java.lang.Object@74a14482, obj2StrongRef = java.lang.Object@74a14482
after gc: obj1WeakRef = null, obj2WeakRef = java.lang.Object@74a14482, obj2StrongRef = java.lang.Object@74a14482

從結果上可以看出:

  • 我們先new了兩個對象(為避免混淆,稱他們為Object1和Object2),分別用變量obj1和obj2指向它們,同時定義了一個obj2StrongRef,也指向Object2,最後把obj1和obj2均指向null
  • 由於Object1沒有變量強引用它了,所以在gc后,Object1被回收了,obj1WeakRef.get()返回了null
  • 由於Object2還有obj2StrongRef在引用它,所以gc后,Object2依然存在,沒有被回收。

那麼,ThreadLocalMap中對key的引用,為什麼是WeakReference的呢?

因為大部分情況下,線程不死

大部分情況下,線程不會頻繁的創建和銷毀,一般都會用線程池。所以線程對象一般不會被清除,線程的threadLocalMap就一直存在。
如果key對ThreadLocal是強引用,那麼key永遠不會被回收,即使我們程序里再也不用它了。

但是key是弱引用的話,情況就會得到改善:只要沒有指向threadLocal的強引用了,這個ThreadLocal對象就會被清理。

我們還是寫代碼測試一下吧。

/**
 * 測試ThreadLocal對象什麼時候被回收
 * @throws InterruptedException
 */
static void testGC() throws InterruptedException {
    Utils.println("-----------------testGC-------------------");
    Thread thread1 = new Thread(() -> {
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

        threadLocal1.set("threadLocal1-value");
        threadLocal2.set("threadLocal2-value");

        try {
            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            Utils.println("print threadLocalMap before gc");
            printThreadLocalMap(threadLocalMap);

            //set threadLocal1 unreachable
            threadLocal1 = null;

            forceGC();

            Utils.println("print threadLocalMap after gc");
            printThreadLocalMap(threadLocalMap);


        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread1");

    thread1.start();
    thread1.join();
    Utils.println("------------------------------------------");
}

我們在一個線程里為兩個ThreadLocal對象賦值,最後把其中一個對象的強引用移除,gc后打印當前線程的threadLocalMap。
輸出結果如下:

-----------------testGC-------------------
print threadLocalMap before gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = java.lang.ThreadLocal@7bf9cebf, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@56342d38, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
print threadLocalMap after gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = java.lang.ThreadLocal@7bf9cebf, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = null, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
------------------------------------------

從輸出結果可以看到,當我們把threadLocal1的強引用移除並gc之後,table[10]的key變成了null,說明threadLocal1這個對象被回收了;threadLocal2的強引用還在,所以table[1]的key不是null,沒有被回收。

但是你發現沒有,table[10]的key雖然是null了,但value還活着! table[10]這個entry對象,也活着!

是的,因為只有key是WeakReference….

無用的entry什麼時候被回收?

通過查看ThreadLocal的源碼,發現在ThreadLocal對象的get/set/remove方法執行時,都有機會清除掉map中已經無用的entry。

最容易驗證清除無用entry的場景分別是:

  • remove:這個不用說了,這哥們本來就是做這個的
  • get:當一個新的threadLocal對象(沒有set過value)發生get調用時,也會作為新的entry加入map,在加入的過程中,有機會清除掉無用的entry,邏輯和下面的set相同。
  • set: 當一個新的threadLocal對象(沒有set過value)發生set調用時,會在map中加入新的entry,此時有機會清除掉無用的entry,清除的邏輯是:
    • 清除掉table數組中的那些無用entry中的一部分,記住是一部分,這個一部分可能全部,也可能是0,具體算法請看ThreadLocalMap.cleanSomeSlots,這裏不解釋了。
    • 如果上一步的”一部分”是0(即清除了0個),並且map的size(是真實size,不是table.length)大於等於threshold(table.length的2/3),會執行一次rehash,在rehash的過程中,清理掉所有無用的entry,並減小size,清理后的size如果還大於等於threshold – threshold/4,則把table擴容為原來的兩倍大小。

還有其他場景,但不好驗證,這裏就不提了。

ThreadLocal源碼就不貼了,貼了也講不明白,相關邏輯在setInitialValue、cleanSomeSlots、expungeStaleEntries、rehash、resize等方法里。

在我們寫代碼驗證entry回收邏輯之前,還需要簡單的提一下ThreadLocalMap的hash算法。

entry數組的下標如何確定?

每個ThreadLocal對象,都有一個threadLocalHashCode變量,在加入ThreadLocalMap的時候,根據這個threadLocalHashCode的值,對entry數組的長度取余(hash & (len – 1)),餘數作為下標。

那麼threadLocalHashCode是怎麼計算的呢?看源碼:

public class ThreadLocal<T>{
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    ...
}

ThreadLocal類維護了一個全局靜態字段nextHashCode,每new一個ThreadLocal對象,nextHashCode都會遞增0x61c88647,作為下一個ThreadLocal對象的threadLocalHashCode。

這個0x61c88647,是個神奇的数字,只要以它為遞增值,那麼和2的N次方取余時,在有限的次數內不會發生重複。
比如和16取余,那麼在16次遞增內,不會發生重複。還是寫代碼驗證一下吧。

int hashCode = 0;
int HASH_INCREMENT = 0x61c88647;
int length = 16;

for(int i = 0; i < length ; i ++){
    int h = hashCode & (length - 1);
    hashCode += HASH_INCREMENT;
    System.out.println("h = " + h + ", i = " + i);
}

輸出結果為:

h = 0, i = 0
h = 7, i = 1
h = 14, i = 2
h = 5, i = 3
h = 12, i = 4
h = 3, i = 5
h = 10, i = 6
h = 1, i = 7
h = 8, i = 8
h = 15, i = 9
h = 6, i = 10
h = 13, i = 11
h = 4, i = 12
h = 11, i = 13
h = 2, i = 14
h = 9, i = 15

你看,h的值在16次遞增內,沒有發生重複。 但是要記住,2的N次方作為長度才會有這個效果,這也解釋了為什麼ThreadLocalMap的entry數組初始長度是16,每次都是2倍的擴容。

驗證新threadLocal的get和set時回收部分無效的entry

為了驗證出結果,我們需要先給ThreadLocal的nextHashCode重置一個初始值,這樣在測試的時候,每個threadLocal的數組下標才會按照我們設計的思路走。

static void resetNextHashCode() throws NoSuchFieldException, IllegalAccessException {
    Field nextHashCodeField = ThreadLocal.class.getDeclaredField("nextHashCode");
    nextHashCodeField.setAccessible(true);
    nextHashCodeField.set(null, new AtomicInteger(1253254570));
}

然後在測試代碼里,我們先調用resetNextHashCode方法,然後加兩個ThreadLocal對象並set值,gc前把強引用去除,gc后再new兩個新的theadLocal對象,分別調用他們的get和set方法。
在每個關鍵點打印出threadLocalMap做比較。

static void testExpungeSomeEntriesWhenGetOrSet() throws InterruptedException {
    Utils.println("----------testExpungeStaleEntries----------");
    Thread thread1 = new Thread(() -> {
        try {
            resetNextHashCode();

            //注意,這裏必須有兩個ThreadLocal,才能驗證出threadLocal1被清理
            ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
            ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

            threadLocal1.set("threadLocal1-value");
            threadLocal2.set("threadLocal2-value");


            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            //set threadLocal1 unreachable
            threadLocal1 = null;
            threadLocal2 = null;
            forceGC();

            Utils.println("print threadLocalMap after gc");
            printThreadLocalMap(threadLocalMap);

            ThreadLocal<String> newThreadLocal1 = new ThreadLocal<>();
            newThreadLocal1.get();
            Utils.println("print threadLocalMap after call a new newThreadLocal1.get");
            printThreadLocalMap(threadLocalMap);

            ThreadLocal<String> newThreadLocal2 = new ThreadLocal<>();
            newThreadLocal2.set("newThreadLocal2-value");
            Utils.println("print threadLocalMap after call a new newThreadLocal2.set");
            printThreadLocalMap(threadLocalMap);


        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread1");

    thread1.start();
    thread1.join();
    Utils.println("------------------------------------------");
}

程序輸出結果為:

----------testExpungeStaleEntries----------
print threadLocalMap after gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = null, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = null, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
print threadLocalMap after call a new newThreadLocal1.get
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = null, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> entry key = java.lang.ThreadLocal@2b63dc81, value = null
--------table[9] -> null
--------table[10] -> null
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
print threadLocalMap after call a new newThreadLocal2.set
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> null
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> entry key = java.lang.ThreadLocal@2b63dc81, value = null
--------table[9] -> null
--------table[10] -> null
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> entry key = java.lang.ThreadLocal@2e93c547, value = newThreadLocal2-value
------------------------------------------

從結果上來看,

  • gc后table[1]和table[10]的key變成了null
  • new newThreadLocal1.get后,新增了table[8],table[10]被清理了,但table[1]還在(這就是cleanSomeSlots中some的意思)
  • new newThreadLocal2.set后,新增了table[15],table[1]被清理了。

驗證map的size大於等於table.length的2/3時回收所有無效的entry

    static void testExpungeAllEntries() throws InterruptedException {
        Utils.println("----------testExpungeStaleEntries----------");
        Thread thread1 = new Thread(() -> {
            try {
                resetNextHashCode();

                int threshold = 16 * 2 / 3;
                ThreadLocal[] threadLocals = new ThreadLocal[threshold - 1];
                for(int i = 0; i < threshold - 1; i ++){
                    threadLocals[i] = new ThreadLocal<String>();
                    threadLocals[i].set("threadLocal" + i + "-value");
                }

                Object threadLocalMap = getThreadLocalMap(Thread.currentThread());

                threadLocals[1] = null;
                threadLocals[8] = null;
                //threadLocals[6] = null;
                //threadLocals[4] = null;
                //threadLocals[2] = null;
                forceGC();

                Utils.println("print threadLocalMap after gc");
                printThreadLocalMap(threadLocalMap);

                ThreadLocal<String> newThreadLocal1 = new ThreadLocal<>();
                newThreadLocal1.set("newThreadLocal1-value");
                Utils.println("print threadLocalMap after call a new newThreadLocal1.get");
                printThreadLocalMap(threadLocalMap);

            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }

        }, "thread1");

        thread1.start();
        thread1.join();
        Utils.println("------------------------------------------");
    }

我們先創建了9個threadLocal對象並設置了值,然後去掉了其中2個的強引用(注意這2個可不是隨意挑選的)。
gc后再添加一個新的threadLocal,最後打印出最新的map。輸出為:

----------testExpungeStaleEntries----------
print threadLocalMap after gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = null, value = threadLocal1-value
--------table[2] -> entry key = null, value = threadLocal8-value
--------table[3] -> null
--------table[4] -> entry key = java.lang.ThreadLocal@60523912, value = threadLocal6-value
--------table[5] -> null
--------table[6] -> entry key = java.lang.ThreadLocal@48fccd7a, value = threadLocal4-value
--------table[7] -> null
--------table[8] -> entry key = java.lang.ThreadLocal@188bbe72, value = threadLocal2-value
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@19e0ebe8, value = threadLocal0-value
--------table[11] -> entry key = java.lang.ThreadLocal@688bcb6f, value = threadLocal7-value
--------table[12] -> null
--------table[13] -> entry key = java.lang.ThreadLocal@46324c19, value = threadLocal5-value
--------table[14] -> null
--------table[15] -> entry key = java.lang.ThreadLocal@38f1283, value = threadLocal3-value
print threadLocalMap after call a new newThreadLocal1.get
thread1
----threadLocals (ThreadLocalMap), table.length = 32
--------table[0] -> null
--------table[1] -> null
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> entry key = java.lang.ThreadLocal@48fccd7a, value = threadLocal4-value
--------table[7] -> null
--------table[8] -> null
--------table[9] -> entry key = java.lang.ThreadLocal@1dae16b1, value = newThreadLocal1-value
--------table[10] -> entry key = java.lang.ThreadLocal@19e0ebe8, value = threadLocal0-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> entry key = java.lang.ThreadLocal@46324c19, value = threadLocal5-value
--------table[14] -> null
--------table[15] -> null
--------table[16] -> null
--------table[17] -> null
--------table[18] -> null
--------table[19] -> null
--------table[20] -> entry key = java.lang.ThreadLocal@60523912, value = threadLocal6-value
--------table[21] -> null
--------table[22] -> null
--------table[23] -> null
--------table[24] -> entry key = java.lang.ThreadLocal@188bbe72, value = threadLocal2-value
--------table[25] -> null
--------table[26] -> null
--------table[27] -> entry key = java.lang.ThreadLocal@688bcb6f, value = threadLocal7-value
--------table[28] -> null
--------table[29] -> null
--------table[30] -> null
--------table[31] -> entry key = java.lang.ThreadLocal@38f1283, value = threadLocal3-value
------------------------------------------

從結果上看:

  • gc后table[1]和table[2](即threadLocal1和threadLocal8)的key變成了null
  • 加入新的threadLocal后,table的長度從16變成了32(因為此時的size是8,正好等於10 – 10/4,所以擴容),並且threadLocal1和threadLocal8這兩個entry不見了。

如果在gc前,我們把threadLocals[1、8、6、4、2]都去掉強引用,加入新threadLocal後會發現1、8、6、4、2被清除了,但沒有擴容,因為此時size是5,小於10-10/4。這個邏輯就不貼測試結果了,你可以取消註釋上面代碼中相關的邏輯試試。

大部分場景下,ThreadLocal對象的生命周期是和app一致的,弱引用形同虛設

回到現實中。

我們用ThreadLocal的目的,無非是在跨方法調用時更方便的線程安全地存儲和使用變量。這就意味着ThreadLocal的生命周期很長,甚至和app是一起存活的,強引用一直在。

既然強引用一直存在,那麼弱引用就形同虛設了。

所以在確定不再需要ThreadLocal中的值的情況下,還是老老實實的調用remove方法吧!

代碼地址

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

心裏有紅黑樹

Why 紅黑樹

為什麼大家都這麼推崇紅黑樹呢? 這就是數據結構的魅力!!! 下面我簡述一下常用數據結構的優缺點

  • 數組

大家對數組很熟悉, 都知道對數組來說,它底層的存儲空間是連續的,因此如果我們根據index去獲取元素,速度是相當快, 但是對於數組來說有時候查詢也不見得就一定塊, 比如我們查詢數組中名字叫張三的人, 也不得不從開始遍歷這個數組

如果我們想往數組中插入一個元素, 也不見得一定就慢, 比如我們往數組中最後的位置插入就很快, 但是要是往開始的位置插入的話, 肯定會很慢, 需要將現有數組中所有的元素往後移動一位, 才能空出開始的位置,給新元素用

  • 鏈表

一說鏈式存儲, 大家也都知道, 這種數據結構僅僅是邏輯連續, 物理存儲不連續, 因此我們有可以通過玩指針或者引用很快的完成元素的刪除和添加

對鏈表的查詢來說, 一定是慢的, 無論查詢誰, 查哪個, 都得從第一個節點開始遍歷

  • AVL樹

AVL樹, 就是二叉平衡樹, 這種有序的樹形結果就將鏈式存儲添加刪除塊, 順序存儲的查找快兩大有點進行了一次中和, 在絕大部分情況下, AVL樹在增刪改查方面的性能都原超過數組和鏈表

  • 紅黑樹

紅黑樹是對AVL樹是又一次重大升級, AVL樹,對於樹的平衡要求太嚴格了, 每當添加,刪除節點時,都不得不進行調整

對於AVL樹個紅黑樹來說, 每次添加一個新的節點都是最多進行兩次旋轉(左旋右旋)就能重新使樹變的平衡,

但是當我們刪除一個恭弘=叶 恭弘子節點時, AVL樹重新調整成平衡狀態時最多需要進行旋轉O(logN)次, 而紅黑樹最多旋轉3次就能重新平衡,時間複雜度是O(1)

還有就是紅黑樹並不是完全意義上的AVL樹, 也就是說它其實並不是真的像AVL樹那樣嚴格要求對一個節點來說左右子樹的高度差不能超過1, 而是選擇使用染成紅色和黑色進行維護

簡單來說, 因為紅黑樹並不像AVL樹那樣完全平衡, 可能會導致紅黑樹的讀性能略遜於AVL, 但是紅黑樹的維護成本絕對是遠遠低於AVL, 在空間上的開銷和AVL樹基本持平, 因此紅黑樹被大家極力推崇, 和學習java的同學直接相關的就是jdk8的 hashmap

紅黑樹的特性

紅黑樹主要存在下面的7條性質

  1. 節點非紅即黑
  2. 根節點必定是黑色
  3. 恭弘=叶 恭弘子節點全部是黑色, (這裏說的恭弘=叶 恭弘子節點是我們想象在肉眼看到的節點上再多加一層子節點)
  4. 紅節點的子節點必定是黑色
  5. 紅節點的父節點必定是黑色
  6. 從根節點到任意子節點的路徑上,都要經歷相同數目的黑節點
  7. 從根節點到任意子節點的路徑上不可能存在兩個連續相同的紅節點

常見的誤區

如上圖, 看着挺像紅黑樹, 其實他不是, 看它node10, 並不滿足上面的性質6. 因為我們認為node10的左子節點是黑色的節點, 這樣的話, 從node20到node10的左子節點就經歷了兩個黑節點, 而其他的 node15, node25, node35 經歷的黑色子節點數都是三個

如上圖它也不是紅黑樹, 因為我們認為node30的右節點是黑色的節點, 這樣的話從node60到node30的右節點就經歷了三個黑色的節點, 而其他的所有子節點都經歷了4個, 故, 他不是紅黑樹

紅黑樹與2-3-4樹等價

如上圖中,當我們將一個紅黑樹中的黑色節點和紅色節點融合在一起時,我們會發現, 這個紅黑樹其實就是一顆2-3-4樹, 一顆四階B樹

並且, 紅黑樹中黑色節點的每一個合併完成后的節點中都有一個黑色的節點, 換句話說就是紅黑樹中黑色節點的個數等於2-3-4樹中節點的個數

添加

添加節點其實就是構造紅黑樹的過程, 只要我們嚴格遵循上面的7條限制, 構造出來的樹就是紅黑樹

通過上圖其實我們發現, 紅黑樹真的可以和四階B樹之間進行等價代換, 換句話說就是 4階B樹的性質對於紅黑樹來書其實也是存在的, 主要是如下兩條性質

  • 所有新添加進去的節點都被放在了恭弘=叶 恭弘子節點上
  • 2-3-4樹中每一個節點中允許承載的元素的個數 [1,3]

經驗推薦: 就是新添加的節點盡量全部是紅色, 如果你畫一畫就會發現, 如果我們新添加的節點是紅色的話,上面所說的7條性質中, 除了第四條(紅節點的子節點必定的黑節點). 其他的限制都可以滿足

於是看一下一顆四階B樹插入節點時有哪些種情況

數一數: 一共 4+3+3+2 = 12種情況, 換句話說, 只要我們處理好了這12種情況, 我們就完成了添加節點的邏輯

  • 情況1, 就是假設我們添加進去的是紅色的節點, 並且這個紅色節點的父節點是黑色節點時, 直接添加進行,不需要其他任何變換, 就想下圖這樣, 直接簡單粗暴的添加就行

除去第一種情況外, 還剩下8中情況出現了紅紅節點相鄰, 於是繼續往下看, 我們對他進行一次修復

  • 情況2: 如下圖

插入的node57, node64, 什麼情況呢? 就是當前節點是node5556, 首先這個節點中現存兩個元素, 並且是往這個黑色的節點的左側的左側插入, 或者是右側的右側插入一個紅色節點

看上圖出現了兩個紅色節點相鄰,於是我們第一件事就是進行重新染色,

  1. 將插入節點的父節點染成黑色
  2. 將插入節點的祖父節點染成紅色
  3. 將祖父節點進行旋轉, 如果這個新節點被插入在父節點的右側. 左旋轉它的祖父節點

經過上面的變換后, 我們重新得到標準的紅黑樹如下

  • 情況3: 新添加的節點的叔叔節點不是紅色

第三種情況和第二種情況相似, 還是插入 node57和node64. 判斷的條件是 插入節點的叔叔節點(父節點的兄弟節點)不是紅節點,

簡稱 LR , 或者是RL , 需要進行如下的調整

  1. 染色: 將自己染成黑色,祖節點染成紅色
  2. LR: 父節點左旋轉, 祖父節點右旋轉
  3. RL: 祖父點右旋轉, 父節點左旋轉

LR舉例:

經過上面的變化,我們重新得到平衡的紅黑樹

接着往下看剩下的四種情況

  • 情況4: 新添加的節點的叔叔節點是紅色, 其實就是需要上溢的情況, 也很好處理

像上圖這樣, 新添加的紅色節點 node15, 它本身的父節點是node20, 父節點的叔叔節點是紅色的node25, 我們比較node15和node20的大小, 發現node15本來是應該放在node20的左邊的, 但是對於一顆2-3-4樹來說, 單個節點最多就有3個元素, 如果再加上node15 就會出現上溢的情況, 怎麼辦呢? 我們上溢調整, 選擇這個節點中間位置的元素向上和父節點合併, 選擇node20, node30其實都是可以的, 為了方便我們選擇node30

好,下面開始修復這個紅黑樹

  1. 將插入的節點的父節點和它的叔叔節點染成黑色
  2. 發生了上溢, 將他的父節點的染成紅色, 遞歸插入到根節點上, 這時候根節點可能又會發生上溢

然後上溢

當我們將新插入的節點的父節點node30染成紅色時, 再插入到根節點, 實際上就是重複我們枚舉出來的這12種情況中的一種. 紅黑樹一定會被修復, 當然這時候很可能會出現根節點也容納不了新的元素, 需要根節點也進行上溢, 然後將根節點染黑

還有一種情況是像下面這樣, 同樣是在情況4下的新插入的節點的叔叔節點是紅色

像下面這樣調整:

  1. 將父節點和叔叔節點染成黑色
  2. 祖父節點上溢

然後就是這種情況

調整的思路和前面一樣

  1. 將父節點和叔叔節點染成黑色
  2. 將祖父節點上溢

至此紅黑樹的添加的12種情況就全部枚舉完成了

刪除

對於刪除來說總共兩大種四小種情況

  • 第一種就是刪除的節點就是紅色節點, 如果真是這樣的話,直接刪除就ok
  • 第二種是刪除的節點是黑色節點
    • 刪除擁有1個red節點的黑色節點
    • 刪除擁有2個red節點的黑色節點,
    • 刪除黑色節點

如果一個像下面這樣, 下面的黑色節點有兩個子節點, 這種情況下,黑色節點肯定不會直接被刪除的, 需要進行變換,讓他的恭弘=叶 恭弘子節點去替換他,進而實現刪除的目的

  • 情況1: 刪除擁有1個紅節點的黑色節點,像下圖這樣

怎麼判斷這就是我們想刪除的情況呢? 當我們確定用來替代這個被刪除的黑節點是紅色,則符合當前的情況

也就是說我們想刪除 node40 和 node70, 於是我們這樣做

  1. 讓這個指向被刪除的節點的指針指向這個被刪除的節點的子節點
  2. 將替代它的節點染成黑色

於是我們接得到下圖這樣的結果

  • 情況2: 刪除的節點是黑色的恭弘=叶 恭弘子節點, 並且可向兄弟節點借

首先,如果這個恭弘=叶 恭弘子節點就是根節點的話,直接刪除就ok

看下面的這個圖, 我們就刪除其中node90, 即,刪除黑色恭弘=叶 恭弘子節點

如果想刪除上圖中的node90也是由竅門的,規律和2-3-4樹是擦不多的

假設它就是2-3-4樹, 如果我們將node90刪了, 我們計算一下, 對於2-3-4樹來說, 每一個節點位置上至少有 ⌈ 4/2 ⌉ -1 = 1個元素, 但是把node90刪除了這個位置上的節點中沒有元素, 因此產生了 下溢

出現下溢,我們首先考慮的情況就是看看可不可以向它的兄弟節點借一個,但是和B樹是有取別的, 多了下面的限制

  1. 被刪除的這個節點的兄弟節點必須是黑色的
  2. 被刪除的這個節點的兄弟節點一定的有紅色的子節點才ok, 就像上圖那樣, 可以在左邊,右邊,或者都有
  3. 直接刪除掉指定的node(因為它在恭弘=叶 恭弘子節點的位置上)
  4. 進行旋轉,旋轉時注意, 兩點:第一點: 比如下面的原來根節點位置上的元素88是紅色的, 經過旋轉上來替換它的節點的顏色必須染成紅色, 如果node88是黑色, 那麼經過旋轉上來替換他的節點的顏色必須染成黑色 ,第二點: 旋轉完成后,新的跟節點的直接左右子節點的顏色轉換為黑色

怎麼進行旋轉呢? 就像下圖這樣

  • 情況3: 刪除的節點是黑色的恭弘=叶 恭弘子節點, 並且它的兄弟是黑色,而且它的兄弟節點不能借給他元素

像這種情況:我像刪除node99,但是沒辦法像他的兄弟節點借元素,於是

  1. 將父節點向下合併,父節點染成黑色
  2. 將它的兄弟節點染成紅色

也有特殊的情況, 就是它的父節點只有一個,還是黑色

這時候,我們將他的父節點下溢, 原位置的節點捨棄

  • 還有最後一種情況就是, 刪除的是黑色的節點, 它的兄弟節點的是紅色的節點

就像上圖那樣,我們想刪除node99, 但是node99的兄弟節點其實是node55, 而不是node77, 我們怎麼樣才能轉換為前面說的那些情況呢?

  1. 將被刪除節點的父節點染成紅色, 兄弟節點染黑

  2. 讓被刪除的父節點進行右旋轉(node88右轉)

    得到下圖

於是我們就將這種兄弟節點為紅節點的情況轉化成了兄弟節點為黑色節點的樣子, 按照原來的方式進行刪除修整即可

  1. 讓原父節點下溢
  2. 原染成黑色
  3. 兄弟節點,染成紅色

至此本文就結束, 歡迎關注我,後續我更新更多的關於開發相關的筆記

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

全球Q2電動汽車發展指數 中國首度躍居整體排名第一

中證網報導,羅蘭貝格與德國汽車研究機構亞琛汽車工程技術公司共同發佈《2017年第二季全球電動汽車發展指數》。報告中顯示,中國大陸首次在電動汽車發展指數的整體排名中躍居全球第一,並直指儘管政府新能源政策收緊,補貼力道減弱,中國電動汽車和電池製造市場份額仍將保持強有力的增長,進一步擴大領先優勢。

該報告對中國、德國、法國、義大利、美國、日本和韓國電動汽車的發展現狀進行比較。整體而言,中國首次躍居指數整體排名第一,美國與德國分居第二、三位,而在上一季指數排名中位列第一的日本則失去領先地位。報告預測,在可預見的未來,中國將統領電動汽車的行業與市場。

在技術層面,法國超越德國,位居首位,主要由於有更多的德國整車廠大批量生產續航能力和最高電動時速都較低的插電式混合動力汽車,導致其電動汽車技術能力略有下降;日本排名第三,其整車廠的電動汽車技術水準較高且價格更加實惠;中國整車廠則仍主要定位於技術含量較低的領域。

在行業總量層面,中國正在逐步擴大其領先優勢;在電池製造領域,中國的優勢也更加明顯;反觀日本在電動汽車產量和全球電池產量份額上都處於不利地位,排名維持在第三;美國行業成績有所提升,位居第二。至於在市場規模層面,中國的需求進一步急劇增長,但電動汽車所占市場份額仍略低於法國,排在第二,美國名列第三。

資料顯示,2016年中國生產了超過35萬輛插電式混合動力和純電動乘用車,銷售額保持兩位數增長,市佔率從0.8%上升至1.3%;同年,德國、法國與美國電動汽車的註冊數量均實現了兩位數的增長。但整體而言,2016年僅有法國與中國兩個國家的純電動和插電式混合動力汽車市場份額超過1%。

報告認為,中國電動汽車銷量的快速增長主要得益於政府大幅度補貼和主要城市對汽油車的限牌政策,但政府對於汽車廠商的政策正在收緊。對此,羅蘭貝格合夥人鄭贇表示,雖然大陸政府的激勵政策在初期對行業發展起到了重要的推動作用,但難以長久維持,政府需要控制成本,也有意讓本土廠商培育自身能力,電動汽車產業的發展將由政府推動向市場推動轉變,其最新版的新能源汽車雙積分管理意見徵求稿就明確地傳達了此訊號。

根據羅蘭貝格的估算,要達到新能源汽車積分比例2020年12%的目標,該年電動汽車的總銷量需達到約160萬輛。鄭贇指出,汽車設計、配件整合以及供應商管理能力將成為大陸本土廠商所面臨的重大挑戰,想要在政府退補的情況下實現增長、完成積分目標,成本控制是關鍵;只有成本控制能力和價格競爭力的提升才能幫助其本土廠商在國際電動汽車市場上保持長期的競爭優勢。

(本文內容由授權使用。圖片出處:pixabay CC0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

另類思考!電動車熱未必有害原油、說不定恰恰相反?

國際車廠爭相研發電動車,汽油車似乎被宣判死刑,原油也會跟著步上末日。不過有專家大膽推估,電動車發展會削減流入原油業的資金,油市將汰弱留強,油價會維持低檔,也許將阻礙電動車普及之路。

加拿大能源私募基金龍頭ARC Financial Corp.首席能源經濟學家Peter Tertzakian,13日在OilPrice發文稱,上週Volvo宣布,2019年起只生產電動車、停產汽柴油車。法國政府也宣布,2040年起停售汽柴油車。與此同時,特斯拉創辦人馬斯克貼出平價電動車Model 3照片,讓油市陷入低迷。

電動車發展迅速,不少人認為原油業末日倒數計時。但是Tertzakian不以為然,指出環保車買氣疲弱。2013~2014年中,油價處於每桶100美元的高點時,美國電動車銷售提高,可是之後油價崩盤,電動車動能隨之消散。能源有兩股相反力量,現在的消費趨勢是原油便宜、使用率高,相對的,電動車滲透率低迷。但是市場對未來的預期恰恰相反,斷言原油將亡。

未來幾年究竟會如何發展,Tertzakian推測有兩種可能結果,一是資本緊縮,汰除沒有效率的生產商,剩餘業者將不斷創新壓低成本,在低油價時也能存活,油價會繼續低迷,消費者缺乏誘因改買電動車。第二種可能是資本投資緊縮,將使原油供給下降,帶動油價上揚,電動車需求因此大增。諷刺的是,厲害的油商在兩種情況下,都能有好表現。可以確定的是,未來投入原油業的資金勢必減少。

原油期貨報價從去(2016)年12月開始一路往下盤跌,高盛全球原物料研究部主管Jeffrey Currie對這提出了一個簡單解釋:市場上的錢太多。

CNBC報導,Currie 12日在接受專訪時指出,市場上的原油其實並不是太多,只是錢潮淹腳目、委實過多。舉例來說,人們對石油探鑽活動的投資,是墨西哥最近能發現十數億桶原油的主因。

Currie說,石油市場正在試著尋找供需均衡點,而產油商的損益兩平點在跌破高盛2016年估計的50-55美元後,新的平衡點在哪,則變得愈來愈難預測。不過,在被問到油市有沒有可能崩盤時,Currie說雖然他希望看到市場能有些波動,但其實崩盤機率極低。

(本文內容由授權使用。圖片出處:public domain CC0)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

馬斯克看好電動車市場,Tesla 將在美國建 3 座超級工廠

電動車製造商特斯拉(Tesla)和伊隆·馬斯克(Elon Musk)在過去幾個月中都多次提及了投資建造超級工廠的計畫,新製造工廠的選址將在年底公開,據悉 Tesla 將建造 5 座超級工廠,其中美國將有 2 到 3 座全新的超級工廠。   特斯拉將在海外建造超級工廠的消息已經傳了幾個月的時間了,在美國本土的超級工廠建造計畫則一直沒有進展。該公司 CEO 馬斯克確認將有 2 到 3 座超級工廠選址在美國本土。   2017 年 6 月特斯拉公司在股東大會上確認有 3 座超級工廠選址已經啟動,這些工廠包括了電動車和電池生產線。   據之前媒體曝光的消息顯示,特斯拉至少將在海外市場建造兩座超級工廠,分別位於歐洲和中國,特斯拉已經與中國上海市政府簽訂合作協議,共同建造電動車製造工廠。   在美國州長協議的會議上,馬斯克公開表示,將會有 2 到 3 座超級工廠選址在美國本土,他面對所有州長做出這一表態,也是希望政府部門能夠提供工廠建造和電動車生產方便的優惠政策,顯然許多州長都對 Tesla 超級工廠非常感興趣。   特斯拉在內華達州的超級工廠給該州帶來了超過 50 億美元的投資,創造了一萬個工作職缺,馬斯克表示,吸引 Tesla 把超級工廠建造內華達州的因素很多,包括稅收方面的優惠。   馬斯克希望政府部門能夠在立法上做出更多進步,讓新的技術能夠更快地商業化,之前他曾多次公開表示內華達州政府具有前瞻性,在超級工廠的建造過程中展示了前所未有的高效。   Tesla 未來的超級工廠將把電動車製造和電池的生產放在同一座工廠,有效地提升電動車的產能,而不是像現在這樣電池和電動車分開製造,再運往組裝工廠。   (合作媒體:。圖片出處:Tesla)  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

特斯拉啟用臺南奇美博物館超級充電站,下一站臺中七期將啟用

特斯拉(Tesla) 今日(7/20) 啟用臺灣第二組超級充電站,為全亞洲第一組設在博物館園區的超級充電站。而第三組超級充電站則會在下週於臺中七期啟用。

此次啟用的台南超級充電站位於台南奇美博物館,是全亞洲第一組設立於博物館的超級充電設施,共有8 組超級充電設備,足以應付開車環島遊,以及跨縣市長途旅遊等的需要。這次的項目更獲得台南政府大力支持,鼓勵特斯拉對於環保理念的實踐。特斯拉在台的首座超級充電站設於台北花博園區,該充電站提供6 組超級充電設備。

下週特斯拉將啟用位於臺中七期的第三組超級充電站,鄰近國道一號臺中交流,方便南來北往的特斯拉車主休息同時,快速充電再上路。

特斯拉的超級充電站充電30 分鐘,就能供應行駛270 公里所需電力,為臺北臺南間七成路程。而設在百貨公司的目的地充電站,已經從臺灣頭的翡翠灣,到臺灣尾的墾丁,遍佈14 個縣市,超過80 個地點,250 支的目的地充電座。

(合作媒體:。圖片出處:特斯拉提供)  

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!