2024年java面试准备--多线程篇(3)

简介: 2024年java面试准备--多线程篇(3)

面试注意

启动线程方法 start()和 run()有什么区别?

只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。

多线程同步有哪几种方法?

Synchronized 关键字,Lock 锁实现,分布式锁等。

死锁

死锁就是两个线程相互等待对方释放对象锁

多线程之间如何进行通信

wait/notify

线程怎样拿到返回结果

实现Callable 接口

多线程执行问题:

Q1:有 A、B、C 三个线程,如何保证三个线程同时执行?

保证线程同时执行可以用于并发测试。可以使用倒计时锁CountDownLatch实现让三个线程同时执行。

将其计数器初始为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()方法时,计数器变为0,多个线程同时被唤醒执行任务。

代码如下所示:

ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        executorService.submit(()->{
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程A执行,执行时间:" + System.currentTimeMillis());
        });
        executorService.submit(()->{
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程B执行,执行时间:" + System.currentTimeMillis());
        });
        executorService.submit(()->{
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程C执行,执行时间:" + System.currentTimeMillis());
        });
        countDownLatch.countDown();

打印内容如下,通过时间可以证明三个线程是同时执行的。


线程A执行,执行时间:1617811258309
线程C执行,执行时间:1617811258309
线程B执行,执行时间:1617811258309

让三个线程同时执行,也可以使用栅栏 CyvlivBarrier 来实现,当三个线程都到达栅栏处,才开始执行。

Q2:有 A、B、C 三个线程,在并发情况下,如何保证三个线程依次执行?
  1. 用 join 方法

使用 join() 方法可以保证线程的顺序执行。在 Java 中,join() 方法是用来等待一个线程执行完成的方法,当调用某个线程的 join() 方法时,当前线程会被阻塞,直到该线程执行完成后才会继续执行。

具体来说,我们可以在 T1 线程结束时调用 T2 的 join() 方法,这样 T2 就会等待 T1 执行完成后再开始执行;同理,在 T2 结束时调用 T3 的 join() 方法,以确保 T3 在 T2 执行完成后才开始执行。这样就可以保证 T1、T2、T3 按照顺序依次执行。

  1. 使用CountDownLatch(闭锁)

使用 CountDownLatch(闭锁)方法可以保证线程的顺序执行。CountDownLatch 是一个同步工具类,它可以让某个线程等待多个线程完成各自的工作之后再继续执行。


@Test
    public void testUseCountDownLatch() {
        ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch aLatch = new CountDownLatch(1);
        CountDownLatch bLatch = new CountDownLatch(1);
        CountDownLatch cLatch = new CountDownLatch(1);
        executorService.submit(() -> {
            try {
                aLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("A - " + i);
            }
            bLatch.countDown();
        });
        executorService.submit(() -> {
            try {
                bLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("B - " + i);
            }
            cLatch.countDown();
        });
        executorService.submit(() -> {
            try {
                cLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("C - " + i);
            }
        });
        aLatch.countDown();
    }
  1. volatile 使用一个变量进行判断执行哪个线程。没有轮到的线程在不停循环,没有停止线程


private volatile int count = 0;
/**
 * 使用一个变量进行判断执行哪个线程。没有轮到的线程在不停循环,没有停止线程
 */
public void useVolatile() {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.submit(() -> {
        while (true) {
            if (count == 0) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("A - " + i);
                }
                count = 1;
                break;
            }
        }
    });
    executorService.submit(() -> {
        while (true) {
            if (count == 1) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("B - " + i);
                }
                count = 2;
                break;
            }
        }
    });
    executorService.submit(() -> {
        while (true) {
            if (count == 2) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("C - " + i);
                }
                count = 3;
                break;
            }
        }
    });
}
  1. 使用单个线程池

使用单个线程池可以保证t1、t2、t3顺序执行,因为单个线程池只有一个工作线程每次只会执行一个任务。我们可以将t1、t2、t3三个任务按照顺序提交给单个线程池,这样就可以确保它们按照顺序依次执行。

Q3:有 A、B、C 三个线程,如何保证三个线程有序交错执行?

实现三个线程交错打印,可以使用ReentrantLock以及3个Condition来实现

线程池启动线程 submit()和 execute()方法有什么不同

execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。

submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。

活锁、饥饿、无锁、死锁

死锁

死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。举个例子,A同学抢了B同学的钢笔,B同学抢了A同学的书,两个人都相互占用对方的东西,都在让对方还给自己自己再还,这样一直争执下去等待对方还而又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。

活锁

活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。

饥饿

我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。

无锁

无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过JDK的CAS原理及应用即是无锁的实现。

什么是原子性、可见性、有序性

原子性

原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量n++100次,如果n初始值为0,n最后的值应该是100,所以说它们是互不干扰的,这就是传说的中的原子性。但n++并不是原子性的操作,要使用AtomicInteger保证原子性。

可见性

可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。像CPU的缓存优化、硬件优化、指令重排及对JVM编译器的优化,都会出现可见性的问题。

有序性

我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高CPU的处理性能,JVM和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果。

什么是守护线程?有什么用?

