面试题004-Java-Java多线程(下)

面试题004-Java-Java多线程(下)

题目自测

  • 1. synchronized 关键字的作用?
  • 2. volatile 关键字的作用?
  • 3. synchronized 和 volatile 的区别?
  • 4. synchronized 和 ReentrantLock 的区别?
  • 5. ThreadLocal有什么用?
  • 6. 线程池有什么用?为什么不推荐使用内置线程池?
  • 7. 如何自定义线程池?
  • 8. Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
  • 9. 线程池处理任务的流程了解吗?
  • 10. 如何给线程池命名?为什么建议给线程池命名?

题目答案

1. synchronized 关键字的作用?

答:在Java中synchronized关键字用于实现线程同步,主要解决是多个线程之间访问相同资源不会发生数据不一致的问题。它可以作用于方法或代码块上,以保证同一时间只有一个线程可以执行被同步的方法或代码块。

  • 同步实例方法: 表示该方法在同一时间只能由一个线程访问同一个实例。

    public class SynchronizedExample {
        public synchronized void synchronizedMethod() {
            // 同步代码
        }
    }
    
  • 同步静态方法:表示该方法在同一时间只能由一个线程访问同一个类的所有实例。

    public class SynchronizedExample {
        public static synchronized void synchronizedStaticMethod() {
            // 同步代码
        }
    }
    
  • 同步代码块:表示该代码块在同一时间只能由一个线程访问该对象。

    public class SynchronizedExample {
        private final Object lock = new Object();
    
        public void synchronizedBlock() {
            synchronized (this) {
                // 同步代码
            }
        }
    }
    

2. volatile 关键字的作用?

答:在Java中volatile关键字用于声明变量的可见性和防止指令重排,从而提供一种轻量级的同步机制。
在可见性方面,用volatile修饰的变量,来确保一个线程对该变量的修改对其他线程立即可见。
在防止指令重排方面,volatile会禁止JVM对变量操作的指令进行重新排序。

3. synchronized 和 volatile 的区别?

答:synchronized和volatile两个关键字都用于实现线程同步的机制。

  • volatile是线程同步的轻量级实现,volatile性能比synchronized要好。volatile关键字只能用于变量,而synchronized关键字可以修饰方法和代码块。
  • volatile能保证数据的可见性,但不能保证数据的原子性。synchronized两者都能保证。
  • volatile主要解决变量在多个线程之间的可见性,而synchronize解决的是多个线程之间访问资源的同步性。

4. synchronized 和 ReentrantLock 的区别?

答:synchronized和ReentrantLock都是Java中用于实现线程同步的机制,并且都是可重入锁。

  • synchronized:
    • 是Java语言的内置关键字,用于对代码块或方法进行同步。
    • 简单易用,直接在方法或代码块上使用即可。
    • 由JVM实现,使用方便,但功能较为有限。
  • ReentrantLock:
    • ReentrantLock是java.util.concurrent.locks包中的类,提供了更灵活和丰富的锁机制(响应中断、尝试获取锁、使用条件变量等)。
    • 需要显示的加锁和解锁,通过代码来控制锁的获取和释放。
    • 由Java库实现,功能强大,灵活性高。

5. ThreadLocal有什么用?

答:ThreadLocal在Java中用于创建线程局部变量,确保每个变量都有自己的独立副本变量,从而避免多线程共享一个变量带来的线程安全问题,同时提高了并发性能。然而在使用ThreadLocal时需要注意内存泄露问题,由于ThreadLocal变量的生命周期与线程线相同,如果线程池中线程长时间不被销毁,而ThreadLocal变量没有被正确移除,可能会导致内存泄露问题,因此建议在不再使用ThreadLocal变量时显示调用remove()方法。
它的使用场景有用户会话管理、数据库连接管理、事物管理等。

6. 线程池有什么用?为什么不推荐使用内置线程池?

答:Java中的线程池是一种基于池化技术设计用于执行异步任务的框架,它维护了一定数量的线程,避免频繁地创建和销毁线程带来的性能开销和资源浪费。他的主要作用是提高资源复用、提高系统稳定性、便于管理和提供灵活的并发策略。

不推荐使用内置线程池的主要原因是内置线程池等配置选项有限、不能满足所有的应用场景的需求。Java库中提供了几种预定义线程的实现,如Executors类中的newFixedThreadPoolnewCacheThreadPoolnewSingleThreadExecutor等。

  • FixedThreadPool 和 SingleThreadExecutor 使用无界队列(LinkedBlockingQueue),在任务提交速度超过执行速度时,任务队列可能无限增长,导致内存耗尽。
  • CacheThreadPool 使用无界线程池,在高并发下可能会创建大量线程、导致系统资源耗尽。未被回收的线程可能会长时间占用资源,造成线程泄漏问题。

7. 如何自定义线程池?

答:在Java中,自定义线程池可以通过ThreadPoolExecutor类来实现。ThreadPoolExecutor提供了丰富的配置选项,如核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、任务队列(workQueue)和拒绝策略(rejectedExecutionHandler)等,可以根据具体需求进行灵活配置。

