Java——15个关于Java中多线程并发的面试题

简介: Java——15个关于Java中多线程并发的面试题

文章目录:


1.多线程的创建方式,你知道几种?

1.1 继承Thread类(重写run()方法)

1.2 实现Runnable接口(重写run()方法)

1.3 实现Callable接口(重写call()方法)

2.说说实现RunnableCallable这两个接口的区别?

3.说说synchronizedLock的区别?

4.synchronizedvolatile的区别?

5.说说waitsleep这两个方法的不同?

6.JVM对锁的优化了解吗?

7.什么是线程池,如何使用?

8.线程池的启动策略?

9.请说出同步线程及线程调度相关的方法?

10.什么是CAS

11.简单聊聊乐观锁与悲观锁

12.启动线程时,调用的是start方法还是run方法?

13.写一段简单的死锁代码

14.你是如何合理配置线程池大小的?

15.线程池的具体配置参数(七大参数 + 四种拒绝策略)

1.多线程的创建方式,你知道几种?


这个问题了话,其实已经说烂了,天天学,天天说,不就是那三种吗?继承Thread(重写run()方法)、实现Runnable接口(重写run()方法)、实现Callable接口(重写call()方法)。


1.1 继承Thread类(重写run()方法)

package com.szh.begin;
/**
 * 实现多线程的第一种方式:继承Thread类,重写run()方法
 */
public class Test01 {
    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
    public static void main(String[] args) {
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();
        t1.setName("t1");
        t2.setName("t2");
        //start方法的作用是:启动一个分支线程,在JVW中开辟一个新的栈空间
        //只要栈空间开辟出来,start方法就结束了,线程就启动成功了,启动成功的线程会自动调用run方法
        //run方法在分支线程的栈底部,main方法在主线程的栈底部,run和main是平级的
        t1.start();
        t2.start();
    }
}

如果你觉得上面main方法中代码写的比较多,你也可以修改为下面这种方式:👇👇👇

package com.szh.begin;
/**
 * 实现多线程的第一种方式:继承Thread类,重写run()方法
 */
public class Test01 {
    static class MyThread extends Thread {
        public MyThread(String name) {
            super();
        }
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
    public static void main(String[] args) {
        new MyThread("t1").start();
        new MyThread("t2").start();
    }
}

1.2 实现Runnable接口(重写run()方法)

传统写法

package com.szh.begin;
/**
 * 实现多线程的第二种方式:实现Runnable接口,重写run()方法
 * 传统写法
 */
public class Test02 {
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new MyRunnable(),"t1").start();
        new Thread(new MyRunnable(),"t2").start();
    }
}

匿名内部类写法

package com.szh.begin;
/**
 * 实现多线程的第二种方式:实现Runnable接口,重写run()方法
 * 匿名内部类写法
 */
public class Test03 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " ---> " + i);
                }
            }
        },"t1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " ---> " + i);
                }
            }
        },"t2").start();
    }
}

1.3 实现Callable接口(重写call()方法)

package com.szh.begin;
import java.util.concurrent.*;
/**
 * 实现多线程的第三种方式:实现Callable接口,重写call()方法
 * 使用Future接收线程的执行结果
 */
public class Test04 {
    static class MyCallable implements Callable<Object> {
        @Override
        public Object call() throws Exception {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
            return "当前执行线程的id为:" + Thread.currentThread().getId();
        }
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个固定大小的线程池
        ExecutorService service= Executors.newFixedThreadPool(2);
        //提交执行
        Future<Object> future1=service.submit(new MyCallable());
        Future<Object> future2=service.submit(new MyCallable());
        //获取call()方法的返回值(即线程执行结果的返回值)
        Object obj1=future1.get();
        Object obj2=future2.get();
        //打印
        System.out.println(obj1);
        System.out.println(obj2);
        //关闭线程池
        service.shutdown();
    }
}

2.说说实现RunnableCallable这两个接口的区别?


·       实现Runnable接口时,重写的是run()方法,该方法不能抛出异常;而实现Callable接口时,重写的是call()方法,该方法可以抛出checked exception编译时异常(受检异常)。

