JAVA多線程——(二)多線程編程

JAVA多線程——(二)多線程編程

【一】ReentrantLock

雖然在性能上ReentrantLock和synchronized沒有什麼區別,但ReentrantLock相比synchronized而言功能更加豐富,使用起來更爲靈活,也更適合複雜的併發場景。

  • lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。

  • tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待

  • tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  • lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態

  • 我們可以在創建ReentrantLock對象時,通過以下方式來設置鎖的公平性:
    ReentrantLock lock = new ReentrantLock(true);


public class Main {
     Lock lock = new ReentrantLock();
     AtomicInteger integer = new AtomicInteger(1);

    public void print(){
        //使用ReentrantLock加鎖的時候,必須在finally中釋放鎖,不然可能造成死鎖
        lock.lock();
        try {
            System.out.println(integer.get());
            integer.getAndIncrement();
        }finally {
            lock.unlock();
        }

    }

    public static void main(String args[]){
        Main main = new Main();

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.print();
                
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.print();
            }
        });

        thread1.start();
        thread2.start();
    }
}


Lock詳解:https://blog.csdn.net/wenge1477/article/details/98476731

Lock和synchronized差別:https://blog.csdn.net/wenge1477/article/details/98533427

【二】ReadWriteLock

讀寫鎖是一種通用技術,在其他編程語言或者數據庫中都有對應實現。讀寫鎖一般遵守下面三條規則:

  • 允許多個線程獲取讀鎖
  • 只允許一個線程獲取寫鎖
  • 如果某個線程獲取了寫鎖,其他線程不能再獲取讀鎖
    由於規則1多個線程在只讀的情況下可以同時讀取數據獲取共享變量,所以讀寫鎖優於互斥鎖。

讀寫鎖適用於讀多寫少:

ReadWriteLock接口的兩個方法:

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();
}

例子:


public class Main {

    ReadWriteLock lock = new ReentrantReadWriteLock();
    AtomicInteger count = new AtomicInteger(1);

    public void get(){
        //獲取數據時,獲取讀鎖
        Lock readLock = this.lock.readLock();
        readLock.lock();
        try {
            System.out.println(count);
        }finally {
            readLock.unlock();
        }
    }

    public void add(){
        //寫的時候,獲取寫鎖
        Lock writeLock = this.lock.writeLock();
        writeLock.lock();
        try {
            count.getAndIncrement();
        }finally {
            writeLock.unlock();
        }
    }

    public static void main(String args[]){
        Main main = new Main();

        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.get();
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.add();
            }
        });

        thread1.start();
        thread2.start();
    }
}


【三】Condition

在java Lock體系下依然會有同樣的方法實現等待/通知機制,而Condition與Lock配合完成等待通知機制。

  • Condition能夠支持不響應中斷,而通過使用Object方式不支持;
  • Condition能夠支持多個等待隊列(new 多個Condition對象),而Object方式只能支持一個;
  • Condition能夠支持超時時間的設置,而Object不支持

await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。
await(long time, TimeUnit unit) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。
awaitNanos(long nanosTimeout) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。返回值表示剩餘時間,如果在nanosTimesout之前喚醒,那麼返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了。
awaitUninterruptibly() :造成當前線程在接到信號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。
awaitUntil(Date deadline) :造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false
signal() :喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
signal()All :喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖。

例子:


public class Main {
    public Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public Stack<Integer> stack = new Stack();

    public int i = 1;

