Java面试篇(多线程相关专题)

news2024/9/22 19:31:31

文章目录

  • 0. 前言
  • 1. 线程基础
    • 1.1 线程和进程
      • 1.1.1 进程
      • 1.1.2 线程
      • 1.1.3 进程和线程的区别
    • 1.2 并行和并发
      • 1.2.1 单核 CPU 的情况
      • 1.2.2 多核 CPU 的情况
      • 1.2.3 并行和并发的区别
    • 1.3 线程创建的方式
      • 1.3.1 继承 Thread 类,重写 run 方法
      • 1.3.2 实现 Runnable 接口,重写 run 方法
      • 1.3.3 实现 Callable 接口,重写 call 方法
      • 1.3.4 通过线程池创建线程(项目中的使用方式)
      • 1.3.5 Runnable 接口与 Callable 接口有什么区别
      • 1.3.6 在启动线程的时候,可以使用 run 方法吗,run 方法和 start 方法有什么区别
    • 1.4 线程包括哪些状态,状态之间是如何转换的
      • 1.4.1 线程的状态
      • 1.4.2 线程状态之间是如何转换的
    • 1.5 新建三个线程,如何保证它们按照顺序执行
    • 1.6 notify 方法和 notifyAll 方法有什么区别
    • 1.7 wait 方法和 sleep 方法的区别
      • 1.7.1 共同点
      • 1.7.2 不同点
    • 1.8 如何停止一个正在运行的线程
  • 2. 线程安全
    • 2.1 synchronized 关键字的底层原理
    • 2.2 synchronized 关键字的底层原理-进阶
    • 2.3 Java 的内存模型
    • 2.4 CAS
    • 2.5 volatile 关键字
      • 2.5.1 保证共享变量在线程间的可见性
      • 2.5.2 禁止指令重排序
    • 2.6 AQS
      • 2.6.1 AQS 与 synchronized 的区别
      • 2.6.2 AQS 的常见实现类
      • 2.6.3 AQS 的基本工作机制
    • 2.7 ReentrantLock 的底层实现原理
    • 2.8 synchronized 和 Lock 有什么区别
      • 2.8.1 语法层面
      • 2.8.2 功能层面
      • 2.8.3 性能层面
    • 2.9 死锁产生的条件及排查方案
    • 2.10 ConcurrentHashMap
    • 2.11 导致并发程序出现问题的根本原因
      • 2.11.1 原子性
      • 2.11.2 可见性
      • 2.11.3 有序性

0. 前言

相信说到多线程,很多同学都麻了,因为我们在学习过程中基本上没有使用过多线程,而且在项目开发的过程中好像也没怎么用到多线程,但面试官是真爱问,我们必须要研究一下

与多线程相关的面试题一般都有两个标签:高频、难以回答

与多线程相关的面试题大概分为四类

在这里插入图片描述

  • 第一类:线程的基础知识,涉及到很多面试题,但相对来说比较好回答
  • 第二类:线程中的并发安全,这一类的内容被问到的概率非常高,并且不太好回答,大部分都是跟锁的内容有关
  • 第三类:线程池,一般在项目中使用多线程的话,都会配合线程池一起使用
  • 第四类:使用场景,这个是最让人头疼的,前三类问题死记硬背可能还没啥问题,但是遇到场景问题可能就毫无思路了,万一面试官一上来就问你们在项目中哪里用到线程池,像这些场景题,因为大家没有经历过,项目中根本没用过,不太好回答,不过没关系,这次我们一起来搞定这些问题

其实学习多线程相关的知识,不仅仅是为了应付面试,更是为了个人技能水平的提升,万一以后项目中真要使用到多线程,也能快速上手

线程池和使用场景部分可以查看我的另一篇博文:Java面试篇(线程池相关专题)

1. 线程基础

1.1 线程和进程

在这里插入图片描述

1.1.1 进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU ,将数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理 IO 的

简单地来说,当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个进程

进程也可细分为多实例进程和单实例进程

在这里插入图片描述

1.1.2 线程

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

一个进程之内可以分为一到多个线程

在这里插入图片描述

1.1.3 进程和线程的区别

  1. 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  2. 不同的进程使用不同的内存空间,进程下的所有线程可以共享进程的内存空间
  3. 线程更轻量,线程上下文切换的成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

