Tesla 將啟用 Model S 電池自動交換站 過程只需 90 秒

  漫長的充電時間為電動車推廣的一大問題,而美國電動汽車公司特斯拉(Tesla )在 1 年前公佈該公司的自動電池交換站(automated battery swap stations)的構想,終於將在下週與大家見面。   電動車漫長的充電時間,經常讓消費者卻步,Tesla 為了解決這個問題,乾脆讓電池用「換」的。特斯拉的官方部落格中公告,該公司將邀請位於美國舊金山以及洛杉磯的 Model S 使用者,參與測試他們的自動電池交換站。   在現場示範的影片中,電池交換過程使用了約花費 90 秒的時間,但特斯拉之後為 Model S 換上更堅固的鋁製偏轉板和鈦製底盤防護罩,兩者的組合將能有效防止電池刺穿或是由路面雜物所造成的危險情況,使得時間增加至 3 分鐘左右。   目前使用特斯拉的快速充電站(Supercharger)充電不收費,自動電池交換站需收費 60 至 80 美元,且前往更換電池之前須先預約。

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

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

帝斯曼亮相2015印度塑料展 展示最新汽車與移動通信应用創新成果

作為全球生命科學與材料科學領先企業,荷蘭皇家帝斯曼集團將攜多項全新應用亮相2015年2月5日至10日期間舉辦的2015印度塑膠展(PlastIndia 2015)。帝斯曼的工程塑料賦予了這些應用更出色的性能、更輕的重量和更佳的可持續性,涉及汽車、移動電子及電氣設備等領域。屆時,帝斯曼新近推出的部分新型材料也將在展會上亮相。帝斯曼的展位號為14R-D4

在2015印度塑料展上,帝斯曼将重点展示其在生产中所应用的“绿色”技术。

在汽車領域中,其中的一項主要應用為帝斯曼新一代的Diablo耐高溫聚醯胺。該新型材料是帝斯曼Stanyl® 聚醯胺46和Akulon®聚醯胺6系列產品的一員,主要應用於發動機艙內的中冷集成進氣歧管等。帝斯曼最新推出的Stanyl Diablo聚醯胺46能夠承受230°C的連續使用溫度,而新型Akulon Diablo則可持續在220°C的高溫環境中工作。值得關注的是,這兩個新類型產品均提高了對短期高溫峰值的抵抗能力。

帝斯曼最新推出的Stanyl Diablo聚酰胺46能够承受230°C的连续使用温度,而新型Akulon Diablo则可持续在220°C的高温环境中工作。

此外,帝斯曼還將展示汽車領域的其他創新應用,例如以甲烷或氫氣為燃料的兩輪或四輪汽車的複合儲氣罐,以及一款獲得多項大獎的曲軸端蓋。該燃料儲氣罐是一款壓力容器,其高阻隔性內膽由帝斯曼的Akulon® 燃油阻隔6吹塑成型,外殼為連續纖維增強塑膠。其重量約為鋼罐的三分之一,因此燃料利用率得到了明顯的改善。其外殼甚至可以捨棄传统的热固性树脂,轉而採用热塑性塑料——比如由可再生原料制成的帝斯曼高性能EcoPaXX® 聚酰胺410。

此前, EcoPaXX®材料剛剛憑藉其在汽車領域的輕型多功能曲軸端蓋應用斬獲獎項。該獎項為美國塑膠工程師協會(SPE)汽車事業部創新獎項競賽中動力總成類產品的桂冠。EcoPaXX曲轴端盖由帝斯曼的汽车零部件专业合作伙伴KACO在德国制造,专供大众集团研发的最新一代柴油发动机使用。其重量比採用鋁材製成的類似幾何形狀的曲軸端蓋減輕了約40%。

帝斯曼在電子電氣領域的廣泛應用也值得關注,尤其是移動通信方面。例如,為了满足不斷增加的微型化、薄壁化、无卤阻燃及热能管理方面的要求,公司开发了一系列新型解决方案。在帝斯曼的展位上,觀眾将近距離與行業专家探討连接器、线圈骨架、接线盒、电线电缆、天线及分离器等元器件的最新材料选择。

在電子電氣領域的其他方面,帝斯曼走在無鹵阻燃高性能聚醯胺研發的前沿,為塑殼斷路器(MCCBs)中的強化玻璃熱固性複合材料研製出了一種更高效、更經濟、更環保的替代材料。帝斯曼在LED照明領域也推出了諸多創新解決方案,其導熱複合材料可以替代鋁材,使散熱片更易製造、更輕便並具備更強的功能性。領先的照明設備生產商歐司朗(Osram)近期已開始使用以聚醯胺46為基材的帝斯曼Stanyl TC复合材料为其全新的LED筒灯系列制造散热片。帝斯曼还有专门针对电线电缆的无卤材料,比如用于电气及通信电缆的Arnitel热塑性弹性体(TPE)。

帝斯曼走在无卤阻燃高性能聚酰胺研发的前沿,为塑壳断路器(MCCBs)中的强化玻璃热固性复合材料研制出了一种更高效、更经济、更环保的替代材料。

除此之外,用於醫療領域的Arnitel也是帝斯曼的突破性技術之一。Arnitel VT能夠極大程度地隔離傳染性病毒,並且因為具有透氣性,穿著舒適,適合作為一次性醫用防護服及窗簾的織物層壓材料。

帝斯曼浦那工廠利用太陽能及風能大幅降低能源消耗

在2015印度塑膠展上,帝斯曼也將重點展示其在生產中所應用的“綠色”技術。今年9月,帝斯曼的太陽能技術演示中心在其位於印度浦那的聚酯和聚醯胺複合物生產工廠落成。這一先進技術中心旨在展示帝斯曼太陽能技術的創新成果,並將通過太陽能滿足工廠25%的電力需求,同時減少工廠的二氧化碳足跡。

帝斯曼致力於通過新型技術和材料的研發與商業化,提高太陽能模組的效率與產量,從而推動太陽能發電的發展。為此,帝斯曼先進表面業務部開發了一款名為KhepriCoat®的防反射塗層材料,可顯著提高太陽能設備的透光率。帝斯曼先進表面業務部立志成為世界領先的太陽能光伏材料解決方案供應商。

帝斯曼工程塑料工廠現可以通過可再生能源滿足自身總電力需求的50%,公司計畫進一步提高這個比例。在該太陽能技術展示中心揭幕之前,其中25%的能源已經通過風力發電實現。

浦那工廠在水資源高效管理方面也保持著領先優勢。作為帝斯曼首批完全消除生產過程廢水的工廠之一,浦那工廠所使用的全部水源均經過處理、檢測並重新用於園藝等用途。

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

【其他文章推薦】

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

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

奧迪無懼油價狂跌 擬在 2018 年推 2 款電動車

油價狂跌,電動車買氣直落,不過德國車廠 Volkswagen 旗下的豪華轎車品牌奧迪 (Audi) 無懼跌勢,計畫在 2018 年前開賣 2 款純電動車,正面迎戰對手特斯拉 (Tesla) 和 BMW。   路透社 26 日報導,奧迪執行長 Rupert Stadler 接受德國《法蘭克福匯報 (Frankfurter Allgemeine Zeitung)》訪問宣布,計畫推出 1 款電動跑車和 1 款電動跨界休旅車 (SAV)。Stadler 宣稱,奧迪電動休旅車將採四輪傳動,充電一次可跑 310 英哩 (500 公里)。為加速電動車研發,2019 年前該公司將加碼投資 20 億歐元。   CNET 26 日報導,特斯拉的經典跑車「Roadster」,將推出強悍升級版「 Roadster 3.0」,新款跑車充電一次可行駛 400 英哩 (644 公里),續航力擊敗市面上所有電動車。  

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

特斯拉經典跑車 Roadster 推升級版 續航力達 644 公里

  美國豪華電動車商特斯拉 (Tesla) 的經典跑車「Roadster」,將推出強悍升級版!「 Roadster 3.0」充電一次可行駛 400 英哩 ( 644 公里),CNET 稱,續航力擊敗市面上所有電動車。   特斯拉執行長馬斯克 (Elon Musk) 26 日在推特宣布,新款 Roadster 可從洛杉磯一路開到舊金山,里程數將近 400 英哩,續航力無人能及,舊款 Roadster 僅能行駛 244 英哩,特斯拉熱銷的 Model S 也只有 265 英哩。   特斯拉表示,續航里程大增主要因為電池效能增加 31%,由 53kWh 增至 70kWh;新空氣力學設計減少汽車 15% 阻力;升級版的輪胎和軸承也降低 20% 的滾動阻力。   特斯拉 26 日股價漲 2.50%,收在 227.82 美元,但仍與 9 月 4 日歷史收盤新高的 286.04 美元相比,重挫了20%。     (圖片為特斯拉 Roadster 2.5,來源:)

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

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

北京2014年度剩餘新能源車指標將不再併入明年

12月25日,北京本年度最後一次小客車購車搖號舉行,227萬申請者爭奪普通小客車指標,基礎中簽比達到151∶1。新能源車指標綽綽有餘,無需搖號。

經市公安交通管理局審核確認,2014年4月26日中簽過期未用個人普通小客車配置指標734個,按規定納入本期個人普通小客車指標配置,因此本期將隨機搖出個人普通小客車指標19804個;搖出單位普通小客車指標1300個。2014年4月26日中簽過期未用個人示範應用新能源小客車配置指標1618個,2014年第5期未配置的個人示範應用新能源小客車指標869個,按規定納入本期個人范應用新能源小客車指標配置,因此本期將配置個人示範應用新能源小客車指標4157個;2014年第5期未配置的單位示範應用新能源小客車指標2646個,按規定納入本期單位示範應用新能源小客車指標配置,因此本期將配置單位示範應用新能源小客車指標4316個。因本期個人和單位示範應用新能源小客車指標申請數均小於本期指標配額,無需搖號,直接配置。

不過按照規定,本期剩餘的新能源車指標將不再併入下一年。

根據北京市去年底發佈的《北京市2013—2017年機動車排放污染控制工作方案》任務分解表,2015至2017年,每年共將配置機動車指標15萬個,但普通小客車指標將逐年縮水。2014年普通小客車指標13萬個、示範應用新能源小客車指標2萬個,2015年普通小客車指標將縮1萬個至12萬個,新能源車指標將增加1萬個至3萬個,新能源車的中簽幾率將進一步增加。2016年和2017年,普通車指標和新能源車指標將分別調整為9萬個和6萬個。

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

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

