和付費網盤說再見,跟着本文自己起個網盤(Java 開源項目)_租車

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

本文適合有 Java 基礎知識的人群,跟着本文可學習和運行 Java 網盤項目。

本文作者:HelloGitHub-秦人

HelloGitHub 推出的《講解開源項目》系列。

今天給大家帶來一款開源 Java 版網盤項目—— kiftd-source,本文將用 3 分鐘帶大家搭建一個個人網盤,技術便利生活,你值得擁有~

項目地址:https://github.com/KOHGYLW/kiftd-source

一、項目介紹

kiftd 是一款開源、使用簡單、功能完整的 Java 網盤/雲盤系統。支持在線視頻播放、文檔在線預覽、音樂播放、圖片查看等功能的文件雲存儲平台。

技術棧

  • JDK 版本:1.8.0_131
  • 項目管理框架:Maven(m2e 1.8.0 for Eclipse)
  • Archetype:mavem-archetype-quickstart 1.1
  • Spring Boot:SpringBoot 基於 Spring 開發,旨在提高微服務的開發效率。
  • MyBatis:一款優秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。
  • H2 DB:一款開源的嵌入式數據庫引擎,採用 Java 語言編寫,不受平台的限制。

二、網盤搭建

2.1 Windows 環境運行

2.1.1 下載安裝包

直接從官網下載最新的安裝包,安裝地址:https://kohgylw.gitee.io/

項目比較溫馨,支持三種下載方式:Github、阿里雲、Gitee 下載。如下圖:

2.1.2 檢查配置

這裏主要檢查一下本地 JDK 是否已安裝,在命令行窗口執行 java -version 查看 Java 版本。如下所示表示已安裝,就可以進行下一步操作。

java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

2.1.3 運行 jar

雙擊 kiftd-1.0.29-RELEASE.jar,或者在命令行執行 java -jar kiftd-1.0.29-RELEASE.jar 命令都運行可以jar 文件,會彈出安裝的界面,如下圖:

這個界面的這幾個按鈕說明一下:

  • 開啟(Start):運行網盤服務,初次啟動的端口默認是 8080。
  • 文件(Files):這個按鈕菜單中主要有網盤文件導入,導出,刪除,刷新功能。
  • 設置(Setting):設置功能主要可以設置網盤的服務端口,網盤的物理存儲路徑等信息。
  • 退出(Exit):關閉網盤系統。

點擊 開啟(Start) 按鈕即可運行網盤,這裏我設置的端口是 8090,在瀏覽器訪問: localhost:8090,運行效果如下圖:

項目是運行了,發現一個問題無法上傳文件?因為我們忘了登錄這個操作。點擊系統 登錄按鈕,填入賬號和密碼即可登錄。那麼登錄密碼在哪裡呢?這裏我直接告訴大家,用戶信息在 conf/account.properties,文件內容如下:

#<This is the default kiftd account setting file. >
#Sun May 10 21:56:28 CST 2020
admin.pwd=000000  #用戶名.密碼=000000
authOverall=l
admin.auth=cudrm
  • 用戶名:admin
  • 密碼:000000

這樣登錄之後就可以使用網盤的所有功能了。

2.2 Linux 環境運行

2.2.1 安裝 Screen 工具

Screen 工具能夠虛擬出一個終端並執行相應的操作。因為本篇所講的網盤需要一個終端。執行如下命令安裝 Screen:

yum install screen

2.2.2 Screen 常用命令

screen -S myScreen #創建虛擬終端
java -jar kiftd-1.0.29-RELEASE.jar -console #在虛擬終端中以命令模式啟動 kiftd
screen -r myScreen #返回之前的虛擬終端並繼續操作 kiftd。

2.2.3 命令行操作

在 Linux 環境上使用 console 模式啟動的效果是這樣的:

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

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

命令行輸入 -start 即可運行項目。例如輸入 -files 控制台显示是這樣的:

其實和 Windows 上一樣,包括文件導入,導出,刪除功能,多了幾個命令是查看文件,切換目錄等功能。

三、開發環境運行

3.1 下載項目

兩種方式下載項目,使用 GitBash 下載項目:

git clone https://github.com/KOHGYLW/kiftd-source.git

另外一種方式直接下載 zip 壓縮包,如下圖:

3.2 運行

打開 kohgylw.kiftd.mc.MC 類,進行測試運行。注意:本文使用 Eclipse 工具打開。

3.3 閱讀代碼

3.3.1 前台請求

就以創建目錄這個功能為例。我們先看前端功能。點擊“操作”->“新建文件夾”,填寫文件夾名稱,點擊保存如下圖:

我們知道前台 新建文件夾 功能調用的後台接口是 newFolder.ajax

3.3.2 後端接口
通過前台請求可知調用的後台接口為 homeController/newFolder.ajax。打開代碼實現,我們會看到下面這個方法。

public String newFolder(final HttpServletRequest request) {
		
        ...
        //參數校驗的部分代碼已省略
		Folder f = new Folder();
		f.setFolderId(UUID.randomUUID().toString());
		f.setFolderName(folderName);
		f.setFolderCreationDate(ServerTimeUtil.accurateToDay());
		if (account != null) {
			f.setFolderCreator(account);
		} else {
			f.setFolderCreator("匿名用戶");
		}
		f.setFolderParent(parentId);
		int i = 0;
		while (true) {
			try {
                // 數據庫插入新建文件夾的數據
				final int r = this.fm.insertNewFolder(f);
				if (r > 0) {
					if (fu.isValidFolder(f)) {
						this.lu.writeCreateFolderEvent(request, f);
						return "createFolderSuccess";
					} else {
						return "cannotCreateFolder";
					}
				}
				break;
			} catch (Exception e) {
				f.setFolderId(UUID.randomUUID().toString());
				i++;
			}
			if (i >= 10) {
				break;
			}
		}
		return "cannotCreateFolder";
	}

四、功能說明

4.1 上傳

  1. 點擊 操作,可以上傳文件和上傳文件夾,如下圖:

  2. 將本地需要上傳的文件,拖拽網盤頁面也可以上傳此文件。

4.2 視頻/音頻播放

  1. 上傳視頻到網盤,網盤也支持在線視頻播放,效果如下圖:

  2. 上傳音頻,例如我最喜歡 周杰倫 的歌曲,可以在線播放了。

4.3 快捷鍵使用

網盤還對一些常用功能添加了快捷鍵。功能和快捷鍵參照如下:

功能 快捷鍵
上傳文件夾 Shift +U
上傳文件 Shift +F
新建文件 Shift +N
複製 Shift +C
剪切 Shift +X
刪除 Shift +D

4.4 配置文件修改

配置文件在項目 conf 目錄,包括兩個配置文件:

  • account.properties:配置賬號信息,權限信息
  • server.properties:服務器的配置文件,可配置服務器端口,緩衝文件大小等

4.5 在線預覽

網盤支持文檔 txt、pdf、docx、ppt 在線預覽功能,支持圖片的在線預覽。圖片預覽效果如下:

pdf 文件預覽效果如下:

4.6 分享下載鏈接

網盤也考慮文件的分享,它可以生成下載鏈接,瀏覽器訪問下載鏈接就可以直接下載文件。選擇需要下載的文件,點擊 下載 按鈕,選擇 下載鏈接+,既可以生成文件下載鏈接。如下圖:

五、最後

教程至此已經結束,你自己的網盤跑起來了嗎?網盤是不是還不錯?而且搭建也特別簡單。一些重要的東西就可以存放到自己的網盤啦!說到底,編程語言只是工具,我們只要很好的使用工具,再加上自己天馬行空的思想,我想會創造出更多不可思議的項目。

Java 語言為什麼經久不衰,因為它能做的事情太多了,而且生態也特別豐富。如果你也有興趣那就加入 Javaer 開發者的大家庭吧!開源分享讓我們彼此認識,有了開源項目讓我們看到編程語言的絢麗多彩。

教程至此,你應該也能快速運行個人網盤了。編程是不是也特別有意思呢?先下載安裝包給自己部署一套網盤系統吧。對源碼感興趣的朋友可以開始學習項目源碼了~

關注公眾號加入交流群

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

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

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

Python編程思想(3):数字及其相關運算_包裝設計

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

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

Python 提供了三種數值類型:int(整型),float(浮點型)和complex(複數)。

  • int:通常被稱為整型或者整數,如200、299、10都屬於整型;
  • float:浮點數包含整數和小數部分,如3.1415926,2.71828都屬於浮點數;
  • complex:複數包含實數部分和虛數部分,形如 a+bj,其實部和虛部都是浮點類型;

需要注意的是,Python3 已經廢棄了 Python2 的 Long(長整型),在 Python3 中,int 的大小沒有限制,可以作為 Long 使用。這也是為什麼Python非常適合科學計算的原因,因為Python可以處理無限大的整數。在Python中進行數值運算,並不需要考慮溢出問題,因為Python的數值永遠不會溢出。  
1. 數值類型之間的轉換   Python 的三種數值類型可以進行相互轉換,轉換方式為:数字類型+圓括號,如下實例:

a = 456
b = 2.71828
print("int(b)=",int(b))
print("float(a)=",float(a))
print("complex(a)=",complex(a))
print("complex(a,b)=",complex(a,b))

執行結果如下圖所示:    
2. 常用的數學函數   Python 提供了豐富的數學函數以降低編程實現的難度,本問將介紹一些常用的函數。

import math
#求絕對值:abs(x)
print("abs(-200)=",abs(-200))

#向上取整:ceil(x)
print("ceil(3.1415)=",math.ceil(3.1415))

#向下取整:floor(x)
print("floor(3.678)=",math.floor(3.678))

#四舍五入:round(x)
print("round(3.678)=",round(3.678))

#乘方運算:pow(x,y),x的y次方
print("pow(3,4)=",pow(3,4))

#求平方根:sqrt(x)
print("sqrt(144)=",math.sqrt(144))

