欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

徹底搞懂Java多線程(五)

 更新時(shí)間:2021年07月03日 16:46:03   作者:保護(hù)眼睛  
這篇文章主要給大家介紹了關(guān)于Java面試題之多線程和高并發(fā)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

單例模式與多線程

單例模式就是全局唯一但是所有程序都可以使用的對(duì)象

寫(xiě)單例模式步驟:

1.將構(gòu)造函數(shù)設(shè)置為私有的

2.創(chuàng)建一個(gè)靜態(tài)的類(lèi)變量

3.提供獲取單例的方法

立即加載/餓漢模式

/**
 * user:ypc;
 * date:2021-06-13;
 * time: 21:02;
 */
//餓漢方式實(shí)現(xiàn)單例模式
public class Singleton {
    //1.將構(gòu)造函數(shù)設(shè)置為私有的,不然外部可以創(chuàng)建
    private Singleton(){
    }
    //2.創(chuàng)建靜態(tài)的類(lèi)變量(讓第三步的方法進(jìn)行返回)
    private static Singleton singleton = new Singleton();
    //給外部接口提供的獲取單例的方法
    public static Singleton getInstance(){
        return singleton;
    }
}

測(cè)試餓漢的單例模式

    //測(cè)試餓漢方式實(shí)現(xiàn)的單例模式,創(chuàng)建兩個(gè)線程,看是不是得到了一個(gè)實(shí)列對(duì)象,如果為true就說(shuō)明餓漢的單例模式?jīng)]有問(wèn)題
    static Singleton singleton1 = null;
    static Singleton singleton2 = null;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            singleton1 = Singleton.getInstance();
        });
        Thread thread2 = new Thread(() -> {
            singleton2 = Singleton.getInstance();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(singleton1 == singleton2);
    }

在這里插入圖片描述

延時(shí)加載/懶漢模式

不會(huì)隨著程序的啟動(dòng)而啟動(dòng),而是等到有人調(diào)用它的時(shí)候,它才會(huì)初始化

/**
 * user:ypc;
 * date:2021-06-13;
 * time: 21:22;
 */
//懶漢方式實(shí)現(xiàn)單例模式
public class Singleton2 {
    static class Singleton {
        //1.設(shè)置私有的構(gòu)造函數(shù)
        private Singleton() {
        }
        //2.提供一個(gè)私有的靜態(tài)變量
        private static Singleton singleton = null;
        //3.提供給外部調(diào)用,返回一個(gè)單例對(duì)象給外部
        public static Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
}

那么這樣寫(xiě)有什么問(wèn)題呢?我們來(lái)看看多線程情況下的懶漢方式實(shí)現(xiàn)單例模式:

/**
 * user:ypc;
 * date:2021-06-13;
 * time: 21:22;
 */
//懶漢方式實(shí)現(xiàn)單例模式
public class Singleton2 {
    static class Singleton {
        //1.設(shè)置私有的構(gòu)造函數(shù)
        private Singleton() {
        }
        //2.提供一個(gè)私有的靜態(tài)變量
        private static Singleton singleton = null;
        //3.提供給外部調(diào)用,返回一個(gè)單例對(duì)象給外部
        public static Singleton getInstance() throws InterruptedException {
            if (singleton == null) {
                Thread.sleep(100);
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    static Singleton singleton1 = null;
    static Singleton singleton2 = null;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                singleton1 = Singleton.getInstance();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                singleton2 = Singleton.getInstance();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(singleton1 == singleton2);
    }
}

結(jié)果:

在這里插入圖片描述

所以發(fā)生了線程不安全的問(wèn)題

那么要如何更改呢?

加鎖:👇

在這里插入圖片描述

結(jié)果就是true了:

在這里插入圖片描述

給方法加鎖可以實(shí)現(xiàn)線程安全,但是所鎖的粒度太大。

使用雙重校驗(yàn)鎖優(yōu)化后:

    static class Singleton {
        //1.設(shè)置私有的構(gòu)造函數(shù)
        private Singleton() {
        }
        //2.提供一個(gè)私有的靜態(tài)變量
        private static Singleton singleton = null;
        //3.提供給外部調(diào)用,返回一個(gè)單例對(duì)象給外部
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

在這里插入圖片描述

那么這樣寫(xiě)就沒(méi)有問(wèn)題了嗎?

不是的:有可能還會(huì)發(fā)生指令重排的問(wèn)題

當(dāng)有線程在進(jìn)行第一次初始化的時(shí)候,就有可能發(fā)生問(wèn)題👇

先來(lái)看初始化的過(guò)程

1.先分配內(nèi)存空間

2.初始化

3.將singleton指向內(nèi)存

有可能指令重排序之后:

線程1執(zhí)行的順序變成了 1 --> 3 --> 2

在線程1執(zhí)行完1、3之后時(shí)間片使用完了

線程2再來(lái)執(zhí)行,線程2得到了未初始化的singleton,也就是的到了一個(gè)空的對(duì)象

也就發(fā)生了線程不安全的問(wèn)題

那么要如何解決指令重排序的問(wèn)題呢?那就是使用volatile關(guān)鍵字👇:

/**
 * user:ypc;
 * date:2021-06-13;
 * time: 21:22;
 */
//懶漢方式實(shí)現(xiàn)單例模式
public class Singleton2 {
    static class Singleton {
        //1.設(shè)置私有的構(gòu)造函數(shù)
        private Singleton() {
        }
        //2.提供一個(gè)私有的靜態(tài)變量
        private static volatile Singleton singleton = null;
        //3.提供給外部調(diào)用,返回一個(gè)單例對(duì)象給外部
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

這樣就沒(méi)有問(wèn)題了

餓漢/懶漢對(duì)比

餓漢方式: 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,不存在線程安全的問(wèn)題,因?yàn)轲I漢的方式是隨著程序的啟動(dòng)而初始化的,因?yàn)轭?lèi)加載是線程安全的,所以它是線程安全的。缺點(diǎn):隨著程序的啟動(dòng)而啟動(dòng),有可能在整個(gè)程序的運(yùn)行周期都沒(méi)有用到,這樣就帶來(lái)了不必要的開(kāi)銷(xiāo)。

阻塞隊(duì)列的實(shí)現(xiàn)

import java.util.Random;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 8:57;
 */
public class MyBlockingQueue {
    private int[] values;
    private int first;
    private int last;
    private int size;
    MyBlockingQueue(int maxSize) {
        this.values = new int[maxSize];
        this.first = 0;
        this.last = 0;
        this.size = 0;
    }
    public void offer(int val) throws InterruptedException {
        synchronized (this) {
            if (this.size == values.length) {
                this.wait();
            }
            this.values[last++] = val;
            size++;
            //變?yōu)檠h(huán)隊(duì)列
            if (this.last == values.length) {
                this.last = 0;
            }
            //喚醒消費(fèi)者
            this.notify();
        }
    }
    public int poll() throws InterruptedException {
        int result = 0;
        synchronized (this) {
            if (size == 0) {
                this.wait();
            }
            result = this.values[first++];
            this.size--;
            if (first == this.values.length) {
                this.first = 0;
            }
            //喚醒生產(chǎn)者開(kāi)生產(chǎn)數(shù)據(jù)
            this.notify();
        }
        return result;
    }
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);
        //生產(chǎn)者
        Thread thread1 = new Thread(() -> {
            while (true) {
                try {
                    int num = new Random().nextInt(100);
                    myBlockingQueue.offer(num);
                    System.out.println("生產(chǎn)者生產(chǎn)數(shù)據(jù):" + num);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //消費(fèi)者
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        int res = myBlockingQueue.poll();
                        System.out.println("消費(fèi)者消費(fèi)數(shù)據(jù):" + res);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

可以看到生產(chǎn)者每生產(chǎn)一個(gè)數(shù)據(jù)都會(huì)被取走:

在這里插入圖片描述

常見(jiàn)的鎖策略

樂(lè)觀鎖

它認(rèn)為程序在一般的情況下不會(huì)發(fā)生問(wèn)題,所以他在使用的時(shí)候不會(huì)加鎖,只有在數(shù)據(jù)修改的時(shí)候才會(huì)判斷有沒(méi)有鎖競(jìng)爭(zhēng),如果沒(méi)有就會(huì)直接修改數(shù)據(jù),如果有就會(huì)返回失敗信息給用戶(hù)自行處理。

CAS

樂(lè)觀鎖的經(jīng)典實(shí)現(xiàn) Compare and Swap

CAS 實(shí)現(xiàn)的三個(gè)重要的屬性:

(V,A,B)

V:內(nèi)存中的值

A:預(yù)期的舊值

B:新值

V == A? V -> B : 修改失敗

修改失之后:

自旋對(duì)比和替換

CAS 的底層實(shí)現(xiàn):

CAS在Java中是通過(guò)unsafe來(lái)實(shí)現(xiàn)的,unsafe時(shí)本地類(lèi)和本地方法,它是c/c++實(shí)現(xiàn)的原生方法,通過(guò)調(diào)用操作系統(tǒng)Atomic::cmpxchg原子指令來(lái)實(shí)現(xiàn)的

CAS在java中的應(yīng)用

i++、i–問(wèn)題

可以使用加鎖、ThreadLocal 解決問(wèn)題

也可以使用atomic.AtomicInteger來(lái)解決問(wèn)題,底層也使用了樂(lè)觀鎖。

import java.util.concurrent.atomic.AtomicInteger;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 10:12;
 */
public class ThreadDemo1 {
    private static AtomicInteger count  = new AtomicInteger(0);
    private static final int MaxSize = 100000;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MaxSize; i++) {
                    count.getAndIncrement();//i++
                }
            }
        });
        thread1.start();
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < MaxSize; i++) {
             count.getAndDecrement();//i--
            }
        });
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

在這里插入圖片描述

CAS 的ABA問(wèn)題

當(dāng)有多個(gè)線程對(duì)一個(gè)原子類(lèi)進(jìn)行操作的時(shí)候,某個(gè)線程在短時(shí)間內(nèi)將原子類(lèi)的值A(chǔ)修改為B,又馬上將其修改為A,此時(shí)其他線程不感知,還是會(huì)修改成功。

來(lái)看:

import java.util.concurrent.atomic.AtomicInteger;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 10:43;
 */
public class ThreadDemo2 {
    //線程操作資源,原子類(lèi)ai的初始值為4
    static AtomicInteger ai = new AtomicInteger(4);
    public static void main(String[] args) {
        new Thread(() -> {
            //利用CAS將ai的值改成5
            boolean b = ai.compareAndSet(4, 5);
            System.out.println(Thread.currentThread().getName()+"是否成功將ai的值修改為5:"+b);
            //休眠一秒
            try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
            //利用CAS將ai的值改回4
            b = ai.compareAndSet(5,4);
            System.out.println(Thread.currentThread().getName()+"是否成功將ai的值修改為4:"+b);
        },"A").start();
        new Thread(() -> {
            //模擬此線程執(zhí)行較慢的情況
            try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}
            //利用CAS將ai的值從4改為10
            boolean b = ai.compareAndSet(4, 10);
            System.out.println(Thread.currentThread().getName()+"是否成功將ai的值修改為10:"+b);
        },"B").start();
        //等待其他線程完成,為什么是2,因?yàn)橐粋€(gè)是main線程,一個(gè)是后臺(tái)的GC線程
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("ai最終的值為:"+ai.get());
    }
}

上面例子模擬的是A、B兩個(gè)線程操作一個(gè)資源ai,A的執(zhí)行速度比B的快,在B執(zhí)行前,A就已經(jīng)將ai的值改為5之后馬上又把a(bǔ)i的值改回為4,但是B不感知,所以最后B就修改成功了。

那么會(huì)造成會(huì)有什么問(wèn)題呢?

假設(shè)A現(xiàn)在有100元,要給B轉(zhuǎn)賬100元,點(diǎn)擊了兩次轉(zhuǎn)賬按鈕,第一次B只會(huì)得到100元,A現(xiàn)在剩余0元。第二次A是0元,預(yù)期的舊值是100,不相等,就不會(huì)執(zhí)行轉(zhuǎn)賬操作。

如果點(diǎn)擊第二次按鈕之前,A又得到了100元,B不能感知的到,此時(shí)A得到了轉(zhuǎn)賬100元,預(yù)期的舊值就是100,又會(huì)轉(zhuǎn)給B100元。

那么如何解決這個(gè)問(wèn)題呢?👇

ABA 問(wèn)題的解決

我們可以給操作加上版本號(hào),每次修改的時(shí)候判斷版本號(hào)和預(yù)期的舊值,如果不一樣就不會(huì)執(zhí)行操作了。

即是預(yù)期的舊值和V值相等,但是版本號(hào)不一樣,也不會(huì)執(zhí)行操作。

在Java中的實(shí)現(xiàn):

import java.util.concurrent.atomic.AtomicStampedReference;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 11:05;
 */
public class ThreadDemo3 {
    static AtomicStampedReference<Integer> ai = new AtomicStampedReference<>(4,0);
    public static void main(String[] args) {
        new Thread(() -> {
            //四個(gè)參數(shù)分別是預(yù)估內(nèi)存值,更新值,預(yù)估版本號(hào),初始版本號(hào)
            //只有當(dāng)預(yù)估內(nèi)存值==實(shí)際內(nèi)存值相等并且預(yù)估版本號(hào)==實(shí)際版本號(hào),才會(huì)進(jìn)行修改
            boolean b = ai.compareAndSet(4, 5,0,1);
            System.out.println(Thread.currentThread().getName()+"是否成功將ai的值修改為5:"+b);
            try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
            b = ai.compareAndSet(5,4,1,2);
            System.out.println(Thread.currentThread().getName()+"是否成功將ai的值修改為4:"+b);
        },"A").start();
        new Thread(() -> {
            try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}
            boolean b = ai.compareAndSet(4, 10,0,1);
            System.out.println(Thread.currentThread().getName()+"是否成功將ai的值修改為10:"+b);
        },"B").start();
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("ai最終的值為:"+ai.getReference());
    }
}

