转载

Java 并发面试题集

阅读 6

Java 并发面试题集

Java 并发面试题集

傻瓜源码-内容简介

傻瓜源码-内容简介

【职场经验】(持续更新)

如何日常学习、如何书写简历、引导面试官、系统准备面试、选择offer、提高绩效、晋升TeamLeader.....

【源码解读】(持续更新)

1. 源码选材:Java架构师必须掌握的所有框架和类库源码

2. 内容大纲:按照“企业应用Demo”讲解执行源码:总纲“阅读指南”、第一章“源码基础”、第二章“相关Java基础”、第三章“白话讲源码”、第四章“代码解读”、第五章“设计模式”、第六章“附录-面试习题、相关JDK方法、中文注释可运行源码项目”

3. 读后问题:粉丝群答疑解惑

已收录: HashMap 、 ReentrantLock 、 ThreadPoolExecutor 、 《Spring源码解读》 、 《Dubbo源码解读》 .....

【面试题集】(持续更新)

1. 面试题选材:Java面试常问的所有面试题和必会知识点

2. 内容大纲:第一部分”注意事项“、第二部分“面试题解读”(包括:”面试题“、”答案“、”答案详解“、“实际开发解说”)

3. 深度/广度:面试题集中的答案和答案详解,都是对齐一般面试要求的深度和广度

4. 读后问题:粉丝群答疑解惑

已收录: Java基础面试题集 、 Java并发面试题集 、 JVM面试题集 、 数据库(Mysql)面试题集 、 缓存(Redis)面试题集 .....
【粉丝群】(持续更新) 收录:阿里、字节跳动、京东、小米、美团、哔哩哔哩等大厂内推

:stuck_out_tongue: 作者介绍:Spring系源码贡献者、世界五百强互联网公司、TeamLeader、Github开源产品作者

:stuck_out_tongue: 作者微信:wowangle03 (企业内推联系我)

加入我的粉丝社群,阅读更多内容。从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入!

Java 并发面试题集

注意事项

  1. 阅读本文前,建议先阅读“如何成为值钱的 Java 开发-指南”一文,文中写有面试题集推荐学习顺序。
  2. 建议按照文章顺序阅读面试题(内容前后顺序存在依赖关系)。

一. Java 线程

1. 进程和线程的关系?

当我们启动执行一个 Java 程序时,操作系统会创建 1 个 Java 进程,而这个 Java 进程至少会创建 1 个线程真正执行程序。所以进程是程序的一次执行过程;线程是进程的实际执行单元。进程之间系统资源互不影响;线程之间共享进程资源。

2. 线程上线文切换是什么?

多线程处理任务,并不是真的并发执行;而是 CPU 给每个线程分配一段时间来执行自己任务(这段时间称之为 CPU 时间片,也可以说是 CPU 使用权),就算线程当前分配的任务没有处理完成,但是时间片用完了,也会切换到另一个线程;只不过,在切换前,CPU 会把任务的执行状态记录下来,好让线程重新获得时间片时,能够继续执行。

线程上下文切换的过程中,分配的 CPU 时间片和切换所用时间都非常短,所以用户是感知不到任务的切换,只会觉得是多个线程并发执行。但是因为线程的上下文切换本身也需要成本,所以对多线程的执行效率还是有一定影响的。

3. Java 中如何创建并启动线程?

答案:

创建线程”本质“上有以下 3 种方式。引申出来的方式还有:线程池等。

  1. 通过继承 Thread 类;
  2. 通过实现 Runnable 接口;
  3. 通过 Callable 和 FutureTask 创建线程;

需要注意,只有使用第 3 种方式,才可以获取线程执行任务的返回值。

解释:

1. 继承 Thread 类

class ThreadTest {
    public static void main(String[] args) throws Exception {
        // 1.执行 main 方法,启动主线程

        // 2.主线程创建新线程对象 thread
        MyThread thread = new MyThread();
        // 3.主线程启动 thread 线程,thread 线程执行 run() 方法中定义的逻辑
        thread.start();
        // 4.主线程运行结束
        System.out.println("主线程执行结束!");
    }
}