執行結果如下圖所示:

 
3. 運算符   計算機的最基本用途之一就是執行數學運算,作為一門計算機編程語言,Python 也提供了一套豐富的運算符來滿足各種運算需求。 Python 運算符主要可以分為6種:算術運算符、比較運算符、賦值運算符、邏輯運算符、位運算符和成員運算符。  
(1)算術運算符 對於算術運算,大家並不陌生,常用的加減乘除就是算術運算。不過,在編程語言里,算術運算符特殊一些,Python 中的算術運算有7種:加(+)、減(-)、乘( * )、除(/)、取模(%)、冪運算( ** )和取整預算(//)。以下通過實例演示算術運算符的用法。

#初始化測試數據
x = 30
y = 24
z = 12
#分別進行7種算術運算
z = x + y
print("x + y =", z)
z = x - y
print("x - y =", z)
z = x * y
print("x * y =", z)
z = x / y
print("x / y =", z)
z = x % y
print("x % y =", z)
z = x ** y
print("x ** y =", z)
z = x // y
print("x // y =", z)

執行結果如下圖所示:

 

  (2)比較運算符

比較無處不在,大於、小於、等於、不等於……和 C/C++、Java 等編程語言一樣,Python 也提供了6種比較運算符:>(大於),<(小於),==(等於),!=(不等於),>=(大於等於),<=(小於等於)。比較運算的結果是一個布爾值,True 或者 False,看下面的案例代碼:

# 初始化變量
x = 12
y = 7
#分別進行6種比較運算
print("x == y:", x == y)
print("x != y:", x != y)
print("x > y:", x > y)
print("x < y:", x < y)
print("x >= y:", x >= y)
print("x <= y:", x <= y)

執行結果如下圖所示:  
(3)賦值運算符 其實在前面的代碼中已經用到賦值運算,如 x = 12,就是一個最簡單的賦值運算,“=”就是最簡單的賦值運算符。將簡單的賦值運算與算術運算結合,Python 形成了更豐富的賦值運算符:+=、-=、=、/=、%=、*=、//=。看下面的案例代碼:

#初始化變量
x = 12
y = 21
#分別進行7種賦值運算
y = x
print("y = x, y =", y)
y += x
print("y += x, y =", y)
y -= x
print("y -= x, y =", y)
y *= x
print("y *= x, y =", y)
y /= x
print("y /= x, y =", y)
y **= x
print("y **= x, y =", y)
y //= x
print("y //= x, y =", y)

執行結果:

 

 

(4)邏輯運算符 所謂邏輯運算,就是:與、或、非。Python 中的3種邏輯運算符分別為:and(與),or(或),not(非),邏輯運算的結果是布爾值:True 或者 False。

  1. A and B:當A和B有一個為False時,結果就為False,只有A和B都為True時,結果才為True;
  2. A or B:只有當A和B都是False時,結果才為False,只要有一個為True,結果就為True;
  3. not A:當 A 為 True 時,返回 False,否則返回 True。

看下面的案例:

#初始化變量
a = 2
b = 3
c = 5
#分別執行3種邏輯運算
print("a>b and a<c :", a>b and a<c)
print("a<b and c :", a<b and c)
print("a>b or c :", a>b or a<c)
print("a<b or c :", a<b or c)
print("a or a<c :", a or a<c)
print("not a :", not a)
print("not a<b :", not a<b)

執行結果如下圖所示:

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

 
(5)位運算符 程序中的所有數值在計算機內存中都是以二進制的形式儲存的。位運算就是直接對整數在內存中的二進制位進行操作。Python 中有6種位運算符:

  • &:按位與,參与運算的兩個值,如果兩個相應位都為1,則該位的結果為1,否則為0;
  • |:按位或,只要對應的2個二進位有一個為1時,結果位就為1;
  • ^:按位異或,當兩對應的二進位相異時,結果為1;
  • ~:按位取反,對數據的每個二進制位取反,即把1變為0,把0變為1;
  • >>:按位右移,將>>左側的數按位向右移動>>右邊的數指定的位;
  • <<:按位左移,將<<左側的數按位向左移動<<右邊的數指定的位;

看下面的案例代碼:

a = 21
b = 6
print('a & b = ',a & b)
print('a | b = ',a | b)
print('a ^ b = ',a ^ b)
print('~a = ',~a)
print('a << 2 = ',a << 2)
print('a >> 2 = ',a >> 2)

執行結果如下圖所示:

 

 

 這裏講a=21,b=6,轉換為二進制如下:

a = 0001 0101
b = 0000 0110

a&b = 0000 0100
a|b = 0001 0111
a^b = 0001 0011
~a = 1110 1010
a<<2 = 01010100
a>>2 = 0000 0101

(6)成員運算符 除了前面介紹的5種運算符,Python 還支持成員運算符。介紹成員運算符之前,我們需要提前了解一個概念:數據結構,如字符串、列表、元組、字典。在接下來的文章中我們將詳細介紹這些基礎的數據結構。字符串、列表、字典,它們就像一個集合,其中包含若干元素,這些元素就是集合的成員;對於一個給定的元素,它有可能在一個給定的集合中,也可能不在,Python 中採用成員運算符來判斷元素是否屬於成員,成員運算的結果為布爾值,True 或者 False。 看下面的代碼:

#初始化字符串和列表
temp1 = "abcdefg"
temp2 = [4,2,3,5,8,9]
a = "cdf"
b = 5
c = "cde"
print("a in temp1?", a in temp1)
print("b in temp2?", b in temp2)
print("c in temp1?", c in temp1)

  執行結果如下圖所示:    

 

 

 

 

 

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

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

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

居然還有人這樣解說mybatis運行原理_台中搬家

※台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

目錄

  • Mybatis基本認識
    • 動態代理
      • JDK實現
      • CGLIB動態代理
      • 總結
    • 反射
  • Configuration對象作用
  • 映射器結構
  • sqlsession執行流程(源碼跟蹤)
    • Executor
    • StatementHandler
    • 結果處理器(ResultSetHandler)
    • 總結
  • 主題

mybatis運行分為兩部分,第一部分讀取配置文件緩存到Configuration對象中。用以創建SqlSessionFactory,第二部分是SqlSession的執行過程。

Mybatis基本認識

動態代理

  • 之前我們知道Mapper僅僅是一個接口,而不是一個邏輯實現類。但是在Java中接口是無法執行邏輯的。這裏Mybatis就是通過動態代理實現的。關於動態代理我們常用的有Jdk動態代理和cglib動態代理。兩種卻別這裏不做贅述。關於CGLIB代理在框架中使用的比較多。

  • 關於動態代理就是所有的請求有一個入口,由這個入口進行分發。在開發領域的一個用途就是【負載均衡】

  • 關於Mybatis的動態代理是使用了兩種的結合。

  • 下面看看JDK和cglib兩種實現

JDK實現

  • 首先我們需要提供一個接口 , 這個接口是對我們程序員的一個抽象。 擁有編碼和改BUG的本領

public interface Developer {

    /**
     * 編碼
     */
    void code();

    /**
     * 解決問題
     */
    void debug();
}

  • 關於這兩種本領每個人處理方式不同。這裏我們需要一個具體的實例對象

public class JavaDeveloper implements Developer {
    @Override
    public void code() {
        System.out.println("java code");
    }

    @Override
    public void debug() {
        System.out.println("java debug");
    }
}

  • 我們傳統的調用方式是通過java提供的new 機制創造一個JavaDeveloper對象出來。而通過動態代理是通過java.lang.reflect.Proxy對象創建對象調用實際方法的。

  • 通過newProxyInstance方法獲取接口對象的。而這個方法需要三個參數
    ClassLoader loader : 通過實際接口實例對象獲取ClassLoader
    Class<?>[] interfaces : 我們抽象的接口
    InvocationHandler h : 對我們接口對象方法的調用。在調用節點我們可以進行我們的業務攔截


JavaDeveloper jDeveloper = new JavaDeveloper();
Developer developer = (Developer) Proxy.newProxyInstance(jDeveloper.getClass().getClassLoader(), jDeveloper.getClass().getInterfaces(), (proxy, method, params) -> {
    if (method.getName().equals("code")) {
        System.out.println("我是一個特殊的人,code之前先分析問題");
        return method.invoke(jDeveloper, params);
    }
    if (method.getName().equals("debug")) {
        System.out.println("我沒有bug");

    }
    return null;
});
developer.code();
developer.debug();

CGLIB動態代理

  • cglib動態代理優點在於他不需要我們提前準備接口。他代理的實際的對象。這對於我們開發來說就很方便了。

public class HelloService {
    public HelloService() {
        System.out.println("HelloService構造");
    }

    final public String sayHello(String name) {
        System.out.println("HelloService:sayOthers>>"+name);
        return null;
    }

    public void sayHello() {
        System.out.println("HelloService:sayHello");
    }
}

  • 下面我們只需要實現cglib提供的MethodInterceptor接口,在初始化設置cglib的時候加載這個實例化對象就可以了

public class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("======插入前置通知======");
        Object object = methodProxy.invokeSuper(o, objects);
        System.out.println("======插入後者通知======");
        return object;
    }
}

  • 下面我們就來初始化設置cglib

public static void main(String[] args) {
    //代理類class文件存入本地磁盤方便我們反編譯查看源代碼
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/root/code");
    //通過CGLIB動態代理獲取代理對象過程
    Enhancer enhancer = new Enhancer();
    //設置enhancer對象的父類
    enhancer.setSuperclass(HelloService.class);
    // 設置enhancer的回調對象
    enhancer.setCallback(new MyMethodInterceptor());
    //創建代理對象
    HelloService helloService = (HelloService) enhancer.create();
    //通過代理對象調用目標方法
    helloService.sayHello();
}

  • 仔細看看cglib和spring的aop特別像。針對切點進行切面攔截控制。

總結

  • 通過對比兩種動態代理我們很容易發現,mybatis就是通過JDK代理實現Mapper調用的。我們Mapper接口實現通過代理到xml中對應的sql執行邏輯

反射

  • 相信有一定經驗的Java工程師都對反射或多或少有一定了解。其實從思想上看不慣哪種語言都是有反射的機制的。
  • 通過反射我們就擺脫了對象的限制我們調用方法不再需要通過對象調用了。可以通過Class對象獲取方法對象。從而通過invoke方法進行方法的調用了。

Configuration對象作用

  • Configuration對象存儲了所有Mybatis的配置。主要初始化一下參數
    • properties
    • settings
    • typeAliases
    • typeHandler
    • ObjectFactory
    • plugins
    • environment
    • DatabaseIdProvider
    • Mapper映射器

映射器結構

  • BoundSql提供三個主要的屬性 parameterMappings 、parameterObject、sql

  • parameterObject參數本身。我們可以傳遞java基本類型、POJO、Map或者@Param標註的參數。

  • 當我們傳遞的是java基本類型mybatis會轉換成對應的包裝對象 int -> Integer

  • 如果我們傳遞POJO、Map。就是對象本身

  • 我們傳遞多個參數且沒有@Param指定變量名則parameterObject 類似
    {“1″:p1,”2″:p2,”param1″:p1,”param2”:p2}

    ※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

    台中搬家公司推薦超過30年經驗,首選台中大展搬家

  • 我們傳遞多個參數且@Param指定變量名 則parameterObject類似
    {“key1″:p1,”key2″:p2,”param1″:p1,”param2”:p2}

  • parameterMapping 是記錄屬性、名稱、表達式、javaType,jdbcType、typeHandler這些信息

  • sql 屬性就是我們映射器中的一條sql. 正常我們在常見中對sql進行校驗。正常不需要修改sql。

sqlsession執行流程(源碼跟蹤)

  • 首先我們看看我們平時開發的Mapper接口是如何動態代理的。這就需要提到MapperProxyFactory這個類了。該類中的newInstance方法

protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  • 通過上滿代碼及上述對jdk動態代理的表述。我們可以知道mapperProxy是我們代理的重點。
  • MapperProxy是InvocationHandler的實現類。他重寫的invoke方法就是代理對象執行的方法入口。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
    if (Object.class.equals(method.getDeclaringClass())) {
    return method.invoke(this, args);
    } else if (isDefaultMethod(method)) {
    return invokeDefaultMethod(proxy, method, args);
    }
} catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}


private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
    & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
    && method.getDeclaringClass().isInterface();
}

  • 通過源碼發現。invoke內部首先判斷對象是否是類 。 通過打斷點發現最終會走到cacheMapperMethod這個方法去創建MapperMethod對象。
  • 繼續查看MapperMethod中execute方法我們可以了解到內部實現其實是一個命令行模式開發。通過判斷命令從而執行不同的語句。判斷到具體執行語句然後將參數傳遞給sqlsession進行sql調用並獲取結果。到了sqlsession就和正常jdbc開發sql進行關聯了。sqlsession中Executor、StatementHandler、ParameterHandler、Resulthandler四大天王

Executor

  • 顧名思義他就是一個執行器。將java提供的sql提交到數據庫。Mybatis提供了三種執行器。

  • Configuration.class中newExecutor源碼

  • 根據uml我們不難看出mybatis中提供了三類執行器分別SimpleExecutor、ReuseExecutor、BatchExecutor

public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // 得到configuration 中的environment
      final Environment environment = configuration.getEnvironment();
      // 得到configuration 中的事務工廠
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 獲取執行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 返回默認的SqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  • 通過上述源碼我們知道在sqlsession獲取一個數據庫session對象時我們或根據我們的settings配置加載一個Executor對象。在settings中配置也很簡單

<settings>
<!--取值範圍 SIMPLE, REUSE, BATCH -->
	<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>

  • 我們也可以通過java代碼設置

factory.openSession(ExecutorType.BATCH);

StatementHandler

  • 顧名思義,StatementHandler就是專門處理數據庫回話的。這個對象的創建還是在Configuration中管理的。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

  • 很明顯Mybatis中StatementHandler使用的是RoutingStatementHandler這個class
  • 關於StatementHandler和RoutingStatementHandler之間的關係我們通過源碼可以看出這裏和Executor一樣都是適配器模式。採用這種模式的好處是方便我們對這些對象進行代理。這裏讀者可以猜測一下是使用了哪種動態代理。給點提示 這裏使用了接口哦

  • 在查看BaseStatementHandler結構我們會發現和Executor一模一樣。同樣的Mybatis在構造RoutingStatementHandler的時候會根據setting中配置來加載不同的具體子類。這些子類都是繼承了BaseStatementHandler.

  • 前一節我們跟蹤了Executor。 我們知道Mybatis默認的是SimpleExecutor。 StatementHandler我們跟蹤了Mybaits默認的是PrePareStatementHandler。在SimpleExecutor執行查詢的源碼如下

  • 我們發現在executor查詢錢會先讓statementHandler構建一個Statement對象。最終就是StatementHandler中prepare方法。這個方法在抽象類BaseStatmentHandler中已經封裝好了。
  • 這個方法的邏輯是初始化statement和設置連接超時等一些輔助作用
  • 然後就是設置一些參數等設置。最後就走到了執行器executor的doquery
  • PrepareStatement在我們jdbc開發時是常見的一個類 。 這個方法執行execute前我們需要設置sql語句,設置參數進行編譯。這一系列步驟就是剛才我們說的流程也是PrepareStatementHandler.prepareStatement幫我們做的事情。那麼剩下的我們也很容易想到就是我們對數據結果的封裝。正如代碼所示下馬就是resultSetHandler幫我們做事情了。