在這里插入圖片描述

注意:里面的舊值對(duì)比的是引用。

如果范圍在-128 - 127 里,會(huì)使用緩存的值,如果超過(guò)了這個(gè)范圍,就會(huì)重新來(lái)new對(duì)象

可以將Integer 的高速緩存的值的邊界調(diào)整

悲觀鎖

悲觀鎖認(rèn)為只要執(zhí)行多線程的任務(wù),就會(huì)發(fā)生線程不安全的問(wèn)題,所以正在進(jìn)入方法之后會(huì)直接加鎖。

直接使用synchronzied關(guān)鍵字給方法加鎖就可以了

獨(dú)占鎖、共享鎖、自旋鎖、可重入鎖

獨(dú)占鎖:指的是這一把鎖只能被一個(gè)線程所擁有

比如:synchronzied、Lock

共享鎖: 指的是一把鎖可以被多個(gè)線程同時(shí)擁有

ReadWriterLock讀寫(xiě)鎖就是共享鎖

讀鎖就是共享的,將鎖的粒度更加的細(xì)化

import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 11:42;
 */
public class ThreadDemo4 {
    //創(chuàng)建讀寫(xiě)鎖
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        //讀鎖
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        //寫(xiě)鎖
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 1000,
                TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100), new ThreadPoolExecutor.DiscardPolicy());

        //任務(wù)一:讀鎖演示
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了讀鎖,時(shí)間:" + new Date());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    readLock.unlock();
                }
            }
        });
        //任務(wù)二:讀鎖演示
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                readLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了讀鎖,時(shí)間:" + new Date());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    readLock.unlock();
                }
            }
        });
        //任務(wù)三:寫(xiě)鎖

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了寫(xiě)鎖,時(shí)間:" + new Date());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });
        //任務(wù)四:寫(xiě)鎖

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                writeLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了寫(xiě)鎖,時(shí)間:" + new Date());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    writeLock.unlock();
                }
            }
        });

    }
}