1.2 并行和并发

在这里插入图片描述

1.2.1 单核 CPU 的情况

单核 CPU 下线程实际还是串行执行的

在这里插入图片描述

操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(Windows 操作系统下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 CPU 在线程间的切换非常快(时间片很短),人类感觉是同时运行的

总结为一句话就是:微观上是串行的,宏观上是并行的

一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)

1.2.2 多核 CPU 的情况

每个核(core)都可以调度运行线程,这时候线程可以是并行的

在这里插入图片描述

1.2.3 并行和并发的区别

并发(concurrent)是同一时间应对(dealing with)多件事情的能力

并行(parallel)是同一时间动手做(doing)多件事情的能力


举一个例子方便大家理解:

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
  • 家庭主妇雇了 3 个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行

1.3 线程创建的方式

在这里插入图片描述

共有四种方式可以创建线程,分别是:

  • 继承 Thread 类,重写 run 方法
  • 实现 Runnable 接口,重写 run 方法
  • 实现 Callable 接口,重写 call 方法
  • 通过线程池创建线程(项目中的使用方式)

1.3.1 继承 Thread 类,重写 run 方法

public class MyThread extends Thread {
    
    @Override
    public void run() {
        System.out.println("MyThread is running");
    }

}
import cn.edu.scau.thread.MyThread;

public class ThreadDemo {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start();
        t2.start();
    }

}

1.3.2 实现 Runnable 接口,重写 run 方法

public class MyRunnable implements Runnable {
    
    @Override
    public void run() {
        System.out.println("MyRunnable is running");
    }
    
}
import cn.edu.scau.runnable.MyRunnable;

public class RunnableDemo {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

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

}

1.3.3 实现 Callable 接口,重写 call 方法

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("MyCallable is calling");
        return "ok";
    }

}
import cn.edu.scau.callable.MyCallable;

import java.util.concurrent.FutureTask;

public class CallableDemo {

    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();

        FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(stringFutureTask);
        thread.start();

        String result;
        try {
            result = stringFutureTask.get();
            System.out.println("result = " + result);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

}

1.3.4 通过线程池创建线程(项目中的使用方式)

package cn.edu.scau.threadpool;

public class MyExecutor implements Runnable {

    @Override
    public void run() {
        System.out.println("MyExecutor is running");
    }

}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo {

    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 提交任务
        executorService.submit(new MyExecutor());
        // 关闭线程池
        executorService.shutdown();
    }

}

1.3.5 Runnable 接口与 Callable 接口有什么区别

  • Runnable 接口的 run 方法没有返回值
  • Callable 接口 call 方法有返回值,是个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果
  • Callable 接口的 call 方法允许抛出异常,而 Runnable 接口的 run 方法的异常只能在内部处理(使用 try-catch 代码块),不能继续上抛

1.3.6 在启动线程的时候,可以使用 run 方法吗,run 方法和 start 方法有什么区别

  • start 方法:用来启动线程,通过该线程调用 run 方法执行 run 方法中所定义的逻辑代码,start 方法只能被调用一次,如果多次调用 start 方法会抛出异常(IllegalThreadStateException
  • run 方法:封装了要被线程执行的代码,可以被调用多次
  • 调用 run 方法使用的是原线程,而调用 start 方法会另开一个线程

下面是一个例子

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.err.println("currentThread = " + Thread.currentThread().getName());
        System.out.println("MyRunnable is running");
    }

}
import cn.edu.scau.runnable.MyRunnable;

public class RunnableDemo {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();

        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

        t1.run();
        t1.run();

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

}

输出结果如下

在这里插入图片描述

1.4 线程包括哪些状态,状态之间是如何转换的

在这里插入图片描述

1.4.1 线程的状态

线程的状态可以参考 JDK 中 Thread 类中的枚举类 State,主要有以下六个状态:

  • NEW:新建
  • RUNNABLE:可运行
  • BLOCKED:阻塞
  • WAITING:等待
  • TIMED_WAITING:计时等待
  • TERMINATED:终止
public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called {@code Object.wait()}
     * on an object is waiting for another thread to call
     * {@code Object.notify()} or {@code Object.notifyAll()} on
     * that object. A thread that has called {@code Thread.join()}
     * is waiting for a specified thread to terminate.
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;
}

1.4.2 线程状态之间是如何转换的

