Java多线程基础

201 阅读18分钟

[TOC]

多线程基础

一、实现多线程的方法(2种)

实现Runnable接口的run方法,传给Thread

public class TestRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("测试runnable接口");
    }

    public static void main(String[] args) {
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

继承Thread类,重写run方法

public class TestThread extends Thread{
    @Override
    public void run() {
        System.out.println("测试thread run方法");
    }

    public static void main(String[] args) {
        TestThread thread = new TestThread();
        thread.start();
    }
}

说明

  1. 两种实现的本质:都是实现了Thread类的run方法,一个是调用,一个是重写
  2. 线程池、callable、future、定时器timer本质上都是通过这两种方式创建线程

面试问题:两种实现方式对比?实现runnable接口更好

  1. 代码架构:线程执行的任务应该与线程本身的创建、运行机制解耦
  2. 资源利用:利用线程池可以减少创建销毁线程带来的性能损耗
  3. 扩展性:继承thread类后无法继承别的类

二、启动线程的正确姿势

案例:打印当前线程名称

public class TestRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        // 执行run方法
        thread.run();
        // 执行start方法
        thread.start();
        // 执行两次start方法
        thread.start();
    }
}

image-20230325160407932

说明

  1. run方法打印了main线程的名称

  2. start方法启动了新线程,本质是执行了虚拟机的native方法start0

  3. 第二次执行会报线程状态异常

面试问题:执行两次start方法会怎么样?

会抛出线程状态异常

三、如何正确停止线程

原理:使用interrupt通知,而不是强制

1、普通情况下停止线程

public class TestInterruptThread implements Runnable{

    @Override
    public void run() {
        int num = 0;
        // 执行条件中判断当前线程是否被打断
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000的倍数");
            }
            num ++;
        }
        System.out.println("执行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        TestInterruptThread runnable = new TestInterruptThread();
        Thread thread = new Thread(runnable);
        // 启动线程
        thread.start();
        Thread.sleep(500);
        // 线程通知打断
        thread.interrupt();
    }
}

image-20230325164623808

说明:主线程发出interrupt指令后,线程任务中有interrupt状态的判断,所以任务被终止

2、阻塞的情况下停止线程

public class TestInterruptThread implements Runnable {

    @Override
    public void run()  {
        int num = 0;
        // 这里很快执行完
        while (!Thread.currentThread().isInterrupted() && num <= 300) {
            if (num % 100 == 0) {
                System.out.println(num + "是100的倍数");
            }
            num ++;
        }
        // 这里进入阻塞
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        TestInterruptThread runnable = new TestInterruptThread();
        Thread thread = new Thread(runnable);
        thread.start();
        // 主线程sleep 1s让新线程把逻辑跑完
        Thread.sleep(1000);
        // 主线程通知打断
        thread.interrupt();
    }
}

image-20230325165750006

说明:线程在sleep状态下被interrupt,会抛出InterruptedException异常

3、迭代阻塞的情况下停止线程

public class TestInterruptThread implements Runnable {

    @Override
    public void run()  {
        int num = 0;
        // 执行条件中判断当前线程是否被打断
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
            if (num % 10 == 0) {
                System.out.println(num + "是100的倍数");
            }
            num ++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
		System.out.println("执行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        TestInterruptThread runnable = new TestInterruptThread();
        Thread thread = new Thread(runnable);
        // 启动线程
        thread.start();
        Thread.sleep(500);
        // 线程通知打断
        thread.interrupt();
    }
}

image-20230325175915188

说明:在while循环中对阻塞(sleep)进行异常捕获,并且在while条件中加入当前线程的isInterrupted的判断是不会终止循环的,原因是:

阻塞(sleep)状态下被interrupt会抛出异常并将当前线程的interrupt标记清除掉,所以需要在catch中显式的添加interrupt标记

try {
    Thread.sleep(10);
} catch (InterruptedException e) {
    e.printStackTrace();
    // 显式终止线程
    Thread.currentThread().interrupt();
}

image-20230325180917711

3、停止线程的错误方式

①弃用的stop、suspend、resume方法

②用volatile设置Boolean标记位

public class TestInterruptThread implements Runnable {