Day12-微信小程序實戰-交友小程序-優化“附近的人”頁面與serach組件的布局和樣式以及搜索歷史記錄和本地緩存*內附代碼)

回顧/:我們已經實現了显示附近的人的功能了,可以多個人看到附近的人頁面了

但是還是要進行優化有幾個問題:1、我們用戶選擇了其他的自定義頭像之後,在首頁可以看到頭像的變化,但是在附近的人中頭像會變成報錯的樣式:如:

 

 

 也就是500了,也就是找不到這個圖片了,解決方法:看開發文檔-》雲開發

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

其中有一個 “換取臨時鏈接”的功能(通過這個方法可以臨時的拿到一個圖片的路徑了),然後這個路徑就可以對應到我們的iconpath中了,有直接看demo

wx.cloud.getTempFileURL({
  fileList: ['cloud://xxx.png'],
  success: res => {
    // fileList 是一個有如下結構的對象數組
    // [{
    //    fileID: 'cloud://xxx.png', // 文件 ID
    //    tempFileURL: '', // 臨時文件網絡鏈接
    //    maxAge: 120 * 60 * 1000, // 有效期
    // }]
    console.log(res.fileList)
  },
  fail: console.error
})

我們剛剛換了頭像的測試號,可以看到在數據庫中

 

 

 

 正常的試https這樣的,但是我們修改了之後,它的路徑變成了我們設置的默認的,cloud開始的了

所以我們就可以直接在near.js裏面用for來判斷每個字段符不符合條件即可了,一旦找到了這個cloud開頭的路徑的話,也就是if裏面進行的東西

我們就要換取臨時的路徑即可了,如果else的話,我們還是和之前一樣的,直接push進去即可了

if裏面的話直接copy文檔裏面的demo即可了

我們通過

console.log(res.fileList) 打印出來的東西試一個數組: 

 

 裏面的那個tempFileURL就是一個臨時的路徑了

 getNearUsers(){
    db.collection('users').where({
      location: _.geoNear({
        geometry: db.Geo.Point(this.data.longitude, this.data.latitude),
        minDistance: 0,
        maxDistance: 5000
        //這1000和5000的單位是米
      }),
      islocation : true
    }).field({
      longitude : true,
      latitude : true ,
      userPhoto : true
    }).get().then((res)=>{
      console.log(res.data);
      let data = res.data;
      let result = [];
      if(data.length){

        for(let i=0;i<data.length;i++){
          if(data[i].userPhoto.includes('cloud://')){
            wx.cloud.getTempFileURL({
              fileList: [data[i].userPhoto ],
              success: res => {
                // console.log(res.fileList[0].tempFileURL)
                result.push({
                  // 然後就是把我們獲取到的臨時路徑直接賦值給iconpath即可了
                  iconPath: res.fileList[0].tempFileURL,
                  id: data[i]._id,
                  latitude: data[i].latitude,
                  longitude: data[i].longitude,
                  width: 30,
                  height: 30
                });
                
              }
            })
          }
          else{
            result.push({
              iconPath: data[i].userPhoto,
              id: data[i]._id,
              latitude: data[i].latitude,
              longitude: data[i].longitude,
              width: 30,
              height: 30
            });
          }
        
        }
        this.setData({
          markers : result
        });
      }
    });
  }

如果只是這個代碼的話,會發現我們測試賬號的如何信息都無法渲染出來,這個是因為js是異步操作的,我們要在if之後立馬就進行 setdata操作即可了

如何在全部for結束之後也再次的進行setdata操作即可了,完整代碼就是

getNearUsers(){
    db.collection('users').where({
      location: _.geoNear({
        geometry: db.Geo.Point(this.data.longitude, this.data.latitude),
        minDistance: 0,
        maxDistance: 5000
        //這1000和5000的單位是米
      }),
      islocation : true
    }).field({
      longitude : true,
      latitude : true ,
      userPhoto : true
    }).get().then((res)=>{
      console.log(res.data);
      let data = res.data;
      let result = [];
      if(data.length){

        for(let i=0;i<data.length;i++){
          if(data[i].userPhoto.includes('cloud://')){
            wx.cloud.getTempFileURL({
              fileList: [data[i].userPhoto ],
              success: res => {
                // console.log(res.fileList[0].tempFileURL)
                result.push({
                  // 然後就是把我們獲取到的臨時路徑直接賦值給iconpath即可了
                  iconPath: res.fileList[0].tempFileURL,
                  id: data[i]._id,
                  latitude: data[i].latitude,
                  longitude: data[i].longitude,
                  width: 30,
                  height: 30
                });
                this.setData({
                  markers: result
                });
              }
            })
          }
          else{
            result.push({
              iconPath: data[i].userPhoto,
              id: data[i]._id,
              latitude: data[i].latitude,
              longitude: data[i].longitude,
              width: 30,
              height: 30
            });
          }
        
        }
        this.setData({
          markers : result
        });
      }
    });
  }

 

 

 得到的效果就是,可以看到另外一個用戶剛剛它換的頭像了