在这里插入图片描述

  • 创建线程对象是新建状态
  • 调用了 start 方法后转变为可执行状态
  • 线程获取到了 CPU 的执行权,且执行结束,是终止状态
  • 在可执行状态的过程中,如果没有获取 CPU 的执行权,可能会切换其他状态
    • 如果没有获取锁(synchronized 或 lock )进入阻塞状态,获得锁再切换为可执行状态
    • 如果线程调用了wait 方法进入等待状态,其他线程调用 notify 方法唤醒线程后可切换为可执行状态
    • 如果线程调用了 sleep 方法,进入计时等待状态,到时间后可切换为可执行状态

1.5 新建三个线程,如何保证它们按照顺序执行

在这里插入图片描述

要保证新建的三个线程按照顺序执行,可以使用线程中的 join 方法,以下是一个示例

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        // 创建线程对象
        Thread t1 = new Thread(() -> System.out.println("t1"));

        Thread t2 = new Thread(() -> {
            try {
                t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2");
        });

        Thread t3 = new Thread(() -> {
            try {
                t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3");
        });

        // 启动线程
        t3.start();
        t2.start();
        t1.start();
    }

}

1.6 notify 方法和 notifyAll 方法有什么区别

在这里插入图片描述

  • notifyAIL 方法:唤醒所有处于 WAITING 状态的线程
  • notify 方法:随机唤醒一个处于 WAITING 状态的线程

1.7 wait 方法和 sleep 方法的区别

在这里插入图片描述

1.7.1 共同点

wait()、wait(long) 和 sleep(long) 方法的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

1.7.2 不同点

方法归属不同:

  • sleep(long)是 Thread 的静态方法
  • 而 wait()、wait(long) 都是 Object 类的方法,每个对象都有

醒来时机不同:

  • 执行 sleep(long) 和 wait(long) 方法的线程都会在等待相应毫秒后醒来,wait(long) 和 wait() 可以被 notify 唤醒,wait() 如果不唤醒就会一直等下去
  • 它们都可以被打断唤醒

锁特性不同(重点):

  • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
  • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(放弃 CPU 的执行权,但其它线程可以用)
  • sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(放弃 CPU 的执行权,但其它线程也用不了)

1.8 如何停止一个正在运行的线程

在这里插入图片描述

有三种方式可以停止线程:

  1. 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
  2. 使用 stop 方法强行终止(不推荐,方法已废用)
  3. 使用 interrupt 方法中断线程
    1. 打断阻塞的线程(sleep、wait、join),线程会抛出 InterruptedException 异常
    2. 打断正常的线程,可以根据打断状态来标记是否退出线程

使用退出标志的示例

public class InterruptDemo01 extends Thread {

    volatile boolean flag = false; // 线程执行的退出标记

    @Override
    public void run() {
        while (!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建MyThread对象
        InterruptDemo01 t1 = new InterruptDemo01();
        t1.start();

        // 主线程休眠6秒
        Thread.sleep(6000);

        // 更改标记为true
        t1.flag = true;
    }

}

使用 stop 方法的示例

public class InterruptDemo02 extends Thread {

    volatile boolean flag = false; // 线程执行的退出标记

    @Override
    public void run() {
        while (!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建MyThread对象
        InterruptDemo02 t1 = new InterruptDemo02();
        t1.start();

        // 主线程休眠6秒
        Thread.sleep(6000);

        // 调用stop方法
        t1.stop();
    }

}

使用 interrupt 方法的示例

public class InterruptDemo03 {

