java基础-多线程安全

后端 / 笔记 / 2021-09-25

什么是线程安全问题

多线程同时对一个全局变量做读写操作,可能会受到其他线程的干扰从而导致多线程安全问题。

image.png

package thread;

public class ThreadCount implements Runnable {
    private int count = 10;

    @Override
    public void run() {
        while (count > 1) {
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println(Thread.currentThread().getName() + "," + count);
        }
    }

    public static void main(String[] args) {
        ThreadCount threadCount = new ThreadCount();
        new Thread(threadCount).start();
        new Thread(threadCount).start();
    }
}

Thread-1,9
Thread-0,9
Thread-0,7
Thread-1,7
Thread-0,6
Thread-1,6
Thread-0,5
Thread-1,4
Thread-0,3
Thread-1,2
Thread-1,0
Thread-0,0

通过上面执行结果:理想很丰满,现实很骨感。

如何解决线程安全问题

核心思想:上锁

在哪上锁?

可能会发生线程安全性问题的代码,对象,方法上锁。

在同一个jvm中,多个线程去竞争锁,最终只能又有一个线程得到锁一个女孩子,可以被很多人追,但最终结婚的只能一个公平锁

image.png

Thread-0,9
Thread-0,8
Thread-0,7
Thread-0,6
Thread-0,5
Thread-0,4
Thread-0,3
Thread-0,2
Thread-0,1

虽然上锁解决了资源争抢问题,但是由于线程内是死循环,所以一直被一个线程占用锁,从而导致其他线程处于围观状态,公平吗?公平吗?

这种方法是不讲武德的,作为高级开发我们能允许这样的事情出现吗?不允许!

说好的公平锁,结果被你一人独占,宝,你下次和ta接吻时,记得涂我送你的口红怎么也得让我有点参与感啊,hhh适度玩梗。

怎么办,怎么办?

缩小上锁范围this锁

上面的代码存在很大的问题,既然存在问题我们就要解决问题。

我们尝试缩小上锁范围

image.png

就这?这就解决了?tg不得好死,拒绝当tg

当线程1执行完count--后释放锁,线程2介入执行count--

同步代码块内代码执行完毕后自动释放锁

synchronized 锁的基本用法

  • 修饰代码块 指定加锁对象
  • 修饰实例方法 当前实例加锁
  • 修饰静态方法 当前类对象加锁

修饰代码块

多线程情况下,需要同一个对象锁

synchronized(对象锁){
	safeCode
}

手动指定一个上锁对象

image.png

修饰实例方法

方法锁很简单:只需要把要上锁的内容写到一个单独的方法内,然后在改方法上使用synchronized关键字修饰即可

image.png

修饰静态方法

静态方法上锁就有一点小窍门了,我们要知道Class记录了一个类的信息,多个源于同一个类的实例,Class一定是相同的,借助这一点我们可以通过Class来上锁。

image.png

  • 如果作用在实例方法上,用this
  • 如果作用在静态方法上,用类名.class

死锁问题

某面试大会
面试官:给我讲下什么是死锁,我就给你offer
程序猿:你给我offer我就给你讲,什么是死锁。

什么是死锁?

一个对象一直拿住另一个对象的锁不放手,并且另一个对象也拿到该对象的锁。从而造成一种僵持的巨变,故取名为DeadLock死锁。

package thread;


public class DeadLock implements Runnable {
    private Object thisLock = new Object();
    private Object thatLock = new Object();
    private int count;

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        new Thread(deadLock).start();
        new Thread(deadLock).start();
    }

    @Override
    public void run() {

        while (true) {
            count++;
            if (count % 2 == 0) {
                synchronized (thatLock) {
                    methodA();
                }
            } else {
                synchronized (thisLock) {
                    methodB();
                }
            }
        }

    }

    private void methodA() {
        synchronized (thisLock) {
            System.out.println("hr:给我讲什么是死锁我就给你offer");
        }
    }

    private void methodB() {
        synchronized (thatLock) {
            System.out.println("fang:你给我offer我就给你讲什么是死锁");
        }
    }
}

