notes notes
首页
读书笔记
系统设计
项目实战
学习笔记
源码
运维
其它
极客时间 (opens new window)
GitHub (opens new window)
首页
读书笔记
系统设计
项目实战
学习笔记
源码
运维
其它
极客时间 (opens new window)
GitHub (opens new window)
  • 并发编程

    • 并发编程
    • 多线程
      • 线程基础
        • 实现线程的方式
        • 两种实现
        • 对比
        • 如何正确的启动线程
        • start()源码
        • run()源码
        • 如何正确的停止线程
        • 通常情况下停止线程
        • 停止带有阻塞的线程
        • 每次循环中都有阻塞
        • 循环中处理中断的问题
        • 处理中断最佳实践
        • 传递中断
        • 恢复中断
        • 错误的停止方式
        • 线程的生命周期
        • Thread 和 Object 的重要方法
        • 方法概览
        • wait、notify、notifyAll
        • 生产者消费者模式
        • 两个线程交替打印0~100奇偶数
        • 题目
        • sleep方法
        • join方法
        • yield方法
        • 线程的属性
        • Java异常体系
        • 未捕获异常如何处理
        • 多线程导致的问题
        • 线程安全
        • 死锁
        • 对象发布和初始化的时候安全问题
        • 性能问题
      • Java内存模型
        • JMM
        • 重排序
        • 重排序好处:提高处理速度
        • 重排序的情况
        • 可见性
        • 为什么会有可见性问题
        • JMM的抽象:主内存和本地内存
        • 主内存和本地内存的关系
        • 什么是happens-before
        • 什么不是happens-before
        • happens-before规制
        • volatile
        • 是什么
        • 使用场景
        • 作用
        • 和synchronized关系
        • 总结
        • 原子性
        • Java中的原子操作有哪些
        • long和double的原子性
        • 原子操作 + 原子操作 != 原子操作
        • 单例模式
        • 单例模式的作用
        • 单例模式的适用场景
        • 饿汉模式:静态常量
        • 饿汉模式:静态代码块
        • 懒汉模式:双重检查
        • 静态内部类
        • 枚举
        • 对比
        • 常见问题
      • 死锁
        • 是什么死锁
        • 死锁的危害
        • 死锁演示
        • 简单的死锁
        • 转账发生死锁
        • 多人转账发生死锁
        • 死锁的4个必要条件
        • 如何定位死锁
        • JDK自带工具
        • ThreadMXBean类
        • 修复死锁
        • 线上发生死锁怎么办
        • 常见修复策略
        • 哲学家就餐
        • 问题描述
        • 代码演示
        • 解决方案
        • 改变顺序(避免策略)
        • 检查与恢复策略
        • 如何避免死锁
        • 活锁
        • 什么是活锁
        • 工程中的活锁实例:消息队列
        • 如何解决活锁
        • 饥饿
    • 高级篇
  • 设计模式

    • 设计模式
  • 网络编程

    • Netty

      • NIO基础
      • Netty入门
      • Netty进阶
      • 优化与源码
  • 源码篇

    • 环境搭建
    • Spring
  • 云原生

    • Kubernetes
    • Helm
  • ElasticSearch

    • ElasticSearch
  • Java 虚拟机

    • 深入拆解 Java 虚拟机
    • JVM与GC调优
  • MQ

    • RabbitMQ

      • RabbitMQ笔记
      • RabbitMQ集群搭建文档
  • Redis

    • Redis进阶
  • ShardingSphere

    • Sharding-JDBC
  • SpringCloud

    • SpringCloud
  • ZooKeeper

    • ZooKeeper
  • 学习笔记
  • 并发编程
starry
2023-08-03
目录

多线程

# 线程基础

# 实现线程的方式

# 两种实现

官网文档 java/lang/Thread.java

有两种方法可以创建一个新的执行线程。

一种是将类声明为Thread的子类。这个子类应该覆盖类Thread的run方法。然后可以分配和启动子类的实例。

另一种方法是声明一个实现Runnable接口的类。然后该类实现run方法。然后可以分配该类的实例,在创建Thread时将其作为参数传递并启动。

# 对比

两种方法对比:实现Runnable接口更好

  • 从架构考虑,run方法即要执行的任务的创建、运行等,应该和Thead类是解耦的,不应该强耦合。
  • 从资源消耗来说,使用Thread类创建线程,只能新建一个独立的线程,创建、执行、销毁是比较耗费资源;使用Runnable后续可以借助线程池大大减少消耗。
  • Thread是一个类,如果对象基础了Thread类,就不能继承其他类了。单继承 Runnable是一个接口,一个对象可以实现多个接口。

本质对比:

  • new Thread().start(); 重写了run方法,直接间Thread的方法进行覆盖了
public void run() {
    if (target != null) {
        target.run();
    }
}
  • new Thread(runnable).start(); 传入的runnable对象就是target,run方法没有被重写,调用runnable的run方法

同时使用Runnable和Thread

public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("from runnable");
        }
    }){
        @Override
        public void run() {
            System.out.println("from thread");
        }
    }.start();
    // from thread
}

虽然传入了runnable对象,但是后续重写了thread的run方法,就不会执行target了

# 如何正确的启动线程

start()和run()的比较

public static void main(String[] args) {
    Runnable runnable = () -> {
        System.out.println(Thread.currentThread().getName());
    };

    runnable.run();

    new Thread(runnable).start();
}

输出

main
Thread-0

直接调用run方法是主线程执行,start是新建一个线程去执行。