class MyThread extends Thread {
    /**
     * 用于封装线程执行的逻辑
     */
    @Override
    public void run() {
        try {
            // 模拟任务执行耗时
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test thread!");
    }
}
复制代码

打印结果:

主线程执行结束! test thread!

2. 实现 Runnable 接口

class ThreadTest {
    public static void main(String[] args) {
        // 1.执行 main 方法,启动主线程

        // 2.主线程初始化线程的执行体 runnable
        MyRunnable runnable = new MyRunnable();
        // 3.主线程启动 thread 线程,thread 线程执行 run() 方法中定义的逻辑
        new Thread(runnable).start();
        // 4.主线程运行结束
        System.out.println("主线程执行结束!");
    }
}

class MyRunnable implements Runnable {
    /**
     * 用于封装线程执行的逻辑
     */
    @Override
    public void run() {
        try {
            // 模拟任务执行耗时
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test thread!");
    }
}
复制代码

打印结果:

主线程执行结束! test thread!

3. 实现 Callable 接口

class ThreadTest {
    public static void main(String[] args) throws Exception {
        // 1.执行 main 方法,启动主线程

        // 2.主线程创建用于执行任务的 callable 对象
        MyCallable callable = new MyCallable();
        // 3.主线程创建用于获取返回值的 FutureTask 对象
        FutureTask<String> result = new FutureTask(callable);
        // 4.主线程启动 thread 线程,thread 线程执行 call() 方法中定义的逻辑
        new Thread(result).start();
        // 5.主线程调用 FutureTask.get 方法,获取 thread 线程的返回结果;如果线程没有执行完任务,get 方法则会阻塞,直到有返回结果
        System.out.println(result.get());
        // 6.主线程运行结束
        System.out.println("主线程执行结束!");
    }
}

class MyCallable implements Callable<String> {
    /**
     * 用于封装线程执行的逻辑
     */
    @Override
    public String call() {
        try {
            // 模拟任务执行耗时
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "test thread!";
    }
}
复制代码

打印结果:

test thread! 主线程执行结束!

4. 何时需要异步执行方法?

执行耗时长,并且不希望等待方法返回的,通常使用异步的方式处理。

5. 创建启动线程,run() 和 start() 方法的区别?

1. run()

只是用于封装线程执行的逻辑,并不包含启动新线程的逻辑。

**2. start() **

用于启动新线程,新线程执行 run() 方法中定义的逻辑。

6. 守护线程是什么?

守护线程和非守护线程的区别就在于:系统运行结束时,JVM 会等待非守护线程执行结束后再关闭,但是 JVM 不会等待守护线程。例如:JVM 中的垃圾回收线程就是守护线程。

通过线程池等方式创建的线程都默认是非守护线程。

7. Java 中,线程的生命周期?

答案:

在 Java 8 中,线程一共有 6 个状态,分别是:

  1. 新建(NEW)
  2. 运行中(RUNNABLE)
  3. 阻塞(BLOCKED)
  4. 等待(WAITING)
  5. 超时等待(TIMED_WAITING)
  6. 执行完成(TERMINATED)

解释:

  1. 新建:新建完 Thread 对象,但还没调用该线程对象的 start() 方法;
  2. 运行中:包括以下两个阶段:
    1. 调用了该线程对象的 start() 方法,线程等待 CPU 调度,获得 CPU 使用权;
    2. 线程获得 CPU 使用权,开始执行;
  3. 阻塞:处于”运行中“状态的线程,让出 CPU 的使用权给其它线程,进入阻塞;比如:获取 synchronized 锁失败;
  4. 等待:处于”运行中“状态的线程,让出 CPU 的使用权给其它线程,等待其它线程唤醒或者中断本线程;比如:调用线程的 wait();
  5. 超时等待:处于“运行中”状态的线程,让出 CPU 的使用权给其它线程,等待指定时间后,自动重新获取 CPU 使用权,进入“运行中”状态;比如:调用 sleep(long millis) 方法;
  6. 执行完成:线程执行完毕。

8. 线程安全是什么?

在多线程的场景下,程序的执行结果与单线程场景下一致。

9. SpringMVC 的 Controller 是线程安全吗?

答案:

默认情况下,是线程不安全的。

解释:

因为 Spring 管理的 bean 默认作用域是单例的,所以 Spring 创建的 Controller 默认也是单例的。如果是单例的对象,所有请求都会共享同一个 Controller 对象,也就是说所有请求都共享同一个 Controller 对象的成员变量;这时,如果这个成员变量是有状态的(有状态就是说有数据存储功能的变量),就会引发线程安全问题。

我们可以通过 Spring 将 Controller 作用域设置成原型模式,这样做,每个线程都有自己独立的 Controller 对象,就不会引发线程安全问题了。

实际开发解说:

实际开发中,我们使用的 Controller 就是单例模式的,的确不是线程安全的,但是开发者不会在 Controller 层写有状态的共享变量,所以不会引发线程安全问题。

10. Servlet 是线程安全吗?

答案:

是线程不安全的。

解释:

Servlet 在 Web 容器中的实例对象是单例的,故也有相同问题。

二. Java 线程池

1. ThreadPoolExecutor 线程池是什么?

答案:

ThreadPoolExecutor 线程池是 JDK 并发包(java.util.concurrent)里的类;就是通过统一的方式创建线程,并进行管理,使得线程能够重复使用,支持更多功能。

实际开发解说:

实际开发中,使用 ThreadPoolExecutor (企业自己会封装线程池工具)或者 ThreadPoolTaskExecutor(Spring 中的)创建多线程。

例子:

public class ThreadPoolExecutorTest {

    @Test
    public void testThreadPoolExecutor() throws ExecutionException, InterruptedException {
        // 初始化线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,              // 指定 corePoolSize (核心线程数)
                1,              // 指定的 maximumPoolSize (线程池可以创建的最大线程数)
                0,             // 指定的 keepAliveTime(线程空闲的情况下,可以存活的时间)
                TimeUnit.SECONDS, // keepAliveTime 的时间单位
                new LinkedBlockingQueue<Runnable>()// 指定的 workQueue 对象(装任务的队列)
        );
        // 初始化任务
        Task task = new Task();
        // 向线程池提交任务
        // 打印结果: Task 任务被执行!
        executor.execute(task);

        // 关闭线程池有以下两种方法:

        // 关闭线程池:
        // 1.线程池不会接受新的任务,执行拒绝任务处理器(默认是抛出异常)
        // 2.线程池执行完已经提交了的任务再结束
        executor.shutdown();

        // 关闭线程池:
        // 1.线程池不会接受新的任务,执行拒绝任务处理器(默认是抛出异常)
        // 2.正在执行任务的线程不会被终止,直到任务处理完毕;
        // 3.任务队列中没有线程处理的等待任务,会直接被终止;
        // 4.返回未执行的任务
        executor.shutdownNow();
    }

}

class Task implements Runnable {

    @Override
    public void run() {
        System.out.println(" Task 任务被执行!");
    }
}

}
复制代码

参数解释:

1. corePoolSize

用于控制线程池逻辑:当用户提交任务时,只要工作线程数小于 corePoolSize 时,就会创建工作线程执行任务(工作线程是指线程池创建的存活线程);如果等于 corePoolSize,就不会创建工作线程,而是将任务放到 workQueue 队列对象里。

核心线程只是一个概念,在代码中并没有标记某个线程是否是核心线程;也就是说当工作线程数小于等于 corePoolSize 时,线程池中这部分工作线程就是核心线程。

2. keepAliveTime

表示线程空闲的情况下,可以存活的时间;核心线程不受 keepAliveTime 控制;除非 allowCoreThreadTimeOut 置为 true

3. allowCoreThreadTimeOut

表示是否允许核心线程空闲超时,默认为 false。如果配置 true,核心线程也会受 keepAliveTime 控制。

4. workQueue

当工作线程数等于 corePoolSize 时,提交的任务就会放在 workQueue 指定的队列对象里。

5. maximumPoolSize

表示线程池中最多能够容纳多少工作线程数。

当工作线程数等于配置的 corePoolSize,并且 workQueue 已满时,线程池会创建新的非核心线程,直到线程池中的工作线程总数等于 maximumPoolSize。

6. handler

拒绝任务处理器,默认是抛出异常;如果【任务队列已满,并且工作线程总数等于 maximumPoolSize 】或者【已经调用 shutdown() 方法或 shutdownNow() 方法】,向线程池提交任务,则会执行 handler 。

2. ThreadPoolExecutor 线程池的好处?

答案:

线程池可以减少性能开销、减少资源浪费、支持更多功能。

解释:

  1. **减少性能开销、减少资源浪费:**通过重复使用创建线程的方式,可以避免频繁创建、关闭线程;
  2. **支持更多功能:**通过设置 corePoolSize、maximumPoolSize 等参数可以达到不同的使用效果;

3. ThreadPoolExecutor 线程池都有哪些常用参数?

常用的参数有 6 个:

  • 核心线程数(corePoolSize)
  • 线程空闲存活时间(keepAliveTime)
  • 是否允许核心线程空闲超时(allowCoreThreadTimeout)
  • 线程池所使用的队列实例(workQueue)
  • 最大线程数(maximumPoolSize)
  • 拒绝任务处理器(handler)

4. 向线程池提交任务时,execute() 和 submit() 的区别?

答案:

execute() 和 submit() 都是用来提交任务的,区别在于:submit() 方法可以返回线程执行任务的返回值。

实际开发解说:

使用线程池时,都可能会用到。

代码示例:

public class ThreadPoolExecutorTest {

    @Test
    public void testThreadPoolExecutor() throws ExecutionException, InterruptedException {
        // 初始化线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,              
                1,              
                0,             
                TimeUnit.SECONDS, 
                new LinkedBlockingQueue<Runnable>()
        );

        // 创建任务
        TaskCallable taskCallable = new TaskCallable();
        // 向线程池提交任务,返回 future 对象
        Future future = executor.submit(taskCallable);
        // future.get() 用于获取线程执行任务的返回值;如果线程没有执行完任务,get 方法则会阻塞,直到返回执行结果
        // 打印结果: TaskFuture 任务被执行!
        System.out.println(future.get());

        // 关闭线程池
        executor.shutdown();

    }

}

class TaskCallable implements Callable<String> {

    @Override
    public String call() {
        return " TaskCallable 任务被执行!";
    }
}
复制代码

5. 向线程池提交任务时,Future 与 Futuretask 的区别?

答案:

1. Future

Future 是接口,不能实例化。

2. Futuretask

Futuretask 实现了 Runnable 和 Future;能够直接实例化;并且还因为实现了 Runnable,还可以直接作为任务的载体对象提交给线程池。

实际开发解说:

使用线程池时,都可能会用到。

代码示例:

public class ThreadPoolExecutorTest {

    @Test
    public void testThreadPoolExecutor() throws ExecutionException, InterruptedException {
        // 初始化线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,              
                1,              
                0,             
                TimeUnit.SECONDS, 
                new LinkedBlockingQueue<Runnable>()
        );

        // 创建任务
        TaskCallable taskCallable = new TaskCallable();
        // 创建用于获取返回值的 FutureTask 对象
        FutureTask<String> futureTask = new FutureTask(taskCallable);
        // 向线程池提交任务
        // 使用 FutureTask,不能同时使用 submit() 方法。如果使用 submit() 方法,返回值是 null
        executor.execute(futureTask);
        // futureTask.get() 用于获取线程执行任务的返回值,如果线程没有执行完任务,get 方法则会阻塞,直到返回执行结果
        // 打印结果: TaskFuture 任务被执行!
        System.out.println(futureTask.get());

        // 关闭线程池
        executor.shutdown();

    }
}

class TaskCallable implements Callable<String> {