    /** 为什么可以用volatile,因为volatile声明的变量是线程可见的,线程实时更新的 */
    private volatile boolean cancel = false;

    @Override
    public void run()  {
        int num = 0;
        try {
            // 执行条件中判断当前线程是否被打断
            while (!cancel && num <= Integer.MAX_VALUE / 2) {
                if (num % 10 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num ++;
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
        System.out.println("执行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        TestInterruptThread runnable = new TestInterruptThread();
        Thread thread = new Thread(runnable);
        // 启动线程
        thread.start();
        Thread.sleep(500);
        // 线程通知打断
        runnable.cancel = true;
    }
}

image-20230325200948290

说明:volatile关键字声明的变量在线程中是可见的,因此可以作为线程间通信的标志位

为什么这样做是有问题的?

在上述场景下,使用volatile关键字完美实现了interrupt的功能,但是在长时间阻塞的情况下,无法进行中断操作

以生产者、消费者模式说明:

/** 生产者 */
class Producer implements Runnable{

    /** 中断标记 */
    public volatile boolean cancel = false;

    private BlockingQueue<Integer> queue;

    public Producer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        int num = 0;
        // 执行条件中判断当前线程是否被打断
        try {
            while (!cancel && num <= Integer.MAX_VALUE / 2) {
                if (num % 10 == 0) {
                    queue.put(num);
                    System.out.println("仓库生产出:" + num);
                }
                num ++;
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("仓库已满");
        }
    }
}

/** 消费者 */
class Consumer implements Runnable{

    private BlockingQueue<Integer> queue;

    public Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (Math.random() > 0.15) {
                System.out.println(queue.take() + "被消费");
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("消费者已不再需要数据");
        }
    }
}


public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
        Producer pd = new Producer(queue);
        new Thread(pd).start();
        Thread.sleep(1000);
        Consumer cn = new Consumer(queue);
        new Thread(cn).start();
        Thread.sleep(5000);
        pd.cancel = true;
    	System.out.println(pd.cancel);
    }

image-20230325233343819

说明:上述生产者消费者案例中,生产者的生产速度明显快于消费者的消费速度,当前消费者执行完毕时,生产者处于满队列状态,生产者线程此时进入阻塞状态。

此时将volatile的中断标记设为true,但生产者线程并没有被中断

4、interrupt()、isInterrupted()、Thread.interrupted()方法对比

方法作用作用域
thread.interrupt()设置中断标记对象
thread.isInterrupted()获取中断标记,不清除对象
Thread.interrupted()获取中断标记,并清除

面试问题:如何正确的停止线程?

  1. 原理:使用interrupt来请求
  2. 停止线程,需要请求方、被停止方相互配合
  3. volatile关键字无法处理长时间阻塞的情况

四、线程的生命周期

1、线程的6中状态

图片转存失败,建议将图片保存下来直接上传

public class TestRunnable implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        System.out.println(thread.getState());
        thread.start();
        System.out.println(thread.getState());
        Thread.sleep(2000);
        System.out.println(thread.getState());
    }
}

image-20230326001512867

public class TestRunnable implements Runnable{
    
    @Override
    public void run() {
        toDo();
    }

    private synchronized void toDo() {
        try {
            Thread.sleep(10000);
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable = new TestRunnable();
        Thread thread1 = new Thread(runnable);
        thread1.start();
        Thread.sleep(2000);
        Thread thread2 = new Thread(runnable);
        thread2.start();
        System.out.println(thread1.getState());
        System.out.println(thread2.getState());
        Thread.sleep(11000);
        System.out.println(thread1.getState());

    }
}

图片转存失败,建议将图片保存下来直接上传

面试问题:线程有哪几种状态?生命周期是什么?

上图解释

五、Thread、Object线程相关方法

1、wait、notify、notifyAll作用、用法

public class TestRunnable{
    
    /** 锁对象 */
    private static final Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread1();
        thread1.start();
        // 保证线程1进入wait
        Thread.sleep(200);
        Thread thread2 = new Thread2();
        thread2.start();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println("进入thread1方法");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread1重新获得锁继续执行直到结束");
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (object) {
                System.out.println("进入thread2方法");
                object.notify();
            }
            System.out.println("thread2继续执行直到结束");
        }
    }
}