在這里插入圖片描述

可重入鎖:

當(dāng)一個(gè)線程擁有了鎖之后,可以重復(fù)的進(jìn)入,就叫可重入鎖。

synchronzied就是典型的可重入鎖的代表

讀鎖的時(shí)間在一秒內(nèi),所以?xún)蓚€(gè)線程讀到的鎖是一把鎖,即讀鎖是共享鎖

而寫(xiě)鎖的時(shí)間剛好是一秒,所以寫(xiě)鎖是獨(dú)占鎖。

在這里插入圖片描述

在這里插入圖片描述

自旋鎖:相當(dāng)于死循環(huán),一直嘗試獲取鎖

詳解synchronized鎖的優(yōu)化問(wèn)題

synchroized加鎖的整個(gè)過(guò)程,都是依賴(lài)于Monitor(監(jiān)視器鎖)實(shí)現(xiàn)的,監(jiān)視器鎖在虛擬機(jī)中又是根據(jù)操作系統(tǒng)的Metux Lock(互斥量)來(lái)實(shí)現(xiàn)的,這就導(dǎo)致在加鎖的過(guò)程中需要頻繁的在操作系統(tǒng)的內(nèi)核態(tài)和和JVM級(jí)別的用戶(hù)態(tài)進(jìn)行切換,并且涉及到線程上下文的切換,是比較消耗性能的。所以后來(lái)有一位大佬Doug Lea基于java實(shí)現(xiàn)了一個(gè)AQS的框架,提供了Lock鎖,性能遠(yuǎn)遠(yuǎn)高于synchroized。這就導(dǎo)致Oracle公司很沒(méi)有面子,因此他們?cè)贘DK1.6對(duì)synchroized做了優(yōu)化,引入了偏向鎖和輕量級(jí)鎖。存在一個(gè)從無(wú)鎖-》偏向鎖–》輕量級(jí)鎖–》重量級(jí)鎖的升級(jí)過(guò)程,優(yōu)化后性能就可以和Lock鎖的方式持平了。