    @Override
    public String call() {
        return " TaskCallable 任务被执行!";
    }
}
复制代码

6. ThreadPoolExecutor 原理?

见“ThreadPoolExecutor 源码解读”一文。

7. Executors 是什么?

答案:

Executors 是并发包里的一个类,在 ThreadPoolExecutor 基础上实现了更多模式的线程池。

解释:

尽管 Executors 实现了更多模式的线程池,但是因为严重的弊端,导致实际开发中通常不被使用。例如:ThreadPoolExecutor 中 workQueue 指定队列的长度,都默认为 Integer.MAX_VALUE,无法修改。开发时,很有可能会引发任务大量堆积,从而导致内存溢出(OOM)。

8. Executors 能够创建多少种模式的线程池?

答案:

有以下 6 种线程池:

  1. 单一线程池(newSingleThreadExecutor)
  2. 固定线程池(newFixedThreadPool)
  3. 缓存线程池(newCachedThreadPool)
  4. 定时任务线程池(newScheduledThreadPool)
  5. 单一定时任务线程池(newSingleThreadScheduledExecutor)
  6. 任务窃取线程池(newWorkStealingPool)

解释:

以上 1、2、3 种只是通过改变 corePoolSize 、workQueue 等参数,来实现不同线程池的使用效果。

1. 单一线程池

使用这种线程池,表示线程池至多只会创建 1 个线程来处理任务。

Executors 底层将 corePoolSize 设置为 1,workQueue 设置为 LinkedBolckingQueue 对象,LinkedBolckingQueue 对象默认大小为 Integer 的最大值(用以实现 workQueue 不会被装满的效果;因为在装满前,一般 JVM 就会先抛出 OOM)。

2. 固定线程池

使用这种线程池,表示线程池至多只能创建 N 个线程。

Executors 底层将 corePoolSize 设置为 N,workQueue 设置为 LinkedBolckingQueue 对象,LinkedBolckingQueue 对象默认大小为 Integer 的最大值(用以实现 workQueue 不会被装满的效果;因为在装满前,一般 JVM 就会先抛出 OOM)。

3. 缓存线程池

使用这种线程池,表示只要用户提交任务,线程池都会保证立刻有线程执行,无需等待;反之,如果没有要执行的任务,工作线程数为 0 。(如果使用不当,很有可能导致 OOM)

Executors 底层将 corePoolSize 设置为 0,maximumPoolSize 设置为 Integer 的最大值,keepAliveTime 设置为 60 s,workQueue 设置为 SynchronousQueue 对象(SynchronousQueue 是只能装载一个元素的阻塞队列,插入一个元素后,SynchronousQueue 就会进入阻塞状态,等待元素移除后,才可以继续插入下一个元素)。

4. 定时任务线程池

使用这种线程池,表示线程池至多只能创建 N 个线程。并且能够做到多长时间后执行一次任务,还能做到多长时间后开始执行任务,每隔多长时间再次执行。

4. 单一定时任务线程池

这种线程池,就是封装了定时任务线程池,N 指定为 1。

6. 任务窃取线程池

这种线程池,是 JDK 8 开始引入的;能够根据当前服务器 CPU 创建相应个数的线程。底层不是通过 ThreadPoolExecutor 实现的。

三. 原子性

1. 原子性是什么?

答案:

满足以下几个特点,我们就说这个操作支持原子性,线程安全:

  1. 原子操作中的所有子操作,要不全成功、要不全失败;
  2. 线程执行原子操作过程中,不会受到其它线程的任何影响;
  3. 其它线程只能感知到原子操作开始前和结束后的变化。

解释:

包含多个操作单元,但仍支持原子性,通常都是由锁实现的。

代码示例:

class Test {
    int x = 0;
    int y = 0;

    public void test() {
        // 原子操作
        x = 10; 

        // 大致分为两步:1)获取 x 的值到缓存里;2)取出缓存里的值,赋值给 y
        // 不支持原子性;获取 x 的值到缓存里之后,其它线程可能修改 x 的值,导致 y 值错误
        y = x; 

        // 大致分为三步:1)获取 x 的值到缓存里;2)取出缓存里的值加一;3)赋值给 x
        // 不支持原子性;原理类似 y = x;
        x++; 

    }

}
复制代码