    public static void main(String[] args) throws InterruptedException {
        // 1.打断阻塞的线程
        // Thread t1 = new Thread(() -> {
        //     System.out.println("t1 正在运行...");
        //     try {
        //         Thread.sleep(5000);
        //     } catch (InterruptedException e) {
        //         e.printStackTrace();
        //     }
        // }, "t1");
        // t1.start();
        // Thread.sleep(500);
        // t1.interrupt();
        // System.out.println(t1.isInterrupted());

        // 2.打断正常的线程
        Thread t2 = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if (interrupted) {
                    System.out.println("打断状态:" + interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        t2.interrupt();
    }

}

2. 线程安全

2.1 synchronized 关键字的底层原理

在这里插入图片描述

我们先来回忆一下 synchronized 关键字的基本使用场景——卖票

public class TicketDemo {

    private static final Object lock = new Object();

    private int ticketNum = 10;

    public synchronized void getTicket() {
        synchronized (lock) {
            if (ticketNum <= 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
            // 非原子性操作
            ticketNum--;
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(ticketDemo::getTicket).start();
        }
    }

}

synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就进入阻塞状态


在了解 synchronized 关键字的底层原理前,我们需要先了解一下 Monitor

Monitor 被翻译为监视器,由 JVM 提供,C++ 语言实现,Monitor 的大致结构如下

在这里插入图片描述

下面来看一个使用 synchronized 关键字的例子

在这里插入图片描述

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  • EntryList:关联没有抢到锁的线程,处于 Blocked 状态的线程
  • WaitSet:关联调用了 wait 方法的线程,处于 WAITING 状态的线程

注意:EntryList 中的线程并不是按照先来后到的顺序获取 Owner 的,谁抢到了 Owner ,谁就拿到了锁

总结:

  • synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
  • synchronized 的底层由 Monitor 实现的,Monitor 是 JVM 级别的对象(由C++实现),线程获得锁需要使用对象(锁)关联 Monitor
  • 在 Monitor 内部有三个属性,分别是 Owner、EntryList、WaitSet
    • Owner 关联的是获得锁的线程,并且只能关联一个线程
    • Entrylist 关联的是处于阻塞状态的线程
    • Waitset 关联的是处于 WAITING 状态的线程

2.2 synchronized 关键字的底层原理-进阶

面试官可能会问:Monitor 实现的锁属于重量级锁,你了解过锁升级吗

利用 Monitor 实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低

该部分的内容比较难懂,可观看视频:synchronized 关键字的底层原理-进阶

2.3 Java 的内存模型

在这里插入图片描述

JMM:Java Memory Model,Java内存模型,定义了共享内存中多线程程序读写操作的规则,通过这些规则来规范对内存的读写操作,从而保证指令的正确性

在这里插入图片描述

总结:

  • JMM(Java Memory Model),Java内存模型,定义了共享内存中多线程程序读写操作的规则,通过这些规则来规范对内存的读写操作从而保证指令的正确性
  • JMM 把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程之间交互需要通过主内存

2.4 CAS

在这里插入图片描述

CAS:Compare And Swap(先比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性,很多框架的底层都采用了 CAS 的思想

JUC(java.util.concurrent)包下也有很多类都用到了 CAS 操作

  • AbstractQueuedSynchronizer(AQS框架)
  • AtomicXXX 类

下面是 CAS 的一个示例

在这里插入图片描述

一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当旧的预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false

如果 CAS 操作失败,通过自旋的方式等待并再次尝试,直到成功

那什么是自旋呢,自旋可以理解为一个死循环

在这里插入图片描述

因为自旋没有加锁,所以线程不会陷入阻塞,效率较高。如果竞争激烈,重试频繁发生,效率会受影响


CAS 底层依赖于 Unsafe 类,利用 Unsafe 类直接调用操作系统底层的 CAS 指令,以下是 Unsafe 类的部分源码

在这里插入图片描述

JUC 包下的 ReentrantLock 类也采用了 CAS 思想,以下是 ReentrantLock 类中的 compareAndSetState 方法的源码(其中 U 是一个 Unsafe 类的实例)

在这里插入图片描述


  • CAS 是基于乐观锁的思想:乐观地估计,不怕别的线程来修改共享变量,就算其它线程改了也没关系,吃亏点再重试
  • synchronized 是基于悲观锁的思想:悲观地估计,得防着其它线程来修改共享变量,上了锁之后其它线程都无法修改,修改完后解开锁,其它线程才有机会修改

2.5 volatile 关键字

在这里插入图片描述

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 关键字修饰之后,那么就具备了两层语义:

  • 保证共享变量在线程间的可见性
  • 禁止进行指令重排序

2.5.1 保证共享变量在线程间的可见性

用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

我们来看以下代码

在这里插入图片描述

public class ForeverLoop {

    static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            System.out.println(Thread.currentThread().getName() + ":modify stop to true...");
        }, "t1").start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + stop);
        }, "t2").start();

        new Thread(() -> {
            int i = 0;
            while (!stop) {
                i++;
            }
            System.out.println("stopped... c:" + i);
        }, "t3").start();
    }

}