宝,我们交往吧
好啊这个程序自己停止了我就和你处

然后....再也没有然后了

fang:你给我offer我就给你讲什么是死锁
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer
fang:你给我offer我就给你讲什么是死锁
hr:给我讲什么是死锁我就给你offer

如何避免死锁?

成年人要学会及时止损

不要写嵌套锁,尽量保证锁的原子性

通过jconsole来进行诊断。

打开jconsole

open /Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/bin/jconsole

选中当前进程

image.png

检测死锁

image.png

image.png

如何保证线程同步

  • synchronized 锁
  • Lock 锁
  • Threadlocal 可能存在内存泄露
  • 原子类 CAS 非阻塞

在springboot中如何保证线程安全?

springICO 容器默认是 单例模式,因此会存在线程安全问题。

@RestController
public class SyncController {
    private int count;
    @RequestMapping("/count")
    public synchronized String count() throws InterruptedException {
        count++;
        Thread.sleep(3000);
        return String.valueOf(count);
    }
}

以上代码造成浏览器阻塞3秒后再次响应,效率是非常低的。

那么我们取消单例模式试试看

@RestController

// 取消为单例模式
@Scope(value = "prototype")
public class SyncController {
    private int count;
    @RequestMapping("/count")
    public synchronized String count() throws InterruptedException {
        count++;
        Thread.sleep(3000);
        return String.valueOf(count);
    }
}

然后问题就解决了,就这?

多线程通信问题

都是需要 配合 synchronized 结合使用

  • wait() 释放锁当前线程进入阻塞状态
  • notify() 唤醒沉睡线程,但是不会释放锁
  • notifyAll() 唤醒所有沉睡线程,不释放锁
package thread;

public class ThreadCorrespond implements Runnable {
    private Object objLock = new Object();
    private void doSomething(){
        synchronized (objLock){
            try {
                System.out.println(">1<");
                objLock.wait();
                System.out.println(">2<");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        // 创建子线程  子线程内有阻塞代码
        ThreadCorrespond threadCorrespond = new ThreadCorrespond();
        new Thread(threadCorrespond).start();
        
        // 主线程 3秒后 唤醒子线程
        try {
            Thread.sleep(3_000);
            synchronized (threadCorrespond.objLock){
                // 开始唤醒
                threadCorrespond.objLock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        doSomething();
    }
}

>1<
>2<

生产者消费者模型

一个简单的生产者消费者模型

package thread;

public class ThreadProduct {
    /**
     * 共享对象
     */
    class Res {
        public String userName;
        public char sex;
    }

    class InputThread extends Thread {
        private Res res;

        public InputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            boolean isOk =false;
            while (true) {
                if (isOk) {
                    res.userName = "luckyFang";
                    res.sex = '男';
                } else {
                    res.userName = "alice";
                    res.sex = '女';
                }
                isOk=!isOk;
            }
        }
    }

    /**
     * 消费者线程
     */
    class OutputThread extends Thread{
        private Res res;
        public OutputThread(Res res){
            this.res=res;
        }
        @Override
        public void run() {
            while (true){
                System.out.println(res.userName+","+res.sex);
            }
        }
    }

    public static void main(String[] args) {
        new ThreadProduct().print();
    }


    public void print(){
        // global res
        Res res = new Res();
        InputThread inputThread = new InputThread(res);
        OutputThread outputThread = new OutputThread(res);

        inputThread.start();
        outputThread.start();
    }
}

luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,女
luckyFang,男
luckyFang,女
luckyFang,男
luckyFang,男
alice,女

通过上面输出结果我们不难看出,这个生产者消费者模型存在很大的问题。

因为两个线程共享一个对象,就可能造成资源争抢问题,本来luckyFang是男的,结果被另一个线程影响变成了女的。这是不妥的

通过上锁解决

package thread;

public class ThreadProduct {
    /**
     * 共享对象
     */
    class Res {
        public String userName;
        public char sex;
    }

    class InputThread extends Thread {
        private Res res;

        public InputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            boolean isOk = false;
            while (true) {
                synchronized (res) {
                    if (isOk) {
                        res.userName = "luckyFang";
                        res.sex = '男';
                    } else {
                        res.userName = "alice";
                        res.sex = '女';
                    }
                    isOk = !isOk;
                }
            }
        }
    }

    /**
     * 消费者线程
     */
    class OutputThread extends Thread {
        private Res res;

        public OutputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (res) {
                    System.out.println(res.userName + "," + res.sex);
                }
            }
        }
    }