結果處理器(ResultSetHandler)


@Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

  • 這個方法我們可以導出來是結果xml中標籤配置對結果的一個封裝。

總結

  • SqlSession在一個查詢開啟的時候會先通過CacheExecutor查詢緩存。擊穿緩存後會通過BaseExector子類的SimpleExecutor創建StatementHandler。PrepareStatementHandler會基於PrepareStament執行數據庫操作。並針對返回結果通過ResultSetHandler返回結果數據

主題

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

※台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

萬字超強圖文講解AQS以及ReentrantLock應用(建議收藏)_台中搬家公司

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

寫在前面

進入源碼階段了,寫了十幾篇的 併發系列 知識鋪墊終於要派上用場了。相信很多人已經忘了其中的一些理論知識,別擔心,我會在源碼環節帶入相應的理論知識點幫助大家回憶,做到理論與實踐相結合,另外這是超長圖文,建議收藏,如果對你有用還請點贊讓更多人看到

Java SDK 為什麼要設計 Lock

曾幾何時幻想過,如果 Java 併發控制只有 synchronized 多好,只有下面三種使用方式,簡單方便

public class ThreeSync {

	private static final Object object = new Object();

	public synchronized void normalSyncMethod(){
		//臨界區
	}

	public static synchronized void staticSyncMethod(){
		//臨界區
	}

	public void syncBlockMethod(){
		synchronized (object){
			//臨界區
		}
	}
}

如果在 Java 1.5之前,確實是這樣,自從 1.5 版本 Doug Lea 大師就重新造了一個輪子 Lock

我們常說:“避免重複造輪子”,如果有了輪子還是要堅持再造個輪子,那麼肯定傳統的輪子在某些應用場景中不能很好的解決問題

不知你是否還記得 Coffman 總結的四個可以發生死鎖的情形 ,其中【不可剝奪條件】是指:

線程已經獲得資源,在未使用完之前,不能被剝奪,只能在使用完時自己釋放

要想破壞這個條件,就需要具有申請不到進一步資源就釋放已有資源的能力

很顯然,這個能力是 synchronized 不具備的,使用 synchronized ,如果線程申請不到資源就會進入阻塞狀態,我們做什麼也改變不了它的狀態,這是 synchronized 輪子的致命弱點,這就強有力的給了重造輪子 Lock 的理由

顯式鎖 Lock

舊輪子有弱點,新輪子就要解決這些問題,所以要具備不會阻塞的功能,下面的三個方案都是解決這個問題的好辦法(看下面表格描述你就明白三個方案的含義了)

特性 描述 API
能響應中斷 如果不能自己釋放,那可以響應中斷也是很好的。Java多線程中斷機制 專門描述了中斷過程,目的是通過中斷信號來跳出某種狀態,比如阻塞 lockInterruptbly()
非阻塞式的獲取鎖 嘗試獲取,獲取不到不會阻塞,直接返回 tryLock()
支持超時 給定一個時間限制,如果一段時間內沒獲取到,不是進入阻塞狀態,同樣直接返回 tryLock(long time, timeUnit)

好的方案有了,但魚和熊掌不可兼得,Lock 多了 synchronized 不具備的特性,自然不會像 synchronized 那樣一個關鍵字三個玩法走遍全天下,在使用上也相對複雜了一丟丟

Lock 使用範式

synchronized 有標準用法,這樣的優良傳統咱 Lock 也得有,相信很多人都知道使用 Lock 的一個範式

Lock lock = new ReentrantLock();
lock.lock();
try{
	...
}finally{
	lock.unlock();
}

既然是範式(沒事不要挑戰更改寫法的那種),肯定有其理由,我們來看一下

標準1—finally 中釋放鎖

這個大家應該都會明白,在 finally 中釋放鎖,目的是保證在獲取到鎖之後,最終能被釋放

標準2—在 try{} 外面獲取鎖

不知道你有沒有想過,為什麼會有標準 2 的存在,我們通常是“喜歡” try 住所有內容,生怕發生異常不能捕獲的

在 try{} 外獲取鎖主要考慮兩個方面:

  1. 如果沒有獲取到鎖就拋出異常,最終釋放鎖肯定是有問題的,因為還未曾擁有鎖談何釋放鎖呢
  2. 如果在獲取鎖時拋出了異常,也就是當前線程並未獲取到鎖,但執行到 finally 代碼時,如果恰巧別的線程獲取到了鎖,則會被釋放掉(無故釋放)

不同鎖的實現方式略有不同,範式的存在就是要避免一切問題的出現,所以大家盡量遵守範式

Lock 是怎樣起到鎖的作用呢?

如果你熟悉 synchronized,你知道程序編譯成 CPU 指令后,在臨界區會有 moniterenter 和 moniterexit 指令的出現,可以理解成進出臨界區的標識

從範式上來看:

  • lock.lock() 獲取鎖,“等同於” synchronized 的 moniterenter指令

  • lock.unlock() 釋放鎖,“等同於” synchronized 的 moniterexit 指令

那 Lock 是怎麼做到的呢?

這裏先簡單說明一下,這樣一會到源碼分析時,你可以遠觀設計輪廓,近觀實現細節,會變得越發輕鬆

其實很簡單,比如在 ReentrantLock 內部維護了一個 volatile 修飾的變量 state,通過 CAS 來進行讀寫(最底層還是交給硬件來保證原子性和可見性),如果CAS更改成功,即獲取到鎖,線程進入到 try 代碼塊繼續執行;如果沒有更改成功,線程會被【掛起】,不會向下執行

但 Lock 是一個接口,裏面根本沒有 state 這個變量的存在:

它怎麼處理這個 state 呢?很顯然需要一點設計的加成了,接口定義行為,具體都是需要實現類的

Lock 接口的實現類基本都是通過【聚合】了一個【隊列同步器】的子類完成線程訪問控制的

那什麼是隊列同步器呢? (這應該是你見過的最強標題黨,聊了半個世紀才入正題,評論區留言罵我)

隊列同步器 AQS

隊列同步器 (AbstractQueuedSynchronizer),簡稱同步器或AQS,就是我們今天的主人公

問:為什麼你分析 JUC 源碼,要從 AQS 說起呢?

答:看下圖

相信看到這個截圖你就明白一二了,你聽過的,面試常被問起的,工作中常用的

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore(信號量)
  • CountDownLatch
  • 公平鎖
  • 非公平鎖
  • ThreadPoolExecutor (關於線程池的理解,可以查看 為什麼要使用線程池? )

都和 AQS 有直接關係,所以了解 AQS 的抽象實現,在此基礎上再稍稍查看上述各類的實現細節,很快就可以全部搞定,不至於查看源碼時一頭霧水,丟失主線

上面提到,在鎖的實現類中會聚合同步器,然後利同步器實現鎖的語義,那麼問題來了:

為什麼要用聚合模式,怎麼進一步理解鎖和同步器的關係呢?

我們絕大多數都是在使用鎖,實現鎖之後,其核心就是要使用方便

從 AQS 的類名稱和修飾上來看,這是一個抽象類,所以從設計模式的角度來看同步器一定是基於【模版模式】來設計的,使用者需要繼承同步器,實現自定義同步器,並重寫指定方法,隨後將同步器組合在自定義的同步組件中,並調用同步器的模版方法,而這些模版方法又回調用使用者重寫的方法

我不想將上面的解釋說的這麼抽象,其實想理解上面這句話,我們只需要知道下面兩個問題就好了

  1. 哪些是自定義同步器可重寫的方法?
  2. 哪些是抽象同步器提供的模版方法?

同步器可重寫的方法

同步器提供的可重寫方法只有5個,這大大方便了鎖的使用者:

按理說,需要重寫的方法也應該有 abstract 來修飾的,為什麼這裏沒有?原因其實很簡單,上面的方法我已經用顏色區分成了兩類:

  • 獨佔式
  • 共享式

自定義的同步組件或者鎖不可能既是獨佔式又是共享式,為了避免強制重寫不相干方法,所以就沒有 abstract 來修飾了,但要拋出異常告知不能直接使用該方法:

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

暖暖的很貼心(如果你有類似的需求也可以仿照這樣的設計)

表格方法描述中所說的同步狀態就是上文提到的有 volatile 修飾的 state,所以我們在重寫上面幾個方法時,還要通過同步器提供的下面三個方法(AQS 提供的)來獲取或修改同步狀態:

而獨佔式和共享式操作 state 變量的區別也就很簡單了

所以你看到的 ReentrantLock ReentrantReadWriteLock Semaphore(信號量) CountDownLatch 這幾個類其實僅僅是在實現以上幾個方法上略有差別,其他的實現都是通過同步器的模版方法來實現的,到這裡是不是心情放鬆了許多呢?我們來看一看模版方法:

同步器提供的模版方法

上面我們將同步器的實現方法分為獨佔式和共享式兩類,模版方法其實除了提供以上兩類模版方法之外,只是多了響應中斷和超時限制 的模版方法供 Lock 使用,來看一下

先不用記上述方法的功能,目前你只需要了解個大概功能就好。另外,相信你也注意到了:

上面的方法都有 final 關鍵字修飾,說明子類不能重寫這個方法

看到這你也許有點亂了,我們稍微歸納一下:

程序員還是看代碼心裏踏實一點,我們再來用代碼說明一下上面的關係(注意代碼中的註釋,以下的代碼並不是很嚴謹,只是為了簡單說明上圖的代碼實現):

package top.dayarch.myjuc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 自定義互斥鎖
 *
 * @author tanrgyb
 * @date 2020/5/23 9:33 PM
 */
public class MyMutex implements Lock {

	// 靜態內部類-自定義同步器
	private static class MySync extends AbstractQueuedSynchronizer{
		@Override
		protected boolean tryAcquire(int arg) {
			// 調用AQS提供的方法,通過CAS保證原子性
			if (compareAndSetState(0, arg)){
				// 我們實現的是互斥鎖,所以標記獲取到同步狀態(更新state成功)的線程,
				// 主要為了判斷是否可重入(一會兒會說明)
				setExclusiveOwnerThread(Thread.currentThread());
				//獲取同步狀態成功,返回 true
				return true;
			}
			// 獲取同步狀態失敗,返回 false
			return false;
		}

		@Override
		protected boolean tryRelease(int arg) {
			// 未擁有鎖卻讓釋放,會拋出IMSE
			if (getState() == 0){
				throw new IllegalMonitorStateException();
			}
			// 可以釋放,清空排它線程標記
			setExclusiveOwnerThread(null);
			// 設置同步狀態為0,表示釋放鎖
			setState(0);
			return true;
		}

		// 是否獨佔式持有
		@Override
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}

		// 後續會用到,主要用於等待/通知機制,每個condition都有一個與之對應的條件等待隊列,在鎖模型中說明過
		Condition newCondition() {
			return new ConditionObject();
		}
	}

  // 聚合自定義同步器
	private final MySync sync = new MySync();


	@Override
	public void lock() {
		// 阻塞式的獲取鎖,調用同步器模版方法獨佔式,獲取同步狀態
		sync.acquire(1);
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
		// 調用同步器模版方法可中斷式獲取同步狀態
		sync.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock() {
		// 調用自己重寫的方法,非阻塞式的獲取同步狀態
		return sync.tryAcquire(1);
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// 調用同步器模版方法,可響應中斷和超時時間限制
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

	@Override
	public void unlock() {
		// 釋放鎖
		sync.release(1);
	}

	@Override
	public Condition newCondition() {
		// 使用自定義的條件
		return sync.newCondition();
	}
}

如果你現在打開 IDE, 你會發現上文提到的 ReentrantLock ReentrantReadWriteLock Semaphore(信號量) CountDownLatch 都是按照這個結構實現,所以我們就來看一看 AQS 的模版方法到底是怎麼實現鎖

AQS實現分析

從上面的代碼中,你應該理解了lock.tryLock() 非阻塞式獲取鎖就是調用自定義同步器重寫的 tryAcquire() 方法,通過 CAS 設置state 狀態,不管成功與否都會馬上返回;那麼 lock.lock() 這種阻塞式的鎖是如何實現的呢?

有阻塞就需要排隊,實現排隊必然需要隊列

CLH:Craig、Landin and Hagersten 隊列,是一個單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO)——概念了解就好,不要記