image-20230327151217505

说明:

  1. wait()方法会释放锁,所以Thread2会进入同步代码块,notify后Thread1重新获得锁,得以继续执行
  2. thread1启动后主线程sleep是为了保证thread1进入wait,代码的start顺序并不代表线程的启动顺序
public class TestRunnable implements Runnable{

    /** 锁对象 */
    private static final Object object = new Object();

    @Override
    public void run() {
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "获得锁开始执行");
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "被唤醒,并重新获得锁继续执行");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable = new TestRunnable();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        Thread.sleep(200);
        new Thread(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "开始唤醒阻塞线程");
                object.notifyAll();
                System.out.println(Thread.currentThread().getName() + "唤醒完毕");
            }
        }).start();
    }
}

image-20230327152700554

说明:notifyAll唤醒了所有阻塞线程

public class TestRunnable implements Runnable{

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + "获得锁1开始执行");
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + "获得锁2开始执行");
                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "锁2被释放继续执行");
            }
            System.out.println(Thread.currentThread().getName() + "锁2被释放继续执行");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable = new TestRunnable();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

image-20230327194401015

说明:wait()只会释放当前锁

总结

  1. wait、notify、notifyAll操作必须先拥有monitor锁(synchronized)
  2. notify会随机唤醒一个wait的线程
  3. 一个线程拥有多把锁的情况下,wait只会释放一个锁
  4. 由于只有拥有同一个锁的线程可以进行notify,所以唤醒后,当前线程还未释放该锁,所以刚被唤醒的线程此时并不能到runnable状态,而是回到了锁等待的状态,即blocked
  5. wait状态期间发生异常会直接进入terminated状态

2、wait、notify实现生产者消费者模型

import java.util.LinkedList;

public class ProducerConsumerModel{

    static class Producer implements Runnable{

        private Storage storage;

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

        @Override
        public void run() {
            for (int i = 0; i < 1000; 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 < 1000; i++) {
                storage.take();
            }
        }
    }

    static class Storage {

        private int maxSize;
        private LinkedList<String> store;

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

        public synchronized void put() {
            while (store.size() == maxSize) {
                System.out.println("仓库已满");
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            store.add("new stuff");
            System.out.println(Thread.currentThread().getName() + "仓库已生产" + store.size() + "个货物");
            notifyAll();
        }

        public synchronized void take() {
            while (store.isEmpty()) {
                System.out.println("仓库已空");
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "消费者已消费" + (maxSize - store.size() + 1) + "个货物");
            store.poll();
            notifyAll();
        }
    }

    public static void main(String[] args) {
        Storage storage = new Storage();
        Producer pd = new Producer(storage);
        Consumer cs = new Consumer(storage);
        new Thread(pd).start();
        new Thread(cs).start();
    }
}

说明:

  • 仓库类实现生产(put)、消费(take)两个同步方法
  • 生产者、消费者分别在构造函数中传入同一个仓库对象作为锁
  • run方法中分别实现循环生产和消费

3、用wait、notify实现交替打印0-任意数字

public class OddEvenNum{

    static class OddNum implements Runnable{

        private Num num;

        public OddNum(Num num) {
            this.num = num;
        }

        @Override
        public void run() {
            num.readOdd();
        }
    }

    static class EvenNum implements Runnable{

        private Num num;

        public EvenNum(Num num) {
            this.num = num;
        }

        @Override
        public void run() {
            num.readEven();
        }
    }

    static class Num {

        private final int maxNum;
        private int initNum = 1;

        public Num(int maxNum) {
            this.maxNum = maxNum;
        }

        public synchronized void readOdd() {
            while (initNum < maxNum) {
                if (initNum % 2 == 0) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "打印数字: " + initNum++);
                notify();
            }
        }

        public synchronized void readEven() {
            while (initNum < maxNum) {
                if (initNum % 2 != 0) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "打印数字: " + initNum++);
                notify();
            }
        }
    }

    public static void main(String[] args) {
        Num num = new Num(100);
        OddNum oddNum = new OddNum(num);
        EvenNum evenNum = new EvenNum(num);
        new Thread(oddNum).start();
        new Thread(evenNum).start();
    }
}

说明:仿照生产者消费者模型写了交替打印0-100数字