對(duì)象頭

HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充。

在這里插入圖片描述

對(duì)象頭包括兩部分:Mark Word 和 類(lèi)型指針。類(lèi)型指針是指向該對(duì)象所屬類(lèi)對(duì)象的指針,我們不關(guān)注。mark word用于存儲(chǔ)對(duì)象的HashCode、GC分代年齡、鎖狀態(tài)等信息。在32位系統(tǒng)上mark word長(zhǎng)度為32bit,64位系統(tǒng)上長(zhǎng)度為64bit。他不是一個(gè)固定的數(shù)據(jù)結(jié)構(gòu),是和對(duì)象的狀態(tài)緊密相關(guān),有一個(gè)對(duì)應(yīng)關(guān)系的,具體如下表所示:

在這里插入圖片描述

當(dāng)某一線程第一次獲得鎖的時(shí)候,虛擬機(jī)會(huì)把對(duì)象頭中的鎖標(biāo)志位設(shè)置為“01”,把偏向模式設(shè)置為“1”,表示進(jìn)入偏向鎖模式。同時(shí)使用CAS操作將獲取到這個(gè)鎖的線程的ID記錄在對(duì)象的Mark Word中。如果CAS操作成功,持有偏向鎖的線程每次進(jìn)入這個(gè)鎖的相關(guān)的同步塊的時(shí)候。虛擬機(jī)都可以不在進(jìn)行任何的同步操作。