# start()源码

  • 启动新线程检查线程状态
  • 加入线程组
  • 调用start0()
public synchronized void start() {

    // 调用start方法会像判断当前线程状态是不是0(还没启动的状态)
    // 如果调用两次start()就会抛出异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    // 当前线程加入到线程组
    group.add(this);
    boolean started = false;
    try {
        // 调用native方法
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

private native void start0();

# run()源码

就是执行传入的任务

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

# 如何正确的停止线程

使用interrupt来通知,而不是强制

# 通常情况下停止线程

public class StopThreadNormal 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 {
        Thread thread = new Thread(new StopThreadNormal());
        thread.start();
        Thread.sleep(1000);
        // 线程中断
        thread.interrupt();

    }
}

# 停止带有阻塞的线程

public class StopThreadWithSleep implements Runnable{

    @Override
    public void run() {
        int num = 0;
        try {
            // 判断当前线程是不是被中断了
            while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
                if (num % 10000 == 0) {
                    System.out.println(num+ "是10000的倍数");
                }
                num++;
            }
            // 线程阻塞
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // sleep 过程中被中断,处理~~~
            e.printStackTrace();
        }
        System.out.println("任务执行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadWithSleep());
        thread.start();
        Thread.sleep(500);
        // 线程中断
        thread.interrupt();

    }
}

线程sleep过程中被中断会抛出异常

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.starry.multithread.stopthread.StopThreadWithSleep.run(StopThreadWithSleep.java:23)

# 每次循环中都有阻塞

public class StopThreadWithSleepInLoop implements Runnable{

    @Override
    public void run() {
        int num = 0;
        try {
            // 每次循环中都有sleep就不用判断是否中断了 因为sleep被中断了走到catch中进行处理,就在循环外了
            while (num <= Integer.MAX_VALUE / 2) {
                System.out.println(num);
                num++;
                // 线程阻塞
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            // sleep 过程中被中断,处理~~~
            e.printStackTrace();
        }
        System.out.println("任务执行结束");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadWithSleepInLoop());
        thread.start();
        Thread.sleep(5000);
        // 线程中断
        thread.interrupt();

    }
}

输出

0
1
2
3
4
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.starry.multithread.stopthread.StopThreadWithSleepInLoop.run(StopThreadWithSleepInLoop.java:20)
	at java.lang.Thread.run(Thread.java:748)

# 循环中处理中断的问题

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () -> {
        for (int i = 0; i < 100 && !Thread.currentThread().isInterrupted(); i++) {
            System.out.println(i);
            // 循环内try/catch 处理中断
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(1000);
    thread.interrupt();
}

在循环中try/catch处理中断,并不能正常停止任务,即使每次循环前都进行了判断。因为,当前循环中发生了中断,走到了catch中,catch处理完了中断,继续到下一个for循环;因为处理完了中断,interrupt中断标志位会被清除,所以下次for循环进行判断时就没有被中断。for循环还是继续执行

0
1
2
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.starry.multithread.stopthread.CantInterrupt.lambda$main$0(CantInterrupt.java:17)
	at java.lang.Thread.run(Thread.java:748)
3
4
# 处理中断最佳实践
  • 优先选择:传递中断
  • 不想或无法传递:恢复中断
  • 不应屏蔽中断
# 传递中断
public class StopThreadInProd implements Runnable {


    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadInProd());
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }

    private void throwInMethod() throws InterruptedException {
        System.out.println("running");
        Thread.sleep(1000);

        /*
        * 错误实例,不应该把异常吞掉,应将异常向上传递
        * 让run方法去处理中断
        * */
//        try {
//            Thread.sleep(1000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
    }

    @Override
    public void run() {
        while (true) {
            // 调用其他方法
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                // 我们自己去处理中断 保存日志、停止程序
                System.out.println("保存日志 等等。。。");
                e.printStackTrace();
            }
        }
    }
}
# 恢复中断
public class StopThreadInProd2 implements Runnable {


    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThreadInProd2());
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }

    private void throwInMethod() {
        System.out.println("running");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 重新设置中断,让上层方法进行判断
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            // 判断当前是否被中断,中断就break,跳出循环,结束
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("被中断,程序运行结束");
                break;
            }
            // 调用其他方法
            throwInMethod();

        }
    }
}

# 错误的停止方式

被弃用的stop、suspend和resume方法

https://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html (opens new window)

为什么被Thread.stop弃用?

因为它本质上是不安全的(强制停止,即使任务执行到一半)。停止线程会导致它解锁所有已锁定的监视器。 suspend 会带着锁一起暂停,容易造成死锁

resume 用来恢复suspend的线程


用volatile设置boolean标志位

生产者 消费者 使用volatile无法停止(volatile无法处理长时间阻塞的情况)

public class VolatileCantStop {


    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
        Producer producer = new Producer(queue);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(queue);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.queue.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了");
        // 修改生产者的标志位,不需要生产数据了
        producer.canceled = true;
    }
}

class Producer implements Runnable {

    BlockingQueue<Integer> queue;
    public volatile boolean canceled;

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