面试问题

  1. 为什么wait需要在同步代码块内使用,而sleep不需要?

    如果不在同步代码块中,可能会出现在wait之前上下文切换到另一个线程执行了notify方法,导致wait不会被唤醒(lost-wake up问题),因为无法保证代码的同步执行,而wait设计的初衷就是为了让notify可以将其唤醒

  2. 为什么线程通信方法wait、notify、notifyAll被定义在Object里,而sleep被定义在Thread里?、

    • wait、notify、notifyAll都是锁级别的方法,在java中,任意对象都可以作为锁,所以定义在所有对象的父类object是最合理的
    • 一个线程可以拥有多个锁,如果wait、notify、notifyAll方法被定义在Thread类中,则该线程无法知道具体要释放那个锁,唤醒哪个锁阻塞的线程

4、join方法解释

image-20230328000821540

image-20230328000851486

说明:

  1. thread.join()是指子线程TestThread加入主线程main,main线程会进入wait状态,等待子线程运行结束后,继续执行main线程
  2. 根据join原理,thread.join()可用synchronized代码块代替
public static void main(String[] args) throws InterruptedException {
        TestThread thread = new TestThread();
        thread.start();
        // TestThread加入main线程
        System.out.println("等待test线程执行完");
//        thread.join();
        synchronized (thread) {
            thread.wait();
        }
        System.out.println("test线程执行完毕");
    }

六、线程各属性

image-20230328151925408

面试问题:

  1. 什么时候使用守护线程?

    通常情况下不需要设置,JVM已经有足够的守护线程,例如垃圾回收

  2. 应该如何应用线程优先级来帮助程序运行?有哪些禁忌

    通常情况下不使用线程优先级,因为本质还是操作系统的调度,java程序层面只是建议,并不能起到决定性作用

七、如何处理子线程的异常

import java.util.logging.Level;
import java.util.logging.Logger;

public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.WARNING, String.format("%s线程异常终止,并被UncaughtExceptionHandler捕获", t.getName()), e);
    }
}
public class TestThread extends Thread{
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行子子线程的run方法");
        throw new IllegalArgumentException("出错啦~");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        TestThread thread = new TestThread();
        thread.start();
    }
}

image-20230328164126611

说明:设置全局异常捕获器,处理线程异常

八、线程是把双刃剑

1、各种需要考虑线程安全的情况

  1. 访问共享的变量或资源,有并发风险
  2. 所有依赖时序的操作,即使每一步都是安全的,还是存在并发问题
  3. 通常没有声明是线程安全的类,是存在线程安全问题的

2、为什么多线程会带来性能问题?

①、调度:

  • 上下文切换:操作系统内核在CPU上对于进程(线程)进行一些活动

    1. 挂起一个进程,在CPU某处存储该进程的状态
    2. 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
    3. 跳转到程序计数器所指向的位置(跳转到进程被中断时所在的代码行),以恢复该进程
  • 缓存失效:CPU重新缓存,CPU在该进程执行过程中的缓存被清除

  • 导致密集的上下文切换场景:频繁的竞争锁、由于IO等读写操作导致频繁的阻塞

②、协作:内存同步

为了数据正确性,同步手段会禁止编译器优化(指令重排)、使CPU的缓存失效

九、Java内存模型

1、JVM内存结构、Java内存模型、Java对象模型

JVM内存结构:虚拟机的运行时区域,主要是线程共享的堆、方法栈,线程独有的虚拟机栈、本地方法栈、程序计数器

Java内存模型:和并发编程有关

Java对象模型:指Java对象在虚拟机中的表现形式有关

2、JMM是什么

  • 是一组规范,保证了不同虚拟机在处理多线程并发场景的统一机制
  • 是工具类和关键字的原理:volatile、synchronized、Lock
  • 包含三点内容:重排序、可见性、原子性

3、重排序

三种情况:

  • 编译器优化
  • CPU指令重排
  • 内存的”重排序“

好处:提高处理速度

4、可见性

概念:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改

5、JMM抽象

计算机CPU与内存的工作模式

image-20230402224050354

Java内存模型

image-20230402224231278