當(dāng)其他線程進(jìn)入同步塊時(shí),發(fā)現(xiàn)已經(jīng)有偏向的線程了,偏向模式馬上結(jié)束。根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)決定是否撤銷(xiāo)偏向,也就是將偏向模式設(shè)置為“0”,撤銷(xiāo)后標(biāo)志位恢復(fù)到“01”,也就是未鎖定的狀態(tài)或者輕量級(jí)鎖定,標(biāo)志位為“00”的狀態(tài),后續(xù)的同步操作就按照下面的輕量級(jí)鎖那樣去執(zhí)行

1、在線程進(jìn)入同步塊的時(shí)候,如果同步對(duì)象狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志為 01),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄的空間,用來(lái)存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝??截惓晒螅摂M機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock Record 里的 owner 指針指向鎖對(duì)象的 Mark Word。如果更新成功,則執(zhí)行 2,否則執(zhí)行 3。

在這里插入圖片描述

2、如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且鎖對(duì)象的 Mark Word 中的鎖標(biāo)志位設(shè)置為 “00”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),這時(shí)候虛擬機(jī)線程棧與堆中鎖對(duì)象的對(duì)象頭的狀態(tài)如圖所示。

在這里插入圖片描述

3、如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查鎖對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說(shuō)明多個(gè)線程競(jìng)爭(zhēng)鎖,輕量級(jí)鎖就要膨脹為重要量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?“10”,Mark Word 中存儲(chǔ)的就是指向重量級(jí)鎖的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。而當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖。自旋失敗后膨脹為重量級(jí)鎖,被阻塞。

Semaphore

Semaphore的作用:

在java中,使用了synchronized關(guān)鍵字和Lock鎖實(shí)現(xiàn)了資源的并發(fā)訪問(wèn)控制,在同一時(shí)間只允許唯一了線程進(jìn)入臨界區(qū)訪問(wèn)資源(讀鎖除外),這樣子控制的主要目的是為了解決多個(gè)線程并發(fā)同一資源造成的數(shù)據(jù)不一致的問(wèn)題。也就是做限流的作用

Semaphore實(shí)現(xiàn)原理:

Semaphore是用來(lái)保護(hù)一個(gè)或者多個(gè)共享資源的訪問(wèn),Semaphore內(nèi)部維護(hù)了一個(gè)計(jì)數(shù)器,其值為可以訪問(wèn)的共享資源的個(gè)數(shù)。一個(gè)線程要訪問(wèn)共享資源,先獲得信號(hào)量,如果信號(hào)量的計(jì)數(shù)器值大于1,意味著有共享資源可以訪問(wèn),則使其計(jì)數(shù)器值減去1,再訪問(wèn)共享資源。

如果計(jì)數(shù)器值為0,線程進(jìn)入休眠。當(dāng)某個(gè)線程使用完共享資源后,釋放信號(hào)量,并將信號(hào)量?jī)?nèi)部的計(jì)數(shù)器加1,之前進(jìn)入休眠的線程將被喚醒并再次試圖獲得信號(hào)量。

就好比一個(gè)廁所管理員,站在門(mén)口,只有廁所有空位,就開(kāi)門(mén)允許與空側(cè)數(shù)量等量的人進(jìn)入廁所。多個(gè)人進(jìn)入廁所后,相當(dāng)于N個(gè)人來(lái)分配使用N個(gè)空位。為避免多個(gè)人來(lái)同時(shí)競(jìng)爭(zhēng)同一個(gè)側(cè)衛(wèi),在內(nèi)部仍然使用鎖來(lái)控制資源的同步訪問(wèn)。