隊列中每個排隊的個體就是一個 Node,所以我們來看一下 Node 的結構

Node 節點

AQS 內部維護了一個同步隊列,用於管理同步狀態。

  • 當線程獲取同步狀態失敗時,就會將當前線程以及等待狀態等信息構造成一個 Node 節點,將其加入到同步隊列中尾部,阻塞該線程
  • 當同步狀態被釋放時,會喚醒同步隊列中“首節點”的線程獲取同步狀態

為了將上述步驟弄清楚,我們需要來看一看 Node 結構 (如果你能打開 IDE 一起看那是極好的)

乍一看有點雜亂,我們還是將其歸類說明一下:

上面這幾個狀態說明有個印象就好,有了Node 的結構說明鋪墊,你也就能想象同步隊列的接本結構了:

前置知識基本鋪墊完畢,我們來看一看獨佔式獲取同步狀態的整個過程

獨佔式獲取同步狀態

故事要從範式lock.lock() 開始

public void lock() {
	// 阻塞式的獲取鎖,調用同步器模版方法,獲取同步狀態
	sync.acquire(1);
}

進入AQS的模版方法 acquire()

public final void acquire(int arg) {
  // 調用自定義同步器重寫的 tryAcquire 方法
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

首先,也會嘗試非阻塞的獲取同步狀態,如果獲取失敗(tryAcquire返回false),則會調用 addWaiter 方法構造 Node 節點(Node.EXCLUSIVE 獨佔式)並安全的(CAS)加入到同步隊列【尾部】

    private Node addWaiter(Node mode) {
      	// 構造Node節點,包含當前線程信息以及節點模式【獨佔/共享】
        Node node = new Node(Thread.currentThread(), mode);
      	// 新建變量 pred 將指針指向tail指向的節點
        Node pred = tail;
      	// 如果尾節點不為空
        if (pred != null) {
          	// 新加入的節點前驅節點指向尾節點
            node.prev = pred;

          	// 因為如果多個線程同時獲取同步狀態失敗都會執行這段代碼
            // 所以,通過 CAS 方式確保安全的設置當前節點為最新的尾節點
            if (compareAndSetTail(pred, node)) {
              	// 曾經的尾節點的後繼節點指向當前節點
                pred.next = node;
              	// 返回新構建的節點
                return node;
            }
        }
      	// 尾節點為空,說明當前節點是第一個被加入到同步隊列中的節點
      	// 需要一個入隊操作
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
      	// 通過“死循環”確保節點被正確添加,最終將其設置為尾節點之後才會返回,這裏使用 CAS 的理由和上面一樣
        for (;;) {
            Node t = tail;
          	// 第一次循環,如果尾節點為 null
            if (t == null) { // Must initialize
              	// 構建一個哨兵節點,並將頭部指針指向它
                if (compareAndSetHead(new Node()))
                  	// 尾部指針同樣指向哨兵節點
                    tail = head;
            } else {
              	// 第二次循環,將新節點的前驅節點指向t
                node.prev = t;
              	// 將新節點加入到隊列尾節點
                if (compareAndSetTail(t, node)) {
                  	// 前驅節點的後繼節點指向當前新節點,完成雙向隊列
                    t.next = node;
                    return t;
                }
            }
        }
    }

你可能比較迷惑 enq() 的處理方式,進入該方法就是一個“死循環”,我們就用圖來描述它是怎樣跳出循環的

有些同學可能會有疑問,為什麼會有哨兵節點?

哨兵,顧名思義,是用來解決國家之間邊界問題的,不直接參与生產活動。同樣,計算機科學中提到的哨兵,也用來解決邊界問題,如果沒有邊界,指定環節,按照同樣算法可能會在邊界處發生異常,比如要繼續向下分析的 acquireQueued() 方法

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// "死循環",嘗試獲取鎖,或者掛起
            for (;;) {
              	// 獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 只有當前節點的前驅節點是頭節點,才會嘗試獲取鎖
              	// 看到這你應該理解添加哨兵節點的含義了吧
                if (p == head && tryAcquire(arg)) {
                  	// 獲取同步狀態成功,將自己設置為頭
                    setHead(node);
                  	// 將哨兵節點的後繼節點置為空,方便GC
                    p.next = null; // help GC
                    failed = false;
                  	// 返回中斷標識
                    return interrupted;
                }
              	// 當前節點的前驅節點不是頭節點
              	//【或者】當前節點的前驅節點是頭節點但獲取同步狀態失敗
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

獲取同步狀態成功會返回可以理解了,但是如果失敗就會一直陷入到“死循環”中浪費資源嗎?很顯然不是,shouldParkAfterFailedAcquire(p, node) 和 parkAndCheckInterrupt() 就會將線程獲取同步狀態失敗的線程掛起,我們繼續向下看

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      	// 獲取前驅節點的狀態
        int ws = pred.waitStatus;
      	// 如果是 SIGNAL 狀態,即等待被佔用的資源釋放,直接返回 true
      	// 準備繼續調用 parkAndCheckInterrupt 方法
        if (ws == Node.SIGNAL)
            return true;
      	// ws 大於0說明是CANCELLED狀態,
        if (ws > 0) {
            // 循環判斷前驅節點的前驅節點是否也為CANCELLED狀態,忽略該狀態的節點,重新連接隊列
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
          	// 將當前節點的前驅節點設置為設置為 SIGNAL 狀態,用於後續喚醒操作
          	// 程序第一次執行到這返回為false,還會進行外層第二次循環,最終從代碼第7行返回
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

到這裏你也許有個問題:

這個地方設置前驅節點為 SIGNAL 狀態到底有什麼作用?

保留這個問題,我們陸續揭曉

如果前驅節點的 waitStatus 是 SIGNAL狀態,即 shouldParkAfterFailedAcquire 方法會返回 true ,程序會繼續向下執行 parkAndCheckInterrupt 方法,用於將當前線程掛起

    private final boolean parkAndCheckInterrupt() {
      	// 線程掛起,程序不會繼續向下執行
        LockSupport.park(this);
      	// 根據 park 方法 API描述,程序在下述三種情況會繼續向下執行
      	// 	1. 被 unpark 
      	// 	2. 被中斷(interrupt)
      	// 	3. 其他不合邏輯的返回才會繼續向下執行
      	
      	// 因上述三種情況程序執行至此,返回當前線程的中斷狀態,並清空中斷狀態
      	// 如果由於被中斷,該方法會返回 true
        return Thread.interrupted();
    }

被喚醒的程序會繼續執行 acquireQueued 方法里的循環,如果獲取同步狀態成功,則會返回 interrupted = true 的結果

程序繼續向調用棧上層返回,最終回到 AQS 的模版方法 acquire

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

你也許會有疑惑:

程序已經成功獲取到同步狀態並返回了,怎麼會有個自我中斷呢?

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

如果你不能理解中斷,強烈建議你回看 Java多線程中斷機制

到這裏關於獲取同步狀態我們還遺漏了一條線,acquireQueued 的 finally 代碼塊如果你仔細看你也許馬上就會有疑惑:

到底什麼情況才會執行 if(failed) 裏面的代碼 ?

if (failed)
  cancelAcquire(node);

這段代碼被執行的條件是 failed 為 true,正常情況下,如果跳出循環,failed 的值為false,如果不能跳出循環貌似怎麼也不能執行到這裏,所以只有不正常的情況才會執行到這裏,也就是會發生異常,才會執行到此處

查看 try 代碼塊,只有兩個方法會拋出異常:

  • node.processor() 方法

    ※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

  • 自己重寫的 tryAcquire() 方法

先看前者:

很顯然,這裏拋出的異常不是重點,那就以 ReentrantLock 重寫的 tryAcquire() 方法為例

另外,上面分析 shouldParkAfterFailedAcquire 方法還對 CANCELLED 的狀態進行了判斷,那麼

什麼時候會生成取消狀態的節點呢?

答案就在 cancelAcquire 方法中, 我們來看看 cancelAcquire到底怎麼設置/處理 CANNELLED 的

	private void cancelAcquire(Node node) {
        // 忽略無效節點
        if (node == null)
            return;
				// 將關聯的線程信息清空
        node.thread = null;

        // 跳過同樣是取消狀態的前驅節點
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // 跳出上面循環后找到前驅有效節點,並獲取該有效節點的後繼節點
        Node predNext = pred.next;

        // 將當前節點的狀態置為 CANCELLED
        node.waitStatus = Node.CANCELLED;

        // 如果當前節點處在尾節點,直接從隊列中刪除自己就好
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
          	// 1. 如果當前節點的有效前驅節點不是頭節點,也就是說當前節點不是頭節點的後繼節點
            if (pred != head &&
                // 2. 判斷當前節點有效前驅節點的狀態是否為 SIGNAL
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 // 3. 如果不是,嘗試將前驅節點的狀態置為 SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                // 判斷當前節點有效前驅節點的線程信息是否為空
                pred.thread != null) {
              	// 上述條件滿足
                Node next = node.next;
              	// 將當前節點有效前驅節點的後繼節點指針指向當前節點的後繼節點
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
              	// 如果當前節點的前驅節點是頭節點,或者上述其他條件不滿足,就喚醒當前節點的後繼節點
                unparkSuccessor(node);
            }
						
            node.next = node; // help GC
        }

看到這個註釋你可能有些亂了,其核心目的就是從等待隊列中移除 CANCELLED 的節點,並重新拼接整個隊列,總結來看,其實設置 CANCELLED 狀態節點只是有三種情況,我們通過畫圖來分析一下:

至此,獲取同步狀態的過程就結束了,我們簡單的用流程圖說明一下整個過程

獲取鎖的過程就這樣的結束了,先暫停幾分鐘整理一下自己的思路。我們上面還沒有說明 SIGNAL 的作用, SIGNAL 狀態信號到底是干什麼用的?這就涉及到鎖的釋放了,我們來繼續了解,整體思路和鎖的獲取是一樣的, 但是釋放過程就相對簡單很多了

獨佔式釋放同步狀態

故事要從 unlock() 方法說起

	public void unlock() {
		// 釋放鎖
		sync.release(1);
	}

調用 AQS 模版方法 release,進入該方法

    public final boolean release(int arg) {
      	// 調用自定義同步器重寫的 tryRelease 方法嘗試釋放同步狀態
        if (tryRelease(arg)) {
          	// 釋放成功,獲取頭節點
            Node h = head;
          	// 存在頭節點,並且waitStatus不是初始狀態
          	// 通過獲取的過程我們已經分析了,在獲取的過程中會將 waitStatus的值從初始狀態更新成 SIGNAL 狀態
            if (h != null && h.waitStatus != 0)
              	// 解除線程掛起狀態
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

查看 unparkSuccessor 方法,實際是要喚醒頭節點的後繼節點

    private void unparkSuccessor(Node node) {      
      	// 獲取頭節點的waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
          	// 清空頭節點的waitStatus值,即置為0
            compareAndSetWaitStatus(node, ws, 0);
      
      	// 獲取頭節點的後繼節點
        Node s = node.next;
      	// 判斷當前節點的後繼節點是否是取消狀態,如果是,需要移除,重新連接隊列
        if (s == null || s.waitStatus > 0) {
            s = null;
          	// 從尾節點向前查找,找到隊列第一個waitStatus狀態小於0的節點
            for (Node t = tail; t != null && t != node; t = t.prev)
              	// 如果是獨佔式,這裏小於0,其實就是 SIGNAL
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
          	// 解除線程掛起狀態
            LockSupport.unpark(s.thread);
    }

有同學可能有疑問:

為什麼這個地方是從隊列尾部向前查找不是 CANCELLED 的節點?

原因有兩個:

第一,先回看節點加入隊列的情景:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

節點入隊並不是原子操作,代碼第6、7行

node.prev = pred; 
compareAndSetTail(pred, node) 

這兩個地方可以看作是尾節點入隊的原子操作,如果此時代碼還沒執行到 pred.next = node; 這時又恰巧執行了unparkSuccessor方法,就沒辦法從前往後找了,因為後繼指針還沒有連接起來,所以需要從后往前找

第二點原因,在上面圖解產生 CANCELLED 狀態節點的時候,先斷開的是 Next 指針,Prev指針並未斷開,因此這也是必須要從后往前遍歷才能夠遍歷完全部的Node

同步狀態至此就已經成功釋放了,之前獲取同步狀態被掛起的線程就會被喚醒,繼續從下面代碼第 3 行返回執行:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

繼續返回上層調用棧, 從下面代碼15行開始執行,重新執行循環,再次嘗試獲取同步狀態

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

到這裏,關於獨佔式獲取/釋放鎖的流程已經閉環了,但是關於 AQS 的另外兩個模版方法還沒有介紹

  • 響應中斷
  • 超時限制

獨佔式響應中斷獲取同步狀態

故事要從lock.lockInterruptibly() 方法說起

	public void lockInterruptibly() throws InterruptedException {
		// 調用同步器模版方法可中斷式獲取同步狀態
		sync.acquireInterruptibly(1);
	}

有了前面的理解,理解獨佔式可響應中斷的獲取同步狀態方式,真是一眼就能明白了:

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
      	// 嘗試非阻塞式獲取同步狀態失敗,如果沒有獲取到同步狀態,執行代碼7行
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

繼續查看 doAcquireInterruptibly 方法:

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                  	// 獲取中斷信號后,不再返回 interrupted = true 的值,而是直接拋出 InterruptedException 
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

沒想到 JDK 內部也有如此相近的代碼,可響應中斷獲取鎖沒什麼深奧的,就是被中斷拋出 InterruptedException 異常(代碼第17行),這樣就逐層返回上層調用棧捕獲該異常進行下一步操作了

趁熱打鐵,來看看另外一個模版方法:

獨佔式超時限制獲取同步狀態

這個很好理解,就是給定一個時限,在該時間段內獲取到同步狀態,就返回 true, 否則,返回 false。好比線程給自己定了一個鬧鐘,鬧鈴一響,線程就自己返回了,這就不會使自己是阻塞狀態了

既然涉及到超時限制,其核心邏輯肯定是計算時間間隔,因為在超時時間內,肯定是多次嘗試獲取鎖的,每次獲取鎖肯定有時間消耗,所以計算時間間隔的邏輯就像我們在程序打印程序耗時 log 那麼簡單

nanosTimeout = deadline – System.nanoTime()

故事要從 lock.tryLock(time, unit) 方法說起

	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// 調用同步器模版方法,可響應中斷和超時時間限制
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

來看 tryAcquireNanos 方法

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

是不是和上面 acquireInterruptibly 方法長相很詳細了,繼續查看來 doAcquireNanos 方法,看程序, 該方法也是 throws InterruptedException,我們在中斷文章中說過,方法標記上有 throws InterruptedException 說明該方法也是可以響應中斷的,所以你可以理解超時限制是 acquireInterruptibly 方法的加強版,具有超時和非阻塞控制的雙保險

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
      	// 超時時間內,為獲取到同步狀態,直接返回false
        if (nanosTimeout <= 0L)
            return false;
      	// 計算超時截止時間
        final long deadline = System.nanoTime() + nanosTimeout;
      	// 以獨佔方式加入到同步隊列中
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
              	// 計算新的超時時間
                nanosTimeout = deadline - System.nanoTime();
              	// 如果超時,直接返回 false
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                		// 判斷是最新超時時間是否大於閾值 1000    
                    nanosTimeout > spinForTimeoutThreshold)
                  	// 掛起線程 nanosTimeout 長時間,時間到,自動返回
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面的方法應該不是很難懂,但是又同學可能在第 27 行上有所困惑

為什麼 nanosTimeout 和 自旋超時閾值1000進行比較?

    /**
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices
     * to improve responsiveness with very short timeouts.
     */
    static final long spinForTimeoutThreshold = 1000L;

其實 doc 說的很清楚,說白了,1000 nanoseconds 時間已經非常非常短暫了,沒必要再執行掛起和喚醒操作了,不如直接當前線程直接進入下一次循環

到這裏,我們自定義的 MyMutex 只差 Condition 沒有說明了,不知道你累了嗎?我還在堅持

Condition

如果你看過之前寫的 併發編程之等待通知機制 ,你應該對下面這個圖是有印象的:

如果當時你理解了這個模型,再看 Condition 的實現,根本就不是問題了,首先 Condition 還是一個接口,肯定也是需要有實現類的

那故事就從 lock.newnewCondition 說起吧

	public Condition newCondition() {
		// 使用自定義的條件
		return sync.newCondition();
	}

自定義同步器重封裝了該方法:

		Condition newCondition() {
			return new ConditionObject();
		}

ConditionObject 就是 Condition 的實現類,該類就定義在了 AQS 中,只有兩個成員變量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

所以,我們只需要來看一下 ConditionObject 實現的 await / signal 方法來使用這兩個成員變量就可以了

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
          	// 同樣構建 Node 節點,並加入到等待隊列中
            Node node = addConditionWaiter();
          	// 釋放同步狀態
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
              	// 掛起當前線程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

這裏注意用詞,在介紹獲取同步狀態時,addWaiter 是加入到【同步隊列】,就是上圖說的入口等待隊列,這裏說的是【等待隊列】,所以 addConditionWaiter 肯定是構建了一個自己的隊列:

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
          	// 新構建的節點的 waitStatus 是 CONDITION,注意不是 0 或 SIGNAL 了
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
          	// 構建單向同步隊列
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

這裡有朋友可能會有疑問:

為什麼這裡是單向隊列,也沒有使用CAS 來保證加入隊列的安全性呢?

因為 await 是 Lock 範式 try 中使用的,說明已經獲取到鎖了,所以就沒必要使用 CAS 了,至於是單向,因為這裏還不涉及到競爭鎖,只是做一個條件等待隊列

在 Lock 中可以定義多個條件,每個條件都會對應一個 條件等待隊列,所以將上圖豐富說明一下就變成了這個樣子:

線程已經按相應的條件加入到了條件等待隊列中,那如何再嘗試獲取鎖呢?signal / signalAll 方法就已經排上用場了

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

Signal 方法通過調用 doSignal 方法,只喚醒條件等待隊列中的第一個節點

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
              	// 調用該方法,將條件等待隊列的線程節點移動到同步隊列中
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

繼續看 transferForSignal 方法

    final boolean transferForSignal(Node node) {       
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

       	// 重新進行入隊操作
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
          	// 喚醒同步隊列中該線程
            LockSupport.unpark(node.thread);
        return true;
    }

所以我們再用圖解一下喚醒的整個過程

到這裏,理解 signalAll 就非常簡單了,只不過循環判斷是否還有 nextWaiter,如果有就像 signal 操作一樣,將其從條件等待隊列中移到同步隊列中

        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

不知你還是否記得,我在併發編程之等待通知機制 中還說過一句話

沒有特殊原因盡量用 signalAll 方法

什麼時候可以用 signal 方法也在其中做了說明,請大家自行查看吧

這裏我還要多說一個細節,從條件等待隊列移到同步隊列是有時間差的,所以使用 await() 方法也是範式的, 同樣在該文章中做了解釋

有時間差,就會有公平和不公平的問題,想要全面了解這個問題,我們就要走近 ReentrantLock 中來看了,除了了解公平/不公平問題,查看 ReentrantLock 的應用還是要反過來驗證它使用的AQS的,我們繼續吧

ReentrantLock 是如何應用的AQS

獨佔式的典型應用就是 ReentrantLock 了,我們來看看它是如何重寫這個方法的

乍一看挺奇怪的,怎麼裏面自定義了三個同步器:其實 NonfairSync,FairSync 只是對 Sync 做了進一步劃分:

從名稱上你應該也知道了,這就是你聽到過的 公平鎖/非公平鎖了

何為公平鎖/非公平鎖?

生活中,排隊講求先來後到視為公平。程序中的公平性也是符合請求鎖的絕對時間的,其實就是 FIFO,否則視為不公平

我們來對比一下 ReentrantLock 是如何實現公平鎖和非公平鎖的

其實沒什麼大不了,公平鎖就是判斷同步隊列是否還有先驅節點的存在,只有沒有先驅節點才能獲取鎖;而非公平鎖是不管這個事的,能獲取到同步狀態就可以,就這麼簡單,那問題來了:

為什麼會有公平鎖/非公平鎖的設計?

考慮這個問題,我們需重新回憶上面的鎖獲取實現圖了,其實上面我已經透露了一點

主要有兩點原因:

原因一:

恢復掛起的線程到真正鎖的獲取還是有時間差的,從人類的角度來看這個時間微乎其微,但是從CPU的角度來看,這個時間差存在的還是很明顯的。所以非公平鎖能更充分的利用 CPU 的時間片,盡量減少 CPU 空閑狀態時間

原因二:

不知你是否還記得我在 面試問,創建多少個線程合適? 文章中反覆提到過,使用多線程很重要的考量點是線程切換的開銷,想象一下,如果採用非公平鎖,當一個線程請求鎖獲取同步狀態,然後釋放同步狀態,因為不需要考慮是否還有前驅節點,所以剛釋放鎖的線程在此刻再次獲取同步狀態的幾率就變得非常大,所以就減少了線程的開銷

相信到這裏,你也就明白了,為什麼 ReentrantLock 默認構造器用的是非公平鎖同步器

    public ReentrantLock() {
        sync = new NonfairSync();
    }

看到這裏,感覺非公平鎖 perfect,非也,有得必有失

使用公平鎖會有什麼問題?

公平鎖保證了排隊的公平性,非公平鎖霸氣的忽視這個規則,所以就有可能導致排隊的長時間在排隊,也沒有機會獲取到鎖,這就是傳說中的 “飢餓”

如何選擇公平鎖/非公平鎖?

相信到這裏,答案已經在你心中了,如果為了更高的吞吐量,很顯然非公平鎖是比較合適的,因為節省很多線程切換時間,吞吐量自然就上去了,否則那就用公平鎖還大家一個公平

我們還差最後一個環節,真的要挺住

可重入鎖

到這裏,我們還沒分析 ReentrantLock 的名字,JDK 起名這麼有講究,肯定有其含義,直譯過來【可重入鎖】

為什麼要支持鎖的重入?

試想,如果是一個有 synchronized 修飾的遞歸調用方法,程序第二次進入被自己阻塞了豈不是很大的笑話,所以 synchronized 是支持鎖的重入的

Lock 是新輪子,自然也要支持這個功能,其實現也很簡單,請查看公平鎖和非公平鎖對比圖,其中有一段代碼:

// 判斷當前線程是否和已佔用鎖的線程是同一個
else if (current == getExclusiveOwnerThread())

仔細看代碼, 你也許發現,我前面的一個說明是錯誤的,我要重新解釋一下

重入的線程會一直將 state + 1, 釋放鎖會 state – 1直至等於0,上面這樣寫也是想幫助大家快速的區分

總結

本文是一個長文,說明了為什麼要造 Lock 新輪子,如何標準的使用 Lock,AQS 是什麼,是如何實現鎖的,結合 ReentrantLock 反推 AQS 中的一些應用以及其獨有的一些特性

獨佔式獲取鎖就這樣介紹完了,我們還差 AQS 共享式 xxxShared 沒有分析,結合共享式,接下來我們來閱讀一下 Semaphore,ReentrantReadWriteLock 和 CountLatch 等

最後,也歡迎大家的留言,如有錯誤之處還請指出。我的手酸了,眼睛幹了,我去準備擼下一篇…..

靈魂追問

  1. 為什麼更改 state 有 setState() , compareAndSetState() 兩種方式,感覺後者更安全,但是鎖的視線中有好多地方都使用了 setState(),安全嗎?

  2. 下面代碼是一個轉賬程序,是否存在死鎖或者鎖的其他問題呢?

    
    class Account {
      private int balance;
      private final Lock lock
              = new ReentrantLock();
      // 轉賬
      void transfer(Account tar, int amt){
        while (true) {
          if(this.lock.tryLock()) {
            try {
              if (tar.lock.tryLock()) {
                try {
                  this.balance -= amt;
                  tar.balance += amt;
                } finally {
                  tar.lock.unlock();
                }
              }//if
            } finally {
              this.lock.unlock();
            }
          }//if
        }//while
      }//transfer
    }
    

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

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

※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

國外零售商洩漏 Intel 第 11 代 Rocket Lake-S 處理器的售價,i9-11900K 價格比上一代便宜一些_網頁設計公司

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

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

先前 CES 2021 發表會上,Intel 正式透露第 11 代 Rocket Lake-S 處理器(i9-11900K)官方效能跑分之後,相信不少玩家都非常期待,不僅比上一代還強,也勝過對手的 AMD 12 核心 Ryzen 5900X,不過當時並沒有公布售價,也因此想買的人究竟該準備多少,只能參考第 10 代的價格。最近國外就有一間零售商,疑似搶先放上第 11 代 Rocket Lake-S 處理器的價格列表,從 i9、i7、一直到 i5 都有,有些比較貴,有些便宜。

國外零售商洩漏 Intel 第 11 代 Rocket Lake-S 處理器的售價

近日比利時一間 2Compute 電腦硬體零售商在它的官網上,洩漏第 11 代 Rocket Lake-S 處理器的價格列表,而且現在還沒刪除,感覺就像是故意放上去一樣。

根據列表顯示,新一代遊戲旗艦處理器 8 核心 16 執行緒的 i9-11900K 單顆未稅價為 499.70 歐元,含稅之後變成 605 歐元,代表說比上一代 i9-10900K 還便宜 9.8% 左右。

下圖是 i9-11900K 的價格:

i9-10900K 的價格,單顆未稅價 549 歐元,含稅之後 665 歐元:

下方為第 11 代 Rocket Lake-S 處理器與第 10 代 Comet Lake-S 處理器的比較圖,i9-11900KF 也比上一代便宜,但 i9-11900 與 i9-11900F 就沒有,其餘 i7、i5 大多數也比上一代貴:

2Compute 網站洩漏的 Rocket Lake-S 處理器價格清單(含稅價):

  • i5-11400:205 歐元
  • i5-11400F:175 歐元
  • i5-11500:227 歐元
  • i5-11600:250 歐元
  • i5-11600K:294 歐元
  • i5-11600KF:265 歐元
  • i7-11700:371 歐元
  • i7-11700F:342 歐元
  • i7-11700K:456 歐元
  • i7-11700KF:427 歐元
  • i9-11900:494 歐元
  • i9-11900F:465 歐元
  • i9-11900K:605 歐元
  • i9-11900KF:576 歐元

如果比較整體價格,第 11 代 Rocket Lake-S 處理器平均大約比第 10 代 Comet Lake-S 處理器貴 14% 左右。當然,這是零售商搶先洩漏的清單,也因此一切還是要以 Intel 官方為主。

Intel 第 11 代 Rocket Lake-S 處理器預計會在 3 月正式推出,下個月應該會有更確切的消息出現。

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

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

有趣的是,除了 2Compute,國外其實還有其他零售商也洩漏這價格清單,不過大家都不太一樣,讓人看起來有點亂:

Intel 11th Gen Price

10th Gen sheet is for comparison pic.twitter.com/kAvMbIQjKm

— 포시포시 (@harukaze5719) January 17, 2021

資料來源:Video Cardz

Intel 第 11 代「Rocket Lake」與第 12 代「Alder Lake」搶先看

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

2021 小米年貨節將於 1/19~1/25 開跑!活動優惠整理_如何寫文案

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

近期小米台灣除了在台灣推出「小米智慧攝影機雲台版2K」和「小米 WiFi 訊號延伸器 Pro」,也預告將在 1 月 19 日上午 10 點開賣「小米手錶超值版」與「米家螢幕掛燈」,想必大家也有預感又有新活動要來臨吧?今日稍早小米正式宣佈「2021 小米年貨節」將在 1 月 19 日至 1 月 25 日正式開跑囉!

2021 小米年貨節將於 1/19~1/25 開跑!活動優惠整理

去年有哪些小米產品要買卻錯過優惠嗎?今日稍早小米台灣正式宣佈 2021 小米年貨節將在明(19)日起至 1 月 25 日推出囉!這次除了幾款小米新品搭上活動開賣外,消費者可在每日完成指定任務獲得現金券、透過積分獲得額外的現金券,也有像是滿額贈品、滿額折扣以及最直接的直降商品優惠!

2021 小米年貨節(1/19~1/25)
活動頁面:點我前往|活動規則:點我前往

簽到領積分

於 1 月 19 日 10:00 至 1 月 25 日期間簽到領取限量發放簽到積分,連續簽到 7 天另可獲得「NT$100 無門檻現金券」,簽到禮睬每日限量發放、先完成先取得。

小米年貨節優惠券

活動期間每日 10:00 限量發放 NT$100~NT$1500 現金券(手機及新品不適用),每筆小米帳號單日各現金券限領取 1 張,數量有限、領完為止。

頁面領券現金券(手機及新品不適用):

  • NT$100 現金券(滿 NT$1,000 使用)
  • NT$200 現金券(滿 NT$2,000 使用)
  • NT$550 現金券(滿 NT$5,000 使用)
  • NT$850 現金券(滿 NT$7,000 使用)
  • NT$1,500 現金券(滿 NT$10,000 使用)

限量秒殺券&指定商品秒殺券

限量秒殺券於活動期間每日指定時段(10:00、12:00、14:00、18:00、20:00)限量發放,指定商品現金券則於指定日期 10:00 限量發放。限量秒殺券與指定商品秒殺券每個小米帳號活動期間限領取 1 張(數量有限,領完為止)。

限量秒殺券(手機及新品不適用現金券):

  • NT$500現金券(滿 NT$1,000 使用)
  • NT$1,000 現金券(滿 NT$2,021 使用)

指定商品秒殺券:

  • 1月19日:NT$400現金券(僅限購買米家掃拖機器人 G1 使用)
  • 1月20日:NT$400現金券(僅限購買米家空氣淨化器 Pro 使用)
  • 1月21日:NT$800現金券(僅限購買小米手環 5 滿2件及以上使用)
  • 1月22日:NT$200現金券(僅限購買米家無線吸塵器 G10 使用)
  • 1月23日:NT$400現金券(僅限購買小衛質品泡沫洗手液 六瓶裝 滿2件及以上使用)
  • 1月24日:NT$200現金券(僅限購買小米小愛音箱 Art 使用)
  • 1月25日:NT$200現金券(僅限購買小米真無線藍牙耳機 2S 使用)

積分兌換禮

小米年貨節活動期間,用戶可透過分享、下載App、註冊積分會員等方式賺取積分,並能積分兌換現金券使用,積分兌換禮採每日 10:00 限量發放。

超值滿額禮

活動期間單筆訂單滿指定金額,即可獲得相關贈品或現金券:

滿額贈內容:

活動1|消費單筆訂單(不含手機)滿 NT$3,000:
贈小米手環 4C 或 米家 wiha 8合1 棘輪螺絲起子(單筆訂單只贈送一個)

活動2|消費單筆訂單(不含手機)滿 NT$6,000:
贈小米小愛音箱 Play 或 10000 小米行動電源 3 無線版 超值版 白色(單筆訂單只贈送一個)

活動3:消費單筆訂單(不含手機)滿 NT$9,000:
贈小米手環 5 或 米家電動刮鬍刀 S500(單筆訂單只贈送一個)

活動4:消費單筆訂單(不含手機)滿 NT$14,000:
贈小米小愛音箱 Art,單筆訂單只贈送一個
訂單金額滿 NT$14,000 時,活動 1 、2、3、4 需四選一(數量有限,贈完為止)

消費單筆訂單(不含手機)滿 NT$25,000:
贈 NT$3,000 現金券包(包含:NT$800 現金券 2 張、NT$500 現金券 2 張、NT$200 現金券 2 張)

2021 小米年貨節(1/19~1/25)
活動頁面:點我前往|活動規則:點我前往

超值組合

於小米年貨節活動期間購買以下商品套餐組合,即可享套餐優惠價。

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

超值組合內容:

  1. 米家無線吸塵器G10/G9 HEPA 濾芯套裝+米家無線吸塵器mini HEPA濾芯(兩個裝)任意兩件,組合價:NT$360
  2. 米家聲波電動牙刷 T500+米家聲波電動牙刷頭 通用型 / 敏感型(刷頭二選一),組合價:NT$1100
  3. 小米便攜相片印表機+小米便攜印表機即貼相紙,組合價:NT$1,790
  4. 小米34型曲面螢幕+小米木紋滑鼠墊+小米無線雙模滑鼠 靜音版,組合價:NT$13,360
  5. 米家 LED 智慧燈泡 Lite 彩光版 白色 3支裝,組合價:NT$945
  6. 米家電動刮鬍刀 S500+米家電動刮鬍刀S500刀頭,組合價:NT$995
  7. 米家便攜電動刮鬍刀 1S(往復雙刀頭)+刀頭,組合價:NT$1,100
  8. 米兔兒童聲波電動牙刷+刷頭,組合價:NT$1,100
  9. 小米液晶手寫板 兩個裝,組合價:NT$840
  10. 小米濾水壺濾芯三支裝 兩個裝,組合價:NT$690
  11. 小衛質品泡沫洗手液(九瓶裝),組合價:NT$385
  12. LINE FRIENDS 20吋旅行箱 莎莉限定版+小米迷你雙肩包,組合價:NT$1,995
  13. 米家吸頂燈 450+Yeelight 遙控器,組合價:NT$1,790
  14. 米家掃拖機器人類配件,買2件減NT$80,買3件減 NT$150 配件包含:
  • 米家掃拖機器人 1C 塵盒濾網
  • 米家掃拖機器人1C 邊刷
  • 米家掃拖機器人 1C 主刷罩
  • 米家掃拖機器人 1C 主刷
  • 米家掃拖機器人邊刷
  • 米家掃拖機器人拖布
  • 米家掃拖機器人 1C 防潮墊
  • 米家掃地機器人虛擬牆
  • 米家掃拖機器人主刷罩
  • 米家掃拖機器人G1 主刷罩
  • 米家掃拖機器人G1 主刷
  • 米家掃拖機器人G1 一次性拖布
  • 米家掃拖機器人G1 集塵盒濾網
  • 米家掃拖機器人G1 拖布
  • 米家掃拖機器人G1 邊刷

超值加價購

於小米年貨節活動期間購買指定商品,即可以優惠價加價購指定商品。

超值加價購內容:

  • 買米家無線吸塵器 G10 加 NT$140 購米家無線吸塵器G10/G9 HEPA 濾芯套裝一個
  • 買米家無線吸塵器 G10 加 NT$1,495 購米家無線吸塵器G10/G9 增程電池一個
  • 買小米手環 5 加 NT$10 購腕帶一條 或 充電線 一個
    (同一筆訂單購買兩個手環可以加購兩個相同顏色的腕帶,如需不同顏色,可分開下單)
  • 買小米GaN充電器 Type-C 65W 加 NT$10 購線材一條:
    小米二合一傳輸線 100cm / 小米 USB-C 傳輸線 編織線版 100cm 紅色 / 小米 Micro USB 傳輸線 編織線版 100cm 紅色 / 小米 Micro USB 傳輸線 編織線版 100cm 黑色 / 小米二合一傳輸線 30cm / 小米 USB-C 傳輸線 編織線版 100cm 黑色 / 小米 USB type-c to type-c 傳輸線 150cm
  • 買小米GaN充電器 Type-C 65W 加 NT$95 購線材一條:
    小米Type-C轉Lightning傳輸線(1m)
  • 買米家掃拖機器人 G1 加 NT$95 購配件一個(可累計加購,限加購不同配件)配件包含:
    主刷罩/主刷/一次性拖布/集塵盒濾網/拖布/邊刷
  • 買米家掃拖機器人 1C 加 NT$95 購配件一個(可累計加購,限加購不同配件)配件包含:
    塵盒濾網/邊刷/主刷罩/主刷/防潮墊/拖布
  • 買米家無線吸塵器 mini 加 NT$95 購 HEPA 濾芯(兩個裝)一個
  • 買米家自動感應洗手機 單機版 加 NT$255 購洗手液(六瓶裝)一個

2021 小米年貨節(1/19~1/25)
活動頁面:點我前往|活動規則:點我前往

精選年貨,商品直降活動

小米年貨節活動期間,凡參與優惠活動的商品在活動頁面上會顯示「省xxx」標籤,消費者可在小米年貨節活動期間點擊「立即購買」標籤查看參與活動的相關商品型號,參與直降活動的商品內容與幅度依照台灣小米公佈為準。

小米年貨節直降商品清單(完整商品內容依照小米官方公佈為準):

和過去的活動一樣,小米活動頁面的商品並未按照商品類型排列,為方便各位更容易閱讀,筆者也將小米年貨節活動頁面的商品清單重新整理了優惠商品清單如下:

  • 米家無線吸塵器 G10:NT$6,995(省 NT$500)
  • 米家手持無線吸塵器 Lite:NT$4,495(省 NT$500)
  • 米家掃拖機器人 白色: NT$7,995(省 NT$1,000)
  • 米家掃地機器人 白色:NT$7,295(省 NT$600)
  • 米家掃拖機器人 1C:NT$6,095(省 NT$500)
  • 米家掃拖機器人 G1:NT$4,395(省 NT$600)
  • 米家空氣淨化器 Pro:NT$5,495(省 NT$1,400,每天 10:00 限量開賣、預計 1 月下旬發貨)
  • 90分金屬旅行箱 銀色:NT$4,495(省 NT$500)
  • 小米旅行箱 20吋 灰色:NT$1,495(省 NT$100)
  • 米家電磁爐:NT$1,595(省 NT$200)
  • 米家 IH 電子鍋 白色:NT$2,395(省 NT$100)
  • 米家恆溫電水壺 白色:NT$1,095(省 NT$100)
  • 米家直流變頻電風扇:NT$1,595(省 NT$200)
  • 米家吸頂燈 450:NT$1,695(省 NT$100)
  • 米家床頭燈:NT$895(省 NT$400)
  • 米家便攜相片印表機:NT$1,495(省 NT$170)
  • 小米真無線藍牙耳機 2S:NT$1,795(省 NT$200)
  • 小米藍牙耳機 Earbuds 超值版:NT$505(省 NT$40)
  • 小米小愛音箱 Art:NT$1,295(省 NT$200)
  • 小米小愛音箱 Play:NT$535(省 NT$60)
  • 小米手環 5:NT$845(省 NT$100)
  • 小米手環 4C:NT$445(省 NT$50)
  • 米家電動刮鬍刀 S500 黑色:NT$895(省 NT$100)
  • 米家聲波電動牙刷 T500:NT$895(省 NT$100)
  • 20000 小米行動電源 3 高配版:NT$945(省 NT$50)
  • 10000 小米行動電源 3 無線版 超值版 白色:NT$545(省 NT$50)
  • 10000 小米行動電源 QC3.0 高配版 灰色:NT$645(省 NT$150)
  • 米家電動打氣機:NT$945(省 NT$50)
  • 小米無線車充:NT$845(省 NT$100)
  • 小米車用充電器 快充版 37W:NT$245(省 NT$20)
  • 米家電動起子機:NT$795(省NT$100)
  • 米家 wiha 8合1 棘輪螺絲起子 黑色:NT$495(省 NT$50)
  • 小米 USB 3.0 HUB:NT$185(省 NT$40)

2021 小米年貨節(1/19~1/25)
活動頁面:點我前往|活動規則:點我前往

滿 NT$1,000 減 NT$100

除了上述「直降」優惠,部分指定商品另享美滿 NT$1,000 減 NT$100  優惠,參與直降活動商品與優惠幅度一樣依照小米官方公佈為準。

滿 NT$1,000 減 NT$100 商品清單:

部分商品消費滿 NT$1,000 可減價 NT$100 ,筆者也將活動頁面商品重新整理方便各位閱讀:

  • 90分金屬旅行箱 銀色
  • 小米旅行箱 20 吋 灰色
  • 小米極簡都市雙肩包 2
  • 小米經典商務雙肩包 2 深灰
  • 小米經典商務雙肩包 黃色
  • 米家水離子吹風機 白色
  • 米家自動感應洗手機 單機版
  • 米家兒童聲波電動牙刷
  • 米家兒童聲波電動牙刷頭 3 支裝
  • 米家聲波電動牙刷頭 敏感型 3 支裝
  • 米家聲波電動牙刷頭 通用型 3 支裝
  • 貝醫生巴氏牙刷 四色裝
  • 米家便攜電動刮鬍刀 刀頭
  • 米家電動刮鬍刀 S500 刀頭 黑色
  • 米家電動刮鬍刀 S500 黑色
  • 米家便攜電動刮鬍刀 1S(往復雙刀頭)
  • 米家掃地機器人塵盒濾網
  • 米家掃地機器人主刷罩
  • 米家掃地機器人主刷 橘色
  • 米家掃地機器人邊刷
  • 米家掃拖機器人 1C 拖布 灰色
  • 米家掃拖機器人 G1 集塵盒濾網
  • 米家掃拖機器人 G1 拖布
  • 米家掃拖機器人 G1 主刷罩
  • 米家掃拖機器人 G1 主刷
  • 米家掃拖機器人 G1 邊刷
  • 米家掃拖機器人G1一次性拖布
  • 米家掃拖機器人塵盒濾網 白色
  • 米家掃拖機器人主刷 橘色
  • 米家掃拖機器人水箱 黑色
  • 米家掃拖機器人一次性拖布 白色
  • 米家無線吸塵器G10/G9 HEPA 濾芯套裝
  • 米家無線吸塵器mini HEPA濾芯(兩個裝)
  • 米家空氣淨化器濾芯 除甲醛增強版 1S
  • 米家空氣淨化器濾芯 抗菌版
  • 知吾煮湯鍋 米家定製
  • 小米濾水壺 白色
  • 小米濾水壺濾芯三支裝 白色
  • 米家恆溫電水壺 1S
  • 米家調理機
  • 米家電動起子機
  • 小米液晶手寫板
  • 米家自動傘
  • 小米體重計 2
  • 米家 wiha 8合1 棘輪螺絲起子 黑色
  • 米家精修螺絲起子套裝組
  • 小米雙單元半入耳式耳機 黑色
  • 小米圈鐵耳機 Pro 銀色
  • 小米戶外藍牙喇叭
  • 小米方盒子藍牙喇叭 2
  • 小米小愛音箱 Play
  • 小米戶外藍牙喇叭 mini
  • 米家智慧攝影機
  • 米家床頭燈 2 白色
  • 米家床頭燈 金色
  • 米家感應夜燈 2 白色
  • 米家感應夜燈 2 藍牙版
  • 米家 LED 充電式檯燈 白色
  • 米家 LED 智慧燈泡 白光版
  • 米家 LED 智慧燈泡 Lite 彩光版 白色
  • 米家人體感應器 白色
  • 米多功能網關 2
  • 米家智慧插座 ZigBee
  • 米家門窗感應器
  • 小米光照度感應器
  • 米家無線開關
  • Yeelight 遙控器
  • 米家藍牙溫濕度計 2
  • 小米 50W 雙模行動電源 1A1C
  • 小米GaN充電器 Type-C 65W
  • 小米 USB 充電器 36W 快充版
  • 5000 小米行動電源 2 銀色
  • Redmi行動電源 10000 標準版 黑色
  • 10000 小米行動電源 3 快充版 銀色
  • 10000 小米行動電源 QC3.0 高配版 灰色
  • 10000 小米行動電源 3 無線版 超值版 白色
  • 20000 小米行動電源 3 快充版
  • 小米 Micro USB 傳輸線 編織線版 100cm 紅色
  • 小米Type-C轉Lighthing傳輸線(1m)
  • 小米 USB type-C to type-C 傳輸線 150cm
  • 小米 USB-C 傳輸線 編織線版 100cm 紅色
  • 小米 USB-C 傳輸線 編織線版 100cm 黑色
  • 小米二合一傳輸線 100cm
  • 小米二合一傳輸線 30cm
  • 小米 USB3.0 HUB
  • 小米車用充電器 2 快充版 18W
  • 小米車用充電器 快充版 37W
  • 小米延長線 黑色
  • 小米延長線 白色
  • 米家魔方延長線
  • 小米藍牙自拍桿 黑色
  • 小米支架式自拍桿 灰色
  • 小米無線雙模滑鼠 靜音版 黑色
  • 小米木紋滑鼠墊
  • 小米便攜印表機即貼相紙
  • 小米隨身風扇 白色
  • 小米手環 5
  • 小米手環 5 專用充電線
  • 小米手環 5 腕帶 深青色
  • 小米手環 4C
  • 小米手環 4 專用充電線 黑色
  • 小米手環 4C 腕帶 深空藍
  • 小米手環 4C 腕帶 橄欖綠

超夯新品

小米年貨節也推出「小米手錶超值版」、「米家螢幕掛燈」、「米家無線吸塵器 G10/G9 增程電池」以及限量開賣「小米智慧顯示器 65 型」。

超夯新品內容(新品不適用現金券):

  • 小米手錶 超值版:NT$1,495
  • 米家螢幕掛燈:N%T$995
  • 小米智慧顯示器 65 型:NT$18,999
  • 小米 WiFi 訊號延伸器 Pro:NT$365
  • 小米智慧攝影機 雲台版 2K:NT$1,095
  • 米家無線吸塵器 G10/G9 增程電池:NT$1,995

2021 小米年貨節(1/19~1/25)
活動頁面:點我前往|活動規則:點我前往

圖片/消息來源:小米台灣

延伸閱讀:
小米智慧顯示器65型開箱:搭載 4K HDR 10-bit超高畫質面板、DTS-HD 杜比音效無敵性價比的聯網電視

小米摺疊螢幕手機工程機曝光!再次於地鐵車廂被捕獲,只是螢幕摺痕有點顯眼

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

次世代 ROG Phone 騰訊搶先預告登場!傳升級 65W 快充應援你的電競行動生活_網頁設計公司

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

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

2021 年新世代電競手機戰爭最早將在 3 月開打?根據華碩 ROG 在微博最近釋出的預告,可以看到採用新設計「即將升級」的 ROG Phone(可能是 ROG Phone 4 但也有消息指出會跳版號到 ROG Phone 5)即將來襲。繼續閱讀次世代 ROG Phone 騰訊搶先預告登場!傳將升級65W 快充應援你的電競行動生活報導內文。

次世代 ROG Phone 騰訊搶先預告登場!傳升級 65W 快充應援你的電競行動生活

身為電競手機概念的先驅者,ROG 絕對可以被算上一角。現在,他們的旗艦電競手機 ROG Phone 的戰力升級版,也確認將在與騰訊繼續合作之下推出 — 儘管,由內部型號 I003D 跳至 I005DA 的新款 ROG 電競手機,是否會跳號成為 ROG Phone 5 仍是個謎(還是會一次推兩款?)。
根據官方在社群平台的預告圖片,有著明顯可見的銀河星座背景,被許多人推測可能最快將會在 3 月推出。而從手機螢幕刻意在底端「留黑」的圖片也可以斷言,新世代 ROG 電競手機可能將不再延續上下螢幕邊框比例對稱的設計語言,在頂端與左右三邊都將迎接盡可能薄到視覺難以察覺的超薄邊框。

這樣一張簡單的預告圖片,似乎可預期 ROG Phone 將迎接手機內外的全面硬體大改 — 所以老配件可能… 至於前置鏡頭何去何從,這好像對華碩來講也不是問題?(ZenFone 7 Pro 表示:)但就目前流傳的實機照看來,背面很可能與 ROG Phone 3 相比沒有太多的變動,但疑似多了個疑似為遊戲專用的按鈕;側面的充電孔應該依然有保留 。至於「05」的編號,看來也就是會直接跳號了?

▲圖片來源:WHYLAB(微博)

是說,先前已有強大跑分數字流出,以電競遊戲高效能而生做為最主要特色的 ROG Phone,一直以來都提供了許多超前業界的規格。但顯然對使用者而言,仍有著更完美規格方面的期待 — 像是快充規格方面,根據最近的傳言則是指出將從先前的 30W 一舉提升至 65W;電量部分更可能直衝 6,000mAh,應該會是更符合使用者需求的提升。

看來在更多配件曝光之前,已經可以預期 ROG Phone 4 / ROG Phone 5 將「升級」成更完美電競手機了?

▲圖片來源:PhoneArena

本篇圖片 / 引用來源

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

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

延伸閱讀:

中國電動車全面跟上 1,000 公里續航?專家:不可能「全都好」

不只潮更厲害!AirPods Max 開箱使用體驗(同場加映:全色系動眼看!)

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

Safari 導入延伸功能系統,目前開發者反應冷淡_網頁設計

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

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

在 WWDC 2020 時,Apple 宣布將在 Mac 版的Safari 14 中導入在 Chrome、Firefox 等瀏覽器上行之有年且受到用戶大大歡迎的的擴展應用 Web Extensions API,不過至今 6 個月過去了,由於 Apple 本身的遊戲規則獨立於其他平台之外,開發者的反應顯得冷淡,上架的新應用自然也不如預期。

Safari 導入擴展延伸系統,目前開發者反應冷淡

可在 MacOS Big Sur、Catalina 與 Mojave 上運行的 Safari 14,理論上可以為基於 JavaScript 的 Chrome、Firefox、Edge 等瀏覽器開發者提供大致上差不多的程式碼,並且轉換成 Mac 版 Safari 的擴展外掛。不過依照目前的進展來看,在 Mac APP Store 上所提供的 Safari 擴展中基本上都是 Safari 14 發表會之前的項目,僅有極少數新擴展出現。

Andrew Abrahamowicz 在 Library Extension 擴展外掛的開發上投注了 10 年的時間,是一款可以把當地圖書館書況在 Amazon 等網站的圖書賣場中顯示出來的外掛,Abrahamowicz 表示開發這款應用並不是他的本業,因此能夠付出的精力與時間也很有限,為此他入手了新的 M1 Mac 來進行後續的研究開發。支援一個新平台對開發者來說必須負擔很多額外的工作,除了必須利用 Xcode 設定之外,還必須處理 Apple 對延伸功能一些特定的安全限制,所以必須額外撰寫適用的程式碼,以便讓 Safari 版本能夠擁有和其他平台上一樣的功能。

從上面的例子可以看到許多癥結,像是大多數開發者都是業餘進行時間有限、因限定 Apple 硬體設備才能開發而卡住,以及對 Apple 開發工具的不熟悉等,都會成為開發者反應不如預期的原因。其中一個典型的例子就是 Beyond20 這款擴展,該擴展能夠將 D&D Beyond 角色表與 Roll20 等虛擬桌面服務連接起來,如果想要在 Mac 上使用,編輯只能切換到 Chrome 或 Firefox 瀏覽器。該應用項目負責人表示因為不是屬於 Chromium 系統而需要額外工作,加上自己平常並不使用 Safari ,所以不願意在 Safari 上架自家外掛。

目前看來,推動 Web Extensions API 是一項沒有盡頭的任務,在 APP Store 中已經出現極少數的外掛上架, 梭說可能會帶動其他擴展跟著行動,但大多數開發者似乎仍不為所動。國外媒體的意見是,如果最終 Apple 能夠將 Safari 擴展也推動到 iOS 或 iPadOS 上
,或許對於外掛的增長會更有效果。

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

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

◎資料來源:Six colors

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

※網頁設計最專業,超強功能平台可客製化

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

國外網友結合二手健身車與任天堂健身環等設備,組出了一台可以邊運動邊玩「瑪利歐賽車」的鍛鍊神器_貨運

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

瑪利歐賽車是任天堂旗下的人氣遊戲,很多玩家喜歡它豐富的賽道設計,每場比賽中隨機刷出的道具更是給賽事增添不少勝負變數。小到香蕉皮,大到無敵星星,各種道具不但促進了瑪利歐賽車系列的大賣,也不知道撕破了多少人之間的友誼。今天不是要告訴大家瑪利歐賽車的必勝方法。而是介紹一個玩膩《健身環大冒險》,於是用一點小工具跟改裝技巧,土砲出了健身環版瑪利歐賽車專用套件的神人。用這個套件玩《瑪車》未必能讓你百戰百勝,但能讓你邊玩邊消耗熱量,甚至達到健身減重的效果:

▲(圖片翻攝自 Youtube 影片)

瑪利歐賽車系列遊戲打從超任時代就開始風靡許多大小朋友,這系列遊戲有多好玩自然不需要筆者多做介紹。歷經數年演變,畫面也從 2D、3D 到現在高畫質畫面,稱得上是任天堂的一大台柱。很多人購買 Switch 主機以後,往往也會順便來一片《瑪利歐賽車 8 Deluxe》來跟家人朋友同樂,是一款相當適合多人競技的遊戲。

有了健身環,瑪利歐賽車也可以拿來運動

在任天堂推出 Wii 主機以後,體感控制就成為許多新遊戲開創新玩法的靈感來源。從 Wii 推出的時代就有許多運動或競技相關的遊戲採用了體感操控。像是 2006 年推出的 Wii Sport、2007 年的 Wii Fit、2019 對應新主機 Switch 推出的健身環與健身環大冒險遊戲,這些遊戲都顯示運動類遊戲成為任天堂開創新玩法的重要領域。不過,這些遊戲通常都有個缺點,就是綁定的某些套件,例如健身環大冒險之於任天堂健身環。這些套件通常也無法對應到其他遊戲的玩法上。於是一位玩家就跳出來替任天堂健身環進行了一點小小的改造,搭配一台二手健身車,組合成讓人一邊運動一邊玩瑪利歐賽車的新套件「Labo Fit Adventure Kart」:

▲健身環、手把跟改裝過的二手健身車,組成了這個奇妙的健身套件(圖片翻攝自 Youtube 影片)

這套或可取名為「Labo 健身賽車冒險」的套件組合了一個健身環,一個 Switch 手把和一台二手健身車。主要的邏輯是讓健身環、健身車的操控能夠對應到 Switch 手把按鍵上,才能利用這組套件來進行有系。這位名為「Mike Choi(@mechachoi)」的國外網友就利用了自製的裝置「TAPBO」來成為溝通這些裝備的橋樑,最後讓這些裝置能成為玩家的手與腳。一邊進行著遊戲的同時,一邊還可以鍛鍊身體。既能避免沉迷遊戲,還能夠減少身上贅肉,一舉兩得:

▲TAPBO套件主要用處就是接收訊號以控制手把(圖片翻攝自 Youtube 影片)

Mike Choi 網友透過瑪利歐賽車這個遊戲做為示範,來展示自己組裝起來的「Labo Fit Adventure Kart」。簡單的說,使用者必須腳踏健身車來讓遊戲中的卡丁車加速。利用健身環做為虛擬的方向盤轉動卡丁車的前輪。至於瑪利歐賽車中重要的道具發射功能,則是透過擠壓健身環來達成。這些動作說明起來很簡單。但要一邊運動一邊在遊戲中進行排位的攻防,並不是一件簡單的事。若真能靠這個方式成為瑪利歐賽車大師,想必體脂率也大幅降低了不少:

▲作者在自己的推特上宣傳自作套件,吸引不少網友轉推(圖片翻攝自 Twitter 網頁)

▲一邊騎車讓卡丁車加速,一邊控制方向盤(健身環),一邊還得透過擠壓健身環放道具(圖片翻攝自 Youtube 影片)

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

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

Mike Choi 網友也在自己的套件介紹影片中說明了製造流程。就概念來說相信有點電機底子的人都不會覺得太難,只是難在要用這個方式真的在遊戲中跟親友競爭,取勝的機率相對低落的多。Mike Choi 網友甚至還試著玩了一下另一款任天堂的旗下大作「任天堂明星大亂鬥特別版」,從影片中看起來的確不是普通的累。對於有心想健身或減肥的人,藉著遊戲持之以恆的話,說不定真的能有所成效也說不定:

▲連大亂鬥也支援,不過這樣玩直的很累(圖片翻攝自 Youtube 影片)

▲可惜的是他這套純手工健身器材尚未販售,不過這個點子應該會吸引健身器材大廠或是任天堂尋求合作才是(圖片翻攝自 Youtube 影片)

消息來源

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

[C#.NET 拾遺補漏]01:字符串操作_網頁設計公司

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

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

字符串操作在任意編程語言的日常編程中都隨處可見,今天來匯總一下 C# 中關於字符串的一些你可能遺忘或遺漏的知識點。

逐字字符串

在普通字符串中,反斜杠字符是轉義字符。而在逐字字符串(Verbatim Strings)中,字符將被編程器按照原義進行解釋。使用逐字字符串只需在字符串前面加上 @ 符號。

// 逐字字符串:轉義符
var filename = @"c:\temp\newfile.txt";
Console.WriteLine(filenaame);

// 逐字字符串:多行文本
var multiLine = @"This is a
multiline paragraph."
;
Console.WriteLine(multiLine);

// 非逐字字符串
var escapedFilename = "c:\temp\newfile.txt";
Console.WriteLine(escapedFilename);

輸出:

c:\temp\newfile.txt
This is a
multiline paragraph.
c: emp
ewfile.txt

逐字字符串中唯一不被原樣解釋的字符是雙引號。由於雙引號是定義字符串的關鍵字符,所以在逐字字符串中要表達雙引號需要用雙引號進行轉義。

varstr = @"""I don't think so"", he said.";
Console.WriteLine(str);
// 輸出:"I don't think so", he said.

在逐字字符串中也可以 $ 符號實現字符串內插值。

Console.WriteLine($@"Testing \n 1 2 {5 - 2}");
// 輸出:Testing \n 1 2 3

数字格式化轉換

典型的的格式化方法為:

string.Format("{index[:format]}", number)

可使用“0”和“#”佔位符進行補位。“0” 表示位數不夠位數就補充“0”,小數部分如果位數多了則會四舍五入;“#”表示佔位,用於輔助“0”進行補位。

標準格式化用法:

// “0”描述:佔位符,如果可能,填充位
string.Format("{0:000000}",1234); // 結果:001234

// “#”描述:佔位符,如果可能,填充位
string.Format("{0:######}",1234); // 結果:1234
string.Format("{0:#0####}",1234); // 結果:01234
string.Format("{0:0#0####}",1234); // 結果:0001234

// "."描述:小數點
string.Format("{0:000.000}", 1234); // 結果:1234.000
string.Format("{0:000.000}", 4321.12543); // 結果:4321.125

// ","描述:千分表示
string.Format("{0:0,0}", 1234567); //結果:1,234,567

// "%"描述:格式化為百分數
string.Format("{0:0%}",1234); // 結果:123400%
string.Format("{0:#%}", 1234.125); // 結果:123413%
string.Format("{0:0.00%}",1234); // 結果:123400.00%
string.Format("{0:#.00%}",1234.125); // 結果:123412.50%

內置快捷字母格式化用法:

// E-科學計數法表示
(25000).ToString("E"); // 結果:2.500000E+004

// C-貨幣表示,帶有逗號分隔符,默認小數點后保留兩位,四舍五入
(2.5).ToString("C"); // 結果:¥2.50

// D[length]-十進制數
(25).ToString("D5"); // 結果:00025

// F[precision]-浮點數,保留小數位數(四舍五入)
(25).ToString("F2"); // 結果:25.00

// G[digits]-常規,保留指定位數的有效数字,四舍五入
(2.52).ToString("G2"); // 結果:2.5

// N-帶有逗號分隔符,默認小數點后保留兩位,四舍五入
(2500000).ToString("N"); // 結果:2,500,000.00

// X-十六進制,非整型將產生格式異常
(255).ToString("X"); // 結果:FF

ToString 也可以自定義補零格式化:

(15).ToString("000");              // 結果:015
(15).ToString("value is 0"); // 結果:value is 15
(10.456).ToString("0.00"); // 結果:10.46
(10.456).ToString("00"); // 結果:10
(10.456).ToString("value is 0.0"); // 結果:value is 10.5

轉換為二進制、八進制、十六進制輸出:

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

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

int number = 15;
Convert.ToString(number, 2); // 結果:1111
Convert.ToString(number, 8); // 結果:17
Convert.ToString(number, 16); // 結果:f

自定義格式化器:

public class CustomFormat : IFormatProvider, ICustomFormatter
{
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (!this.Equals(formatProvider))
{
return null;
}
if (format == "Reverse")
{
return string.Join("", arg.ToString().Reverse());
}
return arg.ToString();
}

public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
}

使用自定義格式化器:

String.Format(newCustomFormat(), "-> {0:Reverse} <-", "Hello World");
// 輸出:-> dlroW olleH <-

字符串拼接

將數組中的字符串拼接成一個字符串:

var parts = new[] { "Foo", "Bar", "Fizz", "Buzz"};
var joined = string.Join(", ", parts);
// joined = "Foo, Bar, Fizz, Buzz"

以下四種方式都可以達到相同的字符串拼接的目的:

string first = "Hello";
string second = "World";
string foo = first + " " + second;
string foo = string.Concat(first, " ", second);
string foo = string.Format("{0} {1}", firstname, lastname);
string foo = $"{firstname} {lastname}";

字符串內插法

簡單用法:

var name = "World";
var str =$"Hello, {name}!";
// str = "Hello, World!"

帶日期格式化:

var date = DateTime.Now;
var str = $"Today is {date:yyyy-MM-dd}!";

補齊格式化(Padding):

var number = 42;

// 向左補齊
var str = $"The answer to life, the universe and everything is {number, 5}.";
// str = "The answer to life, the universe and everything is ___42." ('_'表示空格)

// 向右補齊
var str = $"The answer to life, the universe and everything is ${number, -5}.";
// str = "The answer to life, the universe and everything is 42___."

結合內置快捷字母格式化:

var amount = 2.5;
var str = $"It costs {amount:C}";
// str = "¥2.50"

var number = 42;
var str = $"The answer to life, the universe and everything is {number, 5:f1}.";
// str = "The answer to life, the universe and everything is ___42.1"

參考:

1.《C# 7.0 in a Nutshell》

2. https://bit.ly/2U1eIK9

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

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

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。