Java 作为高级语言,屏蔽了这些底层细节,用JMM 定义了一套读写内存数据的规范,我们不再需要关心一级缓存和二级缓存的问题,JMM抽象了主内存和本地内存的概念。这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象

为什么会导致可见性的问题?

所有的共享变量存在于主内存中,每个线程有自己的本地内存,会存有主内存的共享变量的拷贝,线程读写共享数据都是通过本地内存交换的,所以存在可见性问题。

6、happens-before(先行发生原则)

概念:两个操作A、B满足happens-before原则的前提下,如果操作A先行发生于操作B,那么操作A产生的结果对于操作B是可见的。

  • 单线程操作:在一个线程内,程序按照代码书写顺序执行
  • 锁操作(synchronized、Lock):一个线程的锁住的代码一定先行发生于另一个线程同一个锁的代码
  • volatile变量:volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动:Thread的start方法先行发生于此线程的所有动作
  • 线程join:线程执行的run方法一定先行发生于join后的操作
  • 线程中断:interrupt方法一定先行发生于检测中断的方法前,意思是可以用interrupt检测是否有中断发生
  • 传递性:操作A先行发生与操作B,操作B先行发生于操作C,那么操作A一定先行发生于操作C

十、volatile关键字

1、特性:

  • 保证此变量对所有线程的可见性
  • 禁止指令重排优化

2、适用场景

  • Boolean flag,如果一个变量自始至终都只被各个线程赋值,没有其他操作,那么就可用volatile修饰,由于赋值是原子操作,所以保证了线程安全
import java.util.concurrent.atomic.AtomicInteger;

public class TestRunnable implements Runnable{

    private volatile Boolean done = false;
    private final AtomicInteger real = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            flipDone();
            real.incrementAndGet();
        }
    }

    private void flipDone() {
        // 只是赋值操作,原子操作
        done = true;
        // 依赖于变量本身的当前值,不是原子操作
        // done = !done;
    }

    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable = new TestRunnable();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(runnable.done);
        System.out.println(runnable.real);
    }
}
  • 作为刷新之前变量的触发器

image-20230403145332644

这个原理是说:线程A将volatile变量赋值为true,由于可见性原理,线程B的判断条件保证了volatile变量赋值操作前的操作一定是可见的,且不存在指令重排

3、不适用场景

凡是该变量的赋值操作依赖于当前变量,都会导致同步失效的可能

我理解本质还是因为凡是涉及计算的都不是原子操作,所以会存在同步失败的情况

import java.util.concurrent.atomic.AtomicInteger;

public class TestRunnable implements Runnable{

    private volatile int count;
    // 保证了自增的原子操作,用来对比同步情况下的计算结果
    private final AtomicInteger real = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            count ++;
            real.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable = new TestRunnable();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(runnable.count);
        System.out.println(runnable.real);
    }
}

image-20230403114334641

4、原子性的说明

Java中,原子性的操作有以下几种:

  1. long、double以外的基本类型的赋值操作
  2. 引用的赋值操作
  3. java.concurrent.Atomic.*包中所有类的操作

5、总结

保证并发同步安全需要三个条件:

  • 操作的原子性
  • 线程的可见性
  • 禁止指令重排序

volatile关键字只保证了可见性和禁止重排序,所以只要对volatile修饰的变量的操作是原子的,那么volatile就是并发安全的。

十一、单例模式

1、适用场景

  • 无状态的工具类:比如日志工具类,不需要在实例上存储任何状态,只需要一个实例即可
  • 全局信息类:比如在一个类中记录网站的访问次数,希望不管访问任何页面都会被记录到,可以使用一个实例即可

2、八种单例写法

饿汉式(静态变量)

public class Singleton1 {

    private static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

饿汉式(静态代码块)

public class Singleton2 {

    private static final Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

饿汉式弊端:无论是否用到,都会首先实例化该对象,浪费内存

懒汉式(线程不安全)

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

懒汉式(线程安全)(不推荐)

public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

懒汉式(同步代码块)(不可用)

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

懒汉式优点:

  • 延迟加载,需要用到才会实例化,优化内存使用

缺点:

  • 写法相对复杂
  • 处理不好存在线程安全问题
  • 线程安全的懒汉式的方式效率太低,针对不需要实例化的操作也被加上了锁

双重检查(面试推荐写法)

public class Singleton6 {

