文章目錄
Java多線程之讀寫鎖——ReadWriteLock
應用說明
- 讀可以多個線程,寫只能有一個線程。
- 寫寫 or 讀寫:互斥
讀讀:不互斥 - 比普通的鎖讀效率高
演示
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class 讀寫鎖 {
public static void main(String[] args) {
ReadWriteLockTest rwlt = new ReadWriteLockTest();
for (int i = 0; i < 100; i++) {
new Thread(rwlt::getNum, "R" + i).start();
}
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
rwlt.setNum(i);
}
}, "W").start();
}
}
class ReadWriteLockTest {
private int num;
ReadWriteLock rwl = new ReentrantReadWriteLock();
Lock readLock = rwl.readLock();
Lock wl = rwl.writeLock();
public void getNum() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " " + num);
} finally {
readLock.unlock();
}
}
public void setNum(int num) {
wl.lock();
try {
this.num = num;
} finally {
wl.unlock();
}
}
}
Java多線程——8鎖問題
描述
題目:判斷打印的 “one” or “two” ?
- 兩個普通同步方法,兩個線程,標準打印, 打印? //one two
- 新增 Thread.sleep() 給 getOne() ,打印? //one two
- 新增普通方法 getThree() , 打印? //three one two
- 兩個普通同步方法,兩個 Number 對象,打印? //two one
- 修改 getOne() 爲靜態同步方法,打印? //two one
- 修改兩個方法均爲靜態同步方法,一個 Number 對象? //one two
- 一個靜態同步方法,一個非靜態同步方法,兩個 Number 對象? //two one
- 兩個靜態同步方法,兩個 Number 對象? //one two
案例
class TestThread8Monitor {
public static void main(String[] args) {
Number number = new Number();
Number number2 = new Number();
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// number.getTwo();
number2.getTwo();
}
}).start();
/*new Thread(new Runnable() {
@Override
public void run() {
number.getThree();
}
}).start();*/
}
}
class Number {
public static synchronized void getOne() {//Number.class
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("one");
}
public synchronized void getTwo() {//this
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
線程八鎖的關鍵:
①非靜態方法的鎖默認爲 this, 靜態方法的鎖爲 對應的 Class 實例
②某一個時刻內,只能有一個線程持有鎖,無論幾個方法。
Java等待喚醒機制JUC版
普通解決線程通信方式
class Demo1 {
String name;
int num;
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
Object o = new Object(); //鎖對象
//分別爲生產者和消費者開啓兩線程
Producer producer = new Producer(demo1,o);
Consumer consumer = new Consumer(demo1,o);
new Thread(consumer::con).start();
new Thread(producer::pro).start();
}
}
class Producer {
Demo1 d1;
Object o;
public Producer(Demo1 d1 , Object o) {
this.o=o;
this.d1 = d1;
}
void pro() {
while (true) {
synchronized (o) {
//使用循環判斷,解決虛假喚醒問題。
while (d1.num > 10) {
System.out.println("庫存滿了");
try {
o.wait();
} catch (InterruptedException e) {
}
}
d1.name = "commodity";
d1.num++;
System.out.println(d1.name+d1.num);
o.notify();
}
}
}
}
class Consumer {
Demo1 d1;
Object o;
public Consumer(Demo1 d1 , Object o) {
this.o=o;
this.d1 = d1;
}
void con() {
while (true) {
synchronized (o) {
while (d1.num <= 0) {
try {
System.out.println("沒庫存了");
o.wait();
} catch (InterruptedException e) {
}
}
d1.num--;
d1.name="Consumer";
System.out.println(d1.name+d1.num);
o.notify();
}
}
}
}
jdk1.5後解決線程通信方式
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Demo1 {
String name;
int num;
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
PCer producer = new PCer(demo1);
new Thread(producer::pro).start();
new Thread(producer::con).start();
}
}
class PCer {
Demo1 d1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
public PCer(Demo1 d1) {
this.d1 = d1;
}
void pro() {
while (true) {
lock.lock();
try {
while (d1.num >= 10) {
System.out.println("庫存滿了");
try {
condition1.await();
} catch (InterruptedException e) {
}
}
d1.name = "commodity";
d1.num++;
System.out.println(d1.name + d1.num);
//喚醒對方
condition2.signal();
} finally {
lock.unlock();
}
}
}
void con() {
while (true) {
lock.lock();
try {
while (d1.num <= 0) {
try {
System.out.println("沒庫存了");
condition2.await();
} catch (InterruptedException e) {
}
}
d1.name = "Consumer";
System.out.println(d1.name + d1.num);
d1.num--;
//喚醒對方
condition1.signal();
} finally {
lock.unlock();
}
}
}
}
線程控制練習
- 編寫一個程序,開啓 3 個線程,這三個線程的 ID 分別爲 A、B、C,每個線程將自己的 ID 在屏幕上打印 10 遍,要求輸出的結果必須按順序顯示。如:ABCABCABC…… 依次遞歸
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class 多線程控制 {
public static void main(String[] args) {
DemoCtrL dc = new DemoCtrL();
Thread thread = new Thread(()-> {
for (int i = 0; i < 10; i++) {
dc.methA();
}
});
thread.setName("A");
thread.start();
Thread thread1 = new Thread(()-> {
for (int i = 0; i < 10; i++) {
dc.methB();
}
});
thread1.setName("B");
thread1.start();
Thread thread2 = new Thread(()-> {
for (int i = 0; i < 10; i++) {
dc.methC();
}
});
thread2.setName("C");
thread2.start();
}
}
class DemoCtrL{
//控制線程標記
private int num = 1;
Lock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
void methA(){
lock.lock();
try {
if (num != 1) {
try {
conditionA.await();
} catch (InterruptedException e) {
}
}
System.out.print(Thread.currentThread().getName());
num=2;
conditionB.signal();
}finally {
lock.unlock();
}
}
void methB(){
lock.lock();
try {
if (num != 2) {
try {
conditionB.await();
} catch (InterruptedException e) {
}
}
System.out.print(Thread.currentThread().getName());
num=3;
conditionC.signal();
}finally {
lock.unlock();
}
}
void methC(){
lock.lock();
try {
if (num != 3) {
try {
conditionC.await();
} catch (InterruptedException e) {
}
}
System.out.print(Thread.currentThread().getName());
num=1;
conditionA.signal();
}finally {
lock.unlock();
}
}
}
Java創建線程的方式——實現Callable接口
說明
之前已經學習創建線程的兩種方式,
- 1.實現Runnable,
- 2.繼承Thread類,
使用Callable接口創建線程與實現Runnable的區別代碼演示:
import java.util.concurrent.Callable;
public class CallableDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
}
class RunnableDemo implements Runnable {
@Override
public void run() {
}
}
上述代碼可以發現,Callable接口從寫的方法名爲call,並可以有返回值和拋異常。
接收返回值和捕獲異常
class Demo {
public static void main(String[] args) {
CallableDemo cd = new CallableDemo();
//用futureTask類來接收
FutureTask<Integer> ft = new FutureTask<>(cd);
new Thread(ft).start();
try {
//get獲取返回值
// 注意,get方法是在主線程中,get方法是在FutureTask線程執行結束後才執行的
//因此我們可以得知,FutureTask可用於閉鎖
Integer integer = ft.get();
System.out.println(integer);
} catch (InterruptedException | ExecutionException e) {
}
}
}
Java多線程開發之volatile關鍵字
說明
- 多線程程序中,內存可見性問題,示例:
如下代碼會出現死循環問題:
class Thread1 implements Runnable {
boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
flag = true;
}
}
public class Volatile {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
new Thread(t1).start();
System.out.println(t1.flag);
while (true) {
if (t1.flag) {
System.out.println("true--------");
break;
}
}
}
}
出現死循環的原因是,每個線程都會開闢一塊緩存內存用於操作共享數據,在堆共享數據操作完成後再返還給主存,在上面的程序中,t1線程執行中睡了1秒,這時主線程獲取了執行權開始循環,並判斷flag爲假,而t1睡眠結束後修改了flag的值後,主線程獲取的flag值是緩存中的flag值,所以一直循環,無法結束。
解決方法:volatile關鍵字修飾線程共享數據volatile boolean flag = false;
。被volatile修飾的變量,被操作時不會在緩存中,而是在主存中,這樣就保證了線程間操作的可見性。
- volatile關鍵字的注意事項:
(1)不具備synchronize的互斥性
(2)不能保證變量的“原子性”
(3)被修飾的變量不會被jvm優化重排序
線程安全之原子性問題
原子性問題說明:
-
i++ 的原子性問題:i++ 的操作實際上分爲三個步驟“讀–改--寫”
int i = 10; i = i++; //10 int temp = i; i = i + 1; i = temp;
-
原子變量:在 java.util.concurrent.atomic 包下提供了一些原子變量。
- AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFieldUpdater
AtomicStampedReference
- 保證原子性方式:
1. volatile 保證內存可見性
2. CAS(Compare-And-Swap) 算法保證數據變量的原子性
CAS 算法是硬件對於併發操作的支持
CAS 包含了三個操作數:
①內存值 V
②預估值 A
③更新值 B
當且僅當 V == A 時, V = B; 否則,不會執行任何操作。(注意區分判斷和賦值)
- CAS算法屏棄了synchronize處理多線程中沒有獲得鎖導致線程放棄執行權的方法,因此高效。
/*
* 模擬 CAS 算法
*/
class TestCompareAndSwap {
public static void main(String[] args) {
final CompareAndSwap cas = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int expectedValue = cas.get();
boolean b = cas.compareAndSet(expectedValue, (int) (Math.random() * 101));
System.out.println(b);
}
}).start();
}
}
}
class CompareAndSwap {
private int value;
//獲取內存值
public synchronized int get() {
return value;
}
//比較
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
this.value = newValue;
}
return oldValue;
}
//設置
public synchronized boolean compareAndSet(int expectedValue, int newValue) {
return expectedValue == compareAndSwap(expectedValue, newValue);
}
}
閉鎖——CountDownLatch類
代碼演示說明
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch(倒計時閂鎖)
* 閉鎖:在完成某些運算時,只有其他所有線程的運算全部完成,當前運算才繼續執行
* 如:多個輔線程計算不同資源,每個輔線程會得到一個結果,再由一個主線程將多個輔線程的結果彙總。
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
//初始線程閉鎖爲10個
CountDownLatch cdl = new CountDownLatch(10);
CDLTest cdlTest = new CDLTest(cdl);
long start = System.currentTimeMillis();
//開啓是個線程
for (int i = 0; i < 10; i++) {
new Thread(cdlTest).start();
}
//如果線程沒執行完,主線程掛起等待,其他線程執行完。
try {
cdl.await();
} catch (InterruptedException e) { }
//閉鎖爲0時計算這些線程耗費的時間。
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
class CDLTest implements Runnable {
private final CountDownLatch cd;
private int sum = 0;
public CDLTest(CountDownLatch cd) {
this.cd = cd;
}
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 50000; i++) {
if (i % 10000 == 0) {
try { Thread.sleep(10); } catch (InterruptedException e) { }
}
sum+=i;
}
System.out.println(Thread.currentThread().getName()+" "+sum);
}finally {//必須執行的程序用finally包裹
sum=0;
//線程計數器,執行完一個線程減減一個線程
cd.countDown();
}
}
}
}
Java線程池
體系結構以及常用類說明
-
線程池:提供了一個線程隊列,隊列中保存着所有等待狀態的線程。避免了創建與銷燬額外開銷,提高了響應的速度。
-
線程池的體系結構:
java.util.concurrent.Executor : 負責線程的使用與調度的根接口
|–**ExecutorService 子接口: 線程池的主要接口
|–ThreadPoolExecutor 線程池的實現類
|–ScheduledExecutorService 子接口:負責線程的調度
|–ScheduledThreadPoolExecutor :繼承 ThreadPoolExecutor, 實現 ScheduledExecutorService
工具類 : Executors
返回值 | 名稱 | 說明 |
---|---|---|
ExecutorService | newFixedThreadPool() | 創建固定大小的線程池 |
ExecutorService | newCachedThreadPool() | 緩存線程池,線程池的數量不固定,可以根據需求自動的更改數量。 |
ExecutorService | newSingleThreadExecutor() | 創建單個線程池。線程池中只有一個線程 |
ScheduledExecutorService | newScheduledThreadPool() | 創建固定大小的線程,可以延遲或定時的執行任務。 |
官方文檔說明:強烈建議程序員使用較爲方便的 Executors 工廠方法 Executors.newCachedThreadPool()(無界線程池,可以進行自動線程回收)、Executors.newFixedThreadPool(int)(固定大小線程池)和 Executors.newSingleThreadExecutor()(單個後臺線程),它們均爲大多數使用場景預定義了設置。
演示
import org.junit.jupiter.api.Test;
import java.util.Random;
import java.util.concurrent.*;
/**
* @author RealC
*/
public class 線程池練習 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
});
executorService.shutdown();
}
/**
* ScheduledExecutorService | newScheduledThreadPool()|
* 創建固定大小的線程,可以延遲或定時的執行任務。
*
* @throws ExecutionException
* @throws InterruptedException
*/
@Test
public void test1() throws ExecutionException, InterruptedException {
//創建線程池,
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
//分配線程任務
for (int i = 0; i < 10; i++) {
Future<Integer> re = pool.schedule(() -> {
int x = new Random().nextInt(10);
System.out.println(Thread.currentThread().getName() + "::" + x);
return x;
}, 3, TimeUnit.SECONDS);
System.out.println(re.get());
}
pool.shutdown();
}
}
Java多線程之ForkJoinPool 分支/合併框架 工作竊取
說明
- 分支–合併框架:把一個大任務,分解並分配給多個線程並行執行,最後對結果進行合併。
工作竊取
ForkJoinPool的底層是工作竊取模式,其作用是:由於對多個任務分解並行處理,如果出現有些線程執行快,有些線程執行慢,在最後合併的時候,快的線程需要等待慢的線程,造成性能下降的問題。因此,執行快的線程會竊取執行慢的線程隊列的任務,加入到自己的線程中來,從而提高性能效率。
其他官方解釋:相對於一般的線程池實現, fork/join框架的優勢體現在對其中包含的任務的處理方式上.在一般的線程池中, 如果一個線程正在執行的任務由於某些原因無法繼續運行, 那麼該線程會處於等待狀態。 而在fork/join框架實現中,如果某個子問題由於等待另外一個子問題的完成而無法繼續運行。 那麼處理該子問題的線程會主動尋找其他尚未運行的子問題來執行.這種方式減少了線程的等待時間, 提高了性能。
性能比較
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
class TestForkJoinPool {
public static void main(String[] args) {
Instant start = Instant.now();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
Long sum = pool.invoke(task);
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗費時間爲:" + Duration.between(start, end).toMillis());//166-1996-10590
}
@Test
public void test1() {
Instant start = Instant.now();
long sum = 0L;
for (long i = 0L; i <= 50000000000L; i++) {
sum += i;
}
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗費時間爲:" + Duration.between(start, end).toMillis());//35-3142-15704
}
//java8 新特性
@Test
public void test2() {
Instant start = Instant.now();
Long sum = LongStream.rangeClosed(0L, 50000000000L)
.parallel()
.reduce(0L, Long::sum);
System.out.println(sum);
Instant end = Instant.now();
System.out.println("耗費時間爲:" + Duration.between(start, end).toMillis());//1536-8118
}
}
class ForkJoinSumCalculate extends RecursiveTask<Long> {
private long start;
private long end;
//臨界值
private static final long THURSHOLD = 10000L;
public ForkJoinSumCalculate(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if (length <= THURSHOLD) {
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long middle = (start + end) / 2;
ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
left.fork(); //進行拆分,同時壓入線程隊列
ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle + 1, end);
right.fork(); //
return left.join() + right.join();
}
}
}