2. Cas 是什么?

答案:

Cas 是 Compare-and-swap(比较并替换)的缩写,是支持原子性的操作;在 Java 中,底层是 native 方法实现,通过 CPU 提供的 lock 信号保证的原子性。

实际开发解说:

常用的 Jedis 方法 incr()(Jedis 是 Redis 的 Java 客户端) ,可以将 Redis 中对应 key 的缓存值加 1。

解释:

想要将数据 V 的原值 O 替换为新值 N,执行 Cas 操作:

  1. 预先读取数据 V 的值 O 作为预期值;
  2. 执行 Cas 操作:
    1. 比较当前数据 V 的值是否是 O;
      1. 如果是,则替换为 N,返回执行成功;
      2. 如果不是,则不替换,返回执行失败;

例子:

以 java.util.concurrent.atomic 包中的 AtomicInteger 为例;

public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(100);
        // 将 AtomicInteger 的值,从 100 替换为 200
        Boolean b = atomicInteger.compareAndSet(100, 200);
        // 返回 true,替换成功
        System.out.println(b);
    }
复制代码

3. Cas 有什么潜藏的问题?

答案:

ABA 问题。

解释:

线程1 线程2
1. 查询数据 V,值为 A,作为预期值 1. 查询数据 V,值为 A,作为预期值
处理数据... 2. 计算结果为 B
处理数据... 3. 执行 Cas 操作,将 A 替换为 B
处理数据... 4. 查询数据 V,值为 B,作为预期值
处理数据... 5. 计算结果又为 A
处理数据... 6. 执行 Cas 操作,将 B 替换为 A
2. 计算结果为 B
3. 执行 Cas 操作,将 A 替换为 B
结论:这就造成了 ABA 问题:在线程 1 操作的时候,完全感知不到线程 2 已经在线程 1 开始之后,结束之前修改过值了。

解决:

进行比较的时候,不再比较目标的值,而是给目标增加一个版本号的字段,每次只比较版本号;然后每次更新都升一个版本。

四. 锁

1. ReentrantLock 是什么?

答案:

ReentrantLock 是 java.util.concurrent 并发包中的可重入同步锁(可重入锁就是指持有锁的线程可以重复进入有该锁的代码块);基于 AQS(AbstractQueuedSynchronized)实现。需要手动释放锁,但是支持更多功能,比如:上公平/非公平锁、可被中断定时锁、可被中断锁等。

代码示例:

@Test
    public void testReentrantLock1() {
        // 多个线程使用同一个 ReentrantLock 对象,上同一把锁,默认上非公平锁
        Lock lockNoFair = new ReentrantLock();
        try {
            // 本线程尝试获取锁;如果锁已经被其它线程持有,则会进入阻塞状态,直到获取到锁
            lockNoFair.lock();
            System.out.println("处理中...");
        } finally {
            // 释放锁
            lockNoFair.unlock();
        }
    }

    @Test
    public void testReentrantLock2() {
        // 多个线程使用同一个 ReentrantLock 对象,上同一把锁,构造函数传 true,上公平锁
        Lock lockFair = new ReentrantLock(true);
        try {
            // 本线程尝试获取锁;如果锁已经被其它线程持有,则会进入阻塞状态,直到获取到锁
            lockFair.lock();
            System.out.println("处理中...");
        } finally {
            // 释放锁
            lockFair.unlock();
        }
    }
复制代码

解释:

AQS(AbstractQueuedSynchronized)

AQS 是一个抽象类,是并发包中部分并发工具类的重要组成部分(大部分并发工具类的内部类 Sync,都继承了 AQS),封装了大部分核心代码。

方法解释:

1. lock()(公平锁)

线程获取锁的顺序完全基于调用 lock() 方法的先后顺序。

场景模拟