什么是守护线程?与守护线程相对应的就是用户线程,守护线程就是守护用户线程,当用户线程全部执行完结束之后,守护线程才会跟着结束。也就是守护线程必 须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,守护线程自然会退出。

举例,GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是VM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

如何创建线程安全的单例模式

  1. 饿汉式:线程安全速度快,饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。
  2. 懒汉式:双重检测锁,第一次减少锁的开销、第二次防止重复、volatile防止重排序导致实例化未完成,而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

如果你提交任务时,线程池队列已满,这时会发生什么

  1. 如果使用的无界队列,那么可以继续提交任务时没关系的
  2. 如果使用的有界队列,提交任务时,如果队列满了,如果核心线程数没有达到上限,那么则增加线程,如果线程数已经达到了最大值,则使用拒绝策略进行拒绝

Synchrpnized和lock的区别

  • synchronized是关键字,lock是一个类
  • synchronized在发生异常时会自动释放锁,lock需要手动释放锁
  • synchronized是可重入锁、非公平锁、不可中断锁,lock的ReentrantLock是可重入锁,可中断锁,可以是公平锁也可以是非公平锁
  • synchronized是JVM层次通过监视器实现的,Lock是通过AQS实现的

常见线程安全的并发容器有哪些

  1. CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap
  2. CopyOnWriteArrayList、CopyOnWriteArraySet采用写时复制实现线程安全
  3. ConcurrentHashMap采用分段锁的方式实现线程安全


相关文章
|
2天前
|
Oracle Java 关系型数据库
一次惨痛的面试:“网易提前批,我被虚拟线程问倒了”
【5月更文挑战第13天】一次惨痛的面试:“网易提前批,我被虚拟线程问倒了”
27 4
|
2天前
|
Java
Java一分钟之-并发编程:线程间通信(Phaser, CyclicBarrier, Semaphore)
【5月更文挑战第19天】Java并发编程中,Phaser、CyclicBarrier和Semaphore是三种强大的同步工具。Phaser用于阶段性任务协调,支持动态注册;CyclicBarrier允许线程同步执行,适合循环任务;Semaphore控制资源访问线程数,常用于限流和资源池管理。了解其使用场景、常见问题及避免策略,结合代码示例,能有效提升并发程序效率。注意异常处理和资源管理,以防止并发问题。
24 2
|
2天前
|
安全 Java 容器
Java一分钟之-并发编程:线程安全的集合类
【5月更文挑战第19天】Java提供线程安全集合类以解决并发环境中的数据一致性问题。例如,Vector是线程安全但效率低;可以使用Collections.synchronizedXxx将ArrayList或HashMap同步;ConcurrentHashMap是高效线程安全的映射;CopyOnWriteArrayList和CopyOnWriteArraySet适合读多写少场景;LinkedBlockingQueue是生产者-消费者模型中的线程安全队列。注意,过度同步可能影响性能,应尽量减少共享状态并利用并发工具类。
17 2
|
2天前
|
Java 程序员 调度
Java中的多线程编程:基础知识与实践
【5月更文挑战第19天】多线程编程是Java中的一个重要概念,它允许程序员在同一时间执行多个任务。本文将介绍Java多线程的基础知识,包括线程的创建、启动和管理,以及如何通过多线程提高程序的性能和响应性。
|
2天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【5月更文挑战第18天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将了解线程池的基本概念,应用场景,以及如何优化线程池的性能。通过实例分析,我们将看到线程池如何提高系统性能,减少资源消耗,并提高系统的响应速度。
13 5
|
3天前
|
消息中间件 安全 Java
理解Java中的多线程编程
【5月更文挑战第18天】本文介绍了Java中的多线程编程,包括线程和多线程的基本概念。Java通过继承Thread类或实现Runnable接口来创建线程,此外还支持使用线程池(如ExecutorService和Executors)进行更高效的管理。多线程编程需要注意线程安全、性能优化和线程间通信,以避免数据竞争、死锁等问题,并确保程序高效运行。
|
3天前
|
存储 Java
【Java】实现一个简单的线程池
,如果被消耗完了就说明在规定时间内获取不到任务,直接return结束线程。
11 0
|
3天前
|
安全 Java 容器
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第18天】随着多核处理器的普及,并发编程变得越来越重要。Java提供了丰富的并发编程工具,如synchronized关键字、显式锁Lock、原子类、并发容器等。本文将深入探讨Java并发编程的核心概念,包括线程安全、死锁、资源竞争等,并分享一些性能优化的技巧。
|
3天前
|
安全 Java 开发者
Java中的多线程编程:理解与实践
【5月更文挑战第18天】在现代软件开发中,多线程编程是提高程序性能和响应速度的重要手段。Java作为一种广泛使用的编程语言,其内置的多线程支持使得开发者能够轻松地实现并行处理。本文将深入探讨Java多线程的基本概念、实现方式以及常见的并发问题,并通过实例代码演示如何高效地使用多线程技术。通过阅读本文,读者将对Java多线程编程有一个全面的认识,并能够在实际开发中灵活运用。
|
6天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
20 0