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

文章目录

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

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

}
java 复制代码
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 方法

java 复制代码
public class MyRunnable implements Runnable {
    
    @Override
    public void run() {
        System.out.println("MyRunnable is running");
    }
    
}
java 复制代码
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 方法

java 复制代码
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";
    }

}
java 复制代码
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 通过线程池创建线程(项目中的使用方式)

java 复制代码
package cn.edu.scau.threadpool;

public class MyExecutor implements Runnable {

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

}
java 复制代码
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 方法会另开一个线程

下面是一个例子

java 复制代码
public class MyRunnable implements Runnable {

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

}
java 复制代码
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:终止
java 复制代码
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 方法,以下是一个示例

java 复制代码
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. 打断正常的线程,可以根据打断状态来标记是否退出线程

使用退出标志的示例

java 复制代码
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 方法的示例

java 复制代码
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 方法的示例

java 复制代码
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 关键字的基本使用场景------卖票

java 复制代码
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 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

我们来看以下代码

java 复制代码
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 的区别

synchronized AQS
关键字,由 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类 的实现通常会提供更好的性能

以下是一个示例

java 复制代码
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 死锁产生的条件及排查方案

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

java 复制代码
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面试篇(线程池相关专题)

相关推荐
乐之者v4 分钟前
leetCode43.字符串相乘
java·数据结构·算法
suweijie7683 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿4 小时前
List深拷贝后,数据还是被串改
java
LCG元7 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
xlsw_7 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹8 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭8 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫9 小时前
泛型(2)
java
超爱吃士力架9 小时前
邀请逻辑
java·linux·后端
南宫生9 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论