環境資訊中心綜合外電;姜唯 編譯;林大利 審校
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※超省錢租車方案
※教你寫出一流的銷售文案?
※網頁設計最專業,超強功能平台可客製化
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
摘錄自2020年5月4日經濟日報報導
在疫情蔓延下,墨西哥電力系統主管機關宣布,對乾淨能源新計畫的關鍵測試無限期喊卡,另採取措施,以提高全國電力系統的穩定性,但批評者擔心,這項措施將傷害再生能源業者。
能源議題
能源轉型
國際新聞
墨西哥
乾淨能源
武漢肺炎
綠電
疫情看氣候與能源
新能源
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※推薦台中搬家公司優質服務,可到府估價
摘錄自2020年5月5日自由時報報導
法國為遏止武漢肺炎(新型冠狀病毒疾病,COVID-19)疫情擴散,自3月17日起實施全國封鎖,期間2度延長封鎖禁令至5月11日;面對下週即將解除的封鎖令,巴黎市長伊達戈(Anne Hidalgo)將把最繁忙的主要交通幹道規劃給自行車,以減少民眾對大眾運輸工具的依賴,進而避免群聚感染。
伊達戈今(5日)指出,城市解封後共將保留50公里原先的汽車道給自行車使用,另外將有30條街道將被設置為行人專用道,她強調,「特別是在學校周圍,以避免人群聚集」。
法國政府也宣布了一項2000萬歐元(約新台幣6.5億元)的自行車計畫,用以刺激民眾在封鎖解除後對自行車的使用度,其中包括每人50歐元(約新台幣1620元)的自行車維修或調整補貼。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※網頁設計最專業,超強功能平台可客製化
摘錄自2020年5月5日自由時報報導
根據《CNN》報導,位於西班牙西北部加利西亞(Galicia)奧倫塞的「O Invernadeiro」自然保護區,近日透過架設在園內的攝影機拍到一頭年輕公熊活動的畫面,據分析,這頭公熊年紀在3至5歲左右,健康狀況良好。
園方表示,棕熊是西班牙原生物種,從1973年起便被列入野生動物保護名單,過往雖有文獻紀錄常出沒在加利西亞地區,但這次是150年內首度有棕熊被觀測到在加利西亞南部出現,意義非凡。
生態保育
生物多樣性
國際新聞
西班牙
棕熊
動物與大環境變遷
武漢肺炎
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※回頭車貨運收費標準
※推薦評價好的iphone維修中心
※超省錢租車方案
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
※推薦台中搬家公司優質服務,可到府估價
99萬作為一款小型SUV,森雅R7擁有着圓潤飽滿的外觀,小巧時尚的設計很討人喜歡,前臉不規則的中網樣式搭配着造型別緻的大燈,增添了幾分硬朗的氣息。內飾的設計很有層次感,黑銀色搭配拉絲面板,給人很運動的感覺,9英寸的中控大屏是一大亮點,包含了手機互聯和導航等功能,自動防炫目后視鏡出現在尊貴型車型上,檔次感瞬間提升了不少。
隨着科技的發展越來越迅速,汽車技術也在不斷的進步,自動變速箱的出現解決了我們黃金左腳的命運,使駕駛者在擁堵的城市中輕鬆地駕駛車輛,那麼隨着燃油價格的不斷提升,人們有沒有想出比較省油的汽車技術呢?
答案是肯定的,那就是發動機自動啟停系統,在車輛臨時停車等紅燈的時候,會自動熄火,待汽車需要重新啟動時,又能快速啟動發動機,大大的減小了油耗和廢氣的排放,綜合下來此項技術可以節約車子一年5%-15%的燃油哦,來看一下哪些自主品牌SUV都有配備這項技術的吧!
奇瑞汽車-瑞虎7
指導價:9.79-15.39萬
說瑞虎7是奇瑞目前最好的SUV一點也不為過,時尚精緻的外觀,凌厲的腰線和車身比例非常的協調,三叉戟式的大燈和造型獨特的進氣格柵使其看上去辨識度很高。
內飾無論是做工還是用料都給人留下深刻的印象,大量帶縫線的皮質材料和軟質搪塑工藝材料,豪華感十足,簡潔的中控大屏、自動頭燈(LED光源)、座椅加熱、無鑰匙進入/啟動等配置十分齊全。
2650mm的軸距雖在同級別對手中並不佔優,但是實際的乘坐感受還是表現很出色的,座椅的包裹性好,肩部支撐很到位,動力方面提供1.5T+6擋手動/雙離合變速器,或者2.0L+CVT變速箱的組合,懸架方面則採用了常規的前麥弗遜后多連桿式獨立懸架。
一汽吉林-森雅R7
指導價:6.89-9.99萬
作為一款小型SUV,森雅R7擁有着圓潤飽滿的外觀,小巧時尚的設計很討人喜歡,前臉不規則的中網樣式搭配着造型別緻的大燈,增添了幾分硬朗的氣息。
內飾的設計很有層次感,黑銀色搭配拉絲面板,給人很運動的感覺,9英寸的中控大屏是一大亮點,包含了手機互聯和導航等功能,自動防炫目后視鏡出現在尊貴型車型上,檔次感瞬間提升了不少。
森雅R7的軸距為2600mm,在這個價位車型中比較有優勢,無論是前後排的頭部空間還是腿部空間都相當寬敞;動力方面全系搭載1.6L自然吸氣發動機,最大功率116馬力,匹配5擋手動或者6擋手自一體變速器,全系標配發動機啟停功能,油耗表現更出色。
長安汽車-長安CS15
指導價:5.79-7.79萬
長安CS15的外觀充滿了個性化的設計元素,稜角分明的造型和豐富的線條相互搭配,看上去顯得更為硬朗,中網的造型也是獨樹一幟,側面較高的腰線設計,使得其車門肌肉感十足,整車是偏向運動的設計路線。
內飾為飛翼式的家族設計風格,紅色縫線的三幅式方向盤、炮筒式的儀錶盤有着濃厚的運動味道,製作工藝堪比合資車,胎壓監測、無鑰匙進入/啟動、上坡輔助、倒車影像等配置一應俱全。
雖然CS15是一款小型SUV,軸距也只有2510mm,但是內部空間完全超出你的想象,乘坐感受相當舒適,大大小小的儲物格達到39處之多,便利性很強,全系採用1.5L+5擋手動/5擋雙離合的動力組合,8萬塊買自動擋性價比是相當高的。
總結:瑞虎7的價格相對來說有些高,但畢竟是跨級別的,做工水平整體很高,堪比合資車,森雅R7的表現中規中矩,全系標配發動機啟停非常厚道,長安CS15的性價比最高,麻雀雖小五臟俱全,適合年輕人的第一台車。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※回頭車貨運收費標準
※台中搬家公司費用怎麼算?
目錄
本文通過ReentrantLock來窺探AbstractQueuedSynchronizer(AQS)的實現原理,在看此文之前。你需要了解一下park、unpark的功能,請移步至上一篇《深入剖析park、unpark》;
根據AbstractQueuedSynchronizer的官方文檔,如果想實現一把鎖的,需要繼承AbstractQueuedSynchronizer,並需要重寫tryAcquire、tryRelease、可選擇重寫isHeldExclusively提供locked state、因為支持序列化,所以需要重寫readObject以便反序列化時恢復原始值、newCondition提供條件;官方提供的java代碼如下(官方文檔見參考連接);
public class MyLock implements Lock, java.io.Serializable {
private static class Sync extends AbstractQueuedSynchronizer {
// Acquires the lock if state is zero
@Override
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Releases the lock by setting state to zero
@Override
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() {
return new ConditionObject();
}
// Deserializes properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
// Reports whether in locked state
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
/**
* The sync object does all the hard work. We just forward to it.
*/
private final Sync sync = new Sync();
@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 timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
private static volatile Integer value = 0;
public static void main(String[] args) {
MyLock myLock = new MyLock();
for (int i = 0; i < 1000; i++) {
new Thread(()->{
myLock.lock();
value ++;
myLock.unlock();
}).start();
}
System.out.println(value);
}
}
上面是一個不可重入的鎖,它實現了一個鎖基礎功能,目的是為了跟ReentrantLock的實現做對比;
ReentrantLock意思為可重入鎖,指的是一個線程能夠對一個臨界資源重複加鎖。ReentrantLock跟常用的Synchronized進行比較;
Synchronized的分析可以參考《深入剖析synchronized關鍵詞》,ReentrantLock可以創建公平鎖、也可以創建非公平鎖,接下來看一下ReentrantLock的簡單用法,非公平鎖實現比較簡單,今天重點是公平鎖;
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(true);
reentrantLock.lock();
try {
log.info("lock");
} catch (Exception e) {
log.error(e);
} finally {
reentrantLock.unlock();
log.info("unlock");
}
}
}
先看一下加鎖方法lock
非公平鎖lock方法
compareAndSetState很好理解,通過CAS加鎖,如果加鎖失敗調用acquire;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final void lock() {
acquire(1);
}
線程繼續等待,仍然保留獲取鎖的可能,獲取鎖流程仍在繼續,分析實現原理
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
總結:公平鎖的上鎖是必須判斷自己是不是需要排隊;而非公平鎖是直接進行CAS修改計數器看能不能加鎖成功;如果加鎖不成功則乖乖排隊(調用acquire);所以不管公平還是不公平;只要進到了AQS隊列當中那麼他就會排隊;
美團畫的AQS的架構圖,很詳細,當有自定義同步器接入時,只需重寫第一層所需要的部分方法即可,不需要關注底層具體的實現流程。當自定義同步器進行加鎖或者解鎖操作時,先經過第一層的API進入AQS內部方法,然後經過第二層進行鎖的獲取,接着對於獲取鎖失敗的流程,進入第三層和第四層的等待隊列處理,而這些處理方式均依賴於第五層的基礎數據提供層。
AQS核心思想是,如果被請求的共享資源空閑,那麼就將當前請求資源的線程設置為有效的工作線程,將共享資源設置為鎖定狀態;如果共享資源被佔用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH隊列的變體實現的,將暫時獲取不到鎖的線程加入到隊列中。
CLH:Craig、Landin and Hagersten隊列,是單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO),AQS是通過將每條請求共享資源的線程封裝成一個節點來實現鎖的分配。
加鎖:
解鎖:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
}
acquire方法首先會調tryAcquire方法,需要注意的是tryAcquire的結果做取反;根據前面分析,tryAcquire會調用子類的實現,ReentrantLock有兩個內部類,FairSync,NonfairSync,都繼承自Sync,Sync繼承AbstractQueuedSynchronizer;
實現方式差別在是否有hasQueuedPredecessors() 的判斷條件
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 獲取lock對象的上鎖狀態,如果鎖是自由狀態則=0,如果被上鎖則為1,大於1表示重入
int c = getState();
if (c == 0) {
// hasQueuedPredecessors,判斷自己是否需要排隊
// 下面我會單獨介紹,如果不需要排隊則進行cas嘗試加鎖
// 如果加鎖成功則把當前線程設置為擁有鎖的線程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果C不等於0,但是當前線程等於擁有鎖的線程則表示這是一次重入,那麼直接把狀態+1表示重入次數+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平鎖
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
先來看下AQS中最基本的數據結構——Node,Node即為上面CLH變體隊列中的節點。
static final class Node {
static final Node SHARED = new Node(); // 表示線程以共享的模式等待鎖
static final Node EXCLUSIVE = null; // 表示線程正在以獨佔的方式等待鎖
static final int CANCELLED = 1; // 表示線程獲取鎖的請求已經取消了
static final int SIGNAL = -1; // 表示線程已經準備好了,就等資源釋放了
static final int CONDITION = -2; // 表示節點在等待隊列中,節點線程等待喚醒
static final int PROPAGATE = -3; // 當前線程處在SHARED情況下,該字段才會使用
volatile int waitStatus; // 當前節點在隊列中的狀態
volatile Node prev; // 前驅指針
volatile Node next; // 後繼指針
volatile Thread thread; // 表示處於該節點的線程
Node nextWaiter; // 指向下一個處於CONDITION狀態的節點
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前驅節點,沒有的話拋出npe
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
再看hasQueuedPredecessors,整個方法如果最後返回false,則去加鎖,如果返回true則不加鎖,因為這個方法被取反操作;hasQueuedPredecessors是公平鎖加鎖時判斷等待隊列中是否存在有效節點的方法。如果返回False,說明當前線程可以爭取共享資源;如果返回True,說明隊列中存在有效節點,當前線程必須加入到等待隊列中。
雙向鏈表中,第一個節點為虛節點,其實並不存儲任何信息,只是佔位。真正的第一個有數據的節點,是在第二個節點開始的。
如果這上面沒有看懂,沒有關係,先來分析一下構建整個隊列的過程;
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// tail為對尾,賦值給pred
Node pred = tail;
// 判斷pred是否為空,其實就是判斷對尾是否有節點,其實只要隊列被初始化了對尾肯定不為空
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
用一張圖來分析一下,整個隊列構建過程;
(1)通過Node(Thread thread, Node mode) 方法構建一個node節點(node2),此時的nextWaiter為空,線程不為空,是當前線程;
(2)如果隊尾為空,則說明隊列未建立,調用enq構建第一個虛擬節點(node1),通過compareAndSetHead方法構建一個頭節點,需要注意的是該頭節點thread是null,後續很多都是用線程是否為null來判讀是否為第一個虛擬節點;
(3)將node1 cas設置為head
(4)將頭節點賦值為tail = head
(5)進入下一次for循環時,會走到else分支,會將傳入的node的指向頭部節點的next,此時node2的prev指向node1(tail)
(6)將node2 cas設置為tail;
(7)將node2指向node1的next;
經過上面的步驟,就構建了一個長度為2的隊列;
添加第二個隊列時,走的是這段代碼,流程就簡單多了,代碼如下
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
再看一下h != t && ((s = h.next) == null || s.thread != Thread.currentThread());因為整個構建過程並不是原子操作,所以這個條件判斷,現在再是不是就看明白了?
addWaiter方法其實就是把對應的線程以Node的數據結構形式加入到雙端隊列里,返回的是一個包含該線程的Node。而這個Node會作為參數,進入到acquireQueued方法中。acquireQueued方法可以對排隊中的線程進行“獲鎖”操作。總的來說,一個線程獲取鎖失敗了,被放入等待隊列,acquireQueued會把放入隊列中的線程不斷去獲取鎖,直到獲取成功或者不再需要獲取(中斷)。
下面通過代碼從“何時出隊列?”和“如何出隊列?”兩個方向來分析一下acquireQueued源碼:
final boolean acquireQueued(final Node node, int arg) {
// 標記是否成功拿到資源
boolean failed = true;
try {
// 標記等待過程中是否中斷過
boolean interrupted = false;
for (;;) {
// 獲取當前節點的前驅節點,有兩種情況;1、上一個節點為頭部;2上一個節點不為頭部
final Node p = node.predecessor();
// 如果p是頭結點,說明當前節點在真實數據隊列的首部,就嘗試獲取鎖(頭結點是虛節點)
// 因為第一次tryAcquire判斷是否需要排隊,如果需要排隊,那麼我就入隊,此處再重試一次
if (p == head && tryAcquire(arg)) {
// 獲取鎖成功,頭指針移動到當前node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 說明p為頭節點且當前沒有獲取到鎖(可能是非公平鎖被搶佔了)或者是p不為頭結點,這個時候就要判斷當前node是否要被阻塞(被阻塞條件:前驅節點的waitStatus為-1),防止無限循環浪費資源。具體兩個方法下面細細分析
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 成功拿到資源,準備釋放
if (failed)
cancelAcquire(node);
}
}
設置當前節點為頭節點,並且將node.thread為空(剛才提到判斷是否為頭部虛擬節點的條件就是node.thread == null。waitStatus狀態併為修改,等下我們再分析;
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
接下來看shouldParkAfterFailedAcquire代碼,需要注意的是,每一個新創建Node的節點是被下一個排隊的node設置為等待狀態為SIGNAL, 這裏比較難以理解為什麼需要去改變上一個節點的park狀態?
每個node都有一個狀態,默認為0,表示無狀態,-1表示在park;當時不能自己把自己改成-1狀態?因為你得確定你自己park了才是能改為-1;所以只能先park;在改狀態;但是問題你自己都park了;完全釋放CPU資源了,故而沒有辦法執行任何代碼了,所以只能別人來改;故而可以看到每次都是自己的后一個節點把自己改成-1狀態;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 獲取前驅節點的狀態
int ws = pred.waitStatus;
// 說明頭結點處於喚醒狀態
if (ws == Node.SIGNAL)
return true;
// static final int CANCELLED = 1; // 表示線程獲取鎖的請求已經取消了
// static final int SIGNAL = -1; // 表示線程已經準備好了,就等資源釋放了
// static final int CONDITION = -2; // 表示節點在等待隊列中,節點線程等待喚醒
// static final int PROPAGATE = -3; // 當前線程處在SHARED情況下,該字段才會使用
if (ws > 0) {
do {
// 把取消節點從隊列中剔除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 設置前任節點等待狀態為SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
調用LockSupport.park掛起當前線程,自己已經park,無法再修改狀態了!
private final boolean parkAndCheckInterrupt() {
// 調⽤用park()使線程進⼊入waiting狀態
LockSupport.park(this);
// 如果被喚醒,查看⾃自⼰己是不不是被中斷的,這⾥里里先清除⼀下標記位
return Thread.interrupted();
}
shouldParkAfterFailedAcquire的整個流程還是比較清晰的,如果不清楚,可以參考美團畫的流程圖;
通過上面的分析,當failed為true時,也就意味着park結束,線程被喚醒了,for循環已經跳出,開始執行cancelAcquire,通過cancelAcquire方法,將Node的狀態標記為CANCELLED;代碼如下:
private void cancelAcquire(Node node) {
// 將無效節點過濾
if (node == null)
return;
// 設置該節點不關聯任何線程,也就是虛節點(上面已經提到,node.thread = null是判讀是否是頭節點的條件)
node.thread = null;
Node pred = node.prev;
// 通過前驅節點,處理waitStatus > 0的node
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 把當前node的狀態設置為CANCELLED,當下一個node排隊結束時,自己就會被上一行代碼處理掉;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
// 如果當前節點是尾節點,將從后往前的第一個非取消狀態的節點設置為尾節點,更新失敗的話,則進入else,如果更新成功,將tail的後繼節點設置為null
if (node == tail && compareAndSetTail(node, pred)) {
// 把自己設置為null
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果當前節點不是head的後繼節點
// 1:判斷當前節點前驅節點的是否為SIGNAL
// 2:如果不是,則把前驅節點設置為SINGAL看是否成功
// 如果1和2中有一個為true,再判斷當前節點的線程是否為null
// 如果上述條件都滿足,把當前節點的前驅節點的後繼指針指向當前節點的後繼節點
if (pred != head &&
((ws = pred.waitStatus) == Node.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 {
// 如果當前節點是head的後繼節點,或者上述條件不滿足,那就喚醒當前節點的後繼節點
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
當前的流程:
獲取當前節點的前驅節點,如果前驅節點的狀態是CANCELLED,那就一直往前遍歷,找到第一個waitStatus <= 0的節點,將找到的Pred節點和當前Node關聯,將當前Node設置為CANCELLED。
根據當前節點的位置,考慮以下三種情況:
(1) 當前節點是尾節點。
(2) 當前節點是Head的後繼節點。
(3) 當前節點不是Head的後繼節點,也不是尾節點。
(1)當前節點時尾節點
(2)當前節點是Head的後繼節點。
這張圖描述的是這段代碼:unparkSuccessor
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
(3)當前節點不是Head的後繼節點,也不是尾節點。
這張圖描述的是這段代碼跟(2)一樣;
通過上面的圖,你會發現所有的變化都是對Next指針進行了操作,而沒有對Prev指針進行操作,原因是執行cancelAcquire的時候,當前節點的前置節點可能已經從隊列中出去了(已經執行過Try代碼塊中的shouldParkAfterFailedAcquire方法了),也就是下圖中代碼1和代碼2直接的間隙就會出現這種情況,此時修改Prev指針,有可能會導致Prev指向另一個已經移除隊列的Node,因此這塊變化Prev指針不安全。
解鎖時並不區分公平和不公平,因為ReentrantLock實現了鎖的可重入,可以進一步的看一下時如何處理的,上代碼:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 自定義的tryRelease如果返回true,說明該鎖沒有被任何線程持有
if (tryRelease(arg)) {
// 獲取頭結點
Node h = head;
if (h != null && h.waitStatus != 0)
// 頭結點不為空並且頭結點的waitStatus不是初始化節點情況,解除線程掛起狀態
unparkSuccessor(h);
return true;
}
return false;
}
這裏的判斷條件為什麼是h != null && h.waitStatus != 0
protected final boolean tryRelease(int releases) {
// 減少可重入次數,setState(c);
int c = getState() - releases;
// 當前線程不是持有鎖的線程,拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果持有線程全部釋放,將當前獨佔鎖所有線程設置為null,並更新state
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
這個方法在cancelAcquire其實也用到了,簡單分析一下
// 如果當前節點是head的後繼節點,或者上述條件不滿足,就喚醒當前節點的後繼節點unparkSuccessor(node);
private void unparkSuccessor(Node node) {
// 獲取結點waitStatus,CAS設置狀態state=0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 獲取當前節點的下一個節點
Node s = node.next;
// 如果下個節點是null或者下個節點被cancelled,就找到隊列最開始的非cancelled的節點
if (s == null || s.waitStatus > 0) {
s = null;
// 就從尾部節點開始找,到隊首,找到隊列第一個waitStatus<0的節點。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果當前節點的下個節點不為空,而且狀態<=0,就把當前節點unpark
if (s != null)
LockSupport.unpark(s.thread);
}
為什麼要從后往前找第一個非Cancelled的節點呢?
原因1:addWaiter方法並非原子,構建鏈表結構時如下圖中 1、2間隙執行unparkSuccessor,此時鏈表是不完整的,沒辦法從前往後找了;
原因2:還有一點原因,在產生CANCELLED狀態節點的時候,先斷開的是Next指針,Prev指針並未斷開,因此也是必須要從后往前遍歷才能夠遍歷完全部的Node;
喚醒后,會執行return Thread.interrupted();,這個函數返回的是當前執行線程的中斷狀態,並清除。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
acquireQueued代碼,當parkAndCheckInterrupt返回True或者False的時候,interrupted的值不同,但都會執行下次循環。如果這個時候獲取鎖成功,就會把當前interrupted返回。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
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);
}
}
如果acquireQueued為True,就會執行selfInterrupt方法。
該方法其實是為了中斷線程。但為什麼獲取了鎖以後還要中斷線程呢?這部分屬於Java提供的協作式中斷知識內容,感興趣同學可以查閱一下。這裏簡單介紹一下:
這裏的處理方式主要是運用線程池中基本運作單元Worder中的runWorker,通過Thread.interrupted()進行額外的判斷處理,可以看下ThreadPoolExecutor源碼的判斷條件;
AQS在JUC中有⽐比較⼴廣泛的使⽤用,以下是主要使⽤用的地⽅方:
至此,通過ReentrantLock分析AQS的實現原理一家完畢,需要說明的是,此文深度參考了美團分析的ReentrantLock,是參考鏈接的第三個,有興趣可以對比差異,感謝!
JDK API 文檔
Java的LockSupport.park()實現分析
[從ReentrantLock的實現看AQS的原理及應用
[Thread的中斷機制(interrupt)
打賞地址
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
※教你寫出一流的銷售文案?
※超省錢租車方案
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※回頭車貨運收費標準
Spring的強大之處不僅僅是提供了IOC容器,能夠通過過濾規則指定排除和只包含哪些組件,它還能夠通過自定義TypeFilter來指定過濾規則。如果Spring內置的過濾規則不能夠滿足我們的需求,那麼我們就可以通過自定義TypeFilter來實現我們自己的過濾規則。
項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation
在使用@ComponentScan註解實現包掃描時,我們可以使用@Filter指定過濾規則,在@Filter中,通過type指定過濾的類型。而@Filter註解的type屬性是一個FilterType枚舉,如下所示。
package org.springframework.context.annotation;
public enum FilterType {
ANNOTATION,
ASSIGNABLE_TYPE,
ASPECTJ,
REGEX,
CUSTOM
}
每個枚舉值的含義如下所示。
(1)ANNOTATION:按照註解進行過濾。
例如,使用@ComponentScan註解進行包掃描時,按照註解只包含標註了@Controller註解的組件,如下所示。
@ComponentScan(value = "io.mykit.spring", includeFilters = {
@Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
}, useDefaultFilters = false)
(2)ASSIGNABLE_TYPE:按照給定的類型進行過濾。
例如,使用@ComponentScan註解進行包掃描時,按照給定的類型只包含PersonService類(接口)或其子類(實現類或子接口)的組件,如下所示。
@ComponentScan(value = "io.mykit.spring", includeFilters = {
@Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {PersonService.class})
}, useDefaultFilters = false)
此時,只要是PersonService類型的組件,都會被加載到容器中。也就是說:當PersonService是一個Java類時,Person類及其子類都會被加載到Spring容器中;當PersonService是一個接口時,其子接口或實現類都會被加載到Spring容器中。
(3)ASPECTJ:按照ASPECTJ表達式進行過濾
例如,使用@ComponentScan註解進行包掃描時,按照ASPECTJ表達式進行過濾,如下所示。
@ComponentScan(value = "io.mykit.spring", includeFilters = {
@Filter(type = FilterType.ASPECTJ, classes = {AspectJTypeFilter.class})
}, useDefaultFilters = false)
(4)REGEX:按照正則表達式進行過濾
例如,使用@ComponentScan註解進行包掃描時,按照正則表達式進行過濾,如下所示。
@ComponentScan(value = "io.mykit.spring", includeFilters = {
@Filter(type = FilterType.REGEX, classes = {RegexPatternTypeFilter.class})
}, useDefaultFilters = false)
(5)CUSTOM:按照自定義規則進行過濾。
如果實現自定義規則進行過濾時,自定義規則的類必須是org.springframework.core.type.filter.TypeFilter接口的實現類。
例如,按照自定義規則進行過濾,首先,我們需要創建一個org.springframework.core.type.filter.TypeFilter接口的實現類MyTypeFilter,如下所示。
public class MyTypeFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return false;
}
}
當我們實現TypeFilter接口時,需要實現TypeFilter接口中的match()方法,match()方法的返回值為boolean類型。當返回true時,表示符合規則,會包含在Spring容器中;當返回false時,表示不符合規則,不會包含在Spring容器中。另外,在match()方法中存在兩個參數,分別為MetadataReader類型的參數和MetadataReaderFactory類型的參數,含義分別如下所示。
接下來,使用@ComponentScan註解進行如下配置。
@ComponentScan(value = "io.mykit.spring", includeFilters = {
@Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class})
}, useDefaultFilters = false)
在FilterType枚舉中,ANNOTATION和ASSIGNABLE_TYPE是比較常用的,ASPECTJ和REGEX不太常用,如果FilterType枚舉中的類型無法滿足我們的需求時,我們也可以通過實現org.springframework.core.type.filter.TypeFilter接口來自定義過濾規則,此時,將@Filter中的type屬性設置為FilterType.CUSTOM,classes屬性設置為自定義規則的類對應的Class對象。
在項目的io.mykit.spring.plugins.register.filter包下新建MyTypeFilter,並實現org.springframework.core.type.filter.TypeFilter接口。此時,我們先在MyTypeFilter類中打印出當前正在掃描的類名,如下所示。
package io.mykit.spring.plugins.register.filter;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import java.io.IOException;
/**
* @author binghe
* @version 1.0.0
* @description 自定義過濾規則
*/
public class MyTypeFilter implements TypeFilter {
/**
* metadataReader:讀取到的當前正在掃描的類的信息
* metadataReaderFactory:可以獲取到其他任務類的信息
*/
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
//獲取當前類註解的信息
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
//獲取當前正在掃描的類的信息
ClassMetadata classMetadata = metadataReader.getClassMetadata();
//獲取當前類的資源信息,例如:類的路徑等信息
Resource resource = metadataReader.getResource();
//獲取當前正在掃描的類名
String className = classMetadata.getClassName();
//打印當前正在掃描的類名
System.out.println("-----> " + className);
return false;
}
}
接下來,我們在PersonConfig類中配置自定義過濾規則,如下所示。
@Configuration
@ComponentScans(value = {
@ComponentScan(value = "io.mykit.spring", includeFilters = {
@Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class})
}, useDefaultFilters = false)
})
public class PersonConfig {
@Bean("person")
public Person person01(){
return new Person("binghe001", 18);
}
}
接下來,我們運行SpringBeanTest類中的testComponentScanByAnnotation()方法進行測試,輸出的結果信息如下所示。
-----> io.mykit.spring.test.SpringBeanTest
-----> io.mykit.spring.bean.Person
-----> io.mykit.spring.plugins.register.controller.PersonController
-----> io.mykit.spring.plugins.register.dao.PersonDao
-----> io.mykit.spring.plugins.register.filter.MyTypeFilter
-----> io.mykit.spring.plugins.register.service.PersonService
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
person
可以看到,已經輸出了當前正在掃描的類的名稱,同時,除了Spring內置的bean名稱外,只輸出了personConfig和person,沒有輸出使用@Repository、@Service、@Controller註解標註的組件名稱。這是因為當前PersonConfig上標註的@ComponentScan註解是使用自定義的規則,而在MyTypeFilter自定義規則的實現類中,直接返回了false值,將所有的bean都排除了。
我們可以在MyTypeFilter類中簡單的實現一個規則,例如,當前掃描的類名稱中包含有字符串Person,就返回true,否則返回false。此時,MyTypeFilter類中match()方法的實現如下所示。
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
//獲取當前類註解的信息
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
//獲取當前正在掃描的類的信息
ClassMetadata classMetadata = metadataReader.getClassMetadata();
//獲取當前類的資源信息,例如:類的路徑等信息
Resource resource = metadataReader.getResource();
//獲取當前正在掃描的類名
String className = classMetadata.getClassName();
//打印當前正在掃描的類名
System.out.println("-----> " + className);
return className.contains("Person");
}
此時,在io.mykit.spring包下的所有類都會通過MyTypeFilter類的match()方法,來驗證類名是否包含Person,如果包含則返回true,否則返回false。
我們再次運行SpringBeanTest類中的testComponentScanByAnnotation()方法進行測試,輸出的結果信息如下所示。
-----> io.mykit.spring.test.SpringBeanTest
-----> io.mykit.spring.bean.Person
-----> io.mykit.spring.plugins.register.controller.PersonController
-----> io.mykit.spring.plugins.register.dao.PersonDao
-----> io.mykit.spring.plugins.register.filter.MyTypeFilter
-----> io.mykit.spring.plugins.register.service.PersonService
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
person
personController
personDao
personService
此時,結果信息中輸出了使用@Repository、@Service、@Controller註解標註的組件名稱,分別為:personDao、personService和personController。
好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!
項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation
如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※超省錢租車方案
※教你寫出一流的銷售文案?
※網頁設計最專業,超強功能平台可客製化
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
在STM32的應用中,我們常常對printf進行重定向的方式來把打印信息printf到我們的串口助手。在MDK環境中,我們常常使用MicroLIB+fputc的方式實現串口打印功能,即:
要實現fputc函數的原因是:printf函數依賴於fputc函數,重新實現fputc內部從串口發送數據即可間接地實現printf打印輸出數據到串口。
不知道大家有沒有看過正點原子裸機串口相關的例程,他們的串口例程里不使用MicroLIB,而是使用標準庫+fputc的方式。相關代碼如:
#if 1
#pragma import(__use_no_semihosting)
//標準庫需要的支持函數
struct __FILE
{
int handle;
};
FILE __stdout;
/**
* @brief 定義_sys_exit()以避免使用半主機模式
* @param void
* @return void
*/
void _sys_exit(int x)
{
x = x;
}
int fputc(int ch, FILE *f)
{
while((USART1->ISR & 0X40) == 0); //循環發送,直到發送完畢
USART1->TDR = (u8) ch;
return ch;
}
#endif
關於這兩種方法的一些說明可以查看Mculover666兄的重定向printf函數到串口輸出的多種方法這篇文章。這篇文章中不僅包含上面的兩種方法,而且也包含着在GCC中使用標準庫重定向printf的方法。
以上的幾種方法基本上是改造C庫的printf函數來實現串口打印的功能。其實我們也可以自己實現一個串口打印的功能。
printf本身就是一個變參函數,其原型為:
int printf (const char *__format, ...);
所以,我們要重新封裝的一個串口打印函數自然也應該是一個變參函數。具體實現如下:
#define TX_BUF_LEN 256 /* 發送緩衝區容量,根據需要進行調整 */
uint8_t TxBuf[TX_BUF_LEN]; /* 發送緩衝區 */
void MyPrintf(const char *__format, ...)
{
va_list ap;
va_start(ap, __format);
/* 清空發送緩衝區 */
memset(TxBuf, 0x0, TX_BUF_LEN);
/* 填充發送緩衝區 */
vsnprintf((char*)TxBuf, TX_BUF_LEN, (const char *)__format, ap);
va_end(ap);
int len = strlen((const char*)TxBuf);
/* 往串口發送數據 */
HAL_UART_Transmit(&huart1, (uint8_t*)&TxBuf, len, 0xFFFF);
}
因為我們使用printf函數基本不使用其返回值,所以這裏直接用void類型了。自定義變參函數需要用到va_start、va_end等宏,需要包含頭文件stdarg.h。關於變參函數的一些學習可以查看網上的一些博文,如:
https://www.cnblogs.com/wulei0630/p/9444062.html
這裏我們使用的是STM32的HAL庫,其給我們提供HAL_UART_Transmit接口可以直接把整個發送緩衝區的內容給一次性發出去。
若是基於STM32的標準庫,就需要一字節一字節的循環發送出去,具體代碼如:
#define TX_BUF_LEN 256 /* 發送緩衝區容量,根據需要進行調整 */
uint8_t TxBuf[TX_BUF_LEN]; /* 發送緩衝區 */
void MyPrintf(const char *__format, ...)
{
va_list ap;
va_start(ap, __format);
/* 清空發送緩衝區 */
memset(TxBuf, 0x0, TX_BUF_LEN);
/* 填充發送緩衝區 */
vsnprintf((char*)TxBuf, TX_BUF_LEN, (const char *)__format, ap);
va_end(ap);
int len = strlen((const char*)TxBuf);
/* 往串口發送數據 */
for (int i = 0; i < len; i++)
{
while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET);
USART_SendData(USART1, TxBuf[i]);
}
}
測試結果:
我們也可以使用我們的MyPrintf函數按照上一篇文章:======的方式封裝一個宏打印函數:
以上就是我們自定義方式實現的一種串口打印函數。
但是,我想說:對於串口打印的使用,我們沒必要自己創建一個打印函數。看到這,是不是有人想要打我了。。。。看了半天,你卻跟我說沒必要用。。。
哈哈,別急,我們不應用在串口打印調試方面,那可以用在其它方面呀。
比如最近我在實際應用中:我們的MCU跑的是我們老大自己寫的一個小的操作系統+我們公司自己開發的上位機。我們MCU端與上位機使用的是串口通訊,MCU往上位機發送的數據有兩種類型,一種是HEX格式數據,一種是字符串數據。
但是我們下位機的這兩種數據,在通過串口發送之前都得統一把數據封包交給那個系統通信任務,然後再由通信任務發出去。在這裏,就不能用printf了。老大也針對他的這個系統實現了一個deb_printf函數用於打印調試。
但是,那個函數既複雜又很雞肋,稍微複雜一點的數據就打印不出來了。因此我利用上面的思路給它新封裝了一個打印調試函數,很好用,完美地兼容了老大的那個系統。具體代碼就不分享了,大體代碼、思路如上。
我們在使用串口與ESP8266模塊通訊時,可利用類似這樣的方式封裝一個發送數據的函數,這個函數的使用可以像printf一樣簡單。可以以很簡單的方式把數據透傳至服務端,比如我以前的畢設中就有這麼應用:
以上就是本次的分享,如有錯誤,歡迎指出!謝謝
我的個人博客:https://www.lizhengnian.cn/
我的微信公眾號:嵌入式大雜燴
我的CSDN博客:https://blog.csdn.net/zhengnianli
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※推薦台中搬家公司優質服務,可到府估價
使用mybatis逆向工程的時候,delete方法的使用姿勢不對,導致表被清空了,在生產上一刷新后發現表裡沒數據了,一股涼意從腳板心直衝天靈蓋。
於是開發了一個攔截器,並寫下這篇文章記錄並分享。
你用過 mybatis 逆向工程(mybatis-generator-maven-plugin)生成相關文件嗎?
就像這樣式兒的:
可以看到逆向工程幫我們生成了實體類、Mapper 接口和 Mapper.xml。
用起來真的很方便,我用了好幾年了,但是前段時間翻車了。
具體是怎麼回事呢,我給大家擺一下。
先說一下需求吧。就是在做一次借據數據遷移的過程中,要先通過 A 服務的接口拿到所有的借據和對應的還款計劃數據,然後再對這些借據進行核查,如果不滿足某些添加,就需要從表中刪除借據和對應的還款計劃。
借據和對應的還款計劃存放在兩張表中,用借據號來關聯。
而上線之後,我在一片歡聲笑語中把還款計劃表清空了,而這個必現的問題,在測試階段同學還沒有測試出來。
事情發生后我趕緊找到了 DBA 協助修複數據:
是怎麼回事呢,為了模擬這個場景,我在本地創建了兩張表,訂單表(orderInfo)和訂單擴展表(orderInfoExt),他們之間用訂單號進行關聯:
僅僅是做演示,所以兩張表是非常簡單的,
我們假設現在表裡面的這條訂單號為 2020060666666 的數據經過判斷是錯誤數據,我當時寫的代碼體現在單元測試裏面是這樣的:
看出問題了嗎?
第 42 行用的 example 對象還是 OrderInfo 的 example。而真正的 OrderInfoExt 對象的 exampleExt 對象沒有進行任何賦值的操作。
為什麼會出現這樣的烏龍呢?
都怪 idea 太智能了!(強行找個借口)
我只需要打一個 ex 然後回個車…. example 就出現在代碼裏面了。
而這種沒有參數的 example 傳進去,在 mapper.xml 裏面是這樣處理的:
執行一下,看看效果:
看到 delete from order_info_ext 語句。你說你慌不慌?
當然在線上的服務器肯定是看不到執行的 SQL 的,但是當報警短信一條一條接着來的時候,當連上數據庫一看錶,發現數據沒了的時候。
你說你慌不慌?
反正我一刷新后發現表裡沒數據了,一股涼意從腳板心直衝天靈蓋。這種時候都還是要小小的心慌一下,先大喊一聲“卧槽!數據怎麼沒了?”
然後趕緊報備,準備找 DBA 撈數據吧。
還好,本次誤刪不影響正常業務。
數據恢復過程就不說了,聊一下這事發生后我的一點思考吧。
哦,對了,還得說一下測試同學為什麼沒有發現這個問題。這個問題確實是一個必現的問題,測試案例上也寫了這個測試點。
但是測試同學查看數據的時候用的是 select 語句,查詢條件給的是確實需要被刪除的數據 。
然後分別在兩個表裡面執行后發現:數據確實是沒了。
是的,是數據確實是沒了。整個表都乾淨了。
看着測試妹子驚慌失措的樣子,我還能怎麼說呢?
這鍋,不甩了,我自己背下來吧。
我們先看看逆向工程幫我們生成的接口:
我相信用過 mybatis 逆向工程的朋友們,一看到這幾個接口就知道了:喲,這都是老朋友了。
當我再去重新審視這些接口的時候我會發現其實還有會有一些問題的。
比如 delete 這樣的高危語句我們還是需要盡量的手寫 xml。
比如 updateByExample 同樣存在由於誤操作沒有 where 條件,導致全表更新的情況。
比如 select 語句是查出了整個對象,但是有時間我們可能只需要對象裏面的某個值而已。
比如 select 語句針對大表、關鍵表操作的時候,不能從代碼的角度限定 SQL 必須帶上索引字段查詢。
上面的這些問題我們怎麼處理呢?
我的建議是不要使用 mybatis 的逆向工程,全都手寫。
開個玩笑。我們肯定不能因噎廢食,何況逆向工程確實是幫我們做了很多工作,極大的方便我們這樣的 CRUD Boy 進行 CRUD。
所以,我想 mybatis 的逆向工程肯定是有什麼配置來控制生成哪些接口的,別問為什麼,問就是直覺。
因為要是讓我去開發這樣的一個插件,我肯定也會提供對應的開關配置。
我現在的想法是不讓它給我生成 delete 相關的接口,這個接口用起來我心裏害怕。
所以怎麼配置呢?
我們去它的 DTD 文件裏面找一下嘛:
這個文件不長,一共也才 213 行,你能發現這一塊東西:
你用腳指頭想也能知道,這就是我們要找的開關配置。從 DTD 文件的描述中來看,這個幾個參數是配置在 table 標籤裏面的。
我們去試一下:
果然是這樣的。然後我們進行相關配置如下:
再生成一下:
果然,delete 相關的接口沒了。
然後我們程序中真的需要 delete 操作的時候,再自己去手寫 xml 文件。
那你自己寫的 xml 文件也忘記寫 where 條件了這麼辦?
這個月工資別領了。自己好好反思反思。
當然,就算你真的忘記寫了,下面這個攔截器還能給你兜個底,幫你一把。
其實這個方案是我想到的第一個方案。導致上面問題的原因很簡單嘛,就是執行了delete 語句卻沒有 where 條件。
那麼我們可以攔截到這個 SQL 語句,然後對其進行兩個判斷:
是否是 delete 語句。 如果是,是否包含 where 條件。
那麼問題來了,我們怎麼去攔截到這個 SQL 呢?
答案就是我們可以開發一個 mybatis 插件呀,就像分頁插件那樣。
插件,聽起來很高端的樣子,其實他就是個攔截器。實現起來非常簡單。
先去官網上看一下:
中文:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
英文:https://mybatis.org/mybatis-3/configuration.html
在官網上,對於插件這一模塊的描述是這樣的:
通過 MyBatis 提供的強大機制,使用插件是非常簡單的,只需實現 Interceptor 接口,並指定想要攔截的方法簽名即可。
正如官網說的這樣,插件開發、使用起來是非常簡單的。只需要三步:
1.實現 Interceptor 接口。
2.指定想要攔截的方法簽名。
3.配置這個插件。
基於上面這三步,大家先看一下我們這插件怎麼寫,以及這個插件的效果。
先說明一下本文涉及到的源碼 mybatis 版本是 3.4.0。
本文用攔截器的目的是判斷 delete 語句中是否有 where 條件。所以,開發出來的插件長這樣:
再來一個複製粘貼直接運行版本:
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
})
public class CheckSQLInterceptor implements Interceptor {
private static String SQL_WHERE = "where";
@Override
public Object intercept(Invocation invocation) throws Throwable {
//獲取方法的第0個參數,也就是MappedStatement。@Signature註解中的args中的順序
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
//獲取sql命令操作類型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
final Object[] queryArgs = invocation.getArgs();
final Object parameter = queryArgs[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql();
if (SqlCommandType.DELETE.equals(sqlCommandType)) {
//格式化sql
sql = sql.replace("\n", "");
if (!sql.toLowerCase().contains(SQL_WHERE)) {
sql = sql.replace(" ", "");
log.info("刪除語句中沒有where條件,sql為:{}", sql);
throw new Exception("刪除語句中沒有where條件");
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
再把插件註冊上(註冊插件還有其他的方法,後面會講到,這裏只是展示Bean注入的方式):
我們先看看配上插件后的執行效果:
可以看到日誌中輸出了:
刪除語句中沒有where條件,sql為:delete from order_info_ext
並拋出了異常。
這樣,我們的擴展表的數據就保住了。在測試階段,測試同學就一定能扯出來問題,瞟一眼日誌就明白了。
就算測試同學忘記測試了,在生產上也不會執行成功,拋出異常后還會有報警短信通知到相應的開發負責人,及時登上服務器去處理。
功能實現了,確實是非常的簡單。
我們再說回代碼,你說說看:當你拿到上面這段代碼后,最迷惑的地方是哪裡?
其中的邏輯是很簡單的了。 沒有什麼特別的地方,我想大多數人拿到這段代碼迷惑的地方在於這個地方吧:
這個 @Intercepts 裏面的 @Signature 裏面為什麼要這樣配置?
我們先看看 @Intercepts 註解:
裏面是個數組,可以配置多個 Signature。所以,其實這樣配置也是可以的:
關鍵的地方在於 @Signature 怎麼配置:
這個問題,我們放到下一節去討論。
上面一小節我們知道了對於開發插件而言,難點在於 @Signature 怎麼配置。
其實這也不能叫難點,只能說你不知道能配置什麼,比較茫然而已。這一小節就來回答這個問題。
要知道怎麼配置就必須要了解mybatis 這四大對象:Executor、ParameterHandler 、ResultSetHandler 、StatementHandler 。
官網上說:
MyBatis 允許你在映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
那官網上說的這四大對象分別是拿來幹啥用的呢?
Executor:Mybatis 的執行器,用於進行增刪改查的操作。
ParameterHandler :參數處理器,用於處理 SQL 語句中的參數對象。
ResultSetHandler:結果處理器,用於處理 SQL 語句的返回結果。
StatementHandler :數據庫的處理對象,用於執行SQL語句
知道攔截的四大對象了,我們就可以先揭秘一下上面的這個註解配置的是啥了:
type 字段存放的是 class 對象,其取值範圍就是上面說的四大對象。
method 字段存放的是 class 對象的具體方法。
args 存放的是具體方法的參數。
看到這幾個參數你想到了什麼?有沒有條件反射式的想到反射?如果沒有的話你再咂摸咂摸,看看能不能品出一點反射的味道。
本文用攔截器的目的是判斷 delete 語句中是否有 where 條件,因此經過上面的分析,Executor 對象就能滿足我們的需求。
所以在本文示例中 @Signature 的 type 字段就是 Executor.class。
那 method 字段我們放哪個方法呢?放 delete 嗎?
這就得看看 Executor 對象的方法有哪些:
可以看到其中並沒有 delete 方法,和 SQL 執行相關的,看起來只有 query和 update。
但是,我們可以大膽猜測一下呀:delete 也是一種 update。
接着去求證一下就行:
可以看到 delete 方法確實是調用了 update 方法。
所以在本文案例中 @Signature 的 method 字段放的是 update 方法。
已經知道具體的方法了,那 args 放的就是方法的入參,所以這段配置就是這樣來的:
真的,我覺得這屬於手摸手教學系列了。經過這個簡單的案例,我希望大家能做到一通百通。
接下來帶大家看看我們常用的分頁插件 pageHelper 是怎麼做的吧。
其實你用腳指頭也能想到,分頁插件肯定是攔截的查詢方法,我們只是需要去驗證一下就可以。
引入 pageHelper 后可以看到 Interceptor 的多了兩個實現:
我們看一下 PageInterceptor 方法吧:
對吧,攔截了兩個 query 方法,一個參數是 4 個,一個參數是 6 個:
同時,在 intercept 的實現裏面有一部分是這樣寫的:
4 個參數和 6 個參數是做了單獨處理的,至於為什麼要這樣處理,至於為什麼要攔截兩個 query 方法,說起來又是一個很長的故事了。
詳細的可以看看這個鏈接: https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Interceptor.md
好了,還是那句話:如果要寫出好的 mybatis 插件,必須知道 @Signature 怎麼去配置。配置后能攔截哪些東西,你心裏應該是有點數的。
前面我們知道攔截器怎麼寫了,接下來簡單的分析一波原理。
前幾天我看到一個觀點是說看開源框架的源碼建議從 mybatis 看起。我是很贊成這個觀點的,確實是優雅,而容易看懂。能品出很多設計模式的使用。
一句話總結 mybatis插件的原理就是:動態代理加上責任鏈。
先看一下 Plugin 類的動態代理:
標號為 ① 的地方一看就知道,InvocationHandler,JDK 動態代理,沒啥說的。
標號為 ② 的地方是 wrap 方法,生成 Plugin 代理對象。
標號為 ③ 的地方是 invoker 方法,圈起來的目的是想說是在這裏判斷當前方法是否是需要被攔截的方法。如果是則用代理對象走攔截器邏輯,如果不是則用目標對象,走正常邏輯。
給大家看一下這個地方的 debug 效果:
一個平平無奇的 if 判斷,是攔截器的關鍵。為什麼這個地方多說了幾句呢?
因為其實這就是細節的地方。當面試的時候面試官問你:mybatis 是怎麼判斷是否需要攔截這個方法的時候你能答上來。說明你是真的看過源碼。
責任鏈是怎麼體現的呢?
就是這個地方: org.apache.ibatis.plugin.InterceptorChain
你看又學到一招,mybatis 裏面的設計模式還有責任鏈。
我們看一下 pluginAll 方法的調用方:
這個地方就體現出之前官網說的了:
插件是作用於這四大對象的:Executor、ParameterHandler 、ResultSetHandler 、StatementHandler 。
上面框起來的這四個框,就是插件調用的地方。
那麼插件在什麼時候被加載,或者說什麼是被註冊上的呢?
還是回到攔截鏈這個類上去:
pluginAll 方法我們已經知道有哪些地方調用了。這個方法裏面其實還有兩個考點。
第一就是 interceptor 這個 List 集合的定義,用了 final 修飾。所以要注意 final 修飾基本類型和引用類型的區別,被 final 修飾的引用類型變量內部的內容是可以發生變化的。
第二就是 getInterceptors 返回的是一個不可修改的 List 。所以,要對集合 interceptors 進行修改,只能通過 addInterceptor 方法進行元素添加,保證了這個集合是可控的。
所以,我們只需要知道哪裡調用了 addInterceptor 方法,哪裡就是插件被註冊的地方。
一個是 SqlSessionFactoryBean ,一個是 XMLConfigBuilder。
使用 XML 配置是這樣的:
熟悉 mybatis 的朋友們肯定知道,無非就是對於標籤的解析而已。
解析到 plugins 標籤,則進入 pluginElement 方法中,在這個方法裏面調用 addInterceptor:
本文沒有使用 XML 的形式配置,所以我們主要看一下 SqlSessionFactoryBean。
怎麼看呢?
不要盲目的走入源碼,加個斷點看調用鏈,跟着調用鏈去走就很清晰了。
在這個地方加一個斷點:
然後 debug 起來,你就可以看到整個調用鏈了:
然後我們根據上面的調用鏈,我們就可以找到源頭了:
在 MybatisAutoConfiguration 的構造方法裏面初始化了 interceptors。
而 interceptorsProvider.getIfAvailable() 方法也解釋了為什麼我們只需要在程序裏面這樣注入我們的攔截器就可以被找到了:
對 getIfAvailable 方法不熟悉的朋友可以去補一下這塊的知識,我這裏只是給大家看一下這個方法上的註釋:
當然,你這樣去注入的話有可能會不生效,你就會大罵一聲:寫的什麼垃圾玩意,配置上了也不對呀。
別著急呀,我還沒說完呢。你看看是不是有自定義的 SqlSessionFactory 在項目里。
看一下注入 SqlSessionFactory 的源碼上面的那個註解了嗎?
@ConditionalOnMissingBean ,看名字也知道了,當你的項目裏面沒有自定義的 SqlSessionFactory 的時候,才會由源碼給你注入,這個時候才會正在的註冊上插件:
如果你有自定義的 SqlSessionFactory,那麼請手動調用 factory.setPlugins 方法。
所以,總結一下插件的三種配置方法:
1.xml方式配置。
2.如果沒有自定義 SqlSessionFactory 直接 @Bean 注入攔截器即可。
3.如果有自定義 SqlSessionFactory 需要在自定義的地方手動調用 factory.setPlugins 方法。
其實我嘗試過第四種方法,在application.properties 裏面配置:
這種配置方式才是符合 SpringBoot 思想的配置。才是真正的絲滑,潤物無聲的絲滑。
可惜,我配置上后,點擊到對應的源碼地方一看:
它調用的是 getInterceptors 方法,我就知道肯定是有問題了:
果然,運行起來會報這樣的錯誤: Failed to bind properties under 'mybatis.configuration.interceptors' to java.util.List<org.apache.ibatis.plugin.Interceptor>
找了一圈原因,最後發現了這個 issue:
github.com/mybatis/spring-boot-starter/issues/180
這個“奇異博士”頭像的用戶提出了和我一樣的問題:
然後下面的回答是這樣的:
別問,問就是不支持。請使用 @Bean 的方式。
點個“贊”吧,周更很累的,不要白嫖我,需要一點正反饋。
才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你指出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被代碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。
歡迎關注我的微信公眾號:why技術。在這裏我會分享一些java技術相關的知識,用匠心敲代碼,對每一行代碼負責。偶爾也會荒腔走板的聊一聊生活,寫一寫書評、影評。感謝你的關注,願你我共同進步。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※網頁設計最專業,超強功能平台可客製化