(後面的優化就是可以點擊這個用戶的頭像之後我們就可以跳轉到它的詳情頁面了

這個功能在實現起來其實頁不複雜的,有一個和markers對應的事件,也就是點擊了這個markers就會觸發這個事件了  

 

通過這個事件其實我們是可以拿到id值得

 

 markertap(ev){
    console.log(ev);
  }

通過在near.js裏面得這個函數,然後我們點擊一下地圖裡面的marker圖片之後,我們得到的值就是:

 

這個markerID其實對應的就是用戶的id值了

  markertap(ev){
    // console.log(ev);
    wx.navigateTo({
      url: '/pages/detail/detail?userId=' + ev.markerId
    })
  }

通過這個代碼其實就可以實現,點擊地圖裡面的圖標的話我們就可以跳轉到這個用戶的詳情頁面去了

3、後面要測試的就是假如測試賬號關閉了共享位置的話

通過測試我們發現,測試號關閉了共享位置的話,在地圖裡面即使是刷新了還是會看到這個用戶的頭像的

 (其實代碼是沒有錯的,把項目關了再重啟之後會看到這個關閉了共享位置的用戶頭像就消失了

(其實還有其他可以優化的,就是可以在地圖的頭像上面加一段語音介紹自己等等的,因為小程序其實也是支持的,或者是可以計算我和你的距離

或者是我去你那邊的話我過去的導航和路線是怎麼樣的

 

二、search組件的布局和樣式

(就是在主頁的上面添加一個查找的框)

1、實現新建一個叫search的組件

 

 創立好了之後,就可以在首頁進行引用了

2、先在index.JSON文件裏面引入這個組件

{
  "usingComponents": {
    "search" : "/components/search/search"
  }
}

3、在主頁裏面和用標籤一樣引用就可以了

可以直接在index.wxml中通過 <search /> 來使用即可了

 

該search組件就被引入了

通過基本的結構wxml

<!--components/search/search.wxml-->
<view class="container">
  <view class="search"> 
    <view class="search-text">
      <text class="iconfont iconsousuo"></text>
      <input type="text" placeholder="搜索喵星人" />
    </view>
    <view class="search-cancel">取消</view>
  </view>
</view>

得到的效果:

 

會發現我們放大鏡圖標沒有显示出來,所以我們要配置一下,讓這個圖標可以穿透出來即可了

也就是之前copyText.js寫過的

  options: {
    styleIsolation: 'apply-shared'
  },

就是為了讓這個圖標可以生效的

 

 

 這樣的話,我們的放大鏡就進來了

之後就可以對search.wxss的樣式進行設計了

 

/* components/search/search.wxss */
.container{position: fixed;left: 0;top: 0;width: 100%;height: 70rpx;z-index: 999;}
.search{ display: flex ; align-items: center;}
.search-text{ display: flex; align-items: center;flex: 1;} 

 

但是發現,圖片和這個組件融合在一起了

 

 這是因為因為是組件的引入的話,就不像在主頁面一樣,可以佔位置的,所以就要到index.wxss設置一下讓index騰出一個空間來放這個搜索框的

通過在

 

 就是直接通過margin來騰出位置即可了

 

 上面其實是在index.wxss中給上面的騰出來100rpx的空間

/* components/search/search.wxss */
.container{position: fixed;left: 0;top: 0;width: 100%;height: 70rpx;z-index: 999;}
.search{ display: flex ; align-items: center; margin:20rpx;}
.search-text{ display: flex; align-items: center;flex: 1;border: 1px #cdcdcd solid;border-radius:10rpx; height: 65rpx} 
.search-text .iconsousuo{margin: 0 10rpx;}
.search-cancel{margin: 0 10rpx;}

得到的效果就是:

 

 但是有一個問題就是:我們在還沒點擊搜索的時候,其實不用显示後面的“取消”按鈕的,這個的話就要通過js邏輯來實現了

定義了一個isfocus來表示光標有沒有显示的(這個取消的按鈕其實是在我們獲取了光標之後才會有的)

通過在取消按鈕加上了一個wx:if判斷之後,得到的效果就是:

 

 並且當我們獲取到了光標之後,這個搜索框會適應整個頁面的高度了

 給contaner加上了  overflow: hidden; 之後得到的效果就是這個搜索框的下邊框“不見了”

 

 這個是因為,我們得container這個大得塊要比我們輸入框得高度要小了,這個時候就可以在wxss裏面通過調節container得height

 

 即可了

因為如果我們點擊了那個輸入框得胡,也就是聚焦了得話,我們得上面得搜索框的大容器显示的樣式是和沒聚焦的時候显示的不同的,所以我們就可以用三目運算符來通過這個isfocus來決定使用哪個容器,也就是說我們可以定義兩個樣式不同的容器了

<view class="{{ isFocus ? 'containerFocus' : 'container' }}">
.containerFocus{position: fixed;left: 0;top: 0;width: 100%;height: 100%;z-index: 999;
background: #ccc}

然後我們自行的吧js文件裏面定義的isFocus變量 定義weighted是true來看看我們獲取光標之後的效果是怎麼樣的:

 

之後我們就要通過邏輯里控制他們的聚焦切換不同的container了,如果是已經點擊聚焦的了話,還有一個就是可以看到我們搜索的歷史記錄,還有列表等等

 

通過:

<view class="search-history">
    <text>歷史記錄</text>
    <text class="iconfont iconshanchu"></text>
  </view>
.search-history{ display: flex;justify-content: space-between;margin:20rpx;}

效果:

 

 然後就是要搞一個搜索池了:

  <view class="search-history-btn">
    <text>小明</text>
    <text>123213</text>
    <text>dsadasd</text>
  </view>
.search-history{ display: flex;justify-content: space-between;margin:20rpx;}
.search-history-btn text{ border: 1px #cdcdcd solid; padding: 10rpx 20rpx;background: white;
border-radius: 20rpx; margin:10rpx;}

效果:(注意上面是給每一個搜索的text進行樣式的定義

上面就吧搜索的關鍵詞的布局搞好了,下面就是要對搜索的列表進行定義了(其實這個搜索的列表和我們好友的列表是很像的,可以直接直接copy 在friendList.wxml裏面的這個結構了

  <navigator wx:for="{{ friendList }}" wx:key="{{ index }}" url="{{ '../detail/detail?userId=' + item._id}}" open-type="navigate">
      <view class="friendList-item">
        <view>
         <image src="{{ item.userPhoto }}" />
         <text> {{ item.nickName }} </text>
        </view>
        <text class="iconfont iconyoujiantou"></text>
      </view>
     </navigator>

然後對  searchList-item 的樣式也是直接拷貝friendList的wxss

.friendList-item{
  /* 這裏可以直接把user.wxss中的樣式複印過來了 */
  height: 120rpx;border-bottom:1px #b4b5b6 dashed;
padding: 10rpx; display: flex;align-items: center;justify-content: space-between;
}
.friendList-item view{display : flex; align-items: center;}
.friendList-item image{width: 100rpx;height: 100rpx;border-radius: 50%;}

 

綜上所述,我們的代碼就是:

CSS

<!--components/search/search.wxml-->
<view class="{{ isFocus ? 'containerFocus' : 'container' }}">
  <view class="search"> 
    <view class="search-text">
      <text class="iconfont iconsousuo"></text>
      <input type="text" placeholder="搜索喵星人" />
    </view>
    <view wx:if="{{ isFocus }}" class="search-cancel">取消</view>
  </view>

  <view class="search-history">
    <text>歷史記錄</text>
    <text class="iconfont iconshanchu"></text>
  </view>
  <view class="search-history-btn">
    <text>小明</text>
    <text>123213</text>
    <text>dsadasd</text>
  </view>

    <navigator url="" open-type="navigate">
      <view class="searchList-item">
        <view>
         <image src="" />
         <text>小喵喵</text>
        </view>
        <text class="iconfont iconyoujiantou"></text>
      </view>
     </navigator>

</view>

html

然後還要在search.js裏面通過

options: { styleIsolation: ‘apply-shared’ } 引入外部樣式 效果圖:(選中搜索框時)

 

(未選中搜索框時

 

 

 三、實現搜索歷史記錄及本地緩存

1、我們先在searc.wxml的輸入框標籤加一個處理點擊這個輸入框的一個點擊事件

bindfocus=”handleFocus”

 還有我們在取消的標籤中,也要加一個點擊事件,點擊了的話就吧isFocus變成是false即可了

 <input type="text" placeholder="搜索喵星人" bindfocus="handleFocus" />


<view wx:if="{{ isFocus }}" class="search-cancel" bindtap="handleCancel">取消</view>
 methods: {
    handleFocus(){
     this.setData({
       isFocus : true
     }); 
    },
    handleCancel(){
      this.setData({
        isFocus: false
      }); 
    }
  }

得到的效果就是:點擊輸入框,就跳轉到輸入,點擊取消,就跳轉到首頁

還有一個小bug就是,因為輸入框的話,會默認只有在一個範圍以內,才可以輸入的,所以我們就可以讓這個輸入框適應整個範圍,可以在

給 search.wxss中添加一個代碼:

.search-text input {flex: 1;}

就讓這個輸入框可以自動的填滿整個的搜索框了

3、之後就是對輸入的東西進行處理了,可以是邊輸入邊搜索,也可以是輸入之後回車了才進行搜索,如果是邊輸入就邊搜索的話,我們可以通過bindinput來進行監聽的,那如果要是按回車的時候搜索怎麼辦呢—這個其實小程序幫我們搞好了

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

就可以通過在input中加上 bindconfirm 這個屬性來完成的,我們就定義了一個 handleConfirm 這個方法是只有我們回車了才會進行觸發的

 

 

 在手機端裏面的回車 其實默認的是 “完成”兩個字的(就是點擊這個輸入框的時候,手機就會彈出軟鍵盤了,它的確定按鈕是“搜索”兩個字的,那這個該怎麼樣去修改呢==微信也提供了

 

 默認的是我們的 done 也就是完成

所以就在input標籤中,吧confirm-type 屬性變成是 search 即可了,(這樣的話在手機的軟鍵盤就會显示 搜索 兩個字了)

(下面我們要做的就是 吧這個搜索的 放在歷史裏面管理起來了)

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

demo:

wx.setStorage({
  key:"key",
  data:"value"
})

設置的話,就是我們用戶點擊回車 之後,就可以吧這個搜索裏面的 ev.detail.value放到本地存儲裏面即可了

因為這個setStorage的話,我們要讓這個data是一個數組才行的,然後我們先通過

data : [111]看看能不能吧這個111存放到這個數組裡面

 

 可以在下面的調試板中 找到Storage 讓我們查看一下

可以看到,我們隨便輸入一點東西,然後按 回車 之後可以看到

先在search.js的data裏面定義一個 數組

然後我們就可以在wxml中,吧我們的歷史消息text,用一個數組來for出來了

 <view class="search-history-btn">
    <text wx:for="{{ historyList }}" wx:key="{{ index }}">{{ item }}</text>
  </view>

 

然後我們在一開始 聚焦了之後,就立馬從storage裏面吧數組拿出來,用getStorage方法:

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

wx.getStorage({
  key: 'key',
  success (res) {
    console.log(res.data)
  }
})

 

使用上面的demo之後,會報錯,這個報錯一般都是因為success的回調的時候要用箭頭函數才行的

   wx.getStorage({
        key: 'searchHistory',
        success:(res)=> {
          this.setData({
            historyList: res.data
          });
        }
      })

修改了之後,我們點擊 聚焦 之後

這個 111 就是我們剛剛寫入到 searchStorage 數組裡面的

 

 (這個有一個小bug,就是,假如我們輸入了兩次相同的搜索,然後存入到歷史記錄再打印出來的話,會有兩個的,我們不應該有兩個相同的歷史記錄的

 但是我們搜索重複詞的話,我們也是显示一次,然後把這個搜索的提升到最前面去),表示最近搜索,並且歷史記錄也要有一個數量的,不能把在一年之間的全部搜索記錄都显示出來的

這個去重的功能:1、實現克隆一份數組

 (unshift的話就是往數組的頭添加東西的,ES6本身就帶有一個set來完成去重功能的)

   handleConfirm(ev){
      // console.log(ev.detail.value);
      let cloneHistoryList = [...this.data.historyList];
      cloneHistoryList.unshift(ev.detail.value);
      wx.setStorage({
        key: "searchHistory",
        data: [...new Set(cloneHistoryList)]
      })
    }

我們的效果就達到了,重複輸入的話,會被提前,=

然後下面我們就要實現 歷史記錄的刪除功能了

就可以直接在這個刪除圖標的wxml中添加一個 bindtap點擊事件  handleDelete 即可了(這個刪除的話,是刪除掉全部的歷史記錄的)

(微信給我們提供的對storage的操作中,remove是操作某一項的,而clear是刪除掉所有的

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/api.html

wx.clearStorage()

直接這樣寫即可了

====**但是這樣可能如果我們後面在storage裏面也定義了其他的東西,這個語句的話會把其他緩存也會清理掉的,所以我們這裏還是使用remove好點的

wx.removeStorage({
  key: 'key',
  success (res) {
    console.log(res)
  }
})

因為我們也是要在這個成功的回到中,把這個歷史數據數組設置為空數組,所以我們就要使用成功返回的箭頭函數才行的

即可實現刪除功能了,

效果就是:

 

 

 之後再次輸入1的時候,

 

 然後就是清空 歷史記錄:

 

 

 

 

下面是這個部分的代碼

//components/search/search.js
Component({
  /**
   * 組件的屬性列表
   */
  options: {
    styleIsolation: 'apply-shared'
  },
  properties: {

  },

  /**
   * 組件的初始數據
   */
  data: {
    isFocus : false,
    historyList : []
  },

  /**
   * 組件的方法列表
   */
  methods: {

    handleFocus(){

      wx.getStorage({
        key: 'searchHistory',
        success:(res)=> {
          this.setData({
            historyList: res.data
          });
        }
      })

     this.setData({
       isFocus : true
     }); 
    },
    handleCancel(){
      this.setData({
        isFocus: false
      }); 
    },
    handleConfirm(ev){
      // console.log(ev.detail.value);
      let cloneHistoryList = [...this.data.historyList];
      cloneHistoryList.unshift(ev.detail.value);
      wx.setStorage({
        key: "searchHistory",
        data: [...new Set(cloneHistoryList)]
      })
    },
    handleHistoryDelete(){
      wx.removeStorage({
        key: 'searchHistory',
        success:(res)=>{
          this.setData({
            historyList : []
          });

        }
      })
    }
  }
})
<!--components/search/search.wxml-->
<view class="{{ isFocus ? 'containerFocus' : 'container' }}">
  <view class="search"> 
    <view class="search-text">
      <text class="iconfont iconsousuo"></text>
      <input type="text" placeholder="搜索喵星人" bindfocus="handleFocus" bindconfirm="handleConfirm" confirm-type="search"/>
    </view>
    <view wx:if="{{ isFocus }}" class="search-cancel" bindtap="handleCancel">取消</view>
  </view>

  <view class="search-history">
    <text>歷史記錄</text>
    <text bindtap="handleHistoryDelete" class="iconfont iconshanchu"></text>
  </view>
  <view class="search-history-btn">
    <text wx:for="{{ historyList }}" wx:key="{{ index }}">{{ item }}</text>
  </view>

    <navigator url="" open-type="navigate">
      <view class="searchList-item">
        <view>
         <image src="" />
         <text>小喵喵</text>
        </view>
        <text class="iconfont iconyoujiantou"></text>
      </view>
     </navigator>

</view>
/* components/search/search.wxss */
.container{position: fixed;left: 0;top: 0;width: 100%;height: 90rpx;z-index: 999;overflow: hidden;}
.containerFocus{position: fixed;left: 0;top: 0;width: 100%;height: 100%;z-index: 999;
background: #ccc}
.search{ display: flex ; align-items: center; margin:20rpx;}
.search-text{ display: flex; align-items: center;flex: 1;border: 1px #cdcdcd solid;border-radius:10rpx; height: 65rpx; background: white;} 
.search-text input {flex: 1;}
.search-text .iconsousuo{margin: 0 10rpx;}
.search-cancel{margin: 0 10rpx;}

.search-history{ display: flex;justify-content: space-between;margin:20rpx;margin-bottom: 30rpx;}
.search-history-btn{ margin-bottom: 30rpx; }
.search-history-btn text{ border: 1px #cdcdcd solid; padding: 10rpx 20rpx;background: white;
border-radius: 20rpx; margin:10rpx;}


.searchList-item{
  /* 這裏可以直接把user.wxss中的樣式複印過來了 */
  height: 120rpx;border-bottom:1px #b4b5b6 dashed;
padding: 10rpx; display: flex;align-items: center;justify-content: space-between;
}
.searchList-item view{display : flex; align-items: center;}
.searchList-item image{width: 100rpx;height: 100rpx;border-radius: 50%;}

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

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

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

Python元類實戰,通過元類實現數據庫ORM框架

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是Python專題的第19篇文章,我們一起來用元類實現一個簡易的ORM數據庫框架。

本文主要是受到了廖雪峰老師Python3入門教程的啟發,不過廖老師的博客有些精簡,一些小白可能看起來比較吃力。我在他的基礎上做了一些補充和註釋,盡量寫得淺顯一些。

ORM框架是什麼

如果是沒有做過後端的小夥伴上來估計會有點蒙,這個ORM框架究竟是什麼?ORM框架是後端工程師常用的一個框架,它的英文全稱是Object Relational Mapping,即對象-關係映射框架。顧名思義就是把關係轉化成對象的框架,關係這個詞我們在哪裡用的最多呢?

顯然應該是數據庫。之前我們在分佈式的文章介紹關係型數據庫和非關係型數據庫的時候就着重介紹過關係的含義。我們常用的MySQL就是經典的關係型數據庫,它存儲的形式是表,但是表承載的數據其實是兩個實體之間的”關係”。比如學生上課這個場景,學生和課程是兩個主體(entity),我們要記錄的是這兩個主體之間的關係,也就是學生上課這件事。

而ORM框架做的事情是將這些關係映射成類,這樣我們可以將這張表當中增刪改查的功能抽象成類當中的方法。這樣我們就可以通過調用類的方式來操作數據庫了,從而達到高度抽象業務邏輯、降低用戶使用難度的目的。

比如Java後端工程師常用的hibernate和ibatis都是用來做這件事情的,明確了框架的功能之後,我們先來設想一下最後的成果。假設我們現在開發出來了這麼一套框架,那麼它用起來的感覺應該是怎樣的?

我們來看下廖老師博客里給的例子:

class User(Model):
    # 定義類的屬性到列的映射:
    id = IntegerField('id')
    name = StringField('username')
    email = StringField('email')
    password = StringField('password')

User類代表了數據庫當中的一張表,它有4個字段:id, name, email和password,我們在定義字段的同時也通過類別指定了它們的類型。這個應該不難理解,上面的這個類等價於我們在數據庫當中執行了這麼一段建表的SQL:

create table if not exists user (
 id int,
    name string,
    email string,
    password string
)

我們定義了表字段之後,接下來要做的就是根據字段創建數據了,其實也就是根據類創建實例。我們希望User類型的實例就對應User表當中的一條記錄,並且我們可以通過調用實例當中的方法,來操作這張表進行增刪改查。

# 創建一個實例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到數據庫:
u.save()

那麼,我們怎樣可以實現這樣的功能呢?

功能實現

我們先從簡單的功能開始實現,首先是Field類,Field類表示數據庫表當中一個字段的類型。這裏的邏輯很容易理清楚,我們需要定義多種類型,比如IntegerField和StringField。我們可以對這些field類抽象出一個父類來:

class Field(object):
    def __init__(self, name, column_type):
        self.name = name
        self.column_type = column_type
        
    def __str__(self):
        return '<{}:{}>'.format(self.__class__.__name__, self.name)

__str__方法當中打印出來的兩個字段,分別是類別的名稱和字段的名稱,這段代碼應該不難理解。

接着,我們實現它的兩個子類,分別是IntegerField和StringField:

class StringField(Field):
    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(100)')
        
        
class IntegerField(Field):
    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'bigint')

這裏也不難理解,只是一個簡單的繼承應用而已。

接下來就到了最關鍵的部分,也就是Model類的實現。我們先來分析一下我們希望Model這個類擁有的功能,由於它是我們定義出來的每一張表的父類,所以它應該能夠獲取子類當中的字段,並且將它存放在一個容器當中。由於我們需要存儲的是字段名和類型的映射,所以將它存儲在dict當中比較合理。

另外一個功能是我們希望它能夠提供增刪改查的接口,能夠根據子類當中定義的字段自動生成相應的SQL語句去調用數據庫。這個也是ORM框架的意義所在。

第二個功能容易實現,只要第一個功能搞定了,做一下字符串處理即可。但是第一個功能有些麻煩,它也是元類的意義所在。因為父類當中的方法是無法獲取子類中定義的類屬性的,只能通過元類,在構建類的時候可以拿到屬性的信息。

所以我們已經很明確了,我們實現元類的目的就是為了實現這個功能。理清楚了之後,再來寫代碼就不難了。我們先來實現這個元類:

class ModelMetaclass(type):

    def __new__(cls, name, bases, attrs):
        # 創建model類的時候不做任何處理
        if name=='Model':
            return type.__new__(cls, name, bases, attrs)
        # 打印表名的信息
        print('Found model: %s' % name)
        # mappings用來存儲字段的信息
        mappings = dict()
        for k, v in attrs.items():
            # 判斷v的類型,只有是Field的子類才會存儲起來
            if isinstance(v, Field):
                print('Found mapping: %s ==> %s' % (k, v))
                mappings[k] = v
        # 將mappings當中的數據從類屬性當中移除,防止關鍵字衝突
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings # 保存屬性和列的映射關係
        attrs['__table__'] = name # 假設表名和類名一致
        return type.__new__(cls, name, bases, attrs)

如果你看過之前的文章,對元類已經很熟悉了,那麼這段代碼對你來說應該不難理解。元類搞定了,剩下的Model就更簡單了。按照規範,我們需要實現增刪改查四個函數,但是這裏我們只是為了展示,所以就只實現其中一個作為例子,其他幾個都可以如法炮製。

class Model(dict, metaclass=ModelMetaclass):
    def __init__(self, **kw):
        # 由於Model的基類是dict,所以創造Model的字段會被解析成dict的構造參數
        # 也就是說字段名和字段值的映射會存儲在dict當中
        super(Model, self).__init__(**kw)
        
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Model' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            # fields存儲字段名
            fields.append(v.name)
            # params填充問號
            params.append('?')
            # 獲取字段的值
            args.append(getattr(self, k, None))
        sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('SQL: %s' % sql)
        print('ARGS: %s' % str(args))

Model當中的save方法不難看懂,但是前面的幾個方法看起來有些多餘。但實際上它們也很重要,這裡有一個關鍵信息是Model類的父類是dict,我們在構建Model的時候傳入的參數會被用來初始化一個dict。所以我們創建數據實例的時候數據的名稱和數據值的映射會被存儲在dict當中,所以我們在save方法當中才會從self的attr當中獲取字段的值。並且我們在初始化User的時候,也必須要填寫每個字段的名稱,原因就在這裏。

最後我們來運行一下:

從結果上來看,我們輸出了User這個類的插入SQL以及它的字段的值。只需要鏈接一下數據庫,我們的這個ORM框架就可以真正投入使用了。

總結

在整個ORM框架實現的過程當中,最重要的是我們對Model這個類創建了元類,但是真正應用的地方卻是在Model的子類。實際上在實際創建User類的時候,解釋器會先搜索User內部是否定義了元類,如果沒有,會上一層去往User的父類也就是Model類搜索元類,如果找到了元類,就會使用元類來創建User。相當於元類被隱形地繼承了下來,但是我們在使用子類的時候卻感知不到。

對於框架的使用者來說,也的確不需要了解框架內部的實現機制,只需要明白使用方法,照着使用就行了。雖然元類的實現和理解很複雜,但是使用起來卻很簡單,這也是它的一個顯著特點。

最後,本文的代碼示例源於廖雪峰老師的博客,向廖雪峰老師致敬。想要查看廖老師博客原文的,請點擊查看原文。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

Flutter開發初探

目前跨端開發比較熱門的就是 React NativeFlutter 了,到底該選哪門技術似乎也快成了大前端圈的一個熱門話題。對於web前端來說,基於web生態的 React Native 應該是一個更加順暢而自然的選擇;但 Flutter 讓人動心的地方就是高性能和 跨端UI一致性。而React Native 發展不太明朗和 Flutter 越發成熟的走勢對比促使我從觀望的心態轉為加入 Flutter

這裏主要就是記錄一下學習Flutter的一些感想和看法:

  • 包管理
  • 布局和樣式
  • json
  • 狀態管理

包管理

pubspec.yaml 文件的作用類似於 npmpackage.json ,而yaml格式也比json方便。但是不能用命令行自動安裝包卻讓習慣了npm的我覺得麻煩。因為Flutter 安裝依賴包是這麼一個流程:

  1. 打開pub.dev網站;
  2. 搜索需要的包,得到包的名稱和版本;
  3. 把包名稱和版本填入pubspec.yaml,最後才開始下載包。

我覺得應該直接命令行安裝包,讓它幫我們下載,名稱版本自動寫入pubspec.yaml。如果沒有指定版本就是默認下載最新版本,因為很多時候我們並不想知道版本號,給我個能用的最新的版本號就ok了。

布局和樣式

就和很多人想的一樣,為什麼不使用 jsx 或者 xml 格式進行布局,因為基於代碼的方式看起來太不直觀了,之所以這樣聽說主要是能更方便的和Dart的hot reload特性配合使用,代碼改動能立刻反映布局變化。但我還是期待有適配轉化 DSL 的框架出現。

Flutter一切都是widget,但是連很多屬性都當成widget 這就讓人有些看不明白了,比如 CenterAlignPadding,為什麼不把常用的樣式屬性都加入到布局組件裏面呢?這導致出現了這麼一種情況:嵌套嚴重,一個很簡單的功能需要層層嵌套才能實現,而且樣式也不能方便的復用。目前比較合理的建議就是適當抽取齣子組件減少嵌套。

Json

Dart 作為強類型的語言,一切皆是對象。Dart要方便操作json就得把json轉化為對象,這就意味着每用到一個json,就需要定義一個對應的類,這也是強類型語言的通病了。這絕對讓人很懷念 js/ts 這種對json操作非常自然順暢的弱類型/函數式語言。當然也不是沒有妥協的解決方案,比較方便的就是 json_model,Flutter實戰作者寫的一個工具庫,步驟也簡單:

  1. 在工程根目錄下創建一個名為 “jsons” 的目錄;
  2. 創建或拷貝Json文件到”jsons” 目錄中 ;
  3. 運行 pub run json_model (Dart VM工程)or flutter packages pub run json_model(Flutter中) 命令生成Dart model類,生成的文件默認在”lib/models”目錄下

狀態管理

Flutter 使用initStatesetState方法設置widget狀態,原理類似React。當然這隻是widget內部控制狀態用的,跨組件通信還是需要其他方案的。官方推薦是使用Provider,使用下來中規中矩吧,當然還可以使用大名鼎鼎的 Redux 以及 mbox。不過Redux本身就以過多的樣板代碼而出名,寫React的時候就不喜歡用,hooks 出來后就果斷就放棄Redux了。hooks才是真香啊,Flutter什麼時候才支持類似的函數式狀態管理方案呢?

總結

說了這麼多,本質就是為什麼 Flutter 不向以 React 為代表的 web 生態看齊?更大的原因是Flutter的很多理念和開發模式其實遠遠落後於 React 。這也是為什麼習慣 react/vue 的 web前端 對於Flutter 感覺很彆扭不順手的原因了。

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

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Java是如何實現Future模式的?萬字詳解!

JDK1.8源碼分析項目(中文註釋)Github地址:

https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs

1 Future是什麼?

先舉個例子,我們平時網購買東西,下單後會生成一個訂單號,然後商家會根據這個訂單號發貨,發貨后又有一個快遞單號,然後快遞公司就會根據這個快遞單號將網購東西快遞給我們。在這一過程中,這一系列的單號都是我們收貨的重要憑證。

因此,JDK的Future就類似於我們網購買東西的單號,當我們執行某一耗時的任務時,我們可以另起一個線程異步去執行這個耗時的任務,同時我們可以干點其他事情。當事情幹完后我們再根據future這個”單號”去提取耗時任務的執行結果即可。因此Future也是多線程中的一種應用模式。

擴展: 說起多線程,那麼Future又與Thread有什麼區別呢?最重要的區別就是Thread是沒有返回結果的,而Future模式是有返回結果的。

2 如何使用Future

前面搞明白了什麼是Future,下面我們再來舉個簡單的例子看看如何使用Future。

假如現在我們要打火鍋,首先我們要準備兩樣東西:把水燒開和準備食材。因為燒開水是一個比較漫長的過程(相當於耗時的業務邏輯),因此我們可以一邊燒開水(相當於另起一個線程),一邊準備火鍋食材(主線程),等兩者都準備好了我們就可以開始打火鍋了。

// DaHuoGuo.java

public class DaHuoGuo {
	public static void main(String[] args) throws Exception {
		FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
			@Override
			public String call() throws Exception {
				System.out.println(Thread.currentThread().getName() + ":" + "開始燒開水...");
				// 模擬燒開水耗時
				Thread.sleep(2000);
				System.out.println(Thread.currentThread().getName() + ":"  + "開水已經燒好了...");
				return "開水";
			}
		});

		Thread thread = new Thread(futureTask);
		thread.start();

		// do other thing
		System.out.println(Thread.currentThread().getName() + ":"  + " 此時開啟了一個線程執行future的邏輯(燒開水),此時我們可以干點別的事情(比如準備火鍋食材)...");
		// 模擬準備火鍋食材耗時
		Thread.sleep(3000);
		System.out.println(Thread.currentThread().getName() + ":"  + "火鍋食材準備好了");
		String shicai = "火鍋食材";

		// 開水已經稍好,我們取得燒好的開水
		String boilWater = futureTask.get();

		System.out.println(Thread.currentThread().getName() + ":"  + boilWater + "和" + shicai + "已經準備好,我們可以開始打火鍋啦");
	}
}

執行結果如下截圖,符合我們的預期:

從以上代碼中可以看到,我們使用Future主要有以下步驟:

  1. 新建一個Callable匿名函數實現類對象,我們的業務邏輯在Callablecall方法中實現,其中Callable的泛型是返回結果類型;
  2. 然後把Callable匿名函數對象作為FutureTask的構造參數傳入,構建一個futureTask對象;
  3. 然後再把futureTask對象作為Thread構造參數傳入並開啟這個線程執行去執行業務邏輯;
  4. 最後我們調用futureTask對象的get方法得到業務邏輯執行結果。

可以看到跟Future使用有關的JDK類主要有FutureTaskCallable兩個,下面主要對FutureTask進行源碼分析。

擴展: 還有一種使用Future的方式是將Callable實現類提交給線程池執行的方式,這裏不再介紹,自行百度即可。

3 FutureTask類結構分析

我們先來看下FutureTask的類結構:

可以看到FutureTask實現了RunnableFuture接口,而RunnableFuture接口又繼承了FutureRunnable接口。因為FutureTask間接實現了Runnable接口,因此可以作為任務被線程Thread執行;此外,最重要的一點就是FutureTask還間接實現了Future接口,因此還可以獲得任務執行的結果。下面我們就來簡單看看這幾個接口的相關api

// Runnable.java

@FunctionalInterface
public interface Runnable {
    // 執行線程任務
    public abstract void run();
}

Runnable沒啥好說的,相信大家都已經很熟悉了。

// Future.java

public interface Future<V> {
    /**
     * 嘗試取消線程任務的執行,分為以下幾種情況:
     * 1)如果線程任務已經完成或已經被取消或其他原因不能被取消,此時會失敗並返回false;
     * 2)如果任務還未開始執行,此時執行cancel方法,那麼任務將被取消執行,此時返回true;TODO 此時對應任務狀態state的哪種狀態???不懂!!
     * 3)如果任務已經開始執行,那麼mayInterruptIfRunning這個參數將決定是否取消任務的執行。
     *    這裏值得注意的是,cancel(true)實質並不能真正取消線程任務的執行,而是發出一個線程
     *    中斷的信號,一般需要結合Thread.currentThread().isInterrupted()來使用。
     */
    boolean cancel(boolean mayInterruptIfRunning);
    /**
     * 判斷任務是否被取消,在執行任務完成前被取消,此時會返回true
     */
    boolean isCancelled();
    /**
     * 這個方法不管任務正常停止,異常還是任務被取消,總是返回true。
     */
    boolean isDone();
    /**
     * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
     */
    V get() throws InterruptedException, ExecutionException;
    /**
     * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
     * 只不過在規定的時間內未獲取到結果,此時會拋出超時異常
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future接口象徵著異步執行任務的結果即執行一個耗時任務完全可以另起一個線程執行,然後此時我們可以去做其他事情,做完其他事情我們再調用Future.get()方法獲取結果即可,此時若異步任務還沒結束,此時會一直阻塞等待,直到異步任務執行完獲取到結果。

// RunnableFuture.java

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

RunnableFutureFutureRunnable接口的組合,即這個接口表示又可以被線程異步執行,因為實現了Runnable接口,又可以獲得線程異步任務的執行結果,因為實現了Future接口。因此解決了Runnable異步任務沒有返回結果的缺陷。

接下來我們來看下FutureTaskFutureTask實現了RunnableFuture接口,因此是FutureRunnable接口的具體實現類,是一個可被取消的異步線程任務,提供了Future的基本實現,即異步任務執行后我們能夠獲取到異步任務的執行結果,是我們接下來分析的重中之重。FutureTask可以包裝一個CallableRunnable對象,此外,FutureTask除了可以被線程執行外,還可以被提交給線程池執行。

我們先看下FutureTask類的api,其中重點方法已經紅框框出。

上圖中FutureTaskrun方法是被線程異步執行的方法,get方法即是取得異步任務執行結果的方法,還有cancel方法是取消任務執行的方法。接下來我們主要對這三個方法進行重點分析。

思考

  1. FutureTask覆寫的run方法的返回類型依然是void,表示沒有返回值,那麼FutureTaskget方法又是如何獲得返回值的呢?
  2. FutureTaskcancel方法能真正取消線程異步任務的執行么?什麼情況下能取消?

因為FutureTask異步任務執行結果還跟Callable接口有關,因此我們再來看下Callable接口:

// Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     */
    V call() throws Exception;
}

我們都知道,Callable<V>接口和Runnable接口都可以被提交給線程池執行,唯一不同的就是Callable<V>接口是有返回結果的,其中的泛型V就是返回結果,而Runnable接口是沒有返回結果的。

思考: 一般情況下,Runnable接口實現類才能被提交給線程池執行,為何Callable接口實現類也可以被提交給線程池執行?想想線程池的submit方法內部有對Callable做適配么?

4 FutureTask源碼分析

4.1 FutureTask成員變量

我們首先來看下FutureTask的成員變量有哪些,理解這些成員變量對後面的源碼分析非常重要。

// FutureTask.java

/** 封裝的Callable對象,其call方法用來執行異步任務 */
private Callable<V> callable;
/** 在FutureTask裏面定義一個成員變量outcome,用來裝異步任務的執行結果 */
private Object outcome; // non-volatile, protected by state reads/writes
/** 用來執行callable任務的線程 */
private volatile Thread runner;
/** 線程等待節點,reiber stack的一種實現 */
private volatile WaitNode waiters;
/** 任務執行狀態 */
private volatile int state;

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 對應成員變量state的偏移地址
private static final long stateOffset;
// 對應成員變量runner的偏移地址
private static final long runnerOffset;
// 對應成員變量waiters的偏移地址
private static final long waitersOffset;

這裏我們要重點關注下FutureTaskCallable成員變量,因為FutureTask的異步任務最終是委託給Callable去實現的。

思考

  1. FutureTask的成員變量runner,waitersstate都被volatile修飾,我們可以思考下為什麼這三個成員變量需要被volatile修飾,而其他成員變量又不用呢?volatile關鍵字的作用又是什麼呢?
  2. 既然已經定義了成員變量runner,waitersstate了,此時又定義了stateOffset,runnerOffsetwaitersOffset變量分別對應runner,waitersstate的偏移地址,為何要多此一舉呢?

我們再來看看stateOffset,runnerOffsetwaitersOffset變量這三個變量的初始化過程:

// FutureTask.java

static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> k = FutureTask.class;
        stateOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("state"));
        runnerOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("runner"));
        waitersOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("waiters"));
    } catch (Exception e) {
        throw new Error(e);
    }
    }

4.2 FutureTask的狀態變化

前面講了FutureTask的成員變量,有一個表示狀態的成員變量state我們要重點關注下,state變量表示任務執行的狀態。

// FutureTask.java

/** 任務執行狀態 */
private volatile int state;
/** 任務新建狀態 */
private static final int NEW          = 0;
/** 任務正在完成狀態,是一個瞬間過渡狀態 */
private static final int COMPLETING   = 1;
/** 任務正常結束狀態 */
private static final int NORMAL       = 2;
/** 任務執行異常狀態 */
private static final int EXCEPTIONAL  = 3;
/** 任務被取消狀態,對應cancel(false) */
private static final int CANCELLED    = 4;
/** 任務中斷狀態,是一個瞬間過渡狀態 */
private static final int INTERRUPTING = 5;
/** 任務被中斷狀態,對應cancel(true) */
private static final int INTERRUPTED  = 6;

可以看到任務狀態變量state有以上7種狀態,0-6分別對應着每一種狀態。任務狀態一開始是NEW,然後由FutureTask的三個方法set,setExceptioncancel來設置狀態的變化,其中狀態變化有以下四種情況:

  1. NEW -> COMPLETING -> NORMAL:這個狀態變化表示異步任務的正常結束,其中COMPLETING是一個瞬間臨時的過渡狀態,由set方法設置狀態的變化;
  2. NEW -> COMPLETING -> EXCEPTIONAL:這個狀態變化表示異步任務執行過程中拋出異常,由setException方法設置狀態的變化;
  3. NEW -> CANCELLED:這個狀態變化表示被取消,即調用了cancel(false),由cancel方法來設置狀態變化;
  4. NEW -> INTERRUPTING -> INTERRUPTED:這個狀態變化表示被中斷,即調用了cancel(true),由cancel方法來設置狀態變化。

4.3 FutureTask構造函數

FutureTask有兩個構造函數,我們分別來看看:

// FutureTask.java

// 第一個構造函數
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

可以看到,這個構造函數在我們前面舉的“打火鍋”的例子代碼中有用到,就是Callable成員變量賦值,在異步執行任務時再調用Callable.call方法執行異步任務邏輯。此外,此時給任務狀態state賦值為NEW,表示任務新建狀態。

我們再來看下FutureTask的另外一個構造函數:

// FutureTask.java

// 另一個構造函數
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

這個構造函數在執行Executors.callable(runnable, result)時是通過適配器RunnableAdapter來將Runnable對象runnable轉換成Callable對象,然後再分別給callablestate變量賦值。

注意,這裏我們需要記住的是FutureTask新建時,此時的任務狀態stateNEW就好了。

4.4 FutureTask.run方法,用來執行異步任務

前面我們有講到FutureTask間接實現了Runnable接口,覆寫了Runnable接口的run方法,因此該覆寫的run方法是提交給線程來執行的,同時,該run方法正是執行異步任務邏輯的方法,那麼,執行完run方法又是如何保存異步任務執行的結果的呢?

我們現在着重來分析下run方法:

// FutureTask.java

public void run() {
    // 【1】,為了防止多線程併發執行異步任務,這裏需要判斷線程滿不滿足執行異步任務的條件,有以下三種情況:
    // 1)若任務狀態state為NEW且runner為null,說明還未有線程執行過異步任務,此時滿足執行異步任務的條件,
    // 此時同時調用CAS方法為成員變量runner設置當前線程的值;
    // 2)若任務狀態state為NEW且runner不為null,任務狀態雖為NEW但runner不為null,說明有線程正在執行異步任務,
    // 此時不滿足執行異步任務的條件,直接返回;
    // 1)若任務狀態state不為NEW,此時不管runner是否為null,說明已經有線程執行過異步任務,此時沒必要再重新
    // 執行一次異步任務,此時不滿足執行異步任務的條件;
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        // 拿到之前構造函數傳進來的callable實現類對象,其call方法封裝了異步任務執行的邏輯
        Callable<V> c = callable;
        // 若任務還是新建狀態的話,那麼就調用異步任務
        if (c != null && state == NEW) {
            // 異步任務執行結果
            V result;
            // 異步任務執行成功還是始遍標誌
            boolean ran;
            try {
                // 【2】,執行異步任務邏輯,並把執行結果賦值給result
                result = c.call();
                // 若異步任務執行過程中沒有拋出異常,說明異步任務執行成功,此時設置ran標誌為true
                ran = true;
            } catch (Throwable ex) {
                result = null;
                // 異步任務執行過程拋出異常,此時設置ran標誌為false
                ran = false;
                // 【3】設置異常,裏面也設置state狀態的變化
                setException(ex);
            }
            // 【3】若異步任務執行成功,此時設置異步任務執行結果,同時也設置狀態的變化
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        // 異步任務正在執行過程中,runner一直是非空的,防止併發調用run方法,前面有調用cas方法做判斷的
        // 在異步任務執行完后,不管是正常結束還是異常結束,此時設置runner為null
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        // 線程執行異步任務后的任務狀態
        int s = state;
        // 【4】如果執行了cancel(true)方法,此時滿足條件,
        // 此時調用handlePossibleCancellationInterrupt方法處理中斷
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

可以看到執行異步任務的run方法主要分為以下四步來執行:

  1. 判斷線程是否滿足執行異步任務的條件:為了防止多線程併發執行異步任務,這裏需要判斷線程滿不滿足執行異步任務的條件;
  2. 若滿足條件,執行異步任務:因為異步任務邏輯封裝在Callable.call方法中,此時直接調用Callable.call方法執行異步任務,然後返回執行結果;
  3. 根據異步任務的執行情況做不同的處理:1) 若異步任務執行正常結束,此時調用set(result);來設置任務執行結果;2)若異步任務執行拋出異常,此時調用setException(ex);來設置異常,詳細分析請見4.4.1小節
  4. 異步任務執行完后的善後處理工作:不管異步任務執行成功還是失敗,若其他線程有調用FutureTask.cancel(true),此時需要調用handlePossibleCancellationInterrupt方法處理中斷,詳細分析請見4.4.2小節

這裏值得注意的是判斷線程滿不滿足執行異步任務條件時,runner是否為null是調用UNSAFECAS方法compareAndSwapObject來判斷和設置的,同時compareAndSwapObject是通過成員變量runner的偏移地址runnerOffset來給runner賦值的,此外,成員變量runner被修飾為volatile是在多線程的情況下, 一個線程的volatile修飾變量的設值能夠立即刷進主存,因此值便可被其他線程可見。

4.4.1 FutureTask的set和setException方法

下面我們來看下當異步任務執行正常結束時,此時會調用set(result);方法:

// FutureTask.java

protected void set(V v) {
    // 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
    // 【思考】此時任務不能被多線程併發執行,什麼情況下會導致任務狀態不為NEW?
    // 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,
    // 因此需要調用CAS方法來做判斷任務狀態是否為NEW
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【2】將任務執行結果賦值給成員變量outcome
        outcome = v;
        // 【3】將任務狀態設置為NORMAL,表示任務正常結束
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        // 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
        finishCompletion();
    }
}

可以看到當異步任務正常執行結束后,且異步任務沒有被cancel的情況下,此時會做以下事情:將任務執行結果保存到FutureTask的成員變量outcome中的,賦值結束後會調用finishCompletion方法來喚醒阻塞的線程(哪裡來的阻塞線程?後面會分析),值得注意的是這裏對應的任務狀態變化是NEW -> COMPLETING -> NORMAL

我們繼續來看下當異步任務執行過程中拋出異常,此時會調用setException(ex);方法。

// FutureTask.java

protected void setException(Throwable t) {
    // 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
    // 【思考】此時任務不能被多線程併發執行,什麼情況下會導致任務狀態不為NEW?
    // 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,
    // 因此需要調用CAS方法來做判斷任務狀態是否為NEW
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【2】將異常賦值給成員變量outcome
        outcome = t;
        // 【3】將任務狀態設置為EXCEPTIONAL
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        // 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
        finishCompletion();
    }
}

可以看到setException(Throwable t)的代碼邏輯跟前面的set(V v)幾乎一樣,不同的是任務執行過程中拋出異常,此時是將異常保存到FutureTask的成員變量outcome中,還有,值得注意的是這裏對應的任務狀態變化是NEW -> COMPLETING -> EXCEPTIONAL

因為異步任務不管正常還是異常結束,此時都會調用FutureTaskfinishCompletion方法來喚醒喚醒阻塞的線程,這裏阻塞的線程是指我們調用Future.get方法時若異步任務還未執行完,此時該線程會阻塞。

// FutureTask.java

private void finishCompletion() {
    // assert state > COMPLETING;
    // 取出等待線程鏈表頭節點,判斷頭節點是否為null
    // 1)若線程鏈表頭節點不為空,此時以“後進先出”的順序(棧)移除等待的線程WaitNode節點
    // 2)若線程鏈表頭節點為空,說明還沒有線程調用Future.get()方法來獲取任務執行結果,固然不用移除
    for (WaitNode q; (q = waiters) != null;) {
        // 調用UNSAFE的CAS方法將成員變量waiters設置為空
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                // 取出WaitNode節點的線程
                Thread t = q.thread;
                // 若取出的線程不為null,則將該WaitNode節點線程置空,且喚醒正在阻塞的該線程
                if (t != null) {
                    q.thread = null;
                    //【重要】喚醒正在阻塞的該線程
                    LockSupport.unpark(t);
                }
                // 繼續取得下一個WaitNode線程節點
                WaitNode next = q.next;
                // 若沒有下一個WaitNode線程節點,說明已經將所有等待的線程喚醒,此時跳出for循環
                if (next == null)
                    break;
                // 將已經移除的線程WaitNode節點的next指針置空,此時好被垃圾回收
                q.next = null; // unlink to help gc
                // 再把下一個WaitNode線程節點置為當前線程WaitNode頭節點
                q = next;
            }
            break;
        }
    }
    // 不管任務正常執行還是拋出異常,都會調用done方法
    done();
    // 因為異步任務已經執行完且結果已經保存到outcome中,因此此時可以將callable對象置空了
    callable = null;        // to reduce footprint
}