时间线 线程 1 线程 2 线程 3
1 线程 1 调用 lockFair.lock() 方法,获取到锁
2 线程 2 调用 lockFair.lock() 方法后,没有获取到锁,将线程 2 顺序放入到链表里排队,进入阻塞状态
3 线程 1 调用 lockFair.unLock() 方法,释放锁,唤醒线程 2
4 线程 3 调用 lockFair.lock() 方法,发现线程 2 已经在链表里等待获得锁;线程 3 就追加到线程 2 之后,进行排队
5 线程 2 被线程 1 唤醒,重新尝试获取锁,获取锁成功

2. lock()(非公平锁)

场景模拟

时间线 线程 1 线程 2 线程 3
1 调用 lockNoFair.lock() 方法,获取到锁
2 调用 lockNoFair.lock() 方法后,没有获取到锁,线程 2 顺序追加到链表后排队,进入阻塞状态
3 调用 lockNoFair.unLock() 方法,释放锁,唤醒线程 2
4 调用 lockNoFair.lock() 方法,成功获取到锁
5 线程 2 被线程 1 唤醒,呆在链表里位置不动,重新尝试获取锁,获取锁失败,已经被线程 3 抢占到了锁,再次进入阻塞状态

3. lockInterruptibly()(可被中断锁)

lockInterruptibly() 也分为公平锁和非公平锁,与 lock() 方法的区别就在于:当线程调用 lockInterruptibly() 方法没有获取到锁,进入阻塞后;如果其它线程对该线程标记为中断状态, lockInterruptibly() 方法则会从阻塞中唤醒,抛出中断异常。

如果线程一开始就被标记为中断状态,再调用 lockInterruptibly() 方法,lockInterruptibly() 方法则会直接抛出中断异常。

4. tryLock(long timeout, TimeUnit unit) 方法(定时锁)

tryLock(long timeout, TimeUnit unit) 也区分公平锁和非公平锁;与 lockInterruptibly() 方法的区别就在于:线程调用 tryLock(long timeout, TimeUnit unit) 方法,获取不到锁,进入阻塞后;如果在指定的时间里,仍然没有被其它释放锁的线程唤醒,则会自动唤醒,直接返回失败。

2. 重入锁是什么?

重入锁就是指持有锁的线程可以重复进入有该锁的代码区域。

3. 重入锁的实现原理?

答案:

​ 在数据结构上,每一个锁都记录了当前持有锁的线程对象 exclusiveOwnerThread 和一个状态值 state;

  • 当状态值为 0 时,表示该锁没有被任何线程持有;
  • 当状态值大于 0 时,则表示该锁已经被占用,并且 state 的值表示当前线程重复获取到锁的次数。

在执行代码时,如果一个线程想要尝试获取锁时,发现 state > 0,但是 exclusiveOwnerThread 的值为当前线程对象,就会将 state 的值加一,成功重复进入有该锁限制的代码区域里。

解释:

见“ReentrantLock 源码解读“一文。

4. ReentrantLock 公平锁和非公平锁的区别?

答案:

1. 非公平锁

非公平锁就是在线程调用 lock() 方法,尝试获取锁的时候,会优先抢锁;抢锁失败再老实追加到链表里排队,等候上一个线程释放锁。

2. 公平锁

公平锁就是在线程调用 lock() 方法,尝试获取锁的时候,直接去链表里排队,按照调用的先后顺序获取锁。

解释:

见“ReentrantLock 源码解读“一文。

5. RentrantLock 的原理?

见“ReentrantLock 源码解读“一文中的白话原理。

6. synchronized 是什么?

答案:

synchronized 是 Java 语言的关键字(也就是说是 JVM 实现),是可重入锁,无需手动释放锁,代码执行结束,JVM 自动释放锁;可以修饰代码块、普通方法、静态方法。当修饰代码块和方法时。sychronized 锁定的是当前对象;如果修饰静态方法,锁定的是这个类的所有对象。如果已经有线程成功获取了锁,其它线程想要获取锁,就会进入阻塞状态。

例子:

public synchronized void test() {...}
复制代码