Semaphore的使用:

Semaphore使用時(shí)需要先構(gòu)建一個(gè)參數(shù)來(lái)指定共享資源的數(shù)量,Semaphore構(gòu)造完成后即是獲取Semaphore、共享資源使用完畢后釋放Semaphore。

使用Semaphore 來(lái)模擬有四輛車(chē)同時(shí)到達(dá)了停車(chē)場(chǎng)的門(mén)口,但是停車(chē)位只有兩個(gè),也就是只能停兩輛車(chē),這就可以使用信號(hào)量來(lái)實(shí)現(xiàn)。👇:

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 14:00;
 */
public class ThreadDemo6 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 200,
                TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100), new ThreadPoolExecutor.DiscardPolicy());

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "到達(dá)了停車(chē)場(chǎng)");
                try {
                    Thread.sleep(1000);
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了停車(chē)場(chǎng)");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    Thread.sleep(1000);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "出了了停車(chē)場(chǎng)");
                semaphore.release();
            }
        });
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "到達(dá)了停車(chē)場(chǎng)");
                try {
                    Thread.sleep(1000);
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了停車(chē)場(chǎng)");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(2000);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "出了了停車(chē)場(chǎng)");
                semaphore.release();

            }
        });
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "到達(dá)了停車(chē)場(chǎng)");
                try {
                    Thread.sleep(1000);
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了停車(chē)場(chǎng)");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    Thread.sleep(500);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "出了了停車(chē)場(chǎng)");
                semaphore.release();
            }
        });
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "到達(dá)了停車(chē)場(chǎng)");
                try {
                    Thread.sleep(1000);
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了停車(chē)場(chǎng)");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    Thread.sleep(1500);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "出了了停車(chē)場(chǎng)");
                semaphore.release();
            }
        });
        threadPoolExecutor.shutdown();
    }
}

在這里插入圖片描述

CountDownLatch\CyclicBarrier

CountDownLatch

一個(gè)可以用來(lái)協(xié)調(diào)多個(gè)線程之間的同步,或者說(shuō)起到線程之間的通信作用的工具類(lèi)。

它能夠使一個(gè)線程在等待另外一些線程完成各自工作之后,再繼續(xù)執(zhí)行。使用一個(gè)計(jì)數(shù)器進(jìn)行實(shí)現(xiàn)。計(jì)數(shù)器初始值為線程的數(shù)量。當(dāng)每一個(gè)線程完成自己任務(wù)后,計(jì)數(shù)器的值就會(huì)減一。當(dāng)計(jì)數(shù)器的值為0時(shí),表示所有的線程都已經(jīng)完成了任務(wù),然后在CountDownLatch上等待的線程就可以恢復(fù)執(zhí)行任務(wù)。

CountDownLatch的用法

某一線程在開(kāi)始運(yùn)行前等待n個(gè)線程執(zhí)行完畢。

CountDownLatch的計(jì)數(shù)器初始化為n:new CountDownLatch(n) ,每當(dāng)一個(gè)任務(wù)線程執(zhí)行完畢,就將計(jì)數(shù)器減1,

countdownlatch.countDown(),當(dāng)計(jì)數(shù)器的值變?yōu)?時(shí),在CountDownLatch上 await() 的線程就會(huì)被喚醒。一個(gè)典型應(yīng)用場(chǎng)景就是啟動(dòng)一個(gè)服務(wù)時(shí),主線程需要等待多個(gè)組件加載完畢,之后再繼續(xù)執(zhí)行。

實(shí)現(xiàn)多個(gè)線程開(kāi)始執(zhí)行任務(wù)的最大并行性。注意是并行性,不是并發(fā),強(qiáng)調(diào)的是多個(gè)線程在某一時(shí)刻同時(shí)開(kāi)始執(zhí)行。做法是初始化一個(gè)共享的CountDownLatch(1),將其計(jì)數(shù)器初始化為1,多個(gè)線程在開(kāi)始執(zhí)行任務(wù)前首先 coundownlatch.await(),當(dāng)主線程調(diào)用 countDown() 時(shí),計(jì)數(shù)器變?yōu)?,多個(gè)線程同時(shí)被喚醒。

CountDownLatch的不足

CountDownLatch是一次性的,計(jì)數(shù)器的值只能在構(gòu)造方法中初始化一次,之后沒(méi)有任何機(jī)制再次對(duì)其設(shè)置值,當(dāng)CountDownLatch使用完畢后,它不能再次被使用。