finishCompletion方法的作用就是不管異步任務正常還是異常結束,此時都要喚醒且移除線程等待鏈表的等待線程節點,這個鏈表實現的是一個是Treiber stack,因此喚醒(移除)的順序是”後進先出”即後面先來的線程先被先喚醒(移除),關於這個線程等待鏈表是如何成鏈的,後面再繼續分析。

4.4.2 FutureTask的handlePossibleCancellationInterrupt方法

4.4小節分析的run方法里的最後有一個finally塊,此時若任務狀態state >= INTERRUPTING,此時說明有其他線程執行了cancel(true)方法,此時需要讓出CPU執行的時間片段給其他線程執行,我們來看下具體的源碼:

// FutureTask.java

private void handlePossibleCancellationInterrupt(int s) {
    // It is possible for our interrupter to stall before getting a
    // chance to interrupt us.  Let's spin-wait patiently.
    // 當任務狀態是INTERRUPTING時,此時讓出CPU執行的機會,讓其他線程執行
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt

    // assert state == INTERRUPTED;

    // We want to clear any interrupt we may have received from
    // cancel(true).  However, it is permissible to use interrupts
    // as an independent mechanism for a task to communicate with
    // its caller, and there is no way to clear only the
    // cancellation interrupt.
    //
    // Thread.interrupted();
}

