環境資訊中心綜合外電;姜唯 編譯;林大利 審校
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※幫你省時又省力,新北清潔一流服務好口碑
※別再煩惱如何寫文案,掌握八大原則!
| 好看請贊,養成習慣
你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想
If you can NOT explain it simply, you do NOT understand it well enough
現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star
有了以上兩篇文章的鋪墊,來理解本文要介紹的既有獨佔式,又有共享式獲取同步狀態的 ReadWriteLock,就非常輕鬆了
ReadWriteLock 直譯過來為【讀寫鎖】。現實中,讀多寫少的業務場景是非常普遍的,比如應用緩存
一個線程將數據寫入緩存,其他線程可以直接讀取緩存中的數據,提高數據查詢效率
之前提到的互斥鎖都是排他鎖,也就是說同一時刻只允許一個線程進行訪問,當面對可共享讀的業務場景,互斥鎖顯然是比較低效的一種處理方式。為了提高效率,讀寫鎖模型就誕生了
效率提升是一方面,但併發編程更重要的是在保證準確性的前提下提高效率
一個寫線程改變了緩存中的值,其他讀線程一定是可以 “感知” 到的,否則可能導致查詢到的值不準確
所以關於讀寫鎖模型就了下面這 3 條規定:
ReadWriteLock 是一個接口,其內部只有兩個方法:
public interface ReadWriteLock {
// 返回用於讀的鎖
Lock readLock();
// 返回用於寫的鎖
Lock writeLock();
}
所以要了解整個讀/寫鎖的整個應用過程,需要從它的實現類 ReentrantReadWriteLock 說起
直接對比ReentrantReadWriteLock 與 ReentrantLock的類結構
他們又很相似吧,根據類名稱以及類結構,按照咱們前序文章的分析,你也就能看出 ReentrantReadWriteLock 的基本特性:
其中黃顏色標記的的 鎖降級 是看不出來的, 這裏先有個印象,下面會單獨說明
另外,不知道你是否還記得,Java AQS隊列同步器以及ReentrantLock的應用 說過,Lock 和 AQS 同步器是一種組合形式的存在,既然這裡是讀/寫兩種鎖,他們的組合模式也就分成了兩種:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
這裏只是提醒大家,模式沒有變,不要被讀/寫兩種鎖迷惑
說了這麼多,如果你忘了前序知識,整體理解感覺應該是有斷檔的,所以先來看個示例(模擬使用緩存)讓大家對 ReentrantReadWriteLock 有個直觀的使用印象
public class ReentrantReadWriteLockCache {
// 定義一個非線程安全的 HashMap 用於緩存對象
static Map<String, Object> map = new HashMap<String, Object>();
// 創建讀寫鎖對象
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 構建讀鎖
static Lock rl = readWriteLock.readLock();
// 構建寫鎖
static Lock wl = readWriteLock.writeLock();
public static final Object get(String key) {
rl.lock();
try{
return map.get(key);
}finally {
rl.unlock();
}
}
public static final Object put(String key, Object value){
wl.lock();
try{
return map.put(key, value);
}finally {
wl.unlock();
}
}
}
你瞧,使用就是這麼簡單。但是你知道的,AQS 的核心是鎖的實現,即控制同步狀態 state 的值,ReentrantReadWriteLock 也是應用AQS的 state 來控制同步狀態的,那麼問題來了:
一個 int 類型的 state 怎麼既控制讀的同步狀態,又可以控制寫的同步狀態呢?
顯然需要一點設計了
如果要在一個 int 類型變量上維護多個狀態,那肯定就需要拆分了。我們知道 int 類型數據佔32位,所以我們就有機會按位切割使用state了。我們將其切割成兩部分:
所以,要想準確的計算讀/寫各自的狀態值,肯定就要應用位運算了,下面代碼是 JDK1.8,ReentrantReadWriteLock 自定義同步器 Sync 的位操作
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
}
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
}
乍一看真是有些複雜的可怕,別慌,咱們通過幾道小小數學題就可以搞定整個位運算過程
整個 ReentrantReadWriteLock 中 讀/寫狀態的計算就是反覆應用這幾道數學題,所以,在閱讀下面內容之前,希望你搞懂這簡單的運算
基礎鋪墊足夠了,我們進入源碼分析吧
由於寫鎖是排他的,所以肯定是要重寫 AQS 中 tryAcquire 方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 獲取 state 整體的值
int c = getState();
// 獲取寫狀態的值
int w = exclusiveCount(c);
if (c != 0) {
// w=0: 根據推理二,整體狀態不等於零,寫狀態等於零,所以,讀狀態大於0,即存在讀鎖
// 或者當前線程不是已獲取寫鎖的線程
// 二者之一條件成真,則獲取寫狀態失敗
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 根據推理一第 1 條,更新寫狀態值
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
上述代碼 第 19 行 writerShouldBlock 也並沒有什麼神秘的,只不過是公平/非公平獲取鎖方式的判斷(是否有前驅節點來判斷)
你瞧,寫鎖獲取方式就是這麼簡單
由於讀鎖是共享式的,所以肯定是要重寫 AQS 中 tryAcquireShared 方法
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 寫狀態不等於0,並且鎖的持有者不是當前線程,根據約定 3,則獲取讀鎖失敗
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 獲取讀狀態值
int r = sharedCount(c);
// 這個地方有點不一樣,我們單獨說明
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 如果獲取讀鎖失敗則進入自旋獲取
return fullTryAcquireShared(current);
}
readerShouldBlock 和 writerShouldBlock 在公平鎖的實現上都是判斷是否有前驅節點,但是在非公平鎖的實現上,前者是這樣的:
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
// 等待隊列頭節點的下一個節點
(s = h.next) != null &&
// 如果是排他式的節點
!s.isShared() &&
s.thread != null;
}
簡單來說,如果請求讀鎖的當前線程發現同步隊列的 head 節點的下一個節點為排他式節點,那麼就說明有一個線程在等待獲取寫鎖(爭搶寫鎖失敗,被放入到同步隊列中),那麼請求讀鎖的線程就要阻塞,畢竟讀多寫少,如果還沒有這點判斷機制,寫鎖可能會發生【飢餓】
上述條件都滿足了,也就會進入
tryAcquireShared代碼的第 14 行到第 25 行,這段代碼主要是為了記錄線程持有鎖的次數。讀鎖是共享式的,還想記錄每個線程持有讀鎖的次數,就要用到 ThreadLocal 了,因為這不影響同步狀態 state 的值,所以就不分析了, 只把關係放在這吧
到這裏讀鎖的獲取也就結束了,比寫鎖稍稍複雜那麼一丟丟,接下來就說明一下那個可能讓你迷惑的鎖升級/降級問題吧
個人理解:讀鎖是可以被多線程共享的,寫鎖是單線程獨佔的,也就是說寫鎖的併發限制比讀鎖高,所以
在真正了解讀寫鎖的升級與降級之前,我們需要完善一下本文開頭 ReentrantReadWriteLock 的例子
public static final Object get(String key) {
Object obj = null;
rl.lock();
try{
// 獲取緩存中的值
obj = map.get(key);
}finally {
rl.unlock();
}
// 緩存中值不為空,直接返回
if (obj!= null) {
return obj;
}
// 緩存中值為空,則通過寫鎖查詢DB,並將其寫入到緩存中
wl.lock();
try{
// 再次嘗試獲取緩存中的值
obj = map.get(key);
// 再次獲取緩存中值還是為空
if (obj == null) {
// 查詢DB
obj = getDataFromDB(key); // 偽代碼:getDataFromDB
// 將其放入到緩存中
map.put(key, obj);
}
}finally {
wl.unlock();
}
return obj;
}
有童鞋可能會有疑問
在寫鎖裏面,為什麼代碼第19行還要再次獲取緩存中的值呢?不是多此一舉嗎?
其實這裏再次嘗試獲取緩存中的值是很有必要的,因為可能存在多個線程同時執行 get 方法,並且參數 key 也是相同的,執行到代碼第 16 行 wl.lock() ,比如這樣:
線程 A,B,C 同時執行到臨界區 wl.lock(), 只有線程 A 獲取寫鎖成功,線程B,C只能阻塞,直到線程A 釋放寫鎖。這時,當線程B 或者 C 再次進入臨界區時,線程 A 已經將值更新到緩存中了,所以線程B,C沒必要再查詢一次DB,而是再次嘗試查詢緩存中的值
既然再次獲取緩存很有必要,我能否在讀鎖里直接判斷,如果緩存中沒有值,那就再次獲取寫鎖來查詢DB不就可以了嘛,就像這樣:
public static final Object getLockUpgrade(String key) {
Object obj = null;
rl.lock();
try{
obj = map.get(key);
if (obj == null){
wl.lock();
try{
obj = map.get(key);
if (obj == null) {
obj = getDataFromDB(key); // 偽代碼:getDataFromDB
map.put(key, obj);
}
}finally {
wl.unlock();
}
}
}finally {
rl.unlock();
}
return obj;
}
這還真是不可以的,因為獲取一個寫入鎖需要先釋放所有的讀取鎖,如果有兩個讀取鎖試圖獲取寫入鎖,且都不釋放讀取鎖時,就會發生死鎖,所以在這裏,鎖的升級是不被允許的
讀寫鎖的升級是不可以的,那麼鎖的降級是可以的嘛?這個是 Oracle 官網關於鎖降級的示例 ,我將代碼粘貼在此處,大家有興趣可以點進去連接看更多內容
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 必須在獲取寫鎖之前釋放讀鎖,因為鎖的升級是不被允許的
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 再次檢查,原因可能是其他線程已經更新過緩存
if (!cacheValid) {
data = ...
cacheValid = true;
}
//在釋放寫鎖前,降級為讀鎖
rwl.readLock().lock();
} finally {
//釋放寫鎖,此時持有讀鎖
rwl.writeLock().unlock();
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
代碼中聲明了一個 volatile 類型的 cacheValid 變量,保證其可見性。
這個過程就是一個完整的鎖降級的過程,目的是保證數據可見性,聽起來很有道理的樣子,那麼問題來了:
上述代碼為什麼在釋放寫鎖之前要獲取讀鎖呢?
如果當前的線程A在修改完cache中的數據后,沒有獲取讀鎖而是直接釋放了寫鎖;假設此時另一個線程B 獲取了寫鎖並修改了數據,那麼線程A無法感知到數據已被修改,但線程A還應用了緩存數據,所以就可能出現數據錯誤
如果遵循鎖降級的步驟,線程A 在釋放寫鎖之前獲取讀鎖,那麼線程B在獲取寫鎖時將被阻塞,直到線程A完成數據處理過程,釋放讀鎖,從而保證數據的可見性
那問題又來了:
使用寫鎖一定要降級嗎?
如果你理解了上面的問題,相信這個問題已經有了答案。假如線程A修改完數據之後, 經過耗時操作后想要再使用數據時,希望使用的是自己修改后的數據,而不是其他線程修改后的數據,這樣的話確實是需要鎖降級;如果只是希望最後使用數據的時候,拿到的是最新的數據,而不一定是自己剛修改過的數據,那麼先釋放寫鎖,再獲取讀鎖,然後使用數據也無妨
在這裏我要額外說明一下你可能存在的誤解:
如果已經釋放了讀鎖再獲取寫鎖不叫鎖的升級
如果已經釋放了寫鎖在獲取讀鎖也不叫鎖的降級
相信你到這裏也理解了鎖的升級與降級過程,以及他們被允許或被禁止的原因了
本文主要說明了 ReentrantReadWriteLock 是如何應用 state 做位拆分實現讀/寫兩種同步狀態的,另外也通過源碼分析了讀/寫鎖獲取同步狀態的過程,最後又了解了讀寫鎖的升級/降級機制,相信到這裏你對讀寫鎖已經有了一定的理解。如果你對文中的哪些地方覺得理解有些困難,強烈建議你回看本文開頭的兩篇文章,那裡鋪墊了非常多的內容。接下來我們就看看在應用AQS的最後一個併發工具類 CountDownLatch 吧
// WriteLock
public Condition newCondition() {
return sync.newCondition();
}
// ReadLock
public Condition newCondition() {
throw new UnsupportedOperationException();
}
個人博客:https://dayarch.top
加我微信好友, 進群娛樂學習交流,備註「進群」
歡迎持續關注公眾號:「日拱一兵」
- 前沿 Java 技術乾貨分享
- 高效工具匯總 | 回復「工具」
- 面試問題分析與解答
- 技術資料領取 | 回復「資料」
以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注……
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※幫你省時又省力,新北清潔一流服務好口碑
※別再煩惱如何寫文案,掌握八大原則!
項目演示地址
React 框架的優雅不言而喻,組件化的編程思想使得React框架開發的項目代碼簡潔,易懂,但早期 React 類組件的寫法略顯繁瑣。React Hooks 是 React 16.8 發布以來最吸引人的特性之一,她簡化了原有代碼的編寫,是未來 React 應用的主流寫法。
本文通過一個實戰小項目,手把手從零開始帶領大家快速入門React Hooks。
在本項目中,會用到以下知識點:
(1)安裝node.js 官網鏈接
(2)安裝vscode 官網鏈接
(3)安裝 creat-react-app 功能組件,該組件可以用來初始化一個項目, 即按照一定的目錄結構,生成一個新項目。
打開cmd 窗口 輸入:
npm install --g create-react-app
npm install --g yarn
(-g 代表全局安裝)
如果安裝失敗或較慢。需要換源,可以使用淘寶NPM鏡像,設置方法為:
npm config set registry https://registry.npm.taobao.org
設置完成后,重新執行
npm install --g create-react-app
npm install --g yarn
(4)在你想創建項目的目錄下 例如 D:/project/ 打開cmd命令 輸入
create-react-app react-exam
去使用creat-react-app命令創建名字是react-exam的項目
安裝完成后,移至新創建的目錄並啟動項目
cd react-exam
yarn start
一旦運行此命令,localhost:3000新的React應用程序將彈出一個新窗口。
右鍵react-exam目錄,使用vscode打開該目錄。
react-exam項目目錄中有一個/public和/src目錄,以及node_modules,.gitignore,README.md,和package.json。
在目錄/public中,重要文件是index.html,其中一行代碼最重要
<div id="root"></div>
該div做為我們整個應用的掛載點
/src目錄將包含我們所有的React代碼。
要查看環境如何自動編譯和更新您的React代碼,請找到文件/src/App.js:
將其中的
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
修改為
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
和豆約翰 Learn React
</a>
保存文件后,您會注意到localhost:3000編譯並刷新了新數據。
1.安裝項目依賴,在package.json中添加:
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"axios": "^0.19.2",
"bootstrap": "^4.5.0",
"he": "^1.2.0",
"react-loading": "^2.0.3",
"reactstrap": "^8.4.1"
},
執行命令:
yarn install
修改index.js,導入bootstrap樣式
import "bootstrap/dist/css/bootstrap.min.css";
修改App.css代碼
html {
width: 80%;
margin-left: 10%;
margin-top: 2%;
}
.ansButton {
margin-right: 4%;
margin-top: 4%;
}
修改App.js,引入Quiz組件
import React from 'react';
import './App.css'
import { Quiz } from './Exam/Quiz';
function App() {
return (
<div className = 'layout'>
<Quiz></Quiz>
</div>
);
}
export default App;
在項目src目錄下新增Exam目錄,Exam目錄中新建Quiz.js
Quiz組件的定義如下:
Quiz.js,引入開始頁面組件Toggle。
import React, { useState } from "react";
import { Toggle } from "./Toggle";
export const Quiz = () => {
const [questionData, setQuestionData] = useState([]);
const questions = questionData.map(({ question }) => [question]);
const answers = questionData.map(({ incorrect_answers, correct_answer }) =>
[correct_answer, incorrect_answers].flat()
);
return (
<>
<Toggle
setQuestionData={setQuestionData}
/>
</>
);
};
Toggle.js,點擊開始按鈕,通過axios訪問遠程接口,獲得題目及答案。
import React from "react";
import axios from "axios";
import ToggleHeader from "./ToggleHeader";
import {
Button,
Form,
} from "reactstrap";
export const Toggle = ({
setQuestionData,
}) => {
const getData = async () => {
try {
const incomingData = await axios.get(
`https://opentdb.com/api.php?amount=10&category=18&difficulty=easy&type=multiple`
);
setQuestionData(incomingData.data.results);
} catch (err) {
console.error(err);
}
};
return (
<>
<ToggleHeader />
<Form
onSubmit={(e) => {
e.preventDefault();
getData();
}}
>
<Button color="primary">開始</Button>
</Form>
</>
);
};
ToggleHeader.js
import React from "react";
import { Jumbotron, Container} from "reactstrap";
export default function ToggleHeader() {
return (
<Jumbotron fluid>
<Container fluid>
<h1 className="display-4">計算機知識小測驗</h1>
</Container>
</Jumbotron>
);
}
https://opentdb.com/api.php接口返回的json數據格式為
{
"response_code": 0,
"results": [{
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "The numbering system with a radix of 16 is more commonly referred to as ",
"correct_answer": "Hexidecimal",
"incorrect_answers": ["Binary", "Duodecimal", "Octal"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "This mobile OS held the largest market share in 2012.",
"correct_answer": "iOS",
"incorrect_answers": ["Android", "BlackBerry", "Symbian"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "How many values can a single byte represent?",
"correct_answer": "256",
"incorrect_answers": ["8", "1", "1024"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "In computing, what does MIDI stand for?",
"correct_answer": "Musical Instrument Digital Interface",
"incorrect_answers": ["Musical Interface of Digital Instruments", "Modular Interface of Digital Instruments", "Musical Instrument Data Interface"]
}, {
"category": "Science: Computers",
"type": "multiple",
"difficulty": "easy",
"question": "In computing, what does LAN stand for?",
"correct_answer": "Local Area Network",
"incorrect_answers": ["Long Antenna Node", "Light Access Node", "Land Address Navigation"]
}]
}
程序運行效果:
當前項目目錄結構為:
Quiz.js,新增toggleView變量用來切換視圖。
const [toggleView, setToggleView] = useState(true);
Quiz.js,其中Question和QuestionHeader 組件,參見後面。
import { Question } from "./Question";
import { Jumbotron } from "reactstrap";
import QuestionHeader from "./QuestionHeader";
...
export const Quiz = () => {
var [index, setIndex] = useState(0);
const [questionData, setQuestionData] = useState([]);
...
return (
<>
{toggleView && (
<Toggle
setIndex={setIndex}
setQuestionData={setQuestionData}
setToggleView={setToggleView}
/>
)}
{!toggleView &&
(
<Jumbotron>
<QuestionHeader
setToggleView={setToggleView}
/>
<Question question={questions[index]} />
</Jumbotron>
)}
</>
);
使用index控制題目索引
var [index, setIndex] = useState(0);
修改Toggle.js
獲取完遠程數據,通過setToggleView(false);切換視圖。
export const Toggle = ({
setQuestionData,
setToggleView,
setIndex,
}) => {
...
return (
<>
<ToggleHeader />
<Form
onSubmit={(e) => {
e.preventDefault();
getData();
setToggleView(false);
setIndex(0);
}}
>
<Button color="primary">開始</Button>
</Form>
</>
);
};
QuestionHeader.js代碼:
同樣的,點擊 返回首頁按鈕 setToggleView(true),切換視圖。
import React from "react";
import { Button } from "reactstrap";
export default function QuestionHeader({ setToggleView, category }) {
return (
<>
<Button color="link" onClick={() => setToggleView(true)}>
返回首頁
</Button>
</>
);
}
Question.js代碼
接受父組件傳過來的question對象,並显示。
其中he.decode是對字符串中的特殊字符進行轉義。
import React from "react";
import he from "he";
export const Question = ({ question }) => {
// he is a oddly named library that decodes html into string values
var decode = he.decode(String(question));
return (
<div>
<hr className="my-2" />
<h1 className="display-5">
{decode}
</h1>
<hr className="my-2" />
<br />
</div>
);
};
程序運行效果:
首頁
點擊開始后,显示問題:
當前項目目錄結構為:
新增LoadingSpin.js
import React from "react";
import { Spinner } from "reactstrap";
export default function LoadingSpin() {
return (
<>
<Spinner type="grow" color="primary" />
<Spinner type="grow" color="secondary" />
<Spinner type="grow" color="success" />
<Spinner type="grow" color="danger" />
</>
);
}
修改Quiz.js
import LoadingSpin from "./LoadingSpin";
export const Quiz = () => {
const [isLoading, setLoading] = useState(false);
return (
<>
{toggleView && (
<Toggle
...
setLoading={setLoading}
/>
)}
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
...
))}
</>
);
};
修改Toggle.js
export const Toggle = ({
...
setLoading,
}) => {
const getData = async () => {
try {
setLoading(true);
const incomingData = await axios.get(
`https://opentdb.com/api.php?amount=10&category=18&difficulty=easy&type=multiple`
);
setQuestionData(incomingData.data.results);
setLoading(false);
} catch (err) {
console.error(err);
}
};
...
};
運行效果:
目前代碼結構:
新增Answer.js,用戶點擊下一題按鈕,修改index,觸發主界面刷新,显示下一題:
import React from "react";
import { Button } from "reactstrap";
export const Answer = ({ setIndex, index }) => {
function answerResult() {
setIndex(index + 1);
}
return (
<Button className="ansButton" onClick={answerResult}>
下一題
</Button>
);
};
修改Quiz.js,添加Answer組件:
import { Answer } from "./Answer";
...
{!toggleView &&
(isLoading ? (
<LoadingSpin />
) :
(
<Jumbotron>
...
<Answer
setIndex={setIndex}
index={index}
/>
</Jumbotron>
))}
運行效果:
點擊下一題:
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※新北清潔公司,居家、辦公、裝潢細清專業服務
※別再煩惱如何寫文案,掌握八大原則!
※教你寫出一流的銷售文案?
※超省錢租車方案
目錄
上篇文章我們講到了JIT中的LogCompilation,將編譯的日誌都收集起來,存到日誌文件裏面,並且詳細的解釋了LogCompilation日誌文件中的內容定義。今天我們再和小師妹一起學習LogCompilation的姊妹篇PrintCompilation,看看都有什麼妙用吧。
小師妹:F師兄,上次你給講的LogCompilation實在是太複雜了,生成的日誌文件又多,完全看不了,我其實只是想知道有哪些方法被編譯成了機器碼,有沒有什麼更加簡單的辦法呢?
真理的大海,讓未發現的一切事物躺卧在我的眼前,任我去探尋- 牛頓(英國)
當然有的,那就給你介紹一下LogCompilation的妹妹PrintCompilation,為什麼是妹妹呢?因為PrintCompilation輸出的日誌要比LogCompilation少太多了。
老規矩,上上我們的JMH運行代碼,文章中使用的代碼鏈接都會在文末註明,這裏使用圖片的原因只是為了方便讀者閱讀代碼:
這裏和上次的LogCompilation不同的是,我們使用:-XX:+PrintCompilation參數。
其實我們還可以添加更多的參數,例如:
-Xbatch -XX:-TieredCompilation -XX:+PrintCompilation
先講一下-Xbatch。
一般來說JIT編譯器使用的是和主線程完全不同的新的線程。這樣做的好處就是JIT可以和主線程并行執行,編譯器的運行基本上不會影響到主線程的的運行。
但是有陰就有陽,有利就有弊。多線程在提高的處理速度的同時,帶給我們的就是輸出日誌的混亂。因為是并行執行的,我們主線程的日誌中,穿插了JIT編譯器的線程日誌。
如果使用-Xbatch就可以強迫JIT編譯器使用主線程。這樣我們的輸出日誌就是井然有序的。真棒。
再講一下TieredCompilation。
為了更好的提升編譯效率,JVM在JDK7中引入了分層編譯Tiered compilation的概念。
大概來說分層編譯可以分為三層:
第一層就是禁用C1和C2編譯器,這個時候沒有JIT進行。
第二層就是只開啟C1編譯器,因為C1編譯器只會進行一些簡單的JIT優化,所以這個可以應對常規情況。
第三層就是同時開啟C1和C2編譯器。
在JDK8中,分層編譯是默認開啟的。因為不同的編譯級別處理編譯的時間是不一樣的,後面層級的編譯器啟動的要比前面層級的編譯器要慢,但是優化的程度更高。
這樣我們其實會產生很多中間的優化代碼,這裏我們只是想分析最終的優化代碼,所以我們需要停止分層編譯的功能。
最後是今天的主角:PrintCompilation。
PrintCompilation將會輸出被編譯方法的統計信息,因此使用PrintCompilation可以很方便的看出哪些是熱點代碼。熱點代碼也就意味着存在着被優化的可能性。
小師妹:F師兄,我照着你的例子運行了一下,結果果然清爽了很多。可是我還是看不懂。
沒有一個人能全面把握真理。小師妹,我們始終在未知的路上前行。不懂就問,不會就學。
我們再截個圖看一下生成的日誌吧。
因為日誌太長了,為了節約大家的流量,我只截取了開頭的部分和結尾的部分。
大家可以看到開頭部分基本上都是java自帶的類的優化。只有最後才是我們自己寫的類。
第一列是方法開始編譯的時間。
第二列是簡單的index。
第三列是一系列的flag的組合,有下面幾個flag:
b Blocking compiler (always set for client)
* Generating a native wrapper
% On stack replacement (where the compiled code is running)
! Method has exception handlers
s Method declared as synchronized
n Method declared as native
made non entrant compilation was wrong/incomplete, no future callers will use this version
made zombie code is not in use and ready for GC
如果我們沒有關閉分層編譯的話,在方法名前面還會有数字,表示是使用的那個編譯器。
分層編譯詳細的來說可以分為5個級別。
0表示是使用解釋器,不使用JIT編譯。
1,2,3是使用C1編譯器(client)。
4是使用C2編譯器(server)。
現在讓我們來看一下最後一列。
最後一列包含了方法名和方法的長度。注意這裏的長度指的是字節碼的長度。
如果字節碼被編譯成為機器碼,長度會增加很多倍。
本文介紹了JIT中PrintCompilation的使用,並再次複習了JIT中的分層編譯架構。希望大家能夠喜歡。
本文的例子https://github.com/ddean2009/learn-java-base-9-to-20
本文作者:flydean程序那些事
本文鏈接:http://www.flydean.com/jvm-jit-printcompilation/
本文來源:flydean的博客
歡迎關注我的公眾號:程序那些事,更多精彩等着您!
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
公司有一個數據處理線,上面的數據經過不同環境處理,然後上線到正式庫。其中一個環節需要將數據進行處理然後導入到另外一個庫(Sql Server)。這個處理的程序是老大用python寫的,處理完後進入另外一個庫后某些字段出現了亂碼。
比如這個字符串:1006⁃267X(2020)02⁃0548⁃10
另外一個庫變成:1006?267X(2020)02?0548?10
線上人員反饋回來后老大由於比較忙,一直沒有排查,然後我問了下估計是什麼原因。老大說他python裏面轉了utf8,可能是編碼問題。我當時問了下就沒下文了,因為我不會python,所以這個事情就擱置了。
然後這個問題拖了很久,線上也不斷反饋。同時自己也負責這塊,空閑時間就主動去排查了下原因。當然這個排查過程還是比較曲折的,所以就把這個過程分享下,同時回顧下涉及到的知識點。
先說結果:最後經過排查是由於python處理后insert語句插入到Sql Server數據庫保存字段前沒有加N。
1.SQL Server數據類型
首先由於數據寫進去出現亂碼,所以第一步就是檢查寫入庫的字段是否設置了正確的數據類型。因為有時候對char與varchar的區別或者varchar與nvarchar的區別不是很在意,所以有可能設置了錯誤的數據類型。至於這幾個字符的數據類型區別是什麼,這裏摘抄官方解釋。
字符數據類型 char(大小固定)或 varchar(大小可變) 。 從 SQL Server 2019 (15.x) 起,使用啟用了 UTF-8 的排序規則時,這些數據類型會存儲 Unicode 字符數據的整個範圍,並使用 UTF-8 字符編碼。 若指定了非 UTF-8 排序規則,則這些數據類型僅會存儲該排序規則的相應代碼頁支持的字符子集。
參數
char [ ( n ) ]
固定大小字符串數據 。 n 用於定義字符串大小(以字節為單位),並且它必須為 1 到 8,000 之間的值 。 對於單字節編碼字符集(如拉丁文),存儲大小為 n 個字節,並且可存儲的字符數也為 n。 對於多字節編碼字符集,存儲大小仍為 n 個字節,但可存儲的字符數可能小於 n。 char 的 ISO 同義詞是 character 。
varchar [ ( n | max ) ]
可變大小字符串數據 。 使用 n 定義字符串大小(以字節為單位),可以是介於 1 和 8,000 之間的值;或使用 max 指明列約束大小上限為最大存儲 2^31-1 個字節 (2GB)。 對於單字節編碼字符集(如拉丁文),存儲大小為 n + 2 個字節,並且可存儲的字符數也為 n。 對於多字節編碼字符集,存儲大小仍為 n + 2 個字節,但可存儲的字符數可能小於 n 。
字符數據類型 nchar(大小固定)或 nvarchar(大小可變) 。 從 SQL Server 2012 (11.x) 起,使用啟用了補充字符 (SC) 的排序規則時,這些數據類型會存儲 Unicode 字符數據的整個範圍,並使用 UTF-16 字符編碼。 若指定了非 SC 排序規則,則這些數據類型僅會存儲 UCS-2 字符編碼支持的字符數據子集。
nchar [ ( n ) ]
固定大小字符串數據。 n 用於定義字符串大小(以雙字節為單位),並且它必須為 1 到 4,000 之間的值 。 存儲大小為 n 字節的兩倍。 對於 UCS-2 編碼,存儲大小為 n 個字節的兩倍,並且可存儲的字符數也為 n。 對於 UTF-16 編碼,存儲大小仍為 n 個字節的兩倍,但可存儲的字符數可能小於 n,因為補充字符使用兩個雙字節(也稱為代理項對)。 nchar 的 ISO 同義詞是 national char 和 national character 。
nvarchar [ ( n | max ) ]
可變大小字符串數據。 n 用於定義字符串大小(以雙字節為單位),並且它可能為 1 到 4,000 之間的值 。 max 指示最大存儲大小是 2^30-1 個字符 (2 GB) 。 存儲大小為 n 字節的兩倍 + 2 個字節。 對於 UCS-2 編碼,存儲大小為 n 個字節的兩倍 + 2 個字節,並且可存儲的字符數也為 n。 對於 UTF-16 編碼,存儲大小仍為 n 個字節的兩倍 + 2 個字節,但可存儲的字符數可能小於 n,因為補充字符使用兩個雙字節(也稱為代理項對)。 nvarchar 的 ISO 同義詞是 national char varying 和 national character varying 。
通過上面的描述我們可以總結:這幾種類型都是存儲字符數據,如果存儲單字節的字符串(比如英文)使用char、varchar,節約空間。如果存儲多字節的字符串(比如包含中文)使用nchar、nvarchar,兼容更多的編碼。雙字節比單字節對應的多了一個n。
單字節雙字節中還有一個區別var,表示可變大小字符串數據。可變是指如果某字段插入的值超過了數據頁的長度,該行的字段值將存放到ROW_OVERFLOW_DATA中。但是會造成多餘的I/O,比如一個VARCHAR列經常被修改,而且每次被修改的數據的長度不同,這會引起‘行遷移’(Row Migration)現象。這裏就不展開了,可以去了解下。
所以我們設計數據庫字段的時候需要根據業務設計合理的數據類型,有利於節約空間和時間。而經過我檢查數據庫字段確實設置的nvarchar,所以不存在存儲不了對應編碼問題。而且問了老大他說python裏面他轉了UTF8編碼,所以下一步就是排查是否轉編碼出了問題。
2.編碼
因為我經常寫C#,C#裏面的字符串是Unicode的,當然對於程序員來說這個編碼是透明的,因為是Unicode編碼可以轉換成其它任何編碼,所以我們日常開發的時候並不需要時刻去關注編碼的問題,其底層已經幫我們進行了處理。既然說是python轉了utf8那麼我就去大概看了下python的基礎並試驗了一把。
先找了一條出現亂碼的數據,在原庫取出來然後進行utf8轉碼,然後再解碼。講道理同一個編碼解碼出來存儲應該還是原來的字符串,所以我才會好奇去試驗。試驗后發現果然沒有什麼問題。
關於編碼可以看下這個講解:編碼,因為講的比較形象而且容易理解,所以我這裏就不累述了。
排除python程序編碼問題,那接下來就是要排查從程序插入到數據庫這一段的問題了。
3.SQL Server排序規則
首先插入這一階段我想到的還是編碼問題,所以去查詢了數據庫編碼。使用sql語句查詢數據庫排序規則
SELECT COLLATIONPROPERTY('Chinese_PRC_Stroke_CI_AI_KS_WS', 'CodePage')
對應的字符集編碼
936 :簡體中文GBK
950 :繁體中文BIG5
437 :美國/加拿大英語
932 :日文
949 :韓文
866 :俄文
65001 :unicode UTF-8
查詢了數據排序規則,導入數據庫是默認排序規則,也就是936 GBK編碼。為什麼要看數據庫排序規則,第1點中可見“數據類型僅會存儲該排序規則的相應代碼頁支持的字符子集”。
排序規則微軟解釋:排序規則
SQL Server 中的排序規則可為您的數據提供排序規則、區分大小寫屬性和區分重音屬性。 與諸如 char 和 varchar 等字符數據類型一起使用的排序規則規定可表示該數據類型的代碼頁和對應字符 。
無論你是要安裝 SQL Server 的新實例、還原數據庫備份,還是將服務器連接到客戶端數據庫,都必須了解正在處理的數據的區域設置要求、排序順序以及是否區分大小寫和重音。
所以通過查看排序規則知道,默認編碼是GBK。然後我就猜測到是GBK編碼問題,因為在python3裏面字符串的默認編碼也是Unicode,測試下把1006⁃267X(2020)02⁃0548⁃10轉成GBK。
可以看到是無法轉碼的,gbk識別不了那個短橫杠,然後我編碼成GB18030能夠編碼。說明短橫杠是更高位的編碼,當然unicode是能存儲的。那為什麼在數據庫裏面就成了亂碼呢?而且字段類型是設置的nvarchar啊。
4、大寫字母“N”作為前綴
通過3點的分析,說明了本該存儲成Unicode的編碼被保存成了默認編碼。所以我們只要把保存成Unicode編碼就行了,所以到此已經和python程序沒什麼關係了,帶着懷疑的態度,我將這段字符直接拿到Sql Sever裏面執行,果然也是亂碼。
最後就是在參數前加N執行
這下結果就正常了。細心的你是否發發現v1字段還是亂碼,因為我為了測試varchar單字節,即使我加了N一樣的是亂碼。所以記得存儲中文最好選nvarchar,原因么請看第一點char和varchar的說明中這樣一句話:若指定了非 UTF-8 排序規則,則這些數據類型僅會存儲該排序規則的相應代碼頁支持的字符子集。也就是它只會存儲我當前數據庫的GBK編碼。
最後我還在python裏面插入的sql語句加了N,同樣可以插入成功。
關於加N的解釋,微軟t-sql文檔關於insert說明:鏈接
5.為什麼我們平時很少加N
既然有這樣的問題為什麼我們平時基本沒加過N?原因有幾點:
SqlParameter會自動加N?帶着懷疑的態度測試下。
首先寫一個測試程序,然後開啟SQL server跟蹤來查看執行的sql。
static void Test()
{
string server = "127.0.0.1";
string database = "TestDB";
string user = "sa";
string password = "******";
string connectionString = $"server={server};database={database};User ID={user};Password={password}";
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
using (SqlCommand cmd = new SqlCommand())
{
cmd.Connection = connection;
cmd.CommandText = "insert into Test1 values('1006⁃267X(2020)02⁃0548⁃10','1006⁃267X(2020)02⁃0548⁃10')";
cmd.ExecuteNonQuery();
cmd.CommandText = "insert into Test1 values(@v1,@v2)";
cmd.Parameters.Add(new SqlParameter
{
ParameterName = "v1",
Value = "1006⁃267X(2020)02⁃0548⁃10"
});
cmd.Parameters.Add(new SqlParameter
{
ParameterName = "v2",
Value = "1006⁃267X(2020)02⁃0548⁃10"
});
cmd.ExecuteNonQuery();
}
}
}
查看跟蹤執行的sql,一個是直接傳入拼接sql執行,一個是使用參數方式執行。
通過一次排查亂碼問題,又回顧或者學習了關於數據類型和編碼,以及sql存儲如何避免亂碼問題。平時設計的時候如果是帶中文的字段首先考慮帶n的char類型。同時在直接使用sql進行insert、update的時候注意在要保存為Unicode編碼字符串前面加N。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
摘錄自2019年10月7日中央社報導
中國7月起派遣探勘船「海洋地質8號」等船隻進入南沙群島最西側淺灘萬安灘(Vanguard Bank)探勘石油,越南將萬安灘稱為思政灘(Tu Chinh),聲稱依據聯合國海洋法公約,萬安灘位於越南200海里專屬經濟區內,出動相關船隻阻擾,與中方船隻發生對峙,越方並多次向中方抗議。兩國對峙數月,越南法律政策與發展研究所6日舉行座談會,與會學者建議越南將中國近期在萬安灘海域所採取威脅和武力行為提交聯合國安全理事會。
學者表示,中國近年來企圖將整個南海變成「家裡水池」,對東沙、西沙、中沙和南沙四大群島提出主權聲索,其中主張在與越南有爭議的西沙和南沙兩個群島設立專屬經濟區和大陸棚,目的是將沒有爭議海域變成有爭議海域,從而提出「共同資源開採」的聲索。
越南法律政策與發展研究所所長黃玉交(HoangNgoc Giao)表示,中國近年來在南海採取的行為,尤其是近期在歸屬越南專屬經濟區的萬安灘海域的既威脅又使用武力的行為,已違反聯合國憲章;國際社會反應也顯示,這是威脅區域和平、安全以及違反國際法基本原則的行為,中國不僅侵犯越南海域,還侵犯馬來西亞和菲律賓海域。越南應趁著擔任聯合國安理會2020年至2021年非常任理事國之際,將中國在南海威脅和武力行為向聯合國安理會提交。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※為什麼 USB CONNECTOR 是電子產業重要的元件?
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※台北網頁設計公司全省服務真心推薦
※想知道最厲害的網頁設計公司"嚨底家"!
※新北清潔公司,居家、辦公、裝潢細清專業服務
※推薦評價好的iphone維修中心
摘錄自2019年10月7日聯合報報導
「反抗滅絕」在全球發起公民不服從運動7日展開,在澳洲和紐西蘭各地有數十名示威者遭到逮捕。這場運動將為期2週,目的為了要求各國政府採取緊急行動對抗氣候變遷。
數百人在繁忙的雪梨市中心道路上靜坐封路,最後警察拖走示威者終止這場活動,隨後有30人遭到起訴。
在布里斯本,一小群運動人士把自己鎖在一座橋上。警方表示,他們在現場逮捕7人,並全數加以起訴。
同一時間在紐西蘭首都威靈頓,示威者用鎖鏈把自己與一輛閃亮的粉紅色車輛綁住,造成市區部分交通癱瘓。警方表示,今天稍晚有30人遭到逮捕,但無人被起訴。
荷蘭阿姆斯特丹市7日出現數百名氣候變遷倡議人士,他們無視警方的禁令,高喊「反抗」,阻礙市中心交通。警方已警告在先,將逮捕違令人士,防止他們擾亂通勤民眾。
荷蘭警方已核准倡議團體「反抗滅絕」(Extinction Rebellion, XR)的抗議計劃,前提是不能阻礙交通。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
摘錄自2019年10月8日中央社報導
抗議厄瓜多總統莫雷諾(Lenin Moreno)政府取消燃料補助導致油價飆漲的示威活動越演越烈,警方和示威者8日在國民議會爆發衝突。
根據Ecuavisa頻道報導,示威者成功突破國會大廈封鎖線,當中有許多是手持棍棒和鞭子的原住民男子。示威者衝進會議室霸占講台,但幾分鐘後就遭到安全部隊驅離。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※新北清潔公司,居家、辦公、裝潢細清專業服務
※教你寫出一流的銷售文案?