什么是 AQS ?
1.什么是AQS?
AQS是英文單詞AbstractQueuedSynchronizer的縮寫,翻譯過來就是隊列同步器。
它是構建鎖或者其他同步組件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并發包的作者(Doug Lea)期望它能夠成為實現大部分同步需求的基礎。它是JUC并發包中的核心基礎組件。
2.AQS的實現方式?
AQS的主要使用方式是繼承,子類通過繼承同步器并實現它的抽象方法來管理同步狀態。
3.AQS的原理
AQS使用一個int類型的成員變量state來表示同步狀態,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操作,當然AQS可以確保對state的操作是安全的。
AQS通過內置的FIFO同步隊列來完成資源獲取線程的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)并將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。
4.AQS的兩種方式?
1)獨占式,同一時刻僅有一個線程持有同步狀態。
2)共享式與獨占式的最主要區別在于同一時刻獨占式只能有一個線程獲取同步狀態,而共享式在同一時刻可以有多個線程獲取同步狀態。例如讀操作可以有多個線程同時進行,而寫操作同一時刻只能有一個線程進行寫操作,其他操作都會被阻塞。
5.CLH同步隊列
CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理,當前線程如果獲取同步狀態失敗時,AQS則會將當前線程已經等待狀態等信息構造成一個節點(Node)并將其加入到CLH同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。
// 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;
/**
* 節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()后,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步狀態獲取將會無條件地傳播下去
*/
static final int PROPAGATE = -3; //傳播
/** 等待狀態 */
volatile int waitStatus;
/** 前驅節點 */
volatile Node prev;
/** 后繼節點 */
volatile Node next;
/** 獲取同步狀態的線程 */
volatile Thread thread;
/** 下一個等待的節點 */
Node nextWaiter;
/** 判斷下一個等待的節點是否是共享式 */
final boolean isShared() {
return nextWaiter == SHARED;
}
/** 獲取前驅節點 */
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;
}
}
CLH同步隊列結構圖:
入列(源碼分析):
/**
* 先通過快速嘗試設置尾節點,如果失敗,則調用enq(Node node)方法設置尾節點
* @param mode
* @return
*/
private Node addWaiter(Node mode) {
//新建Node
Node node = new Node(Thread.currentThread(), mode);
//快速嘗試添加尾節點,從不同步中獲取當前的尾巴節點
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS設置尾節點
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//多次嘗試設置尾節點
enq(node);
return node;
}
/**
* AQS通過“死循環”的方式來保證節點可以正確添加,只有成功添加后,當前線程才會從該方法返回,否則會一直執行下去。
* @param node
* @return
*/
private Node enq(final Node node) {
//自旋
for (; ; ) {
Node t = tail;
//tail不存在,設置為首節點
if (t == null) {
//CAS設置頭節點,原子性操作
if (compareAndSetHead(new Node())) {
tail = head;
}
} else {
//設置為尾節點
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
如圖所示:
出列:
CLH同步隊列遵循FIFO,首節點的線程釋放同步狀態后,將會喚醒它的后繼節點(next),而后繼節點將會在獲取同步狀態成功時將自己設置為首節點,這個過程非常簡單,head執行該節點并斷開原首節點的next和當前節點的prev即可,注意在這個過程是不需要使用CAS來保證的,因為只有一個線程能夠成功獲取到同步狀態。
6.獨占式同步狀態的獲取與釋放
1)獨占式獲取同步狀態(源碼分析):
/**
* 模板方法,該方法為獨占式獲取同步狀態,但是該方法對中斷不敏感,也就是說由于線程獲取同步狀態失敗加入到CLH同步隊列中,
* 后續對線程進行中斷操作時,線程不會從同步隊列中移除
* @param arg
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
//去嘗試獲取鎖,獲取成功則設置鎖狀態并返回true,否則返回false。該方法自定義同步組件自己實現,該方法必須要保證線程安全的獲取同步狀態
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//當前線程會根據公平性原則來進行阻塞等待(自旋),直到獲取鎖為止;并且返回當前線程在等待過程中有沒有中斷過。
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);
}
}
}
/**
* 設置當前線程中斷
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
/**
* 在自旋中,異常情況導致當前節點無法參與正常任務,則要進行取消,隊列移除
* 1。當前節點是尾部節點
* 2。當前節點不是尾部節點
* 取消正在進行嘗試獲取同步狀態的節點
*/
private void cancelAcquire(Node node) {
if (node == null) {
return;
}
node.thread = null;
//前驅節點
Node pred = node.prev;
//前驅節點狀態 > 0 ,則為Cancelled,表明該節點已經超時或者被中斷了,需要從同步隊列中取消
while (pred.waitStatus > 0) {
node.prev = pred = pred.prev;
}
//前驅節點的后繼節點
Node predNext = pred.next;
//設置當前節點的waitStatus為CANCELLED
node.waitStatus = Node.CANCELLED;
//如果當前節點是尾節點,并且設置前驅節點為尾節點成功
if (node == tail && compareAndSetTail(node, pred)) {
//原子操作
compareAndSetNext(pred, predNext, null);
} else {
int ws;
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 {
//喚醒后繼節點
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
/**
* 喚醒后繼節點
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
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;
}
}
}
if (s != null) {
LockSupport.unpark(s.thread);
}
}
2)獨占式獲取同步狀態響應中斷(源碼分析):
AQS提供了acquire(int arg)方法以供獨占式獲取同步狀態,但是該方法對中斷不響應,對線程進行中斷操作后,該線程會依然位于CLH同步隊列中等待著獲取同步狀態。為了響應中斷,AQS提供了acquireInterruptibly(int arg)方法,該方法在等待獲取同步狀態時,如果當前線程被中斷了,會立刻響應中斷拋出異常InterruptedException。
/**
* 獨占式獲取響應中斷
* @param arg
* @throws InterruptedException
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
//獲取同步狀態
if (!tryAcquire(arg)) {
doAcquireInterruptibly(arg);
}
}
/**
* 執行獨占式獲取響應中斷
* @param arg
* @throws InterruptedException
*/
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()) {
throw new InterruptedException();
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
doAcquireInterruptibly(int arg)方法與acquire(int arg)方法僅有兩個差別。
1.方法聲明拋出InterruptedException異常。
2.在中斷方法處不再是使用interrupted標志,而是直接拋出InterruptedException異常。
3)獨占式超時獲取同步狀態(源碼分析):
/**
* 獨占式超時獲取同步狀態
* @param arg
* @param nanosTimeout
* @return
* @throws InterruptedException
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException();
}
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//nanosTimeout <= 0
if (nanosTimeout <= 0L) {
return false;
}
//超時時間
final long deadline = System.nanoTime() + nanosTimeout;
//新增Node節點
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;
//如果沒有超時,則等待nanosTimeout納秒
//注:該線程會直接從LockSupport.parkNanos中返回,
//LockSupport為JUC提供的一個阻塞和喚醒的工具類,后面做詳細介紹
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//線程是否已經中斷了
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
流程圖如下:
4)獨占式同步狀態釋放(源碼分析):
/**
* 獨占式同步狀態釋放
* @param arg
* @return
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) {
//喚醒后繼節點
unparkSuccessor(h);
}
return true;
}
return false;
}
//模版方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
5)獨占式簡單總結
在AQS中維護著一個FIFO的同步隊列,當線程獲取同步狀態失敗后,則會加入到這個CLH同步隊列的對尾并一直保持著自旋。在CLH同步隊列中的線程在自旋時會判斷其前驅節點是否為首節點,
如果為首節點則不斷嘗試獲取同步狀態,獲取成功則退出CLH同步隊列。當線程執行完邏輯后,會釋放同步狀態,釋放后會喚醒其后繼節點。
7.共享式同步狀態的獲取與釋放
1)共享式同步狀態獲取
/**
* 共享式同步狀態獲取
*
* @param arg
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
//模版方法
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//自旋方式獲取同步狀態
private void doAcquireShared(int arg) {
//共享式節點
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
//前驅節點
final Node p = node.predecessor();
//如果其前驅節點,獲取同步狀態
if (p == head) {
//嘗試獲取同步
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted) {
selfInterrupt();
}
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
//設置頭,并且如果是共享模式且propagate大于0,則喚醒后續節點。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) {
doReleaseShared();
}
}
}
//共享式釋放同步狀態
private void doReleaseShared() {
for (; ; ) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
} else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
acquireShared(int arg)方法不響應中斷,與獨占式相似,AQS也提供了響應中斷、超時的方法,分別是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos)。
2)共享式同步狀態獲取
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
因為可能會存在多個線程同時進行釋放同步狀態資源,所以需要確保同步狀態安全地成功釋放,一般都是通過CAS和循環來完成的。
3)阻塞和喚醒線程
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
在線程獲取同步狀態時如果獲取失敗,則加入CLH同步隊列,通過通過自旋的方式不斷獲取同步狀態,但是在自旋的過程中則需要判斷當前線程是否需要阻塞,其主要方法在acquireQueued():
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
/**
* 根據前驅節點判斷當前線程是否應該被阻塞
*
* @param pred
* @param node
* @return
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驅節點狀態
int ws = pred.waitStatus;
//狀態為signal,表示當前線程處于等待狀態,直接返回true
if (ws == Node.SIGNAL) {
return true;
}
//前驅節點狀態 > 0 ,則為Cancelled,表明該節點已經超時或者被中斷了,需要從同步隊列中取消
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//前驅節點狀態為Condition、propagate
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e }
li.li1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e }
li.li2 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none; background-color: rgba(255, 255, 255, 1) }
span.s2 { background-color: rgba(255, 255, 255, 1) }
span.s3 { font-kerning: none }
ol.ol1 { list-style-type: decimal }
這段代碼主要檢查當前線程是否需要被阻塞,具體規則如下:
如果當前線程的前驅節點狀態為SINNAL,則表明當前線程需要被阻塞,調用unpark()方法喚醒,直接返回true,當前線程阻塞
如果當前線程的前驅節點狀態為CANCELLED(ws > 0),則表明該線程的前驅節點已經等待超時或者被中斷了,則需要從CLH隊列中將該前驅節點刪除掉,直到回溯到前驅節點狀態 <= 0 ,返回false
如果前驅節點非SINNAL,非CANCELLED,則通過CAS的方式將其前驅節點設置為SINNAL,返回false
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
如果 shouldParkAfterFailedAcquire(Node pred, Node node) 方法返回true,則調用parkAndCheckInterrupt()方法阻塞當前線程:
//阻塞當前線程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//設置當前線程中斷
return Thread.interrupted();
}
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
parkAndCheckInterrupt() 方法主要是把當前線程掛起,從而阻塞住線程的調用棧,同時返回當前線程的中斷狀態。其內部則是調用LockSupport工具類的park()方法來阻塞該方法。
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e }
span.s1 { font-kerning: none; background-color: rgba(255, 255, 255, 1) }
當線程釋放同步狀態后,則需要喚醒該線程的后繼節點:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//喚醒后繼節點
unparkSuccessor(h);
return true;
}
return false;
}
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e }
span.s1 { font-kerning: none; background-color: rgba(255, 255, 255, 1) }
調用unparkSuccessor(Node node)喚醒后繼節點:
private void unparkSuccessor(Node node) {
//當前節點狀態
int ws = node.waitStatus;
//當前狀態 < 0 則設置為 0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//當前節點的后繼節點
Node s = node.next;
//后繼節點為null或者其狀態 > 0 (超時或者被中斷了)
if (s == null || s.waitStatus > 0) {
s = null;
//從tail節點來找可用節點
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//喚醒后繼節點
if (s != null)
LockSupport.unpark(s.thread);
}
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
可能會存在當前線程的后繼節點為null,超時、被中斷的情況,如果遇到這種情況了,則需要跳過該節點,但是為何是從tail尾節點開始,而不是從node.next開始呢?原因在于node.next仍然可能會存在null或者取消了,所以采用tail回溯辦法找第一個可用的線程。最后調用LockSupport的unpark(Thread thread)方法喚醒該線程。
8.LockSupport
作用:當需要阻塞或者喚醒一個線程的時候,AQS都是使用LockSupport這個工具類來完成的。LockSupport是用來創建鎖和其他同步類的基本線程阻塞原語
每個使用LockSupport的線程都會與一個許可關聯,如果該許可可用,并且可在進程中使用,則調用park()將會立即返回,否則可能阻塞。如果許可尚不可用,則可以調用 unpark 使其可用。但是注意許可不可重入,也就是說只能調用一次park()方法,否則會一直阻塞。
LockSupport定義了一系列以park開頭的方法來阻塞當前線程,unpark(Thread thread)方法來喚醒一個被阻塞的線程。如下:
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e }
p.p2 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none; background-color: rgba(255, 255, 255, 1) }
span.s2 { font-kerning: none }
park(Object blocker)方法的blocker參數,主要是用來標識當前線程在等待的對象,該對象主要用于問題排查和系統監控。
park方法和unpark(Thread thread)都是成對出現的,同時unpark必須要在park執行之后執行,當然并不是說沒有不調用unpark線程就會一直阻塞,park有一個方法,它帶了時間戳(parkNanos(long nanos):為了線程調度禁用當前線程,最多等待指定的等待時間,除非許可可用)。
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none }
p.p1 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e }
p.p2 { margin: 0; text-align: justify; -webkit-hyphens: auto; font: 16px "Helvetica Neue"; color: rgba(62, 62, 62, 1); -webkit-text-stroke: #3e3e3e; background-color: rgba(255, 255, 255, 1) }
span.s1 { font-kerning: none; background-color: rgba(255, 255, 255, 1) }
span.s2 { font-kerning: none }
總結
- 上一篇: nmon命令用法
- 下一篇: 腾讯混元大模型正式亮相,超过50个业务已