思考: 為啥任務狀態是INTERRUPTING時,此時就要讓出CPU執行的時間片段呢?還有為什麼要在義務任務執行后才調用handlePossibleCancellationInterrupt方法呢?

4.5 FutureTask.get方法,獲取任務執行結果

前面我們起一個線程在其`run`方法中執行異步任務后,此時我們可以調用`FutureTask.get`方法來獲取異步任務執行的結果。

// FutureTask.java

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // 【1】若任務狀態<=COMPLETING,說明任務正在執行過程中,此時可能正常結束,也可能遇到異常
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // 【2】最後根據任務狀態來返回任務執行結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消
    return report(s);
}

可以看到,如果任務狀態state<=COMPLETING,說明異步任務正在執行過程中,此時會調用awaitDone方法阻塞等待;當任務執行完后,此時再調用report方法來報告任務結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消。

4.5.1 FutureTask.awaitDone方法

FutureTask.awaitDone方法會阻塞獲取異步任務執行結果的當前線程,直到異步任務執行完成。

// FutureTask.java

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    // 計算超時結束時間
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 線程鏈表頭節點
    WaitNode q = null;
    // 是否入隊
    boolean queued = false;
    // 死循環
    for (;;) {
        // 如果當前獲取任務執行結果的線程被中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        // 【5】如果任務狀態>COMPLETING,此時返回任務執行結果,其中此時任務可能正常結束(NORMAL),可能拋出異常(EXCEPTIONAL)
        // 或任務被取消(CANCELLED,INTERRUPTING或INTERRUPTED狀態的一種)
        if (s > COMPLETING) {
            // 【問】此時將當前WaitNode節點的線程置空,其中在任務結束時也會調用finishCompletion將WaitNode節點的thread置空,
            // 這裏為什麼又要再調用一次q.thread = null;呢?
            // 【答】因為若很多線程來獲取任務執行結果,在任務執行完的那一刻,此時獲取任務的線程要麼已經在線程等待鏈表中,要麼
            // 此時還是一個孤立的WaitNode節點。在線程等待鏈表中的的所有WaitNode節點將由finishCompletion來移除(同時喚醒)所有
            // 等待的WaitNode節點,以便垃圾回收;而孤立的線程WaitNode節點此時還未阻塞,因此不需要被喚醒,此時只要把其屬性置為
            // null,然後其有沒有被誰引用,因此可以被GC。
            if (q != null)
                q.thread = null;
            // 【重要】返回任務執行結果
            return s;
        }
        // 【4】若任務狀態為COMPLETING,此時說明任務正在執行過程中,此時獲取任務結果的線程需讓出CPU執行時間片段
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        // 【1】若當前線程還沒有進入線程等待鏈表的WaitNode節點,此時新建一個WaitNode節點,並把當前線程賦值給WaitNode節點的thread屬性
        else if (q == null)
            q = new WaitNode();
        // 【2】若當前線程等待節點還未入線程等待隊列,此時加入到該線程等待隊列的頭部
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        // 若有超時設置,那麼處理超時獲取任務結果的邏輯
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        // 【3】若沒有超時設置,此時直接阻塞當前線程
        else
            LockSupport.park(this);
    }
}