大家猜一下,线程 t3 中的死循环会停止吗,我们来看一下控制台的输出

在这里插入图片描述

可以看到,线程 t3 中的死循环不会停止(而且 Java 程序也没有结束运行)

为什么线程 t3 中的死循环不会停止呢,明明线程 t1 成功修改了 stop 变量,而且线程 t2 也成功打印了 stop 变量修改后的值

其实是因为 JVM 虚拟机中的 JIT(Just-In-Time,即时编译器)对代码做了优化,优化后的代码大概如下

在这里插入图片描述

有两种解决方案:

  1. 在程序运行的时候添加 VM 参数 -Xint ,禁用即时编译器,但是不推荐,因为其它代码需要使用 JIT
  2. 在修饰变量的时候加上 volatile 关键字,告诉 JIT 不要对 volatile 修饰的变量做优化

2.5.2 禁止指令重排序

该部分内容晦涩难懂,请观看视频:禁止指令重排序

用 volatile 修饰的共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

volatile使用技巧:

  • 写变量时让 volatile 修饰的变量的在代码最后位置
  • 读变量时让 volatile 修饰的变量的在代码最开始位置

2.6 AQS

在这里插入图片描述

AQS:AbstractQueuedSynchronizer,抽象队列同步器,是构建锁或者其他同步组件的基础框架


2.6.1 AQS 与 synchronized 的区别

synchronizedAQS
关键字,由 C++ 语言实现由 Java 语言实现
悲观锁,能够自动释放锁悲观锁,需要手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

2.6.2 AQS 的常见实现类

  • ReentrantLock:阻塞式锁
  • Semaphore:信号量
  • CountDownLatch:倒计时锁

2.6.3 AQS 的基本工作机制

AQS 内部维护了一个先进先出的双向队列,队列中存储的是排队的线程

在这里插入图片描述

如果多个线程共同去抢 state 资源,如何保证原子性呢?其实也是采用了 CAS 的思想,某个线程一抢到 state 资源就将 state 的值设置为 1

在这里插入图片描述

在这里插入图片描述

面试官可能此时又会问了,AQS 是公平锁还是非公平锁?其实 AQS 可以实现公平锁,也可以实现非公平锁,AQS 的不同实现类有不同的方案

那什么是公平锁和非公平锁呢,我们来看一个例子

在这里插入图片描述

线程 0 目前拿到了锁,当线程 0 刚好释放锁的时候,另一个线程 5 刚好也来了

  • 如果新来的线程 5 跟等待队列中的线程争夺锁,就是非公平锁
  • 如果新来的线程 5 加入等待队列中,成为等待队列的最后一个元素,就是非公平锁

在这里插入图片描述

2.7 ReentrantLock 的底层实现原理

在这里插入图片描述

ReentrantLock 翻译过来是可重入锁,相对于 synchronized ,它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与 synchronized 一样,都支持重入

ReentrantLock 的基本使用方法

在这里插入图片描述


ReentrantLock 主要利用 CAS + AQS 队列来实现,支持公平锁和非公平锁,ReentrantLock 的构造方法接受一个可选的公平参数,默认非公平锁,我们可以查看 ReentrantLock 类的源码(其中 sync 是 Sync 类的实例,Sync 类继承自 AbstractQueuedSynchronizer 类)

在这里插入图片描述

当设置为true时,表示公平锁,否则为非公平锁,公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量


ReentrantLock 的实现原理

在这里插入图片描述

  • 线程来抢锁后使用 CAS 的方式修改 state 状态,修改成功则让 exclusiveOwnerThread 属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head 指向双向队列头部,tail 指向双向队列尾部
  • 当 exclusiveOwnerThread 为 null 的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

2.8 synchronized 和 Lock 有什么区别

2.8.1 语法层面

  • synchronized 是关键字,源码在 JVM 中,由 C++ 语言实现
  • Lock 是接口,源码由 JDK 提供,由 Java 语言实现
  • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

2.8.2 功能层面

  • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
  • Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
  • Lock 有适合不同场景的实现类,如 ReentrantLock, ReentrantReadWriteLock(读写锁)

注意:使用 lock 方法的锁不是一个可打断的锁,如果想使用可打断的锁,需要使用 lockInterruptibly 方法