import java.util.concurrent.ArrayBlockingQueue;  
import java.util.concurrent.ThreadPoolExecutor;  
import java.util.concurrent.TimeUnit;  
  
public class CustomThreadPoolExample {  
  
    public static void main(String[] args) {  
        // 核心线程数 :线程池在空闲时保留的线程数,即使没有任务需要处理。
        int corePoolSize = 5;  
        // 最大线程数 :线程池允许创建的最大线程数。
        int maximumPoolSize = 10;  
        // 非核心线程空闲存活时间 :当线程数超过核心线程数时,多余的空闲线程的存活时间。
        long keepAliveTime = 1L;  
        // 时间单位 : 空闲线程存活时间的时间单位。
        TimeUnit unit = TimeUnit.SECONDS;  
        // 任务队列 :用于保存等待执行任务的队列。
        ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);  
        // 线程工厂 : 用于创建新线程。
        ThreadFactory threadFactory = Executors.defaultThreadFactory();  
        // 拒绝策略 : 当线程池和队列都满时,如何处理新任务
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();  
  
        // 创建ThreadPoolExecutor  
        ThreadPoolExecutor executor = new ThreadPoolExecutor(  
                corePoolSize,  
                maximumPoolSize,  
                keepAliveTime,  
                unit,  
                workQueue,  
                threadFactory,  
                handler  
        );  
  
        // 提交任务  
        for (int i = 0; i < 15; i++) {  
            int taskId = i;  
            executor.execute(() -> {  
                System.out.println(Thread.currentThread().getName() + " is processing " + taskId);  
                try {  
                    // 模拟任务执行时间  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            });  
        }  
  
        // 关闭线程池(不再接受新任务,但已提交的任务会继续执行)  
        executor.shutdown();  
  
        // 等待所有任务完成  
        while (!executor.isTerminated()) {  
            // 等待一段时间  
        }  
  
        System.out.println("All tasks completed.");  
    }  
}

8. Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?

答:
线程池的参数

  • corePoolSize(核心线程数):线程池中始终保留的线程数量,即使这些线程处于空闲状态。
  • maximumPoolSize(最大线程数):线程池中允许创建的最大线程数量。当任务队列已满且当前线程数小于最大线程数时,会创建新线程来处理任务。
  • keepAliveTime(空闲线程存活时间):当线程池中的线程数超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
  • timeUnit(时间单位):keepAliveTime 参数的时间单位。常见值包括 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
  • workQueue(任务队列):用于保存等待执行任务的队列l。
  • threadFactory(线程工厂):用于创建新线程。
  • rejectedExecutionHandler(拒绝策略):当线程池和队列都满时,如何处理新任务。

阻塞队列

  • ArrayBlockingQueue:一个基于数组的有界阻塞队列。按FIFO(先进先出)顺序保存任务。
  • LinkedBlockingQueue:一个基于链表的可选有界阻塞队列。通常用于无限制的任务队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待一个相应的移除操作。
  • PriorityBlockingQueue:一个基于优先级的无限阻塞队列。任务按照优先级顺序执行。
  • DelayQueue:一个基于优先级队列的无界阻塞队列,只有在延迟期满时才能从队列中取走元素。

拒绝策略

  • AbortPolicy(默认策略):抛出 RejectedExecutionException 异常,阻止系统正常工作。
  • CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。这种策略会降低新任务的提交速度,从而减轻线程池的负载。
  • DiscardPolicy:直接丢弃任务,不抛出异常。如果允许任务丢失,这种策略可以用于避免系统过载。
  • DiscardOldestPolicy:丢弃队列中最旧的任务(即即将执行的任务),然后重新尝试提交新任务。

9. 线程池处理任务的流程了解吗?

答:线程池处理任务的流程可以总结为:提交任务、检查核心线程数、任务队列处理、非核心线程创建、执行任务以及线程回收。

  1. 通过submit()或execute()方法提交任务
  2. 检查核心线程数,如果当前线程数少于核心线程数,那么就创建新的核心线程来执行任务。如果当前线程数已达到核心线程数,将任务放入任务队列。
  3. 如果任务队列没有满,将任务队列继续放入队列。如果任务队列已满,检查当前线程数是否小于最大线程数。
  4. 如果当前线程数小于最大线程数,创建新的非核心线程执行任务,如果当前线程已经达到了最大线程数,执行拒绝策略。
  5. 线程从任务队列取出任务执行。
  6. 线程池中的非核心线程在完成任务后不会立即销毁,进入保持存活状态,只有当这些线程在空闲时间超过keepAliveTime后被回收。

10. 如何给线程池命名?为什么建议给线程池命名?

答:要给线程池中的线程命名,可以自定义一个 ThreadFactory,在创建线程时设置线程名称。

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程工厂,它设置线程名称,有利于我们定位问题。
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final String name;

    /**
     * 创建一个带名字的线程池生产工厂
     */
    public NamingThreadFactory(String name) {
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
        return t;
    }
}

在 Java 中,为线程池中的线程命名是一种良好的实践。命名线程有助于调试和监控,使得可以轻松识别和跟踪线程的行为和状态。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值