FutureTask.awaitDone方法主要做的事情總結如下:

  1. 首先awaitDone方法裏面是一個死循環;
  2. 若獲取結果的當前線程被其他線程中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException;
  3. 如果任務狀態state>COMPLETING,此時返回任務執行結果;
  4. 若任務狀態為COMPLETING,此時獲取任務結果的線程需讓出CPU執行時間片段;
  5. q == null,說明當前線程還未設置到WaitNode節點,此時新建WaitNode節點並設置其thread屬性為當前線程;
  6. queued==false,說明當前線程WaitNode節點還未加入線程等待鏈表,此時加入該鏈表的頭部;
  7. timed設置為true時,此時該方法具有超時功能,關於超時的邏輯這裏不詳細分析;
  8. 當前面6個條件都不滿足時,此時阻塞當前線程。

我們分析到這裏,可以直到執行異步任務只能有一個線程來執行,而獲取異步任務結果可以多線程來獲取,當異步任務還未執行完時,此時獲取異步任務結果的線程會加入線程等待鏈表中,然後調用調用LockSupport.park(this);方法阻塞當前線程。直到異步任務執行完成,此時會調用finishCompletion方法來喚醒並移除線程等待鏈表的每個WaitNode節點,這裏這裏喚醒(移除)WaitNode節點的線程是從鏈表頭部開始的,前面我們也已經分析過。

