java并发:简单面试问题集锦

多线程

Simultaneous Multithreading,简称SMT。

 

并行

并行性(parallelism)指两个或两个以上的事件在同一时刻发生,在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。

 

并发

并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用。

并发性是对有限物理资源强制行使多用户共享以提高效率,离开了单位时间其实是没有意义的。

所以谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少。

 

线程同步

线程同步就是多线程操作共享资源时的“排队机制”,核心目标:
安全第一(数据正确) ➕ 效率兼顾(减少排队时间)。

 

何为基于共享容器协同的多线程模式以及基于事件协同的多线程模式?

在一些场景中我们需要在多个线程之间对共享的数据进行处理,例如经典的生产者-消费者模式,此即基于共享容器协同的多线程模式;

若一场景中有A、B两个线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态改变或者事件产生与A线程有关,此场景即基于事件协同的多线程模式

 

Java中创建线程的方式

1、实现 Runnable接口;

2、继承 Thread类;

3、使用 Callable

 

使⽤Runnable接⼝更为轻量,通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类

注意事项: run ⽅法是没有返回值的

 

继承Thread的好处是什么

使用继承Thread的方式,在run()方法内获取当前线程直接使用this就可以了,无须使用 Thread.currentThread() 方法

 

继承Thread的缺点是什么

(1)Java 不支持多继承,如果继承了 Thread类,则不能再继承其他类

(2)任务代码与业务代码耦合严重  

 

Thread.start()与Thread.run()有什么区别?

Thread.start()方法用于启动线程,使之进入就绪状态,当cpu分配时间到该线程时,由JVM调度执行run()方法。

详解:

调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状 态。

一旦run方法执行完毕,该线程就处于终止状态。 

为什么需要run()和start()方法,可以只用run()方法来完成任务吗?

需要run()、start()这两个方法是因为JVM创建一个单独的线程不同于普通方法的调用,这项工作由线程的start方法来完成,start由本地方法实现,需要显示地被调用。

使用这两个方法的另外一个好处是任何一个对象都可以作为线程运行,只要实现了Runnable接口,这就避免了因继承Thread类而造成的Java多继承问题。

 

什么是线程组

  在java的多线程处理中有线程组ThreadGroup的概念,ThreadGroup是为了方便线程管理出现的。我们可以统一设定线程组的一些属性,比如设置未捕获异常的处理方法,设置统一的安全策略等,也可以通过线程组方便地获得线程的一些信息。

image

  每一个ThreadGroup都可以包含一组子线程和一组子线程组,在一个进程中线程组是以树的方式存在,通常情况下根线程组是system线程组,system线程组下是main线程组,默认情况下第一级应用的线程组是通过main线程组创建出来的,也就是说system线程组是所有线程最顶级的父线程组。

Thread.currentThread().getThreadGroup();//可以获得当前线程的线程组

ThreadGroup是⼀个标准的向下引⽤的树状 结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回收

 

线程组与线程池的区别

线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。

 

线程的优先级

Java中线程优先级可以指定,范围是1~10;默认的线程优先级为5。

如果某个线程优先级⼤于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取⽽代之的是线程组的最⼤优先级

 

并不是所有的操作系统都⽀持10级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。

注意:

线程的执⾏顺序由调度程序来决定;通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏

 

线程调度器

Java 的线程调度器是操作系统与 JVM 协作的核心组件,负责决定线程的执行顺序和 CPU 时间分配。

image

JVM 层

提供线程优先级(Thread.setPriority(1-10))等抽象
仅作为提示:实际调度仍由操作系统控制

操作系统层

Linux:完全公平调度(CFS)
Windows:多级反馈队列(MLFQ)
macOS:Grand Central Dispatch(GCD)

 

性能调优参数

image

补充:

时间片长度:通常 10-100ms(可通过 -XX:QuantumSize 调整 HotSpot 参数)

 

线程的状态

操作系统中线程的状态转换

image

 

Java中线程的状态转换