解释:

锁定对象的含义就是:

当一个线程进入对象的 synchronized 修饰代码块、普通方法后,其它线程不能进入此对象的其它任何 sychronized 修饰的地方,而是进入阻塞状态。

如果是换成进入对象的 sychronized 修饰静态方法后,其它线程则不能进入此类的所有对象的其它任何 sychronized 修饰的地方。

7. 当一个线程进入对象的 synchronized 方法后,其它线程是否可进入此对象的其它方法?

答案:

如果此对象其它方法没加 synchronized 关键字,则可以。

解释:

因为同一对象内的所有 synchronized 方法都是用当前对象作为锁,也就是说锁是同一把,所以其它线程在没有获取锁的情况下,是进入不了其它 synchronized 方法的。

8. synchronized 可以被继承么?

不能。

9. sychronized 和 Reentrantlock 的区别?

1. sychronized

JVM 实现,自动释放锁。

2. Reentrantlock

并发包中实现,需要手动释放锁,支持更多功能。

10. sychronized 和 Reentrantlock 的使用场景?

实际开发中,一般都采用多台服务器部署同一系统,达到负载均衡的作用, 而 sychronized 和 Reentrantlock 锁只能作用于部署在一台服务器的系统,所以现在一般都使用分布式锁(Redis 锁),而不用 sychronized 和 Reentrantlock 锁了。

11. 悲观锁是什么?

答案:

悲观锁是一种锁的实现方式;表示总是很悲观,每次读/写操作的时候,都认为其它线程会影响到本次的执行结果,所以操作之前就上锁,保证数据只有自己能操作,操作完成之后再释放锁。例如:sychronized 和 Reentrantlock。

应用场景:

用于频发生并发写的操作。如果上锁/释放锁不会成为系统性能瓶颈,也可以使用悲观锁,因为实现方便。

实际开发解说:

实际开发中,用得较多;比如:分布式锁(Redis)。

12. 乐观锁是什么?

答案:

悲观锁是锁的另一种实现方式;表示总是很乐观,每次读/写操作的时候,都认为其它线程不会影响自己的执行结果,所以不会上锁;但是如果提交事务之前发现其它线程影响到了的本次执行结果,则放弃提交。

放弃提交后,可以根据业务需要,编写直接放弃,或者自旋重试的代码逻辑。(自旋就是重复循环的意思)

实现:

  1. 可以给数据添加一个“版本号”字段,用于标识数据的版本。如果提交时的数据版本号和操作之前查询出来的版本号一致,则将数据的版本号递增一个版本,并提交事务;否则放弃提交;
  2. 使用带有 CAS 功能的工具(比如:java.util.concurrent.atomic 并发包中的类)

应用场景:

用于很少发生并发写的操作。

实际开发解说:

实际开发中,用得较少。

13. 死锁是什么?

答案:

死锁就是指永远不会被释放的锁。

解释:

死锁的发生原因一般有以下两点:

  1. 负责释放锁的线程因为各种意外情况,没有被创建或者被杀死了;
  2. 两个或两个以上的线程在获取锁后,因为互相持有对方想要的资源而持续等待的现象。例:
线程 A 线程 B
成功获取了锁 a 成功获取了锁 b
尝试获取锁 b,但是因为锁 b 已经被线程 B 获取,所以进入阻塞状态 尝试获取锁 a,但是因为锁 a 已经被线程 A 获取,所以进入阻塞状态
结果:由于 A、B 两个线程互相持有对方需要的锁,双方都进入了阻塞状态,互相等待对方释放锁,导致死锁。

实际开发解说:

实际开发中,常会用到的锁是分布式锁(Redis 锁)和数据库自带的事务锁;如果编码不正确,可能会遇到。

如何解决,就必须具体情况,具体分析了。

加入我的粉丝社群,阅读剩余全部内容

从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入,这样的知识星球,难道你还要犹豫!

Java 并发面试题集
原文  https://juejin.im/post/5eeff8b3518825658772a7e4
正文到此结束
Loading...