java中的多線程-線程同步(一)

最近幾個月終於有大把時間總結這兩年來所學
2019.5.29

前言

java中的多線程包括下面兩點

  1. 多線程怎麼用
  2. 線程安全

區分幾個概念

區別一下
進程、線程、CPU線程、操作系統線程

  • 進程:操作系統中一塊獨立的區域,和操作系統獨立開,數據不共享,相互隔離。

  • 線程:工作在進程中的工作單元,可以共享資源。

  • CPU線程:CPU在硬件級別同時能做的事情(注意是硬件層面,而非軟件上做的時間切片)。有做過單片機的裸機代碼的同學,對這個概念應該會非常熟悉。

  • 操作系統線程:模仿的CPU線程,把CPU的線程做進一步拆分,分爲一個個的時間切片,輪詢的做各種不同的工作

  • 區別線程和進程,本質上來說,線程(thread)和進程(Process)不是一個概念,進程本身是一個運行的程序,而線程是程序中的工作單元。Thread 的英文本意是棉毛線的意思,而Process是過程,工序的意思。對比一下兩者的英文意思,可以很好地理解兩個概念。

  • 線程池的大小和CPU的核心數。一般線程池的大小和CPU的核心數要成正相關

java中實現多線程

簡要列舉一下java中常用的多線程的實現。

  1. Thread
    創建一個thread,重寫run方法。然後thread.start();
  static void thread() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("use thread");
            }
        };
        thread.start();
    }
  1. runnable
    static void runnable() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("use runnable");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }

Runnable 和 Thread兩個方法關聯是很緊密的,我最近看了看源碼,發現,Thread裏面的run方法是長這個樣子的

    public void run() {
        if (this.target != null) {
            this.target.run();
        }
    }

然後我們看到裏面有一個target,這個target就是 Thread thread = new Thread(runnable);裏面的runnable
所以對於Thread和Runnable兩個方法來說,我們如果重寫了Runnable,就會運行runnable的run方法,如果寫了Thread的run方法,就會運行Thread的run方法。如果兩個都寫了,兩個都會運行。

  1. ThreadFactory
    static void threadFactory() {
        ThreadFactory factory = new ThreadFactory() {
            AtomicInteger count = new AtomicInteger(0);
//            int count = 0;

            @Override
            public Thread newThread(Runnable r) {
//                count++;
                return new Thread(r, "Thread-" + count.incrementAndGet());
            }
        };

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " started!");
            }
        };

        Thread thread = factory.newThread(runnable);
        thread.start();
        Thread thread1 = factory.newThread(runnable);
        thread1.start();
    }

這種方法用的比較少,簡單看看就可以。

  1. Executor 線程池
    這是非常非常非常常用的方法。有四種常用的方法
  • 基本用法
    static void executor() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread with Runnable started!");
            }
        };
        Executor executor = Executors.newCachedThreadPool();
        executor.execute(runnable);
    }

  • 基本構造方法
ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)
變量含義分別爲:
核心線程數;
最大線程數;
keep alive 的時間;單位;
裝runnable的隊列

幾個變量的判斷優先級爲:corePoolSize > workQueue > maximumPoolSize;
解釋一下:
當前線程數 < corePoolSize,新任務來臨,無論是否有空的線程任務,都會優先創建核心線程;
corePoolSize < 當前線程數 < maximumPoolSize,只有當workQueue滿了,纔會創建新的線程去做任務,否則都將默認排隊執行;

  • 下面就這個構造基本的構造方法來介紹一下我們常用的四中線程池。

a.newCachedThreadPool

new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
//核心線程數:0;最大線程數:無窮大;等待時間:60S

從名字也可以看出,這是可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,如果沒有空線程等待回收,則新建線程。

b.newSingleThreadPool :

new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心線程數:1;最大線程數1;永不回收

特點:線程一直存在,隨時可以拿來用。可以用來做取消功能。

c.newFixedThreadPool