在這里插入圖片描述

模擬賽跑:當(dāng)三個(gè)運(yùn)動(dòng)員都到達(dá)終點(diǎn)的時(shí)候宣布比賽結(jié)束

import java.util.Random;
import java.util.concurrent.*;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 14:27;
 */
public class ThreadDemo7 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 200,
                TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100));

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "開(kāi)跑");
                int num = new Random().nextInt(4);
                num += 1;
                try {
                    Thread.sleep(1000*num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "到達(dá)了終點(diǎn)");
                countDownLatch.countDown();
            }
        });
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "開(kāi)跑");
                int num = new Random().nextInt(4);
                num += 1;
                try {
                    Thread.sleep(1000*num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "到達(dá)了終點(diǎn)");
                countDownLatch.countDown();
            }
        });
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "開(kāi)跑");
                int num = new Random().nextInt(4);
                num += 1;
                try {
                    Thread.sleep(1000*num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "到達(dá)了終點(diǎn)");
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
        System.out.println("所有的選手都到達(dá)了終點(diǎn)");
        threadPoolExecutor.shutdown();
    }
}

在這里插入圖片描述

CyclicBarrier

CyclicBarrier 的字面意思是可循環(huán)(Cyclic)使用的屏障(Barrier)。它要做的事情是,讓一組線程到達(dá)一個(gè)屏障(也可以叫同步點(diǎn))時(shí)被阻塞,直到最后一個(gè)線程到達(dá)屏障時(shí),屏障才會(huì)開(kāi)門(mén),所有被屏障攔截的線程才會(huì)繼續(xù)干活。線程進(jìn)入屏障通過(guò)CyclicBarrier的await()方法。

CyclicBarrier默認(rèn)的構(gòu)造方法是CyclicBarrier(int parties),其參數(shù)表示屏障攔截的線程數(shù)量,每個(gè)線程調(diào)用await方法告訴CyclicBarrier我已經(jīng)到達(dá)了屏障,然后當(dāng)前線程被阻塞。

import java.util.concurrent.*;
/**
 * user:ypc;
 * date:2021-06-14;
 * time: 15:03;
 */
public class ThreadDemo8 {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("到達(dá)了循環(huán)屏障");
            }
        });
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 200,
                TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(100));
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(finalI * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "進(jìn)入了任務(wù)");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "退出了任務(wù)");
                }
            });
        }

        threadPoolExecutor.shutdown();
    }
}

在這里插入圖片描述

CyclicBarrier原理

每當(dāng)線程執(zhí)行await,內(nèi)部變量count減1,如果count!= 0,說(shuō)明有線程還未到屏障處,則在鎖條件變量trip上等待。

當(dāng)count == 0時(shí),說(shuō)明所有線程都已經(jīng)到屏障處,執(zhí)行條件變量的signalAll方法喚醒等待的線程。

其中 nextGeneration方法可以實(shí)現(xiàn)屏障的循環(huán)使用:

重新生成Generation對(duì)象

恢復(fù)count值

CyclicBarrier可以循環(huán)的使用。

hashmap/ConcurrentHashMap

hashmap在JDK1.7中頭插死循環(huán)問(wèn)題

來(lái)看👇JDK1.7 hashMap transfer的源碼

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

來(lái)看多線程情況下的問(wèn)題:

在這里插入圖片描述

這樣就會(huì)造成死循環(huán)。

hashmap在JDK1.8中值覆蓋問(wèn)題

在JDK1.8的時(shí)候使用的是尾插法來(lái)看👇:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果沒(méi)有hash碰撞則直接插入元素
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在多線程的情況下:

在這里插入圖片描述

其中第六行代碼是判斷是否出現(xiàn)hash碰撞,假設(shè)兩個(gè)線程1、2都在進(jìn)行put操作,并且hash函數(shù)計(jì)算出的插入下標(biāo)是相同的,當(dāng)線程1執(zhí)行完第六行代碼后由于時(shí)間片耗盡導(dǎo)致被掛起,而線程2得到時(shí)間片后在該下標(biāo)處插入了元素,完成了正常的插入,然后線程A獲得時(shí)間片,由于之前已經(jīng)進(jìn)行了hash碰撞的判斷,所有此時(shí)不會(huì)再進(jìn)行判斷,而是直接進(jìn)行插入,這就導(dǎo)致了線程2插入的數(shù)據(jù)被線程1覆蓋了,從而線程不安全。

