Java后端开发面试题——多线程

创建线程的方式有哪些?

继承Thread类

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread...run...");
    }
    
    public static void main(String[] args) {
        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        MyThread t2 = new MyThread() ;
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
    
}

实现runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }
    public static void main(String[] args) {
        // 创建MyRunnable对象
        MyRunnable mr = new MyRunnable();
        // 创建Thread对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}

实现Callable接口

public class MyCallable implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return "ok";
    }
    public static void main(String[] args) throws ExecutionException, 
        InterruptedException {
        // 创建MyCallable对象
        MyCallable mc = new MyCallable() ;
        // 创建FutureTask
        FutureTask ft = new FutureTask(mc) ;
        // 创建Thread对象
        Thread t1 = new Thread(ft) ;
        Thread t2 = new Thread(ft) ;
        // 调用start方法启动线程
        t1.start();
        // 调用ft的get方法获取执行结果
        String result = ft.get();
        // 输出
        System.out.println(result);
    }
}

线程池创建线程

public class MyExecutors implements Runnable{
    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors()) ;
        // 关闭线程池
        threadPool.shutdown();
    }
}

runnable 和 callable 有什么区别?

Runnable 接口run方法没有返回值

Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果

Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

线程的 run()和 start()有什么区别?

start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。

run(): 封装了要被线程执行的代码,可以被调用多次。

线程包括哪些状态,状态之间是如何变化的

public enum State {
    //尚未启动的线程的线程状态
    NEW,
    //可运行线程的线程状态。
    RUNNABLE,
    //线程阻塞等待监视器锁的线程状态。
    BLOCKED,     
    //等待线程的线程状态
    WAITING,
    //具有指定等待时间的等待线程的线程状态
    TIMED_WAITING,
    //已终止线程的线程状态。线程已完成执行
    TERMINATED;
}

Java后端开发面试题——多线程_第1张图片

新建 T1、T2、T3 三个线程,如何保证它们按顺序执行? 

t.join() 阻塞调用此方法的线程进入timed_waiting 直到线程t执行完成后,此线程再继续执行

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

notify()和 notifyAll()有什么区别?

notifyAll:唤醒所有wait的线程

notify:只随机唤醒一个 wait 线程

java中wait和sleep方法的不同?

共同点

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

不同点

1.方法归属不同 sleep(long) 是 Thread 的静态方法 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有

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

3. 锁特性不同(重点) wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制 wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用) 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

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

使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

使用stop方法强行终止(不推荐,方法已作废)

使用interrupt方法中断线程

        打断阻塞的线程( sleep,wait,join )的线程,线程会抛出InterruptedException异常

         打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字的底层原理

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

它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor

在monitor内部有三个属性,分别是owner、entrylist、waitset

其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

请谈谈你对 volatile 的理解

①保证线程间的可见性

② 禁止进行指令重排序

        写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下

        读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

volatile int x;
int y;
@Actor
public void actor1() {
    x = 1;
    y = 1;
}
@Actor
public void actor2(II_Result r) {
    r.r1 = y;
    r.r2 = x;
}

        写变量让volatile修饰的变量的在代码最后位置

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

什么是AQS?

Java后端开发面试题——多线程_第2张图片

是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的

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

在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源

在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性

新的线程与队列中的线程共同来抢资源,是非公平锁 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

ReentrantLock的实现原理

ReentrantLock表示支持重新进入的锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞

ReentrantLock主要利用CAS+AQS队列来实现

支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁

synchronized和Lock有什么区别 ?

语法层面

synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现

Lock 是接口,源码由 jdk 提供,用 java 语言实现

使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

功能层面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量

Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)

性能层面

在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖

在竞争激烈时,Lock 的实现通常会提供更好的性能

死锁产生的条件是什么?

一个线程需要同时获取多把锁,这时就容易发生死锁

如何进行死锁诊断?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack

jps:输出JVM中运行的进程状态信息

jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁      如果有死锁现象,需要查看具体代码分析后,可修复

可视化工具jconsole、VisualVM也可以检查死锁问题

聊一下ConcurrentHashMap

数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表 采用 CAS + Synchronized来保证并发安全进行实现

CAS控制数组节点的添加

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

Java程序中怎么保证多线程的执行安全

1.原子性     synchronized、lock

2.内存可见性   volatile、synchronized、lock

3.有序性     volatile

说一下线程池的核心参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

corePoolSize 核心线程数目

maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)

keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

Java后端开发面试题——多线程_第3张图片

1.AbortPolicy:直接抛出异常,默认策略;

2.CallerRunsPolicy:用调用者所在的线程来执行任务;

3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

4.DiscardPolicy:直接丢弃任务;

线程池中有哪些常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。

2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。

3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的

4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

Java后端开发面试题——多线程_第4张图片

如何确定核心线程数

IO密集型任务

一般来说:文件读写、DB读写、网络请求等

核心线程数大小设置为2N+1

CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

核心线程数大小设置为N+1

public static void main(String[] args) {
       //查看机器的CPU核数
    System.out.println(Runtime.getRuntime().availableProcessors());
}

参考回答:

① 高并发、任务执行时间短 ( CPU核数+1 ),减少线程上下文的切换

② 并发不高、任务执行时间长

IO密集型的任务  (CPU核数 * 2 + 1)

计算密集型任务 ( CPU核数+1 )

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)

线程池的种类有哪些

 创建使用固定线程数的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue());
}

核心线程数与最大线程数一样,没有救急线程

阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

适用于任务量已知,相对耗时的任务

单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue()));
}

核心线程数和最大线程数都是1

阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

适用于按照顺序执行的任务

可缓存线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue());
}

核心线程数为0 最大线程数是Integer.MAX_VALUE 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

适合任务数比较密集,但每个任务执行时间较短的情况

提供了“延迟”和“周期执行”功能的ThreadPoolExecutor

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler);
}

newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行

为什么不建议用Executors创建线程池

Java后端开发面试题——多线程_第5张图片

  线程池使用场景(CountDownLatch、Future)

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行) 其中构造参数用来初始化等待计数值 await() 用来等待计数归零 countDown() 用来让计数减一

Java后端开发面试题——多线程_第6张图片

批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM

数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能

异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间

如何控制某个方法允许并发访问线程的数量

// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            // 3. 获取许可
       semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            System.out.println("running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end...");
        } finally {
            // 4. 释放许可
       semaphore.release();
        }
    }).start();
}

Semaphore使用步骤

创建Semaphore对象,可以给一个容量

semaphore.acquire(): 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量) semaphore.release():释放一个信号量,此时信号量个数+1

谈谈你对ThreadLocal的理解

它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享

使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接

set(value) 设置值

get() 获取值

remove() 清除值

ThreadLocal内存泄漏问题

ThreadLocalMap 中的 key 是弱引用,值为强引用;

key 会被GC 释放内存,关联 value 的内存并不会释放。

建议主动 remove 释放 key,value

你可能感兴趣的:(java,开发语言)