    @Override
    public void run() {

        try {
            for (int i = 0; i < 10000 && !canceled; i++) {
                // 因为会阻塞在put方法,无法判断canceled的值
                queue.put(i);
                System.out.println(i + "放入队列");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

class Consumer{

    BlockingQueue<Integer> queue;

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

    /**
     * 是否需要消费更多数据(从队列取数据)
     * @return
     */
    public boolean needMoreNums() {
        return !(Math.random() > 0.95D);
    }
}

使用interrupt停止线程

public class VolatileCantStop {


    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
        Producer producer = new Producer(queue);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(queue);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.queue.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了");
        // 修改生产者的标志位,不需要生产数据了
//        producer.canceled = true;
        // 使用中断停止
        producerThread.interrupt();
    }
}

class Producer implements Runnable {

    BlockingQueue<Integer> queue;
    public volatile boolean canceled;

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

    @Override
    public void run() {

        try {
            // 判断是否被中断
            for (int i = 0; i < 10000 && !Thread.currentThread().isInterrupted(); i++) {
                queue.put(i);
                System.out.println(i + "放入队列");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

class Consumer{

    BlockingQueue<Integer> queue;

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

    /**
     * 是否需要消费更多数据(从队列取数据)
     * @return
     */
    public boolean needMoreNums() {
        return !(Math.random() > 0.95D);
    }
}

# 线程的生命周期

线程状态。线程可以处于以下状态之一:

  • NEW 尚未启动的线程处于此状态。
  • RUNNABLE 在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED 阻塞等待监视器锁的线程处于此状态。
  • WAITING 无限期等待另一个线程执行特定操作的线程处于此状态。
  • TIMED_WAITING 等待另一个线程执行操作达指定等待时间的线程处于此状态。
  • TERMINATED 已退出的线程处于此状态。

一个线程在给定的时间点只能处于一种状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。

public enum State {
    /**
     * 尚未启动的线程的线程状态。
     */
    NEW,

    /**
     * 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
     */
    RUNNABLE,

    /**
     * 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调用Object.wait后重新进入同步块/方法。
     */
    BLOCKED,

    /**
     * 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
     * Object.wait没有超时
     * 没有超时的Thread.join
     * LockSupport.park
     * 处于等待状态的线程正在等待另一个线程执行特定操作。例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()或Object.notifyAll()      * 。已调用Thread.join()的线程正在等待指定线程终止。
     */
    WAITING,

    /**
     * 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程     * 处于定时等待状态:
     * Thread.sleep
     * Object.wait超时
     * Thread.join超时
     * LockSupport.parkNanos
     * LockSupport.parkUntil
     */
    TIMED_WAITING,

    /**
     * 已终止线程的线程状态。线程已完成执行。
     */
    TERMINATED;
}

演示NEW、RUNNABLE、TERMINATED三种状态

public class NewRunnableTerminated implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(i);
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new NewRunnableTerminated());
        // 未start,NEW状态
        System.out.println(thread.getState());
        thread.start();
        // 已start,RUNNABLE状态
        System.out.println(thread.getState());
        // 延迟,保证线程是在执行中
        Thread.sleep(10);
        // 线程运行中,RUNNABLE状态
        System.out.println(thread.getState());
        Thread.sleep(1000);
        // 线程运行完,TERMINATED状态
        System.out.println(thread.getState());
    }
}

演示BLOCKED、WAITINGTIMED、WAITING三种状态

package com.starry.multithread.sixthreadstate;

/**
 * @author starry
 * @version 1.0
 * @date 2022/3/5 22:56
 * @Description
 */
public class BlockedWaitingTimedWaiting implements Runnable{
    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() {
        try {
            Thread.sleep(1000);
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new BlockedWaitingTimedWaiting();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

        Thread.sleep(300);
        // 线程一拿到锁,执行sleep,TIMED_WAITING状态
        System.out.println(thread1.getState());
        // 线程一未释放锁,线程二阻塞,BLOCKED状态
        System.out.println(thread2.getState());
        Thread.sleep(1300);
        // 执行wait,进入WAITING状态
        System.out.println(thread1.getState());

    }

}

阻塞状态

一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态;不仅仅是Blocked

# Thread 和 Object 的重要方法

# 方法概览

类 方法名(”相关“指的是重载方法) 简介
Thread sleep相关 让线程休眠
join 等待其他线程执行完毕
yield相关 放弃已经获取到的CPU资源
currentThread 获取当前执行线程的引用
start,run相关 启动线程相关
interrupt相关 中断线程
stop(),suspend(),resuem()相关 已废弃
Object wait/notify/notifyAll相关 让线程暂时休息和唤醒

# wait、notify、notifyAll

必须拥有monitor

阻塞阶段

  • 使用wait进入阻塞

唤醒阶段

notify:随机选取一个线程唤醒

notifyAll:唤醒全部线程

以下情况才会被唤醒:

  • 另一个线程调用这个对象的notify()方法,且刚好被唤醒的是本线程
  • 另一个线程调用这个对象的notifyAll()方法
  • 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待
  • 线程自身调用了interrupt()

遇到中断

  • 如果在wait阶段被中断,会释放monitor
# 生产者消费者模式
public class ProducerConsumerModel {


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

class EventStorage {
    private int maxSize = 10;
    private LinkedList<Integer> list = new LinkedList<>();

    public synchronized void put(int data) {
        while (list.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        list.add(data);
        System.out.println("###生产者生产数据,当前队列容量:"+list.size());
        notifyAll();
    }

    public synchronized void take() {
        while (list.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        list.poll();
        System.out.println(">>>消费者消费数据,当前队列容量:"+list.size());
        notifyAll();
    }

}

class Producer extends Thread {

    EventStorage storage;

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

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

class Consumer extends Thread {

    EventStorage storage;

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

    }
}
# 两个线程交替打印0~100奇偶数
public class TowThreadPrintNumber2 {

    private static final Object object = new Object();

    private static int num = 0;

    public static void main(String[] args) {
        Runnable runnable = () -> {
            while (num <= 100) {
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + ":" + num++);
                    // 本线程打印完,通知其他线程打印
                    object.notifyAll();
                    if (num <= 100) {
                        try {
                            // 本线程打印完,进入等待
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        new Thread(runnable,"偶数").start();
        new Thread(runnable,"奇数").start();
    }
}
# 题目

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

  • 防止死锁。假设threadA使用wait进入等待,threadB使用notifyAll唤醒threadA,如果没有synchronized保证,threadB先执行了notifyAll,然后threadA才执行了wait,那么threadA就一直等待,陷入死锁了。
  • sleep主要是针对当前线程的

为什么线程通信的方法wait()、notify()和notifyAll()被定义在Object类中?而sleep定义在Thread类中?

  • wait()、notify()和notifyAll()是锁级别的操作,锁是属于某个对象的(每个对象的对象头中都有几个bit用来保存锁的状态),所以锁是绑定在某个对象中,而不是线程中
  • 如果把wait定义到Thread里面,确实可以实现等待休眠,但是我们一个线程可能持有多个锁,这些锁之间是相互配合的,就无法做到这么灵活的控制了;把锁定义在每个对象上(Object类中),这样使用就比较灵活

wait方法属于Object对象的,那调用Thread.wait会怎么样?

  • 线程退出的时候,会自动调用notify,这可能不是我们所期望的,所以最好不要用Thread.wait

如何选择notify还是notifyAll?

  • notify是唤起一个线程,选择哪个是随机的。而notifyAll是唤起所有线程,然后这些线程再次抢去夺锁

notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

  • 实质就跟初始状态一样,多个线程抢夺锁,抢不到的线程就等待,等待上一个线程释放锁

# sleep方法

  • 让线程在预期时间执行,其他时间不占用CPU资源
  • 不释放锁。包括synchronized和lock
  • 相应中断,抛出InterruptedException,清除中断状态

sleep可以让线程进入Waiting状态,并且不占用CPU资源,但是不会释放锁,直到指定时间后再执行,休眠期间被中断会抛出异常并清除中断状态

wait/notify、sleep异同?

  • 相同:
    • 都是进入阻塞状态
    • 都可以相应中断
  • 不同:
    • wait/notify需要在同步代码块中
    • wait会释放锁,sleep不会释放
    • wait可以不用存入时间参数,sleep必须传参
    • 所属类不一样,一个Object,一个Thread

# join方法

作用:因为新的线程加入了我们,所以我们要等到他执行完再出发

main等待thread1和thread2执行完毕,再去执行后续操作

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () -> {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println("子线程运行完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread1.start();
    try {
        // main等待thread1执行完
        thread1.join();
        // main等待thread2执行完
        thread2.join();
    } catch (InterruptedException e) {
        // 如果main线程join中被中断,子线程也应该被中断
        thread1.interrupt();
        thread2.interrupt();
        e.printStackTrace();
    }

    System.out.println("主线程运行完毕");
}

join中断处理

如果main线程被中断,也应该把子线程给中断。

在join期间,线程的状态是什么?Waiting

join源码

最多等待millis毫秒让该线程终止。超时0意味着永远等待。 此实现使用以 this.isAlive 为条件的this.wait调用this.isAlive 。当线程终止时,将调用this.notifyAll方法。建议应用程序不要在Thread实例上使用wait 、 notify或notifyAll

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            // 调用object.wait方法
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

可以看到join实际上调用的是Object的wait方法, 所以thread.join();等价于synchronized(thread) {thread.wait()};

public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println("子线程运行完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread1 = new Thread(runnable);
        thread1.start();
//        thread1.join();
        synchronized (thread1) {
            thread1.wait();
        }
        System.out.println("主线程运行完毕");
    }

# yield方法

作用:释放我的CPU时间片

依然是runnable状态,也为不会释放锁,下次CPU还有可能调度到此线程

定位:JVM不保证一定会释放当前线程(不同JVM具体实现不相同)

yield和sleep区别:sleep确保休眠期间不会被调度,yield让出CPU后可能会直接被再次调度

# 线程的属性

属性名称 用途
编号(ID) 每个线程都有自己的ID,用于标识不同的线程;唯一性;不能修改
名称(Name) 作用让用户或程序在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等
是否为守护线程(isDaemon) true代表该线程是【守护线程】,false代表该线程是非守护线程,也就是【用户线程】
优先级(Priority) 目的是告诉线程调度器,用户希望哪些线程相对多运行、相对少运行

线程ID

从1开始,即main线程id是1

private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

线程名称

传入参数,就是指定的线程名;无参就是默认_Thread-_开始从0,依次递增

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

守护线程

  • 守护线程服务于用户。 用户线程未执行结束,JVM不能关闭; 用户线程执行结束,守护线程未结束,JVM可以关闭
  • 线程类型默认继承自父线程
  • main方法是用户线程

整体无区别,唯一就是JVM是否需要等待线程执行结束

线程优先级

10个级别,默认5

// 最低
public final static int MIN_PRIORITY = 1;
// 通常 
public final static int NORM_PRIORITY = 5;
// 最高 
public final static int MAX_PRIORITY = 10;

程序设计不应该依赖于优先级

  • 不同操作系统不一样
  • 优先级会被操作系统改变

比如:windows中有优先级推进器

  • 如果有个线程特别希望被执行,就会越过优先级,优先为他分配时间,这样我们设置的优先级就不可靠了
  • 如果一个任务的优先级被设置的过低,那么只要有任务就不会调度他,就会造成线程饥饿

# Java异常体系

# 未捕获异常如何处理

  • 每个线程执行时都try/catch,太麻烦。
  • 使用UncaughtExceptionHandle接口,设置线程全局处理器 使用
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.WARNING, "线程异常,终止啦" + t.getName(), e.getMessage());

    }
}
public static void main(String[] args) {
    Runnable runnable = () -> {
        throw new IllegalArgumentException();
    };

    Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
}

# 多线程导致的问题

# 线程安全

i++;多个线程对一个值进行修改

# 死锁

public class DeadLock implements Runnable {

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

    int flag = 0;

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

    /**
     * 使用sleep保证两个线程都拿到对方的资源,以达到死锁
     */
    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 0) {
            synchronized (lock1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println(0);
                }
            }
        }
        if (flag == 1) {
            synchronized (lock2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println(1);
                }
            }
        }
    }
}

# 对象发布和初始化的时候安全问题

  • 对象发布:”发布“一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如:将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中 发布一个对象最简单的方法是将对象的引用保存到一个公有的静态变量中,以便任何类和变量都能看见该对象
  • 对象逸出: 一种错误的发布
    • 方法返回一个private对象(private的本意是不让外部访问)
    • 还未初始化完成(构造函数还没完全执行完毕)就把对象提供给外界
public class Escape {

    private HashMap<Integer, String> map;

    public Escape() {
        // 使用另外线程初始化,模拟未初始化完成
        new Thread(()-> {
            this.map = new HashMap<>();
            map.put(1, "周一");
            map.put(2, "周二");
            map.put(3, "周三");
            map.put(4, "周四");
            map.put(5, "周五");
        }).start();
    }

    public HashMap<Integer, String> getMap() {
        return map;
    }

    public static void main(String[] args) throws InterruptedException {
        Escape escape = new Escape();
//        TimeUnit.SECONDS.sleep(1);
        // 还没完成初始化,NullPointerException
        HashMap<Integer, String> map = escape.getMap();
        System.out.println("map.get(1) = " + map.get(1));
//        获取到private的map,直接修改
        map.remove(1);
        System.out.println("map.get(1) = " + map.get(1));

    }
}

如何解决?

  • 返回对象“副本”,将private的对象copy一份给调用方
  • 使用工厂模式来保证对象一定被初始化完毕

需要考虑线程安全的情况:

  • 访问共享的变量或资源,有并发风险。比如对象的属性、静态变量、共享缓存、数据库等
  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-active
  • 不同的数据之间存在捆绑关系的时候
  • 使用其他类的时候,如果对方没有声明自己是线程安全的

# 性能问题

调度:上下文切换

协作:内存同步

# Java内存模型

# JMM

Java Memory Model。java内存模型

为什么需要JMM?

  • C语言不存在内存模型
  • 依赖处理器,不用处理器结果不一样
  • 无法保证并发安全
  • 需要一个标准,让多线程运行的结果可预期

JMM是规范

  • 需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便的开发多线程程序。
  • 如果没有这样的JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题。

JMM是工具类和关键字的原理

  • volatile、synchronized、Lock等的原理都是JMM
  • 如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有JMM,让我们只需要用同步工具和关键字就可以开发并发程序

# 重排序

public class OutOfOrderExecution {


    static AtomicInteger count = new AtomicInteger();

    static int a = 0;
    static int b = 0;
    static int x = 0;
    static int y = 0;


    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            count.incrementAndGet();
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Runnable runnable1 = () -> {
                a = 1;
                x = b;
            };
            Runnable runnable2 = () -> {
                b = 1;
                y = a;
            };
            Thread thread1 = new Thread(runnable1);
            Thread thread2 = new Thread(runnable2);
            thread1.start();
            thread2.start();

            thread1.join();
            thread2.join();

            System.out.println("x = " + x + "\t y=" + y);
            if (x == 0 && y == 0) {
                System.out.println(count.get()+ " 发生了指令重排");
                break;
            }
        }
    }
}

代码看上去执行结果只有:

  • a=1;x=b(0);b=1;y=a(1),最终结果是x=0;y=1
  • b=1;y=a(0);a=1;x=b(1),最终结果是x=1;y=0
  • a=1;b=1;x=a(1);y=b(1),最终结果是x=1;y=1

如果出现x=0;y=0就可能发生了重排序。即:x=b;a=1;y=b;b=1

在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序是不一致的,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是 y=a 和 b=1 这两行语句

# 重排序好处:提高处理速度

重排序前:

重排序后:

重排序后执行更少的指令,提高速度。

# 重排序的情况

  • 编译器优化:包括JVM,JIT编译器等
  • CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重拍

# 可见性

public class FiledVisibility {

    int a = 1;
    int b = 2;

    private void change() {
        a = 3;
        b = a;
    }

    private void print() {
        System.out.println("a=" + a + "\t b=" + b);
    }

    public static void main(String[] args) {
        while (true) {
            FiledVisibility test = new FiledVisibility();
            new Thread(() -> {
                test.change();
            }).start();
            new Thread(() -> {
                test.print();
            }).start();


        }
    }
}

情况分析:

  • a=3,b=2
    • 两个线程交替执行
  • a=1,b=2
    • 还没change就打印
  • a=3,b=3
    • change完打印
  • a=1,b=3
    • change操作完,还没来得及把所有数据同步到主存中,只把b同步到主存中,a还没来的同步,print便执行了,在主存中读取到的是初始值。

由于CPU有多级缓存,数据更新的不及时,所以才会出现可见性问题,可以使用volatile来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

# 为什么会有可见性问题

  • CPU有多级缓存,导致读的数据过期
    • 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
    • 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的
    • 如果所有核心都只用一个缓存,那么也就不存在内存可见性问题了
    • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取到的值是一个过期的值

# JMM的抽象:主内存和本地内存

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

# 主内存和本地内存的关系

  • JMM有以下规定:
    • 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
    • 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
    • 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通讯,必须借助主内存中转来完成
  • 所有的共享变量存在于内存中,每个线程有自己的本地内存,而且线程读写数据也是通过本地内存交换的,所以才导致了可见性问题

# 什么是happens-before

  • happens-before规则是用来解决可见性问题的:在时间山,动作A发生在动作B之前,B保证能看见A,这就是happens-before
  • 两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的

# 什么不是happens-before

两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到,这就不具备happens-before

# happens-before规制

  1. 单线程规则
  2. 锁操作(synchronized和Lock)
  3. volatile变量
  4. 线程启动
  5. 线程join
  6. 传递性
  7. 中断
  8. 构造方法
  9. 并发工具类
    1. 线程安全的容器get一定能看到在此之前的put等存入动作
    2. CountDownLatch
    3. Semaphore
    4. Future
    5. 线程池
    6. CyclicBarrier
  • 单线程规则 一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。
  • 锁操作 无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
  • volatile变量 这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的
  • 线程启动 在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
  • 线程join 如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
  • 传递性 如果hb(A, B) ,且 hb(B, C),那么可以推出hb(A, C)
  • 中断 一个线程被其他线程interrupt,那么检测中断isInterruptd或者抛出InterruptedException一定能看到
  • 构造方法 对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令

# volatile

# 是什么

  • volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
  • 如果一个变量别修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
  • 但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。

# 使用场景

不适用: a++

适用场合1:boolean flag,如果一个共享变量自始至终只被各个线程赋值(先读取再赋值不算,只赋值),而没有其他的操作,那么就可以用volatile来代替,synchronized或代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

适用场合2:作为刷新之前变量的触发器

// 例如声明一个 volatile 关键字
volatile boolean flag = false;
.....
//  Thread A
.....
flag = true  // 赋值为true

....
//  Thread B
if(!flag){      //此时已经刷新了,被线程B完全的看到了
}

# 作用

可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile 属性会立即刷入到主内存 禁止指令重排序优化:解决单例双重锁乱序的问题

# 和synchronized关系

volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的 ,而volatile又保证了可见性,所以就足以保证线程安全。

# 总结

  • volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如 Boolean flag ; 或者作为触发器,实现轻量级同步
  • volatile属性的读写操作都是无锁的,它不能替代synchronized因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  • volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  • volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
  • volatile 提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
  • volatile可以使得long和double的赋值是原子的,后面马上会讲long和double的原子性

# 原子性

一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一般的情况,是不可分割。

# Java中的原子操作有哪些

  • 除 long 和 double 之外的基本类型(int,byte,boolean,short,char,float)的赋值操作
  • 所有引用reference的赋值操作,不管是32位的机器还是64位的机器
  • java.concurrent.Atomic.* 包中所有类的原子操作

# long和double的原子性

https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7 (opens new window)

  • 问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、 读取错误、使用volatile解决
  • 结论:在32位上的JVM上。long 和double 的操作不是原子的,但是在64位的JVM是原子的
  • 实际开发中:商用Java虚拟机中不会出现

# 原子操作 + 原子操作 != 原子操作

  • 简单地把原子操作组合在一起,并不能保证整体依赖具有原子性
  • 比如我去ATM机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借给 别人,也就是被其它线程打断并被修改。
  • 全同步的HashMap也不完全安全

# 单例模式

# 单例模式的作用

为什么需要单例?

  • 节省内存和计算
  • 保证结果正确
  • 方便管理

# 单例模式的适用场景

无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任务状态,这时候我们就只需要一个实例对象即可。

全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。

# 饿汉模式:静态常量

public class Singleton01 {

    private static final Singleton01 INSTANCE  = new Singleton01();

    private Singleton01() {
        //do something
    }

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

# 饿汉模式:静态代码块

public class Singleton02 {

    private static final Singleton02 INSTANCE;

    static {
        INSTANCE  = new Singleton02();
    }

    private Singleton02() {
        //do something
    }

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

# 懒汉模式:双重检查

优点:线程安全;延迟加载;效率较高。 使用 volatile 新建对象的好处:

  1. 新建对象实际上有3个步骤
  2. 重排序会带来NPE
  3. 防止重排序
public class Singleton03 {

    private static volatile Singleton03 INSTANCE;

    private Singleton03() {
        //do something
    }

    public static Singleton03 getInstance() {
        if (INSTANCE == null) {
            synchronized (this) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton03();
                }
            }
        }
        return INSTANCE;
    }
}

# 静态内部类

静态内部类方式在Singleton04类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton04的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:更优雅的方式、规范

  1. 保证懒加载
  2. 线程安全
  3. 效率特别高
public class Singleton04 {

    private static volatile Singleton04 INSTANCE;

    private Singleton04() {
        //do something
    }

    public static class SingletonInstance {
        private static Singleton04 INSTANCE  = new Singleton04();
    }

    public static Singleton04 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

# 枚举

优点:

  1. 线程安全
  2. 只被装载一次
public enum Singleton05 {

    /**
     * 实例
     */
    INSTANCE;
    
    public void myMethod() {
        //do something
    }
}

# 对比

Joshua Bloch 大神在《Effective Java》中明确表达过的观点: "使用"枚举实现单例方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton最佳方法

  1. 写法简单
  2. 线程安全有保障
  3. 避免反序列化破坏单例

各种写法的适用场合

  1. 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
  2. 非线程同步的方法不能使用
  3. 如果程序一开始要嘉爱的资源太多,那么就应该使用懒加载
  4. 饿汉式如果是对象的创建需要配置文件就不适用
  5. 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性

# 常见问题

  • 手写单例模式
  • 说一说什么是Java内存模型
    • https://naotu.baidu.com/file/60a0bdcaca7c6b92fcc5f796fe6f6bc9?token=bcdbae34bb3b0533 (opens new window)
  • 什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作
    1. 新建一个空的 Person 对象
    2. 执行 Person 的构造函数
    3. 把这个对象的地址指向 p

可能发生指令重排,2和3换位置

  • 什么是内存可见性问题
    • 原因:CPU多级缓存的存在
    • JMM的抽象:主内存和本地内存

# 死锁

# 是什么死锁

  • 在并发中
  • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁
  • 多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁

# 死锁的危害

死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力

  • 数据库:检测并放弃事务
  • JVM:无法自动处理

死锁发生几率不高但危害大

  • 虽然很少发生,但是随着时间推移,一定会发生(墨菲定理)
  • 一旦发生,多是高并发场景,影响用户多
  • 整个系统崩溃、子系统崩溃、性能降低
  • 压力测试无法找出所有潜在的死锁

# 死锁演示

# 简单的死锁

public class MustDeadLock implements Runnable {


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

    int flag = 0;

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

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 0) {
            synchronized (lock1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + "成功获取到两把锁");
                }
            }
        }
        if (flag == 1) {
            synchronized (lock2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "成功获取到两把锁");
                }
            }
        }
    }
}