2.8.3 性能层面

  • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能算差
  • 在竞争激烈时,Lock类 的实现通常会提供更好的性能

以下是一个示例

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    // 创建锁对象
    static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();

    static Condition firstCondition = REENTRANT_LOCK.newCondition();

    static Condition secondCondition = REENTRANT_LOCK.newCondition();

    public static void main(String[] args) throws InterruptedException {
        // 可打断
        lockInterrupt();

        // 可超时
        // timeOutLock();

        // 多条件变量
        // conditionTest();
    }

    /**
     * 多条件变量
     */
    public static void conditionTest() {
        new Thread(() -> {
            REENTRANT_LOCK.lock();
            try {
                firstCondition.await();
                System.out.println(Thread.currentThread().getName() + ",acquire lock...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                REENTRANT_LOCK.unlock();
            }
        }, "t1").start();

        new Thread(() -> {
            REENTRANT_LOCK.lock();
            try {
                firstCondition.await();
                System.out.println(Thread.currentThread().getName() + ",acquire lock...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                REENTRANT_LOCK.unlock();
            }
        }, "t2").start();

        new Thread(() -> {
            REENTRANT_LOCK.lock();
            try {
                // 唤醒firstCondition条件的线程
                firstCondition.signalAll();
                // 唤醒secondCondition条件的线程
                // secondCondition.signal();
                System.out.println(Thread.currentThread().getName() + ",acquire lock...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                REENTRANT_LOCK.unlock();
            }
        }, "t3").start();
    }

    /**
     * 锁超时
     *
     * @throws InterruptedException interruptedException
     */
    public static void timeOutLock() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            // 尝试获取锁,如果获取锁成功,返回true,否则返回false
            try {
                if (!REENTRANT_LOCK.tryLock(2, TimeUnit.SECONDS)) {
                    System.out.println("t1-获取锁失败");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println("t1线程-获得了锁");
            } finally {
                REENTRANT_LOCK.unlock();
            }
        }, "t1");

        REENTRANT_LOCK.lock();
        System.out.println("主线程获得了锁");
        t1.start();
        try {
            Thread.sleep(3000);
        } finally {
            REENTRANT_LOCK.unlock();
        }
    }

    /**
     * 可打断
     *
     * @throws InterruptedException interruptedException
     */
    public static void lockInterrupt() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                // 开启可中断的锁
                REENTRANT_LOCK.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("等待的过程中被打断");
                return;
            }
            try {
                System.out.println(Thread.currentThread().getName() + ",获得了锁");
            } finally {
                REENTRANT_LOCK.unlock();
            }
        }, "t1");
        REENTRANT_LOCK.lock();
        System.out.println("主线程获得了锁");
        t1.start();

        try {
            Thread.sleep(1000);
            t1.interrupt();
            System.out.println("执行打断");
        } finally {
            REENTRANT_LOCK.unlock();
        }
    }

}

2.9 死锁产生的条件及排查方案

在这里插入图片描述

一个线程需要同时获取多把锁,就容易发生死锁,我们运行以下示例代码

在这里插入图片描述

package cn.edu.scau.deadlock;


import static java.lang.Thread.sleep;

public class Deadlock {

    public static void main(String[] args) {

        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                System.out.println(Thread.currentThread().getName() + "-lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println(Thread.currentThread().getName() + "-lock B");
                    System.out.println(Thread.currentThread().getName() + "-操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                System.out.println(Thread.currentThread().getName() + "-lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println(Thread.currentThread().getName() + "-lock A");
                    System.out.println(Thread.currentThread().getName() + "-操作...");
                }
            }
        }, "t2");

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

}

可以发现,程序并没有结束,线程 t1 持有 A 的锁等待获取 B 锁,线程 t2 持有 B 的锁等待获取 A 的锁,这种现象就是死锁现象

那么该如何进行死锁诊断呢,当程序出现了死锁现象,我们可以使用 JDK 自带的工具:jps 和 jstack

  • jps:输出 JVM 中运行的进程状态信息
  • jstack:查看 Java 进程内线程的堆栈信息

更方便的排查方案是利用 jconsole ,以下是利用 jconsole 查看死锁情况的页面

在这里插入图片描述

具体的诊断方案可以参考我的另一篇博文:Java面试篇(JVM相关专题) 的 8.3 JVM 调优的工具 章节