·       实现Runnable接口无法获取线程的执行结果;而实现Callable接口可以通过get()方法获取线程的执行结果。

·       实现Callable接口可以返回一个泛型<V>;而实现Runnable接口不可以。

3.说说synchronizedLock的区别?


·       synchronized是一个关键字(一般称内部锁);Lock是一个接口(一般称显示锁)。

·       synchronized不能获得锁的状态;Lock可以。

·       synchronized会自动释放锁,不需要手动释放锁;Lock必须通过unlock()方法手动释放锁。

·       synchronized在发生异常时,会自动释放占有的锁对象,因此不会造成死锁现象;Lock在发生异常时,如果没有主动调用unlock()方法释放占有的锁对象了话,则可能造成死锁现象,因此使用Lock时一般在finally代码块中释放锁对象。


最后简单聊一聊synchronized的原理:


synchronizedJava提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorentermonitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。

执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。

执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。


synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。

4.synchronized和volatile的区别?


·       一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。   禁止进行指令重排序(就是在不影响单线程程序执行结果的情况下进行最优执行排序)。

·       volatile本质上是在告诉JVM,当前共享变量的值在该线程的工作内存中是不确定的,需要从主内存中读取该共享变量的值;synchronized则是锁定当前共享变量,此时只能有一个线程访问它,其他线程会被阻塞。

·       volatile仅能用来修饰变量;synchronized可以用来修饰变量、方法。

·       volatile仅能保证对变量的修改可见性,不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。

·       volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

·       volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

5.说说waitsleep这两个方法的不同?


·       wait方法是Object类中的,是每一个对象都具有的;sleep方法是Thread这个线程类的。

·       执行wait方法之后,当前线程会立刻释放锁对象;而执行sleep方法之后,当前线程仅仅暂停执行,并不会释放锁对象。

·       调用这两个方法时,程序都会产生编译时异常(InterruptedException)。

·       sleep方法不一定非要定义在同步代码中;wait方法必须定义在同步代码中。

6.JVM对锁的优化了解吗?


·       锁偏向:锁偏向是针对加锁操作的一种优化。如果一个线程获得了锁,那么这个锁就进入了偏向模式(偏向这个线程)。当这个线程再次请求锁时,无需进行任何同步操作,这样可以节省有关锁申请的时间,提升了程序性能。

·       轻量级锁:对于锁竞争比较激烈的场景,每次都是不同的线程来请求锁,那么上面说的偏向锁就失效了。如果锁偏向失败,JVM不会立即挂起线程,而是使用另一种优化手段:轻量级锁。  当第一个线程尝试获取锁时,这个锁会进入偏向模式(偏向这个线程),这个线程在修改锁对象头成为偏向锁时使用CAS操作,将锁对象头中的threadId改成自己的id,之后再访问锁对象时,只需要对比id即可。一旦有第二个线程来获取锁对象,因为偏向锁不会主动释放,所以此时就存在竞争了,JVM会检查原来持有锁对象的线程是否存活,如果不存活,则锁重新偏向新的线程;如果存活,则执行原来线程的栈,检查该锁对象的使用情况,如果仍然需要偏向锁,则此时偏向锁升级为轻量级锁。

·       重量级锁:轻量级锁认为竞争存在,但竞争的程度较轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定次数,A线程持有锁、B线程在自旋、又来了一个C线程,那么此时轻量级锁就会膨胀为重量级锁。重量级锁除了持有锁的线程外,其余线程全部阻塞。

7.什么是线程池,如何使用?


线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。

JDKjava.util.concurrent.Executors 这个工具类中提供了创建线程池的多种静态方法。 

1.  //创建一个可缓存的线程池,此线程池不会对大小做限制,其大小完全依赖于操作系统(或者说JVM

2.  ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

3.   

4.  //创建一个固定大小的线程池,每次提交一个任务就创建一个线程,直到达到线程池的最大大小

5.  ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);

6.   

7.  //创建一个固定大小的线程池,该线程池支持定时周期的执行某些任务

8.  ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);

9.   

10. //创建一个单线程的线程池,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

11. ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

8.线程池的启动策略?


1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。


2、当调用execute()方法添加一个任务时,线程池会做如下判断:

1)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

3)如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务;

4)如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者我不能再接受任务了

5)当一个线程完成任务时,它会从队列中取下一个任务来执行。

6)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

9.请说出同步线程及线程调度相关的方法?


·       wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

·       sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;

·       notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;

·       notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

·       注意:java 5通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。

10.什么是CAS


CASCompare And Swap)比较与交换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要与volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。


但是在CAS中会存在一个ABA问题,就是说假如我们期望的count变量值为100,之后有一个A线程访问它将其修改为了200,后来又有一个B线程访问它将其修改回了100,那么是否就认为count变量的值没有被其他线程更新呢?显然不是啊,它明显的被AB两个线程更新过。这个共享变量count就经历了 A → B → A 过程。


如果想要规避ABA问题,可以使用原子变量类中的AtomicStampedReference,它可以为共享变量引入一个修订号(或者叫时间戳),每次修改共享变量时,相应的修订号就会增加1ABA问题的过程就转变为:[A0] → [B1] → [A2] ,每次修改共享变量都会导致修订号的增加,通过修订号就可以准确的判断共享变量是否被其他线程修改过。

11.简单聊聊乐观锁与悲观锁


·       乐观锁:乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

·       悲观锁:就很悲观了,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

12.启动线程时,调用的是start方法还是run方法?


·       start方法:作用是启动一个新的分支线程,在JVM中开辟一个新的栈空间,只有栈空间开辟出来,start方法就结束了,同时该分支线程也就启动成功了。启动成功的线程会执行它的run方法。

·       run方法:就是普通的方法调用,不会启动新的线程。

13.写一段简单的死锁代码


package com.szh.test;
/**
 *
 */
public class DeadLock {
    private static final Object OBJ1=new Object();
    private static final Object OBJ2=new Object();
    static class MyThread implements Runnable {
        @Override
        public void run() {
            if ("a".equals(Thread.currentThread().getName())) {
                synchronized (OBJ1) {
                    System.out.println(Thread.currentThread().getName() + " 获得了OBJ1锁,还需要获得OBJ2锁...");
                    synchronized (OBJ2) {
                        System.out.println(Thread.currentThread().getName() + " 已经获得了OBJ1、OBJ2这两把锁");
                    }
                }
            }
            if ("b".equals(Thread.currentThread().getName())) {
                synchronized (OBJ2) {
                    System.out.println(Thread.currentThread().getName() + " 获得了OBJ2锁,还需要获得OBJ1锁...");
                    synchronized (OBJ1) {
                        System.out.println(Thread.currentThread().getName() + " 已经获得了OBJ1、OBJ2这两把锁");
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new MyThread(),"a").start();
        new Thread(new MyThread(),"b").start();
    }
}

14.你是如何合理配置线程池大小的?


首先,需要考虑到线程池所进行的工作的性质:IO密集型?CPU密集型?

简单的分析来看,如果是CPU密集型的任务,我们应该设置数目较小的线程数,比如CPU数目加1。如果是IO密集型的任务,则应该设置可能多的线程数,由于IO操作不占用CPU,所以,不能让CPU闲下来。当然,如果线程数目太多,那么线程切换所带来的开销又会对系统的响应时间带来影响。 

15.线程池的具体配置参数(七大参数 + 四种拒绝策略)


·       int corePoolSize 核心线程池大小

·       int maximumPoolSize 最大核心线程池大小

·       long keepAliveTime 超时存活时间

·       TimeUnit unit 超时单位

·       BlockingQueue workQueue 阻塞队列

·       ThreadFactory threadFactory 线程工厂,用于创建线程

·       RejectedExecutionHandler handler 拒绝策略

·       AbortPolicy 策略,会抛出异常

·       CallerRunsPolicy 策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务

·       DiscardOldestPolicy 策略,将任务队列中最老的任务丢弃,尝试再次提交新任务

·       DiscardPolicy 策略,直接丢弃这个无法处理的任务

相关文章
|
1天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
3天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
3天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
4天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
20 3
|
4天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
42 2
|
12天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
43 6
|
20天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
20天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
43 3
|
21天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
1天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法