    public static void main(String[] args) {
        new ThreadProduct().print();
    }


    public void print() {
        // global res
        Res res = new Res();
        InputThread inputThread = new InputThread(res);
        OutputThread outputThread = new OutputThread(res);

        inputThread.start();
        outputThread.start();
    }
}

luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男
luckyFang,男

继续分析上面代码,我们发现虽然解决了资源争抢问题,但是往往你解决了一个bug,会诞生一个新的bug:我们生产者线程本意是交错生产对象,但是现在疯狂只生产一个对象。

这时候我们就需要用到两个线程之间的通信了

生产者:等会,我还没生产完呢
消费者:好的呢,我等等就是了

生产者:好了我生产完了,过来拿吧
消费者:收到

package thread;

public class ThreadProduct {
    /**
     * 共享对象
     */
    class Res {
        public String userName;
        public char sex;
        public boolean isBlock = false;
    }

    class InputThread extends Thread {
        private Res res;

        public InputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            boolean isOk = false;
            while (true) {
                synchronized (res) {
                    if (res.isBlock) {
                        try {
                            res.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (isOk) {
                        res.userName = "luckyFang";
                        res.sex = '男';
                    } else {
                        res.userName = "alice";
                        res.sex = '女';
                    }
                    isOk = !isOk;
                    res.isBlock = true;
                    // 生产完了 过来消费吧
                    res.notify();
                }
            }
        }
    }

    /**
     * 消费者线程
     */
    class OutputThread extends Thread {
        private Res res;

        public OutputThread(Res res) {
            this.res = res;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (res) {
                    // 如果拿到了不属于自己的那就放手吧
                    if (!res.isBlock) {
                        try {
                            // 释放锁
                            res.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(res.userName + "," + res.sex);
                    res.isBlock = false;
                    // 消费完了 通知生产吧
                    res.notify();
                }
            }
        }
    }

    public static void main(String[] args) {
        new ThreadProduct().print();
    }


    public void print() {
        // global res
        Res res = new Res();
        InputThread inputThread = new InputThread(res);
        OutputThread outputThread = new OutputThread(res);

        inputThread.start();
        outputThread.start();
    }
}

luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女
luckyFang,男
alice,女

具体实现原理也是非常的简单,我们设置一个isBlock来判断是否是阻塞状态

isBlock状态

  • true 消费者开始消费 生产者开始等待
  • false 生产者开始生产 消费者开始等待

这样看似没有问题,但是他真的没有问题吗?

要知道 两个线程同时去竞争一把锁,并不是每次都是同一个线程都能拿到锁的。

因此我们要建立合约

如果 isBlock = true 但是这个锁被生产者拿到了,根据合约内容生产者要自动放弃这把锁,因为她不属于你,你退出的同时,你要通知锁的主人(消费者)
如果 isBlock = false 但是这个锁被消费者拿到了,根据合约内容消费者要主动放弃这把锁,因为不属于你的你把持不住你退出的同时,你要通知锁的主人(生产者)

那么我们简单总结下:

  • isBlock 决定是生产还是消费
  • 但是如果不是你的锁你就要主动放弃wait,并且通知锁的主人notify

image.png