2.10 ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap 是一种线程安全的高效 Map 集合

底层数据结构:

  • JDK1.7 底层采用分段的数组 + 链表实现
  • JDK1.8 采用的数据结构跟 HashMap 1.8 的结构一样,数组 + 链表 / 红黑二叉树

JDK1.7 中的 ConcurrentHashMap

在这里插入图片描述

在这里插入图片描述


JDK1.8 中的 ConcurrentHashMap

在 JDK 1.8 中,放弃了 Segment 的臃肿设计,ConcurrentHashMap 数据结构跟 HashMap 的数据结构是一样的:数组 + 红黑树 + 链表,采用 CAS + Synchronized 来保证并发安全进行实现

  • CAS 控制数组节点的添加
  • synchronized 只锁定当前链表或红黑二叉树的首节点,只要 hash 不冲突,就不会产生并发的问题,效率得到提升

在这里插入图片描述

2.11 导致并发程序出现问题的根本原因

在这里插入图片描述

Java 并发编程的三大特性:

  1. 原子性
  2. 可见性
  3. 有序性

2.11.1 原子性

一个线程在 CPU 中的操作不可暂停,也不可中断,要么执行完成,要么不执行

2.11.2 可见性

让一个线程对共享变量的修改对另一个线程可见

2.11.3 有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序与代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的


阅读完本文后可学习下一个篇章:Java面试篇(线程池相关专题)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2055364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

工业一体机在工业自动化控制领域的关键性作用

随着工业自动化技术的不断发展和应用范围的不断扩大&#xff0c;工业一体机作为一种集成了多种功能的自动化控制设备&#xff0c;在工业自动化控制领域扮演着越来越重要的角色。 一、 集成性强&#xff0c;简化系统架构 传统工业自动化控制系统通常由多个独立的设备组成&…

php7.1编译安装

1.安装必要的工具&#xff1a; 首先确保您已经安装了 Xcode 和 Command Line Tools&#xff1a;xcode-select --install2.下载 PHP 7.1 源代码&#xff1a; 访问 PHP 官方网站下载 PHP 7.1 的源代码&#xff1a; wget https://www.php.net/distributions/php-7.1.33.tar.gz t…

基于Java企业项目管理系统--论文pf

TOC springboot527基于Java企业项目管理系统--论文pf 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上…

指针详解(五)

目录 1. 回调函数 2. qsort使用举例 1&#xff09;排序整型数据 2&#xff09;排序结构数据 3. qsort函数的模拟实现&#xff08;冒泡&#xff09; 1. 回调函数 回调函数就是一个通过函数指针调用的函数 函数的指针&#xff08;地址&#xff09;作为参数传递给另一个函数…

【python】Python实现XGBoost算法的详细理论讲解与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

AI文字生成漫画短视频系统工具搭建部署,AI文生漫画短视频

目录 前言&#xff1a; 一、AI文生动漫小程序具有以下特点和功能&#xff1a; 二、文生动漫目前有哪些项目&#xff1f; 三、怎么搭建文生动漫小程序&#xff1f; 前言&#xff1a; AI文生动漫小程序是一款基于人工智能技术开发的动漫创作工具。它利用先进的机器学习算法&a…

大数据处理与智慧营销系统性能优化

随着企业数字化转型的加速&#xff0c;客户经营数字化正在向智能化方向发展&#xff0c;构建全场景、全流程、全触点的数字化、智能化的客户经营智慧营销体系。智慧营销系统已运行 5 年&#xff0c;伴随着业务增长&#xff0c;系统业务流程复杂度增大&#xff0c;大表数据量已超…

FreeSWITCH Record

1概述 FreeSWITCH https://signalwire.com/freeswitch是一个开源的电话交换平台。官方给它的定义是–世界上第一个跨平台的、伸缩性极好的、免费的、多协议的电话软交换平台。由这个定义我们可以得出以下几点: FreeSWITCH是跨平台的。它能原生地运行于Windows、MaxOSX、Linux、…

计算几何,CF 993A - Two Squares

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 A - Two Squares 二、解题报告 1、思路分析 由于数据量很小&#xff0c;可…

《Redis核心技术与实战》学习笔记5——内存快照RDB:宕机后,Redis如何实现快速恢复?