控制台输出

手动关闭程序,code不是0,非正常结束运行

flag = 0
flag = 1

Process finished with exit code -1

# 转账发生死锁

  1. 需要两把锁
  2. 获取两把锁成功,查询余额大于0,转出人扣款,收款人余额增加,是原子操作
  3. 顺序相反导致死锁
public class TransferMoney implements Runnable {

    int flag = 0;

    static Account a = new Account(1000);
    static Account b = new Account(1000);

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 0;
        r2.flag = 1;
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("a的余额"+a.balance);
        System.out.println("b的余额"+b.balance);
    }


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

    public static void transferMoney(Account from, Account to, int amount) {
        synchronized (from) {
//            try {
//                TimeUnit.SECONDS.sleep(1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            synchronized (to) {
                int result = from.balance - amount;
                if (result < 0) {
                    System.out.println("余额不足");
                    return;
                }
                from.balance = result;
                to.balance += amount;
                System.out.println("转账成功" + amount + "元");
            }
        }
    }

    static class Account {
        int balance;

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

# 多人转账发生死锁

public class MultiTransferMoney {


    private static final int NUM_ACCOUNTS = 500;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_ITERATION = 10000;
    private static final int NUM_THREADS = 20;

    public static void main(String[] args) {
        Random random = new Random();
        Account[] accounts = new Account[NUM_ACCOUNTS];
        for (int i = 0; i < NUM_ACCOUNTS; i++) {
            accounts[i] = new Account(NUM_MONEY);
        }
        Runnable runnable = () -> {
            for (int i = 0; i < NUM_ITERATION; i++) {
                int fromIndex = random.nextInt(NUM_ACCOUNTS);
                int toIndex = random.nextInt(NUM_ACCOUNTS);
                int amount = random.nextInt(NUM_MONEY);
                TransferMoney.transferMoney(accounts[fromIndex], accounts[toIndex], amount);
            }
            System.out.println("运行结束");

        };
        for (int i = 0; i < NUM_THREADS; i++) {
            new Thread(runnable).start();
        }

    }


}

# 死锁的4个必要条件

  1. 互斥条件
  2. 请求与保持条件
  3. 不剥夺条件
  4. 循环等待条件

四个缺一不可

# 如何定位死锁

# JDK自带工具

jps -l 找到进程的_PID_

jstack pid 来dump堆栈信息

jstack - Stack Trace (oracle.com) (opens new window)

# ThreadMXBean类

public class MustDeadLock implements Runnable {


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

    int flag = 0;

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

        Thread.sleep(3000);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("发现死锁:"+threadInfo.getThreadName());
                // do something
            }
        }
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 0) {
            synchronized (lock1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + "成功获取到两把锁");
                }
            }
        }
        if (flag == 1) {
            synchronized (lock2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "成功获取到两把锁");
                }
            }
        }
    }
}