還有一個特別需要注意的就是awaitDone方法裏面是一個死循環,當一個獲取異步任務的線程進來后可能會多次進入多個條件分支執行不同的業務邏輯,也可能只進入一個條件分支。下面分別舉兩種可能的情況進行說明:

情況1
當獲取異步任務結果的線程進來時,此時異步任務還未執行完即state=NEW且沒有超時設置時:

  1. 第一次循環:此時q = null,此時進入上面代碼標號【1】的判斷分支,即為當前線程新建一個WaitNode節點;
  2. 第二次循環:此時queued = false,此時進入上面代碼標號【2】的判斷分支,即將之前新建的WaitNode節點加入線程等待鏈表中;
  3. 第三次循環:此時進入上面代碼標號【3】的判斷分支,即阻塞當前線程;
  4. 第四次循環:加入此時異步任務已經執行完,此時進入上面代碼標號【5】的判斷分支,即返回異步任務執行結果。

情況2
當獲取異步任務結果的線程進來時,此時異步任務已經執行完即state>COMPLETING且沒有超時設置時,此時直接進入上面代碼標號【5】的判斷分支,即直接返回異步任務執行結果即可,也不用加入線程等待鏈表了。

4.5.2 FutureTask.report方法

get方法中,當異步任務執行結束后即不管異步任務正常還是異常結束,亦或是被cancel,此時獲取異步任務結果的線程都會被喚醒,因此會繼續執行FutureTask.report方法報告異步任務的執行情況,此時可能會返回結果,也可能會拋出異常。

// FutureTask.java

private V report(int s) throws ExecutionException {
    // 將異步任務執行結果賦值給x,此時FutureTask的成員變量outcome要麼保存着
    // 異步任務正常執行的結果,要麼保存着異步任務執行過程中拋出的異常
    Object x = outcome;
    // 【1】若異步任務正常執行結束,此時返回異步任務執行結果即可
    if (s == NORMAL)
        return (V)x;
    // 【2】若異步任務執行過程中,其他線程執行過cancel方法,此時拋出CancellationException異常
    if (s >= CANCELLED)
        throw new CancellationException();
    // 【3】若異步任務執行過程中,拋出異常,此時將該異常轉換成ExecutionException后,重新拋出。
    throw new ExecutionException((Throwable)x);
}

4.6 FutureTask.cancel方法,取消執行任務

我們最後再來看下FutureTask.cancel方法,我們一看到FutureTask.cancel方法,肯定一開始就天真的認為這是一個可以取消異步任務執行的方法,如果我們這樣認為的話,只能說我們猜對了一半。

// FutureTask.java

public boolean cancel(boolean mayInterruptIfRunning) {
    // 【1】判斷當前任務狀態,若state == NEW時根據mayInterruptIfRunning參數值給當前任務狀態賦值為INTERRUPTING或CANCELLED
    // a)當任務狀態不為NEW時,說明異步任務已經完成,或拋出異常,或已經被取消,此時直接返回false。
    // TODO 【問題】此時若state = COMPLETING呢?此時為何也直接返回false,而不能發出中斷異步任務線程的中斷信號呢??
    // TODO 僅僅因為COMPLETING是一個瞬時態嗎???
    // b)當前僅當任務狀態為NEW時,此時若mayInterruptIfRunning為true,此時任務狀態賦值為INTERRUPTING;否則賦值為CANCELLED。
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        // 【2】如果mayInterruptIfRunning為true,此時中斷執行異步任務的線程runner(還記得執行異步任務時就把執行異步任務的線程就賦值給了runner成員變量嗎)
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    // 中斷執行異步任務的線程runner
                    t.interrupt();
            } finally { // final state
                // 最後任務狀態賦值為INTERRUPTED
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    // 【3】不管mayInterruptIfRunning為true還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點
    } finally {
        finishCompletion();
    }
    // 返回true
    return true;
}

以上代碼中,當異步任務狀態state != NEW時,說明異步任務已經正常執行完或已經異常結束亦或已經被cancel,此時直接返回false;當異步任務狀態state = NEW時,此時又根據mayInterruptIfRunning參數是否為true分為以下兩種情況:

  1. mayInterruptIfRunning = false時,此時任務狀態state直接被賦值為CANCELLED,此時不會對執行異步任務的線程發出中斷信號,值得注意的是這裏對應的任務狀態變化是NEW -> CANCELLED
  2. mayInterruptIfRunning = true時,此時會對執行異步任務的線程發出中斷信號,值得注意的是這裏對應的任務狀態變化是NEW -> INTERRUPTING -> INTERRUPTED

最後不管mayInterruptIfRunningtrue還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點。