注:图片来自网络(解读:Java线程的 RUNNABLE 状态其实是包括了传统操作系统线程的 ready 和 running 两个状态

 

image

 

getState方法

image

 

线程状态转换

关键点:Runnable 状态对应 OS 的就绪队列

image

 

Waiting

调⽤如下3个⽅法会使线程进⼊等待状态,处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒

image

 

sleep()、wait()

Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。

sleep()是一个静态方法,只对当前线程有效,一个常见的错误是调用t.sleep()(注:这里的t是一个不同于当前线程的线程)。

sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间。

wait()方法用于线程间通信,object.wait()使当前线程处于“不可运行”状态。

 

从 JMM(Java 内存模型)看本质

image

 

为什么wait和notify、notifyAll方法要在同步块中调用?

wait、notify、notifyAll是Java中Object对象上的三个方法;在多线程中可以把某个对象作为事件对象,通过这个对象的wait、notify和notifyAll方法来完成线程间状态通知(也即线程间协同)。

wait()/notify()的使用方式如下:

 

基本上wait()/notify()与sleep()/interrupt()类似,只是前者需要获取对象锁。

参考资料:

How to work with wait(), notify() and notifyAll() in Java?

 

 

wait() 方法的关键约束

在调用 obj.wait() 方法前,当前线程要先获取 obj 对应的监视器锁(即持有该对象的锁);如果没有获取该对象的监视器锁,直接调用则会抛出 IllegalMonitorStateException异常。

错误示例:

Object lock = new Object();  
lock.wait(); // 抛出异常:未在同步块内获取锁  

 

补充:

监视器锁(Monitor Lock) —— 每个 Java 对象(Object 实例)内置一个监视器锁(通过 synchronized 关键字获取

synchronized (obj) { // 获取 obj 的监视器锁
    obj.wait();      // 合法调用
}

 

wait() 的执行流程

image

 

经典代码片段如下:

 

 

典型使用场景

当前线程被添加到等待队列后,另一线程可以调用notify()方法唤醒等待中的线程。

public class ProducerConsumer {  
    private final Queue<Integer> queue = new LinkedList<>();  
    private final Object lock = new Object();  

    public void produce() throws InterruptedException {  
        synchronized (lock) {  
            while (queue.size() == 10) {  
                lock.wait(); // 释放锁,等待消费  
            }  
            queue.add(1);  
            lock.notifyAll(); // 唤醒消费者  
        }  
    }  

    public void consume() throws InterruptedException {  
        synchronized (lock) {  
            while (queue.isEmpty()) {  
                lock.wait(); // 释放锁,等待生产  
            }  
            queue.poll();  
            lock.notifyAll(); // 唤醒生产者  
        }  
    }  
}

 

 

notify和notifyAll都是唤醒调用某个对象的wait方法的线程,二者的区别是:notify会唤醒一个等待线程,由于共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的;而notifyAll会唤醒所有的等待线程。

Note:

当前线程调用共享变量的 wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。 

被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回;即被唤醒的线程需要和其他线程一起竞争锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

 

wait和notify、notifyAll方法要在同步块中调用还有一个原因是为了避免wait和notify之间产生竞态条件

解决方案 —— 通过 synchronized 将 条件检查 与 等待操作 原子化

public void consume() {
    synchronized (lock) {              // 获取锁
        while (queue.isEmpty()) {      // 原子化检查条件
            lock.wait();               // 释放锁并等待
        }
        queue.poll();
    }                                 // 释放锁
}

保护机制解释:

原子性:锁确保条件检查(queue.isEmpty())和 wait() 调用之间无其他线程干扰
可见性:锁建立的 happens-before 关系,保证生产者修改 queue 后消费者能立即看到变化

 

补充:

竞态条件的具体类型

image

 

 

为什么应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。也可以这么说,在notify()方法调用之后和等待线程醒来之前这段时间,等待线程原来的等待状态可能会改变,这就是在循环中使用wait()方法效果更好的原因。

 

如果需要等待某个事情完成后才能继续往下执行,可以使用什么方法?

Thread 类中的 join 方法可以达到这个目的,该方法不需要任何参数,且其返回值为void。

代码片段如下:

 

假设有三个线程T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法可以让线程按特定顺序执行,如:可以用线程类的join()方法在一个线程中启动另一个线程T,线程T执行完成后原线程继续执行。

Thread worker = new Thread(() -> {
    System.out.println("子线程执行任务");
});
worker.start();
worker.join(); // 主线程阻塞,直到worker结束
System.out.println("主线程继续执行");

解释:

在调用线程(如主线程)中执行 thread.join(),会阻塞当前线程,直到目标线程(thread)执行结束。

 

为了确保三个线程的顺序,应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

threadA.start();
threadA.join();
threadB.start(); // B 等待 A 完成

解释:

确保线程 B 在线程 A 完成后启动

 

Join方法

Thread.join() 是 Java 多线程编程中实现线程同步的核心方法

底层原理

join() 方法通过 synchronized 锁定目标线程对象(非当前线程),内部调用 Object.wait()

image

 

潜在风险

死锁 —— 若多个线程相互调用 join() 形成循环等待(如 A 等 B,B 等 A),会导致死锁。

 

提供重载方法支持超时等待

worker.join(500); // 最多等待500毫秒

 

唤醒机制

当目标线程终止时,JVM 自动调用 Thread.exit(),在其中触发 notifyAll() 唤醒所有等待该线程对象的线程

private void exit() {
    synchronized (this) {
        notifyAll(); // 唤醒所有等待此线程的线程
    }
}

备注:

这段代码为伪代码,实际上notify等方法定义在Object类中,如下图所示

image

 

与 sleep() 的关键区别

image

 

Thread类中的yield方法有什么作用?

Java中的Thread.yield()方法是一个线程调度提示

image

作用原理

调用yield()会提示线程调度器(如操作系统调度器)当前线程愿意让出CPU资源,但调度器可完全忽略该提示(取决于JVM实现和操作系统策略)。
本质是尝试避免某个线程过度占用CPU,以平衡线程间的执行机会。

非确定性行为

不保证调用后立即切换线程(可能继续执行当前线程)。
不保证其他线程获得执行权(可能调度器选择另一个高优先级线程)。

测试:用于复现竞态条件(Race Condition) —— 通过主动让出CPU,增大线程交替执行的不确定性,暴露并发问题(如数据竞争)。

public class YieldTest {
    static int counter = 0;
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                Thread.yield();
                counter++;
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                Thread.yield();
                counter--;
            }
        }).start();
    }
}

 

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),当且仅当指定线程拥有某个具体对象的锁时返回true。

 

