面試中經常手撕的多線程代碼

看了好多的面經,手撕代碼中除了算法題最常出現多線程的問題了,以下幾個問題是總結了下出現比較多的,做一下記錄,方便多看看

多線程下賣票

synchronized實現
只要有票就可以賣,賣之前第二次檢查,保證多線程下不會賣出負數

class SellTacket implements Runnable {

    static int count = 100;
    static Object lock = new Object();

    public static void main(String[] args) {
        SellTacket sellTacket = new SellTacket();
        for (int i = 0; i < 20; i++) {
            new Thread(sellTacket, "賣票員" + i).start();
        }
    }


    @Override
    public void run() {
        while (count > 0) {
            synchronized (lock) {
                if (count > 0) {
                    count--;
                    System.out.println(Thread.currentThread().getName() + "賣出一張票,庫存爲:" + count);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

兩個線程交替打印0-100奇偶數

兩個線程啓動之間睡眠1秒,保證偶數線程先啓動
當打印完成一次後喚醒另一個線程,如果還有打印任務就wait當前線程等待被喚醒

class MyPrint implements Runnable {

    private static int count = 0;
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new MyPrint(), "偶數").start();
        Thread.sleep(1000);
        new Thread(new MyPrint(), "奇數").start();
    }

    @Override
    public void run() {
        while (count <= 100) {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                lock.notifyAll();
                if (count <= 100) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

三個線程交替打印0-100的數

三個線程就不能簡單的執行文喚醒直接執行了
使用ReentrantLock+自旋+雙重檢查操作,如果線程執行到打印位置不該他打印,就自旋等待合適了再操作

class MyPrint {
    private static Lock lock = new ReentrantLock();//定義一個lock鎖
    private static int count = 0;//確定打印的是什麼內容

    public static void main(String[] args) {
        new ThreadA().start();
        new ThreadB().start();
        new ThreadC().start();
    }

    static class ThreadA extends Thread {
        @Override
        public void run() {
            while (count <= 100) {
                try {
                    lock.lock();
                    while (count % 3 == 0 && count <= 100) {//自旋
                        System.out.println(Thread.currentThread().getName() + ":" + count++);
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    static class ThreadB extends Thread {
        @Override
        public void run() {
            while (count <= 100) {
                try {
                    lock.lock();
                    while (count % 3 == 1 && count <= 100) {//自旋
                        System.out.println(Thread.currentThread().getName() + ":" + count++);
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    static class ThreadC extends Thread {
        @Override
        public void run() {
            while (count <= 100) {
                try {
                    lock.lock();
                    while (count % 3 == 2 && count <= 100) {//自旋
                        System.out.println(Thread.currentThread().getName() + ";" + count++);
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }


}

三個線程交替打印十次ABC

思路和打印三個數一致,採用了自旋+ReentrantLock
注意i++要在鎖內,只有成功獲得鎖後才能打印一次

class PrintABC {

    private static Lock lock = new ReentrantLock();//定義一個lock鎖
    private static int state = 0;//確定打印的是什麼內容

    public static void main(String[] args) {
        new ThreadA().start();
        new ThreadB().start();
        new ThreadC().start();
    }

    static class ThreadA extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; ) {
                try {
                    lock.lock();
                    while (state % 3 == 0) {//自旋
                        System.out.println("A");
                        state++;
                        i++;
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    static class ThreadB extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; ) {
                try {
                    lock.lock();
                    while (state % 3 == 1) {//自旋
                        System.out.println("B");
                        state++;
                        i++;
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    static class ThreadC extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; ) {
                try {
                    lock.lock();
                    while (state % 3 == 2) {//自旋
                        System.out.println("C");
                        state++;
                        i++;
                    }
                } finally {
                    lock.unlock();
                }
            }
        }
    }

}

單例模式

使用雙重檢查實現

class MySingleton {
    private volatile static MySingleton singleton;

    public MySingleton(){}

    public static MySingleton getSingleton() {
        if (singleton == null){
            synchronized (MySingleton.class){
                if (singleton == null){
                    singleton = new MySingleton();
                }
            }
        }
        return singleton;
    }
}
  • 爲什麼需要雙重檢查:

如果只有一次檢查,實例還沒創建時如果多個線程經過了第一次檢查等待鎖,那麼會重複的創建實例,所以需要第二次檢查過濾掉以上等待的線程

  • 爲什麼需要volatile 修飾:

因爲對象創建不是原子性的,分爲
- 創建空對象
- 調用構造方法初始化
- 將對象引用賦值

如果不用volatile 修飾可能發生重排序,創建一個對象後先賦值引用再初始化,如果此時有線程進來獲得了這個引用,引用是沒有初始化的。
volatile 修飾還能保證可見性,創建成功後其他線程能立刻可見

必然死鎖

兩個線程以不同順序獲得鎖,獲得一個鎖之後睡眠一秒保證兩個線程都獲得了一個鎖

class MustDeadLock implements Runnable {


    private int flag;
    private static Object o1 = new Object();
    private static Object o2 = new Object();

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();

    }

    @Override
    public void run() {
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1獲得鎖1");
                synchronized (o2) {
                    System.out.println("thread1獲得兩個鎖");
                }
            }
        } else if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread2獲得鎖2");
                synchronized (o1) {
                    System.out.println("thread2獲得兩個鎖");
                }
            }
        }
    }
}

生產者消費者模式

public class Solution {

    /**
     * producer 循環調用 put
     * consumer 循環調用 take
     * storage.put:庫存滿了就等待,然後存入後喚醒消費者
     * storage.take:庫存0就等待,然後消費後喚醒生產者
     */
    
    public static void main(String[] args) {
        Storage storage = new Storage();
        Producer producer = new Producer(storage);
        Consumer consumer = new Consumer(storage);
        
        new Thread(producer).start();
        new Thread(consumer).start();
        
    }

    /**
     * 生產者
     */
    static class Producer implements Runnable {

        private Storage storage;

        public Producer(Storage storage) {
            this.storage = storage;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                storage.put();
            }
        }
    }

    /**
     * 消費者
     */
    static class Consumer implements Runnable {

        private Storage storage;

        public Consumer(Storage storage) {
            this.storage = storage;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                storage.take();
            }
        }
    }

    /**
     * 阻塞隊列
     */
    static class Storage {
        private int maxSize;
        private LinkedList<Integer> storages;

        public Storage() {
            this.maxSize = 10;
            this.storages = new LinkedList<>();
        }

        public synchronized void put() {
            if (storages.size() == maxSize) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            storages.add(new Random().nextInt(1000));
            System.out.println("庫存爲:" + storages.size());
            notify();
        }

        public synchronized void take() {
            if (storages.size() == 0) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("拿到了:" + storages.poll());
            System.out.println("還剩下:" + storages.size());
            notify();
        }
    }
}

銀行轉賬死鎖

兩個賬戶相互轉賬,因爲獲得鎖的順序相反,可能出現相互等待的情況,爲了保證一定會死鎖,讓每個線程獲得第一個鎖後都睡眠1秒,保證雙方都獲得了第一個鎖

class TransferMoney implements Runnable {

    private int flag;
    static Account a = new Account(500);
    static Account b = new Account(500);

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();

        r1.flag = 1;
        r2.flag = 0;

        Thread t1= new Thread(r1);
        Thread t2= new Thread(r2);

        t1.start();
        t2.start();


        t1.join();
        t2.join();

        System.out.println("轉賬完成,a:"+a.balance+",b:"+b.balance);

    }

    @Override
    public void run() {
        if (flag == 1) {
            transfer(a, b, 200);
        } else if (flag == 0) {
            transfer(b, a, 100);
        }
    }

    private void transfer(Account from, Account to, int money) {
        synchronized (from) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":獲得from鎖");
            synchronized (to) {
                if (from.balance < money) {
                    System.out.println("餘額不足");
                } else {
                    from.balance -= money;
                    to.balance += money;
                    System.out.println("轉賬成功,交易了:" + money);
                }
            }
        }
    }

    static class Account {
        private int balance;

        public Account(int balance) {
            this.balance = balance;
        }
    }
}

銀行轉賬如何避免死鎖

造成死鎖的因素是鎖執行的順序相反,爲了保證鎖獲取順序一致,獲取鎖的hash值進行比較,hash值大的鎖先上鎖,如果兩個鎖的hash值相同,再加一層鎖

class TransferMoney implements Runnable {

    private int flag;
    static Account a = new Account(500);
    static Account b = new Account(500);
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();

        r1.flag = 1;
        r2.flag = 0;

        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);

        t1.start();
        t2.start();


        t1.join();
        t2.join();

        System.out.println("轉賬完成,a:" + a.balance + ",b:" + b.balance);

    }

    @Override
    public void run() {
        if (flag == 1) {
            transfer(a, b, 200);
        } else if (flag == 0) {
            transfer(b, a, 100);
        }
    }



    private void transfer(Account from, Account to, int money) {

        //獲取鎖的哈希值
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //hash值大的鎖先鎖
        if (fromHash > toHash) {
            synchronized (from) {
                synchronized (to) {
                    if (from.balance < money) {
                        System.out.println("餘額不足");
                    } else {
                        from.balance -= money;
                        to.balance += money;
                        System.out.println("轉賬成功,交易了:" + money);
                    }
                }
            }
        } else if (fromHash < toHash) {
            synchronized (to) {
                synchronized (from) {
                    if (from.balance < money) {
                        System.out.println("餘額不足");
                    } else {
                        from.balance -= money;
                        to.balance += money;
                        System.out.println("轉賬成功,交易了:" + money);
                    }
                }
            }
        } else {//hash值相等,再加一層鎖
            synchronized (lock) {
                if (fromHash > toHash) {
                    synchronized (from) {
                        synchronized (to) {
                            if (from.balance < money) {
                                System.out.println("餘額不足");
                            } else {
                                from.balance -= money;
                                to.balance += money;
                                System.out.println("轉賬成功,交易了:" + money);
                            }
                        }
                    }
                }
            }
        }
    }

    static class Account {
        int balance;

        public Account(int balance) {
            this.balance = balance;
        }
    }
}

哲學家就餐問題

class Philosopher implements Runnable {

    private Object leftChopstick;
    private Object rightChopstick;

    public Philosopher(Object leftChopstick, Object rightChopstick) {
        this.leftChopstick = leftChopstick;
        this.rightChopstick = rightChopstick;
    }

    public static void main(String[] args) {
        //定義5個哲學家
        Philosopher[] philosophers = new Philosopher[5];
        //定義筷子
        Object[] chopticks = new Object[philosophers.length];
        //初始化筷子
        for (int i = 0; i < chopticks.length; i++) {
            chopticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopticks[i % philosophers.length];
            Object rightChopstick = chopticks[(i + 1) % philosophers.length];
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            new Thread(philosophers[i], "哲學家" + (i + 1)).start();
        }

    }

    @Override
    public void run() {
        try {
            while (true) {
                doAction("think");
                synchronized (leftChopstick) {
                    doAction("拿起左手邊筷子");
                    synchronized (rightChopstick) {
                        doAction("拿起右手邊筷子");
                        doAction("放下右手邊筷子");
                    }
                    doAction("放下左手邊筷子");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //打印在做的事情,並隨機睡眠一段時間
    static void doAction(String action) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":" + action);
        Thread.sleep((long)Math.random()*1000);
    }
}

解決策略

  • 服務員檢查(避免策略)
    由服務員進行判斷分配,如果發現可能會發生死鎖,不允許就餐
  • 改變一個哲學家拿叉子的順序(避免策略)
    改變其中一個拿的順序,破壞環路
  • 餐票(避免策略)
    喫飯必須拿餐票,餐票一共只有4張,喫完了回收
  • 領導調節(檢測與恢復策略)
    定時檢查,如果發生死鎖,隨機剝奪一個的筷子

改變一個哲學家拿的順序最方便實現:
修改哲學家初始化循環

		for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopticks[i % philosophers.length];
            Object rightChopstick = chopticks[(i + 1) % philosophers.length];
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲學家" + (i + 1)).start();
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章