FutureTask.cancel源碼中我們可以得出答案,該方法並不能真正中斷正在執行異步任務的線程,只能對執行異步任務的線程發出中斷信號。如果執行異步任務的線程處於sleepwaitjoin的狀態中,此時會拋出InterruptedException異常,該線程可以被中斷;此外,如果異步任務需要在while循環執行的話,此時可以結合以下代碼來結束異步任務線程,即執行異步任務的線程被中斷時,此時Thread.currentThread().isInterrupted()返回true,不滿足while循環條件因此退出循環,結束異步任務執行線程,如下代碼:

public Integer call() throws Exception {
    while (!Thread.currentThread().isInterrupted()) {
        // 業務邏輯代碼
        System.out.println("running...");

    }
    return 666;
}

注意:調用了FutureTask.cancel方法,只要返回結果是true,假如異步任務線程雖然不能被中斷,即使異步任務線程正常執行完畢,返回了執行結果,此時調用FutureTask.get方法也不能夠獲取異步任務執行結果,此時會拋出CancellationException異常。請問知道這是為什麼嗎?

因為調用了FutureTask.cancel方法,只要返回結果是true,此時的任務狀態為CANCELLEDINTERRUPTED,同時必然會執行finishCompletion方法,而finishCompletion方法會喚醒獲取異步任務結果的線程等待列表的線程,而獲取異步任務結果的線程喚醒后發現狀態s >= CANCELLED,此時就會拋出CancellationException異常了。

5 總結

好了,本篇文章對FutureTask的源碼分析就到此結束了,下面我們再總結下FutureTask的實現邏輯:

  1. 我們實現Callable接口,在覆寫的call方法中定義需要執行的業務邏輯;
  2. 然後把我們實現的Callable接口實現對象傳給FutureTask,然後FutureTask作為異步任務提交給線程執行;
  3. 最重要的是FutureTask內部維護了一個狀態state,任何操作(異步任務正常結束與否還是被取消)都是圍繞着這個狀態進行,並隨時更新state任務的狀態;
  4. 只能有一個線程執行異步任務,當異步任務執行結束后,此時可能正常結束,異常結束或被取消。
  5. 可以多個線程併發獲取異步任務執行結果,當異步任務還未執行完,此時獲取異步任務的線程將加入線程等待列表進行等待;
  6. 當異步任務線程執行結束后,此時會喚醒獲取異步任務執行結果的線程,注意喚醒順序是”後進先出”即後面加入的阻塞線程先被喚醒。
  7. 當我們調用FutureTask.cancel方法時並不能真正停止執行異步任務的線程,只是發出中斷線程的信號。但是只要cancel方法返回true,此時即使異步任務能正常執行完,此時我們調用get方法獲取結果時依然會拋出CancellationException異常。

擴展: 前面我們提到了FutureTaskrunner,waitersstate都是用volatile關鍵字修飾,說明這三個變量都是多線程共享的對象(成員變量),會被多線程操作,此時用volatile關鍵字修飾是為了一個線程操作volatile屬性變量值后,能夠及時對其他線程可見。此時多線程操作成員變量僅僅用了volatile關鍵字仍然會有線程安全問題的,而此時Doug Lea老爺子沒有引入任何線程鎖,而是採用了UnsafeCAS方法來代替鎖操作,確保線程安全性。

6 分析FutureTask源碼,我們能學到什麼?

我們分析源碼的目的是什麼?除了弄懂FutureTask的內部實現原理外,我們還要借鑒大佬寫寫框架源碼的各種技巧,只有這樣,我們才能成長。

分析了FutureTask源碼,我們可以從中學到:

  1. 利用LockSupport來實現線程的阻塞\喚醒機制;
  2. 利用volatileUNSAFECAS方法來實現線程共享變量的無鎖化操作;
  3. 若要編寫超時異常的邏輯可以參考FutureTaskget(long timeout, TimeUnit unit)的實現邏輯;
  4. 多線程獲取某一成員變量結果時若需要等待時的線程等待鏈表的邏輯實現;
  5. 某一異步任務在某一時刻只能由單一線程執行的邏輯實現;
  6. FutureTask中的任務狀態satate的變化處理的邏輯實現。

以上列舉的幾點都是我們可以學習參考的地方。

若您覺得不錯,請無情的轉發和點贊吧!

【源碼筆記】Github地址:

https://github.com/yuanmabiji/Java-SourceCode-Blogs

公眾號【源碼筆記】,專註於Java後端系列框架的源碼分析。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

設計模式系列之中介者模式(Mediator Pattern)——協調多個對象之間的交互

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

很多在一線城市漂泊的朋友或多或少都會遇到租房的難題,你是怎樣找到物美價廉的房子的呢,可以在評論區分享經驗哦。相信大多數小夥伴是通過中介找房子的,實話說,通過中介,只要說出你的預算以及大致需求(比如單間帶獨衛朝南大卧室帶陽台等),中介會快速提供符合你情況房源。這裏可以看出,中介者協調了房東與租客之間錯綜複雜的關係,將一個網狀的關係結構變成一個以中介者為中心的星形結構,讓多對多的關係更容易維護。

未引入中介者,對象之間(這些對象稱為同事對象,它們之間通過彼此的相互作用實現系統的行為)的關係圖如下所示:

引入中介者可以使對象之間的關係數量急劇減少。在這個星形結構中,對象不再直接與另一個對象聯繫,它通過中介者對象與另一個對象發生相互作用。

模式定義

如果在一個系統中對象之間存在多對多的相互關係,我們可以將對象之間的一些交互行為從各個對象中分離出來,並集中封裝在一个中介者對象中,並由該中介者進行統一協調,這樣對象之間多對多的複雜關係就轉化為相對簡單的一對多關係。通過引入中介者來簡化對象之間的複雜交互,中介者模式是“迪米特法則”的一個典型應用。

中介者模式(Mediator Pattern):用一个中介對象(中介者)來封裝一系列的對象交互,中介者使各對象不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的交互。中介者模式又稱為調停者模式,它是一種對象行為型模式。

模式結構圖

在中介者模式中,引入了用於協調其他對象/類之間相互調用的中介者類,為了讓系統具有更好的靈活性和可擴展性,通常還提供了抽象中介者,其結構圖如下圖所示:

在中介者模式結構圖中包含如下幾個角色:

  • Mediator(抽象中介者):它定義一個接口,該接口用於與各同事對象之間進行通信。

  • ConcreteMediator(具體中介者):它是抽象中介者的子類,通過協調各個同事對象來實現協作行為,它維持了對各個同事對象的引用。

  • Colleague(抽象同事類):它定義各個同事類公有的方法,並聲明了一些抽象方法來供子類實現,同時它維持了一個對抽象中介者類的引用,其子類可以通過該引用來與中介者通信。

  • ConcreteColleague(具體同事類):它是抽象同事類的子類;每一個同事對象在需要和其他同事對象通信時,先與中介者通信,通過中介者來間接完成與其他同事類的通信;在具體同事類中實現了在抽象同事類中聲明的抽象方法。

中介者模式的核心在於中介者類的引入,在中介者模式中,中介者類承擔了兩方面的職責:

(1) 中轉作用(結構性):通過中介者提供的中轉作用,各個同事對象就不再需要顯式引用其他同事,當需要和其他同事進行通信時,可通過中介者來實現間接調用。該中轉作用屬於中介者在結構上的支持。

(2) 協調作用(行為性):中介者可以更進一步的對同事之間的關係進行封裝,同事可以一致的和中介者進行交互,而不需要指明中介者需要具體怎麼做,中介者根據封裝在自身內部的協調邏輯,對同事的請求進行進一步處理,將同事成員之間的關係行為進行分離和封裝。該協調作用屬於中介者在行為上的支持。

模式偽代碼

典型的抽象中介者類、抽象同事類典型代碼如下:

// 抽象中介者類
public abstract class Mediator {
    // 用於存儲同事對象
    protected List<Colleague> colleagues = new ArrayList<>();

    // 註冊方法,用於增加同事對象
    public void register(Colleague colleague) {
        colleagues.add(colleague);
    }

    // 聲明抽象的業務方法
    public abstract void operation();
}

// 抽象同事類
public abstract class Colleague {
    // 維持一個抽象中介者的引用
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }

    // 聲明自身方法,處理自己的行為
    public abstract void method1();

    // 定義依賴方法,與中介者進行通信
    public void method2() {
        mediator.operation();
    }
}

具體中介者類、具體同事類實現這些抽象方法,典型代碼如下:

// 具體同事類
public class ConcreteColleague extends Colleague {

    public ConcreteColleague(Mediator mediator) {
        super(mediator);
    }
    
    @Override
    public void method1() {
        // 實現自身方法
    }
}

// 具體中介者類
public class ConcreteMediator extends Mediator {
    @Override
    public void operation() {
        // 通過調用同事類的方法,並增加其他業務邏輯來控制同事之間的交互
    }
}

模式應用

待完善…

模式總結

中介者模式將一個網狀的系統結構變成一個以中介者對象為中心的星形結構,在這個星型結構中,使用中介者對象與其他對象的一對多關係來取代原有對象之間的多對多關係。中介者模式在事件驅動類軟件中應用較為廣泛,特別是基於GUIGraphical User Interface,圖形用戶界面)的應用軟件,此外,在類與類之間存在錯綜複雜的關聯關係的系統中,中介者模式都能得到較好的應用。

主要優點

(1) 中介者模式簡化了對象之間的交互,它用中介者和同事的一對多交互代替了原來同事之間的多對多交互,一對多關係更容易理解、維護和擴展,將原本難以理解的網狀結構轉換成相對簡單的星型結構。

(2) 中介者模式可將各同事對象解耦。中介者有利於各同事之間的松耦合,我們可以獨立的改變和復用每一個同事和中介者,增加新的中介者和新的同事類都比較方便,更好地符合“開閉原則”。

(3) 可以減少子類生成,中介者將原本分佈於多個對象間的行為集中在一起,改變這些行為只需生成新的中介者子類即可,這使各個同事類可被重用,無須對同事類進行擴展。

主要缺點

中介者類中包含了大量同事之間的交互細節,可能會導致具體中介者類非常複雜,使得系統難以維護。

適用場景

(1) 系統中對象之間存在複雜的引用關係,系統結構混亂且難以理解。

(2) 一個對象由於引用了其他很多對象並且直接和這些對象通信,導致難以復用該對象。

(3) 想通過一个中間類來封裝多個類中的行為,而又不想生成太多的子類。可以通過引入中介者類來實現,在中介者中定義對象交互的公共行為,如果需要改變行為則可以增加新的具體中介者類。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!