    private static volatile Singleton6 instance;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

原理解读:

在getInstance方法中,首先判断instance是否为空,只有为空时才需要进行实例化,而为了避免并发问题,使用synchronized将实例化的逻辑锁起来:

public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                instance = new Singleton6();
            }
        }
        return instance;
    }

但是此时是有问题的,如果线程A、B同时进入为空的判断逻辑中,由于锁的原因,线程A进行了实例化,当A退出锁时,B进入实例化的逻辑,就会又进行实例化,从而破坏了单例。针对这个问题,需要再一次进行为空的判断:

public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                // 线程B进来时由于instance已经不为空了,所以不会进行实例化
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }

为什么需要加volatile关键字?

image-20230403213655987

实例化一个对象有三个步骤:

1、创建一个空对象

2、执行该类的构造方法

3、将空对象赋值给引用

volatile关键字禁止指令重排序,防止了CPU将空对象赋值给引用的操作提前而导致空指针

匿名内部类(推荐,但复杂度高)

public class Singleton7 {

    private Singleton7() {
    }

    private static class SingletonInstance{
        private static final Singleton7 SINGLETON_INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.SINGLETON_INSTANCE;
    }
}

枚举(最佳实践)

public enum Singleton8 {

    INSTANCE;

    public void doSomething() {

    }
}

枚举写法的优势:

  • 写法简单
  • 线程安全有保障
  • 同时也是懒加载
  • 避免反序列化破坏单例

十二、死锁

1、案例

image-20230404143324468

代码案例:

public class TestRunnable implements Runnable{

    private boolean flag;

    // 这两个锁是static的,类层级的锁,所以两个runnable对象可以共享
    static final Object o1 = new Object();
    static final Object o2 = new Object();

    public TestRunnable(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        System.out.println("flag:" + flag);
        if (flag) {
            synchronized (o1) {
                System.out.println(Thread.currentThread().getName() + "获得锁1");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + "获得锁2");
                }
            }
        }
        if (!flag) {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + "获得锁2");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + "获得锁1");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable1 = new TestRunnable(true);
        TestRunnable runnable2 = new TestRunnable(false);
        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);
        thread1.start();
        thread2.start();
    }
}

image-20230404154058628

说明:thread1获得锁1后,执行sleep期间thread2获得了锁2,thread2需要等thread1释放锁1,thread1需要thread2释放锁2,造成死锁

2、必要条件

  • 互斥条件:资源不能被共享,只能由一个进程使用。
  • 请求与保持条件:进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放
  • 循环等待:若干个进程形成环形链,每个都占用对方申请的下一个资源

3、定位死锁

jstack工具

image-20230404162840953

使用ThreadMxBean

public static void main(String[] args) throws InterruptedException {
        TestRunnable runnable1 = new TestRunnable(true);
        TestRunnable runnable2 = new TestRunnable(false);
        Thread thread1 = new Thread(runnable1);
        Thread thread2 = new Thread(runnable2);
        thread1.start();
        thread2.start();
        Thread.sleep(5000);
        ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = mxBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = mxBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("发现死锁线程:" + threadInfo.getThreadName());
            }
        }
    }

image-20230404164418065

4、修复死锁的策略

避免策略

  • 服务员检查:在哲学家去拿筷子的时候, 由服务员去判断是否会造成五个人都拿着左边筷子的情况, 如果会, 那么服务员就让某个哲学家等一会再拿左边的筷子, 避免了死锁的情况
  • 改变一个哲学家拿筷子的顺序:此策略没有额外的服务员 . 而是 某一个哲学家不是从左边拿筷子, 是从右边拿筷子, 这样就避免了死锁的环路
  • 餐票:餐票方案是指, 如果有五个哲学家 , 那么只给出四个餐票, 总会有一个哲学家是吃不上饭的, 这样也就避免了死锁. 当某个哲学家吃完饭后, 再把餐票还回去

检测与恢复策略

  • 领导调节:领导调节策略指的是, 有领导定期的巡视, 如果发现出现了死锁, 那么就会剥夺某一个哲学家手中的筷子 , 释放锁资源, 破坏死锁的四个条件中的不剥夺条件