文章目录 给哪些内存数据做快照&#xff1f;快照时数据能修改吗?可以每秒做一次快照吗&#xff1f;小结 大家好&#xff0c;我是大白。 上篇文章记录了 Redis 避免数据丢失的 AOF 方法。这个方法的好处&#xff0c;是每次执行只需要记录操作命令&#xff0c;需要持久化的数据量…

11.4k star! 部署清华开源的ChatGLM3,用私有化大模型无缝替换openai

转自AI技术实战 ChatGLM3 是智谱AI和清华大学 KEG 实验室联合发布的对话预训练模型。ChatGLM3-6B 是 ChatGLM3 系列中的第三代开源模型&#xff0c;对话流畅、部署门槛低&#xff0c;测评显示其基础模型ChatGLM3-6B-Base 具有在 10B 以下的基础模型中最强的性能&#xff0c;同时…

ESP32CAM人工智能教学19

ESP32CAM人工智能教学19 Udp socket服务器 本课的工作模式,正好是第十四课工作模式的相反:第十四课中,ESP32Cam是客户端,运行在PC中的Python程序是服务器,就收到了摄像头数据后,调用openCV组件显示图像。而本课的ESP32Cam是服务器,Python是客户端,正好掉了个个,目的就…

蓝桥杯编程题讲解

给定一个正整数 N ,然后将 N 分解成 3 个正整数之和。 计算出共有多少种符合要求的分解方法。 要求&#xff1a; 分解的 3 3 3个正整数各不相同; 分解的 3 3 3个正整数中都不含数字3和7. 如&#xff1a;N为8&#xff0c;可分解为 ( 1 , 1 , 6 ) (1,1,6) (1,1,6)、 ( 1 , 2 ,…

位图与布隆过滤器 —— 海量数据处理

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;C从入门到精通 目录 &#x1f680; 位图 一&#xff1a; &#x1f525; 位图概念 二&#xff1a; &#x1f525; 位图的实现思路及代码实现三&#xff1a; &#x1f525; 位图的应用四&#xff1a;…

云原生系列 - Nginx(基础篇)

前言 学习视频&#xff1a;尚硅谷Nginx教程&#xff08;亿级流量nginx架构设计&#xff09;本内容仅用于个人学习笔记&#xff0c;如有侵扰&#xff0c;联系删学习文档&#xff1a; 云原生系列 - Nginx(基础篇) 1、简介 1.1、背景介绍 Nginx(enginex)是一个高性能的HTTP和…

SpringBoot教程(二十四) | SpringBoot集成日志AOP切面

SpringBoot教程&#xff08;二十四&#xff09; | SpringBoot集成日志AOP切面 &#xff08;一&#xff09;AOP 概要1. 什么是 AOP &#xff1f;2. 为什么要用 AOP&#xff1f;3. AOP一般用来干什么&#xff1f;4. AOP 的核心概念 &#xff08;二&#xff09;Spring AOP1. 简述2…

【芯智雲城】UDStore定制化存储模组和技术解决方案

一、方案详情&#xff1a; UDStore芯宇存储专注行业应用&#xff0c;根据不同应用场景&#xff0c;为客户提供包括车规级、工业级、工规宽温及高耐久型的存储模组产品和技术解决方案&#xff0c;可提供的产品和解决方案类型包括如下&#xff1a; 二、关键技术&#xff1a; 1&…

WLAN DNS proxy settings (Win 10)

WLAN DNS proxy settings (Win 10) 114.114.114.114 8.8.8.8

Ubuntu 22.04 安装 MySQL 8

Ubuntu 22.04 安装 MySQL 8 本文描述了Ubuntu安装MySQL 8的方法 CentOS7 的安装方法点击此处跳转 Windows 的安装方法点击此处跳转 Docker 的安装方法点击此处跳转 正文开始&#xff1a; 在一切开始之前&#xff0c;建议先切换到root #输入下方名&#xff0c;然后输入当…

【JavaSec】Java反射知识点补充

0x03反射-补充零散知识点 文章目录 0x03反射-补充零散知识点Runtime类setAccessible(true)三种命令执行的方法static变量赋值 前面学过 就不多说final变量赋值InDirect final间接赋值static final 向大佬致敬&#xff1a; https://drun1baby.top Runtime类 Runtime 类中有 …