    public void producer(){
        lock.lock();
        try {
            while (stack.isEmpty()){
                stack.push(new Integer(i++));
                condition.signalAll();
                condition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void consumer(){
        lock.lock();
        try {
            while (!stack.isEmpty()){
                System.out.println(stack.pop());
                condition.signalAll();
                condition.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public static void main(String args[]){
        Main main = new Main();
        Thread thread1 = new Thread(()->{
            main.producer();
        });

        Thread thread2 = new Thread(()->{
            main.consumer();
        });

        thread1.start();
        thread2.start();
    }
}

【四】併發容器

  • ConcurrentHashMap
    主要爲了解決HashMap線程不安全和Hashtable效率不高的問題。
    1、JDK7版本
    分段鎖機制,簡而言之,ConcurrentHashMap在對象中保存了一個Segment數組,即將整個Hash表劃分爲多個分段;而每個Segment元素,即每個分段則類似於一個Hashtable
    ConcurrentHashMap的數據結構:
    在這裏插入圖片描述
    ConcurrentHashMap類結構如上圖所示。由圖可知,在ConcurrentHashMap中,定義了一個Segment<K, V>[]數組來將Hash表實現分段存儲,從而實現分段加鎖;而麼一個Segment元素則與HashMap結構類似,其包含了一個HashEntry數組,用來存儲Key/Value對。Segment繼承了ReetrantLock,表示Segment是一個可重入鎖,因此ConcurrentHashMap通過可重入鎖對每個分段進行加鎖

    2、JDK8版本
    在JDK1.8中,而是選擇了與HashMap類似的數組+鏈表+紅黑樹的方式實現,而加鎖則採用CAS和synchronized實現

    CAS(Compare And Swap,比較交換):CAS有三個操作數,內存值V、預期值A、要修改的新值B,當且僅當A和V相等時纔會將V修改爲B,否則什麼都不做。

    JDK1.8的數據結構:
    在這裏插入圖片描述

public class Main {
    private ConcurrentHashMap<String,Character> map = new ConcurrentHashMap<>();

    public void producer(String key,char value){
        map.put(key,value);
    }

    public void consumer(String key){
        Character value = map.get(key);
        System.out.println(value);
    }


    public static void main(String args[]){
        Main main = new Main();
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.producer(String.valueOf(i), (char) ('a'+i));
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                main.consumer(String.valueOf(i));
            }
        });

        thread1.start();
        thread2.start();
    }
}

  • ConcurrentLinkedQueue
    ConcurrentLinkedQueue是一個基於鏈表的無界非阻塞隊列,並且是線程安全的,它採用的是先進先出的規則,當我們增加一個元素時,它會添加到隊列的末尾,當我們取一個元素時,它會返回一個隊列頭部的元素。
 offer():將指定的元素插入隊列的尾部
poll() :獲取並移除隊列的頭,如果隊列爲空則返回null
peek():獲取表頭元素但不移除隊列的頭,如果隊列爲空則返回null。

remove(Object obj):移除隊列已存在的元素,返回true,
如果元素不存在,返回falseadd(E e):將指定元素插入隊列末尾,成功返回true,
失敗返回false(此方法非線程安全的方法,不推薦使用)。

注意:

雖然ConcurrentLinkedQueue的性能很好,
但是在調用size()方法的時候,會遍歷一遍集合
對性能損害較大,執行很慢,因此應該儘量的減少使用這個方法,
如果判斷是否爲空,最好用isEmpty()方法。

ConcurrentLinkedQueue不允許插入null元素,會拋出空指針異常。

ConcurrentLinkedQueue是無界的,所以使用時,
一定要注意內存溢出的問題。即對併發不是很大中等的情況下使用,
不然佔用內存過多或者溢出,對程序的性能影響很大,甚至是致命的。
  • CopyOnWriteArrayList
1、實現了List接口

2、內部持有一個ReentrantLock lock = new ReentrantLock();

3、底層是用volatile transient聲明的數組 array

4、讀寫分離,寫時複製出一個新的數組,完成插入、
修改或者移除操作後將新數組賦值給array

性能上:
Vector是增刪改查方法都加了synchronized,保證同步,但是每個方法執行的時候都要去獲得鎖,性能就會大大下降。

而CopyOnWriteArrayList 只是在增刪改上加鎖,但是讀不加鎖,在讀方面的性能就好於Vector,CopyOnWriteArrayList支持讀多寫少的併發情況

  • CopyOnWriteArraySet

  • BlockingQueue
    在這裏插入圖片描述

【五】Atomic

  • 線程安全的幾個問題:
    1、原子性:提供了互斥訪問,同一時刻只能有一個線程對它進行操作
    2、可見性:一個線程對主內存的修改可以及時的被其他線程觀察到
    3、有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序

  • 原子更新基本類型

AtomicBoolean: 原子更新布爾類型。 
AtomicInteger: 原子更新整型。 
AtomicLong: 原子更新長整型。
  • 原子更新數組
AtomicIntegerArray: 原子更新整型數組裏的元素。 
AtomicLongArray: 原子更新長整型數組裏的元素。 
AtomicReferenceArray: 原子更新引用類型數組裏的元素。 
  • 原子更新引用類型
AtomicReference: 原子更新引用類型。 
AtomicReferenceFieldUpdater: 原子更新引用類型的字段。 
AtomicMarkableReferce: 原子更新帶有標記位的引用類型
  • 原子更新字段類
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。 
AtomicLongFieldUpdater: 原子更新長整型字段的更新器。 
AtomicStampedFieldUpdater: 原子更新帶有版本號的引用類型。 
AtomicReferenceFieldUpdater: 上面已經說過此處不在贅述。

【六】ExecutorService

  • 一、線程池: 提供一個線程隊列,隊列中保存着所有等待狀態的線程。避免了創建與銷燬的額外開銷,提高了響應的速度。

  • 二、線程池的體系結構:
    java.util.concurrent.Executor 負責線程的使用和調度的根接口
    |–ExecutorService 子接口: 線程池的主要接口
    |–ThreadPoolExecutor 線程池的實現類
    |–ScheduledExceutorService 子接口: 負責線程的調度
    |–ScheduledThreadPoolExecutor : 繼承ThreadPoolExecutor,實現了ScheduledExecutorService

  • 三、工具類 : Executors
    1、ExecutorService newFixedThreadPool() : 創建固定大小的線程池
    2、ExecutorService newCachedThreadPool() : 緩存線程池,線程池的數量不固定,可以根據需求自動的更改數量。
    3、ExecutorService newSingleThreadExecutor() : 創建單個線程池。 線程池中只有一個線程
    4、ScheduledExecutorService newScheduledThreadPool() : 創建固定大小的線程,可以延遲或定時的執行任務

【七】CountDownLatch

CountDownLatch,英文翻譯爲倒計時鎖存器,是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。

1、確保某個計算在其需要的所有資源都被初始化之後才繼續執行;
2、確保某個服務在其依賴的所有其他服務都已經啓動之後才啓動;
3、等待直到某個操作所有參與者都準備就緒再繼續執行。

用法:

  • 第一種:
    某一線程在開始運行前等待n個線程執行完畢。
    將 CountDownLatch 的計數器初始化爲n :new CountDownLatch(n),每當一個任務線程執行完畢,就將計數器減1 countdownlatch.countDown(),當計數器的值變爲0時,在CountDownLatch上 await() 的線程就會被喚醒。一個典型應用場景就是啓動一個服務時,主線程需要等待多個組件加載完畢,之後再繼續執行。

  • 第二種:
    實現多個線程開始執行任務的最大並行性。
    注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。類似於賽跑,將多個線程放到起點,等待發令槍響,然後同時開跑。做法是初始化一個共享的 CountDownLatch 對象,將其計數器初始化爲 1 :new CountDownLatch(1),多個線程在開始執行任務前首先 coundownlatch.await(),當主線程調用 countDown() 時,計數器變爲0,多個線程同時被喚醒。

例子:計算多線程耗時

public class TestCountDownLatch {

    public static void main(String[] args){
		//CountDownLatch 爲唯一的、共享的資源
        final CountDownLatch latch = new CountDownLatch(5);
		
        LatchDemo latchDemo = new LatchDemo(latch);

        long begin = System.currentTimeMillis();

        for (int i = 0; i <5 ; i++) {
            new Thread(latchDemo).start();
        }
        try {
            //多線程運行結束前一直等待
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        
        System.out.println("耗費時間:"+(end-begin));

    }
}

class LatchDemo implements  Runnable{

    private CountDownLatch latch;

    public LatchDemo(CountDownLatch latch){
        this.latch=latch;
    }
    public LatchDemo(){
        super();
    }

    @Override
    public void run() {
        //當前對象唯一,使用當前對象加鎖,避免多線程問題
        synchronized (this){
            try {
                for (int i = 0; i < 50000; i++) {
                    if (i%2==0){
                        System.out.println(i);
                    }
                }
            }finally {
                //保證肯定執行
                latch.countDown();
            }
        }
    }
}

【八】CyclicBarrier

CyclicBarrier可以使一定數量的線程反覆地在柵欄位置處彙集。當線程到達柵欄位置時將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達柵欄位置,那麼柵欄將打開,此時所有的線程都將被釋放,而柵欄將被重置以便下次使用。
在這裏插入圖片描述
CyclicBarrier內部使用了ReentrantLock和Condition兩個類。

public CyclicBarrier(int parties) {
    this(parties, null);
}
 
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程使用await()方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。

CyclicBarrier的另一個構造函數CyclicBarrier(int parties, Runnable barrierAction),用於線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。

public class CyclicBarrierTest {
	// 自定義工作線程
	private static class Worker extends Thread {
		private CyclicBarrier cyclicBarrier;
		
		public Worker(CyclicBarrier cyclicBarrier) {
			this.cyclicBarrier = cyclicBarrier;
		}
		
		@Override
		public void run() {
			super.run();
			
			try {
				System.out.println(Thread.currentThread().getName() + "開始等待其他線程");
				cyclicBarrier.await();
				System.out.println(Thread.currentThread().getName() + "開始執行");
				// 工作線程開始處理,這裏用Thread.sleep()來模擬業務處理
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName() + "執行完畢");
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
 
	public static void main(String[] args) {
	
		CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
		
		for (int i = 0; i < threadCount; i++) {
			System.out.println("創建工作線程" + i);
			Worker worker = new Worker(cyclicBarrier);
			worker.start();
		}
	}
}

【九】Volatile

請查看博客:https://blog.csdn.net/wenge1477/article/details/98476642

【十】ThreadLocal

請查看博客:https://blog.csdn.net/wenge1477/article/details/98476832

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章