new ThreadPoolExecutor(32, 32, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心線程數:自定義;最大線程數:核心線程數;不回收

做集中的事情,比如下載文件,訴求就是,我需要馬上用很多線程,而且就用這麼一下,用完了我就不要了。

d.newSchedualThreadPool:

new ThreadPoolExecutor(32, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue());

可以設置定時,以及週期性執行任務。

  1. callable:一個有返回值的runnable
    後臺任務在後臺執行,返回值返回給誰?和Future類配合使用,將返回的值給future的get方法。如下面的程序段所示
    注意,Future.get是一個阻塞方法,會一直等到callable執行完成,纔會返回數據。
    static void callable() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "Done";
            }
        };

        ExecutorService executor = Executors.newCachedThreadPool();
        Future<String> future = executor.submit(callable);
        try {
            String result = future.get();
            System.out.println("result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

synchronized

  • 原子操作:CPU級別,只執行一次的代碼。不可被拆分
  1. synchronized的互斥
    爲了保護方法或者代碼塊內的數據,重點是保護數據。我們關注的不是方法,而是資源,只有處理同樣數據方法,才需要做互斥操作
    每一個synchronized都有對應一個monitor, 只有相同的monitor才具有互斥性。 非靜態方法的monitor是類的實例化對象。靜態方法的monitor是類。
    public static  synchronized void methodA(){
        //do something
    } 
    
    public synchronized void methodB(){
        //do something
    }

    private final Object monitorC = new Object();
    public void methodC(){
        synchronized (monitorC){
            //do something
        }
    }

看上面的一段代碼,methodA/methodB/methodC三個方法,都加了synchronized,但是他們的monitor分別是:className.class,this,monitorC,所以三個方法可以同時被調用。我們的鎖,是針對每個monitor進行鎖的,兩個方法在同一個monitor下,可以達到鎖的作用,如果不在一個monitor下,就達不到鎖的作用。同時,對於不是操作同樣數據的方法,是沒有必要加互斥鎖的

  1. 死鎖
  • 兩個鎖一起使用的時候,會出現死鎖。比如下面這中情況
public class DeadLock {
    private final Object monitor1 = new Object();
    private final Object monitor2 = new Object();
    
    private void setName(int name){
        synchronized (monitor1){
            name = 1;
            //線程1運行到此處的時候,被線程2打斷,跑到setAge中
            synchronized (monitor2){
                name = 2;
            }
        }
    }
    private void setAge(int age){
        synchronized (monitor2){
            age = 1;
            //線程是運行到此處,就會出現死鎖
            synchronized (monitor1){
                age = 2;
            }
        }
    }
}

上面的代碼中,註釋裏面有詳細說明。monitor1和monitor2在相互等待對方運行完成,導致死鎖產生。

  1. 樂觀鎖、悲觀鎖
    關於數據庫讀寫的問題。比如賬戶餘額,用於存入100塊,需要先讀出原來的金額,然後+100。但是該操作可能會同時進行,比如兩個人同時給你轉賬。
    樂觀鎖:寫入數據前,先覈對一下數據有沒有變化,如果有變化,那麼就加一個鎖。
    悲觀鎖:無論有沒有人寫,都先把把鎖加上。

volatile

  • 相當於一個輕量級的synchronized。保證修飾的變量具有同步性,即被修飾的變量被賦值時,具有原子性。無法解決++的問題。

其他

  1. 解決++的非原子問題:使用Atomic

看看上面的threadFactory的示例代碼中。

  1. 讀寫鎖
    上面有說道,同步方法本質是爲了保護方法中處理的數據。所以我們在讀寫方法中,使用讀寫鎖來解決synchronized過重的問題
public class ReadWriteLockDemo {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private int x = 0;

    private void write() {
        writeLock.lock();
        try {
            x++;
        } finally {
            writeLock.unlock();
        }
    }

    private void read(int time) {
        readLock.lock();
        try {
            for (int i = 0; i < time; i++) {
                System.out.print(x + " ");
            }
            System.out.println();
        } finally {
            readLock.unlock();
        }
    }
}
  • 以上爲線程的相關簡要整理。

再另外,以上都是自己平時所學整理,如果有錯誤,歡迎留言或者添加微信批評指出,一起學習,共同進步,愛生活,愛技術

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