1. 多線程控制類
為了保證多線程的三個特性,Java引入了很多線程控制機制,下面介紹其中常用的幾種:
l ThreadLocal
l 原子類
l Lock類
l Volatile關鍵字
1.1. ThreadLocal
1.1.1. 作用
ThreadLocal提供線程局部變量,即為使用相同變量的每一個線程維護一個該變量的副本。
當某些數據是以線程為作用域並且不同線程具有不同的數據副本的時候,就可以考慮采用ThreadLocal,比如數據庫連接Connection,每個請求處理線程都需要,但又不相互影響,就是用ThreadLocal實現。
1.1.2. 示例
兩個線程分別轉賬
package com.controller; /** * @Auther: lanhaifeng * @Date: 2019/11/21 0021 10:09 * @Description:線程局部變量ThreadLocal * @statement: */ public class ThreadLocaclDemo { //1.創建銀行對象:錢,取款,存款 static class Bank { private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 0; } }; public Integer get(){ return threadLocal.get(); } public void set(Integer money){ threadLocal.set(threadLocal.get()+money); } } //2.創建轉賬對象:從銀行中取錢,轉賬,保存到帳戶 static class Transfer implements Runnable{ private Bank bank; public Transfer(Bank bank){ this.bank = bank; } public void run() { for(int i=0; i<10; i++){ bank.set(10); System.out.println(Thread.currentThread().getName()+"帳戶余額:"+bank.get()); } } } //3.在main方法中使用兩個對象模擬轉賬 public static void main(String[] args){ Bank bank = new Bank(); Transfer transfer = new Transfer(bank); Thread thread1 = new Thread(transfer, "客戶1"); Thread thread2 = new Thread(transfer, "客戶2"); thread1.start(); thread2.start(); } }
執行效果:
1.1.3. 分析
l 在ThreadLocal類中定義了一個ThreadLocalMap,
l 每一個Thread都有一個ThreadLocalMap類型的變量threadLocals
l threadLocals內部有一個Entry,Entry的key是ThreadLocal對象實例,value就是共享變量副本
l ThreadLocal的get方法就是根據ThreadLocal對象實例獲取共享變量副本
l ThreadLocal的set方法就是根據ThreadLocal對象實例保存共享變量副本
1.2. 原子類
Java的java.util.concurrent.atomic包里面提供了很多可以進行原子操作的類,分為以下四類:
l 原子更新基本類型:AtomicInteger、AtomicBoolean、AtomicLong
l 原子更新數組:AtomicIntegerArray、AtomicLongArray
l 原子更新引用:AtomicReference、AtomicStampedReference等
l 原子更新屬性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
提供這些原子類的目的就是為了解決基本類型操作的非原子性導致在多線程並發情況下引發的問題。
1.2.1. 非原子性操作問題演示
非原子性的操作會引發什么問題呢?下面以i++為例演示非原子性操作問題。
i++並不是原子操作,而是由三個操作構成:
tp1 = i; tp2 = tp1+1; i = tp2;
所以單線程i的值不是有問題,但多線程下就會出錯,多線程示例代碼如下:
package com.multithread.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicClass {
static int n = 0;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = 0;
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最終值是:"+n);
j++;
}
}
}
執行結果如下:發現n的最終值可能不是2000
1.2.2. 原子類解決非原子性操作問題
以上代碼修改如下:
package com.multithread.thread;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicClass {
static AtomicInteger n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicInteger(0);
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最終值是:"+n);
j++;
}
}
}
執行結果如下:n的值永遠是2000
1.2.3. 原子類CAS原理分析
1.2.4. CAS的ABA問題及解決
1.2.4.1. ABA問題分析
當前內存的值一開始是A,被另外一個線程先改為B然后再改為A,那么當前線程訪問的時候發現是A,則認為它沒有被其他線程訪問過。在某些場景下這樣是存在錯誤風險的。如下圖:
1.2.4.2. ABA問題解決
package com.multithread.thread;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicClass {
static AtomicStampedReference<Integer>n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicStampedReference<Integer>(0,0);
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
Integer reference;
do{
stamp = n.getStamp();
reference = n.getReference();
} while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
Integer reference;
do{
stamp = n.getStamp();
reference = n.getReference();
} while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最終值是:"+n.getReference());
j++;
}
}
}
執行效果如下:執行結果也是2000
注意:采用AtomicStampedReference會降低性能,慎用。
1.3. Lock類
1.3.1. Lock接口關系圖
Lock和ReadWriteLock是兩大鎖的根接口
Lock 接口支持重入、公平等的鎖規則:實現類 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定義讀取者共享而寫入者獨占的鎖,實現類:ReentrantReadWriteLock。
1.3.2. 可重入鎖
不可重入鎖,即線程請求它已經擁有的鎖時會阻塞。
可重入鎖,即線程可以進入它已經擁有的鎖的同步代碼塊。
publicclassReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock
lock=
newReentrantLock();
for(
inti =
1; i <=
3; i++) {
lock.
lock();
}
for(
inti=
1;i<=
3;i++){
try {
}
finally{
lock.unlock();
}
}
}
}
1.3.3. 讀寫鎖
讀寫鎖,即可以同時讀,讀的時候不能寫;不能同時寫,寫的時候不能讀。
示例代碼:
package com.multithread.thread;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 讀寫操作類
*/
public class ReadWriteLockDemo {
private Map<String, Object>map = new HashMap<String, Object>();
//創建一個讀寫鎖實例
private ReadWriteLock rw = new ReentrantReadWriteLock();
//創建一個讀鎖
private Lock r = rw.readLock();
//創建一個寫鎖
private Lock w = rw.writeLock();
/**
* 讀操作
*
* @param key
* @return
*/
public Object get(String key) {
r.lock();
System.out.println(Thread.currentThread().getName() + "讀操作開始執行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return map.get(key);
} finally {
r.unlock();
System.out.println(Thread.currentThread().getName() + "讀操作執行完成......");
}
}
/**
* 寫操作
*
* @param key
* @param value
*/
public void put(String key, Object value) {
try {
w.lock();
System.out.println(Thread.currentThread().getName() + "寫操作開始執行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
} finally {
w.unlock();
System.out.println(Thread.currentThread().getName() + "寫操作執行完成......");
}
}
public static void main(String[] args) {
final ReadWriteLockDemo d = new ReadWriteLockDemo();
d.put("key1", "value1");
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
}
}
執行效果如下:寫操作為獨占鎖,執行期間不能讀;讀操作可
1.4. Volatile關鍵字
1.4.1. 作用
一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
l 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(注意:不保證原子性)
l 禁止進行指令重排序。(保證變量所在行的有序性)
當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;
在進行指令優化時,不能將在對volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放到其前面執行。
1.4.2. 應用場景
基於volatile的作用,使用volatile必須滿足以下兩個條件:
l 對變量的寫操作不依賴於當前值
l 該變量沒有包含在具有其他變量的不變式中
常見應用場景如下:
狀態量標記:
volatilebooleanflag = false;
while(!flag){
doSomething();
}
publicvoidsetFlag() {
flag = true;
}
volatilebooleaninited = false;
//
線程1:
context = loadContext();
inited = true;
//
線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
雙重校驗:
classSingleton{
privatevolatilestaticSingleton instance = null;
privateSingleton() {
}
publicstaticSingleton getInstance() {
if(instance==null) {
synchronized(Singleton.class) {
if(instance==null)
instance = newSingleton();
}
}
returninstance;
}
}