在静态方法上使用同步会发生什么事?

  同步静态方法会获取该类的“Class”对象,所以当一个线程进入同步的静态方法中时,线程监视器获取类本身的对象锁,其它线程不能进入这个类的任何静态同步方法。它不像实例方法,因为多个线程可以同时访问不同实例的某个同步实例方法。

 

在一个对象上,多个线程是否可以调用不同的同步实例方法?

不能,因为对象同步了实例方法,某个线程调用对象的同步实例方法时获取了对象的对象锁,只有当执行完该方法释放对象锁后才能执行其它同步方法;但是多个线程可以同时访问不同实例的某个同步实例方法。

 

一个线程运行时发生异常会怎样?

如果异常没有被捕获,该线程将会停止执行。

Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。

当一个未捕获异常将造成线程中断的时候,JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。 

 

如何在Java中创建Immutable对象?

Immutable对象可以在没有同步的情况下共享,降低了对某个对象进行并发访问时的同步化开销。

这个问题看起来与多线程没有什么关系, 但不变性有助于简化已经很复杂的并发程序。

Java没有@Immutable这样的注解符,要创建不可变类,要实现下面几个步骤:

  • 将所有的成员声明为私有的
  • 通过构造方法初始化所有成员
  • 对变量不提供setter方法(这样就不允许直接访问这些成员)
  • 在getter方法中不直接返回对象本身,而是克隆对象并返回对象的拷贝

 

Java线程池中submit() 和 execute()方法有什么区别?

  两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类(如:ThreadPoolExecutor和ScheduledThreadPoolExecutor)都有这些方法。

 

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

事实上如果一个任务不能被调度执行,那么ThreadPoolExecutor.submit()方法将会抛出一个RejectedExecutionException异常。

 

什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中因争夺资源而造成的互相等待的现象。

在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

死锁的产生的因素:

  • 互斥

指线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。

如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。 

  • 持有并请求

指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。

  • 不可剥夺

指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。

  • 环路等待

如何避免线程死锁

造成死锁的原因其实和申请资源的顺序有很大关系,前述死锁产生的因素中只有持有并请求和环路等待条件是可以被破坏的;因此使用资源申请的有序性原则可以避免死锁。

资源申请的有序性是:假如线程 A 和线程 B 都需要资源 1、2、3、...、n 时,对资源进行排序,线程 A 和线程 B 只有在获取了资源 n-1 时才能去获取资源 n。 

 

守护线程与用户线程

Java 中的线程分为两类,分别是 daemon 线程(守护线程〉和 user 线程(用户线程)。

在JVM启动时会调用main函数,main函数所在的线程是一个用户线程。

在JVM内部同时还启动了好多守护线程,比如:垃圾回收线程。

 

守护线程和用户线程有什么区别?

守护线程是否结束并不影响JVM的退出;当最后一个非守护线程结束时,JVM会正常退出。

言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。

创建守护线程的代码片段如下:

Note:

main 线程运行结束后,JVM会自动启动一个叫作 DestroyJavaVM 的线程,该线程会等待所有用户线程结束后终止JVM进程。

 

JVM中哪个参数是用来控制线程的栈堆栈小的?

-Xss参数是用来控制线程的堆栈大小的。

 

你如何在Java中获取线程堆栈?

  对于不同的操作系统,有多种方法来获得Java的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows你可以使用Ctrl+ Break组合键来获取线程堆栈,Linux下用kill -3命令。你也可以用jstack这个工具来获取,它对线程id进行操作,你可以用jps这个工具找到id。

 

posted @ 2016-04-12 21:30  时空穿越者  阅读(881)  评论(0)    收藏  举报