除此之前,還有就是代碼的第38行處有個(gè)++size,我們這樣想,還是線程1、2,這兩個(gè)線程同時(shí)進(jìn)行put操作時(shí),假設(shè)當(dāng)前HashMap的zise大小為10,當(dāng)線程1執(zhí)行到第38行代碼時(shí),從主內(nèi)存中獲得size的值為10后準(zhǔn)備進(jìn)行+1操作,但是由于時(shí)間片耗盡只好讓出CPU,線程2快樂(lè)的拿到CPU還是從主內(nèi)存中拿到size的值10進(jìn)行+1操作,完成了put操作并將size=11寫(xiě)回主內(nèi)存,然后線程1再次拿到CPU并繼續(xù)執(zhí)行(此時(shí)size的值仍為10),當(dāng)執(zhí)行完put操作后,還是將size=11寫(xiě)回內(nèi)存,此時(shí),線程1、2都執(zhí)行了一次put操作,但是size的值只增加了1,所有說(shuō)還是由于數(shù)據(jù)覆蓋又導(dǎo)致了線程不安全。

總結(jié)

這個(gè)系列的文章到這里就結(jié)束了,希望可以幫到你,請(qǐng)您多多關(guān)注腳本之家的更多精彩內(nèi)容!

相關(guān)文章

  • java 使用JDOM解析xml文件

    java 使用JDOM解析xml文件

    java中如何使用JDOM解析xml文件呢?以下小編就用實(shí)例為大家詳細(xì)的介紹一下。需要的朋友可以參考下
    2013-07-07
  • Java不用算數(shù)運(yùn)算符來(lái)實(shí)現(xiàn)求和方法

    Java不用算數(shù)運(yùn)算符來(lái)實(shí)現(xiàn)求和方法

    我們都知道,Java的運(yùn)算符除了具有優(yōu)先級(jí)之外,還有一個(gè)結(jié)合性的特點(diǎn)。當(dāng)一個(gè)表達(dá)式中出現(xiàn)多種運(yùn)算符時(shí),執(zhí)行的先后順序不僅要遵守運(yùn)算符優(yōu)先級(jí)別的規(guī)定,還要受運(yùn)算符結(jié)合性的約束,以便確定是自左向右進(jìn)行運(yùn)算還是自右向左進(jìn)行運(yùn)算,但是如果不用運(yùn)算符怎么求和呢
    2022-04-04
  • 求最大子數(shù)組之和的方法解析(2種可選)

    求最大子數(shù)組之和的方法解析(2種可選)

    本文主要對(duì)求最大子數(shù)組之和的方法進(jìn)行詳細(xì)解析,列了兩種方法供大家選擇借鑒,需要的朋友一起來(lái)看下吧
    2016-12-12
  • maven工程打包引入本地jar包的實(shí)現(xiàn)

    maven工程打包引入本地jar包的實(shí)現(xiàn)

    我們需要將jar包發(fā)布到一些指定的第三方Maven倉(cāng)庫(kù),本文主要介紹了maven工程打包引入本地jar包的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-02-02
  • JMeter中的后端監(jiān)聽(tīng)器的實(shí)現(xiàn)

    JMeter中的后端監(jiān)聽(tīng)器的實(shí)現(xiàn)

    本文主要介紹了JMeter中的后端監(jiān)聽(tīng)器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • 詳解Java單例模式的實(shí)現(xiàn)與原理剖析

    詳解Java單例模式的實(shí)現(xiàn)與原理剖析

    單例模式是Java中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類(lèi)型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。本文將詳解單例模式的實(shí)現(xiàn)及原理剖析,需要的可以參考一下
    2022-05-05
  • Java之Spring認(rèn)證使用Profile配置運(yùn)行環(huán)境講解

    Java之Spring認(rèn)證使用Profile配置運(yùn)行環(huán)境講解

    這篇文章主要介紹了Java之Spring認(rèn)證使用Profile配置運(yùn)行環(huán)境講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • Project?Reactor?響應(yīng)式范式編程

    Project?Reactor?響應(yīng)式范式編程

    這篇文章主要為大家介紹了Project?Reactor?響應(yīng)式范式編程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • Java多線程 線程組原理及實(shí)例詳解

    Java多線程 線程組原理及實(shí)例詳解

    這篇文章主要介紹了Java多線程 線程組原理及實(shí)例詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-09-09
  • 使用Spring Boot創(chuàng)建Web應(yīng)用程序的示例代碼

    使用Spring Boot創(chuàng)建Web應(yīng)用程序的示例代碼

    本篇文章主要介紹了使用Spring Boot創(chuàng)建Web應(yīng)用程序的示例代碼,我們將使用Spring Boot構(gòu)建一個(gè)簡(jiǎn)單的Web應(yīng)用程序,并為其添加一些有用的服務(wù),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-05-05

最新評(píng)論