# 修复死锁

# 线上发生死锁怎么办

  • 线上问题都需要防患于未然,不造成损失几乎是不可能
  • 保存案发现场,然后立刻重启服务
  • 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版

# 常见修复策略

  • 避免策略:哲学家就餐的换手方案、转账换序方案
  • 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
  • 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概念极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复

避免策略

  • 思路:避免相反的获取锁的顺序

转账时避免死锁

public class TransferMoney implements Runnable {

    int flag = 0;

    static Account a = new Account(1000);
    static Account b = new Account(1000);
    static Account lock = new Account(1000);

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 0;
        r2.flag = 1;
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("a的余额" + a.balance);
        System.out.println("b的余额" + b.balance);
    }


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


    public static void transferMoney(Account from, Account to, int amount) {
        class Helper {
            /**
             * 转账方法
             */
            public void transfer() {
                int result = from.balance - amount;
                if (result < 0) {
                    System.out.println("余额不足");
                    return;
                }
                from.balance = result;
                to.balance += amount;
                System.out.println("转账成功" + amount + "元");
            }
        }

        // 取hash值,进行比较,决定锁的获取顺序
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);

        if (fromHash > toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (toHash > fromHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else {
            /**
             * hash碰撞,多个线程抢一个锁
             * 生产环境可以根据用户的唯一标识,进行比较(主键,id...)
              */
            synchronized (lock) {
                synchronized (from) {
                    synchronized (to) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }

    static class Account {
        int balance;

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

# 哲学家就餐

# 问题描述

https://baike.baidu.com/item/哲学家就餐问题/10929794?fr=aladdin (opens new window)

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子

哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁 (opens new window)”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

# 代码演示
public class DiningPhilosophers {


    public static class Philosopher implements Runnable {

        private Object leftChopstick;
        private Object rightChopstick;

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

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("思考");
                    synchronized (leftChopstick) {
                        doAction("拿到了左边的筷子");
                        synchronized (rightChopstick) {
                            doAction("拿到了右边的筷子");
                            doAction("放下了右边的筷子");
                        }
                        doAction("放下了左边的筷子");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        public void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + "###" + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {
        int number = 5;

        // 5根筷子
        Object[] chopsticks = new Object[number];
        for (int i = 0; i < number; i++) {
            chopsticks[i] = new Object();
        }
        // 5个哲学家
        Philosopher[] philosophers = new Philosopher[number];
        for (int i = 0; i < number; i++) {
            Object left = chopsticks[i];
            // 右筷子,防止越界,%
            Object right = chopsticks[(i + 1) % number];
            philosophers[i] = new Philosopher(left, right);

            new Thread(philosophers[i], "哲学家" + (i + 1)).start();
        }

    }
}
# 解决方案
  • 服务员检查(避免策略):让服务员先去检查如果资源给了我们会不会发生死锁
  • 改变一个哲学家拿拿叉子的顺序(避免策略):和上面的银行转账类似
  • 餐票(避免策略):信号量
  • 领导调节(检查与恢复策略):引入一个领导,定期检查,发生死锁就kill那个资源
# 改变顺序(避免策略)
public static void main(String[] args) {
    int number = 5;

    // 5根筷子
    Object[] chopsticks = new Object[number];
    for (int i = 0; i < number; i++) {
        chopsticks[i] = new Object();
    }
    // 5个哲学家
    Philosopher[] philosophers = new Philosopher[number];
    for (int i = 0; i < number; i++) {
        Object left = chopsticks[i];
        // 右筷子,防止越界,%
        Object right = chopsticks[(i + 1) % number];

        // 改变(最后)一个哲学家的顺序
        if (i == number - 1) {
            philosophers[i] = new Philosopher(right, left);
        } else {
            philosophers[i] = new Philosopher(left, right);
        }

        new Thread(philosophers[i], "哲学家" + (i + 1)).start();
    }

}
# 检查与恢复策略

# 如何避免死锁

  • 设置【超时】时间
    • Lock的tryLock(long timeout, TimeUnit unit)
    • synchronized不具备尝试锁的能力
    • 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
    • 获取锁失败时:打印错误日志、发报警邮件、重启等
  • 多使用【并发类】而不是自己设计锁
    • ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
    • 实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
    • 多用【并发集合】少用同步集合(Collections.synchronizedMap()和Collections.synchronizedList()),并发集合比同步集合的可扩展性更好
    • 并发场景需要用到map,首先想到用【ConcurrentHashMap】
  • 尽量降低锁的使用【粒度】:用不同的锁而不是一个锁
  • 如果能使用【同步代码块】,就不使用同步方法:方便自己指定锁对象,而不是直接整个方法
  • 给线程起一个有意义的名字:debug和排查时事半功倍,框架和JDK都遵循这个最佳实践
  • 避免锁的【嵌套】:synchronized套synchronized
  • 分配资源前先看下能不能收回来:银行家算法
  • 尽量不要几个功能用同一把锁:专锁专用

# 活锁

# 什么是活锁

  • 虽然线程并没有阻塞,也【始终在运行】(所以叫做“活”锁,线程是“活”的),但程序却【得不到进展】,因为线程始终重复做同样的事(一直询问请求对方的锁)(同时占用着CPU)
  • 如果是死锁,那么就是阻塞,相互等待(不占用CPU)
  • 死锁和活锁的【结果是一样的】,就是相互等待着

# 工程中的活锁实例:消息队列

  • 策略:消息如果处理失败,就放在队列开头重试
  • 由于依赖服务出了问题,处理该消息一直失败
  • 没阻塞,但程序无法继续
  • 解决:放到队列尾部、重试限制

# 如何解决活锁

原因:重试机制不变,消息队列始终重试,吃饭始终谦让

  • 以太网的指数退避算法
  • 加入随机因素

# 饥饿

  • 当线程需要某些资源(例如CPU),但是却始终得不到
  • 线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
  • 饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差

线程优先级

  • 10个级别,默认5
  • 程序设计不应依赖于优先级
  • 不同操作系统不一样
  • 优先级会被操作系统改变
上次更新: 2024/03/03, 08:36:37
并发编程
高级篇

← 并发编程 高级篇→

Theme by Vdoing | Copyright © 2023-2024 Starry | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式