Java面试总结(经典题)(Java多线程)(一)

发布于:2025-07-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

Java多线程

线程池的原理,为什么要创建线程池?创建线程池的方式;
线程的生命周期,什么时候会出现僵死进程;
说说线程安全问题,什么实现线程安全,如何实现线程安全;
创建线程池有哪几个核心参数? 如何合理配置线程池的大小?
volatile、ThreadLocal的使用场景和原理;
ThreadLocal什么时候会出现OOM的情况?为什么?
synchronized、volatile区别、synchronized锁粒度、模拟死锁场景、原子性与可见性;

JVM相关

JVM内存模型,GC机制和原理;
GC分哪两种,Minor GC 和Full GC有什么区别?什么时候会触发Full GC?分别采用什么算法?
JVM里的有几种classloader,为什么会有多种?
什么是双亲委派机制?介绍一些运作过程,双亲委派模型的好处;
什么情况下我们需要破坏双亲委派模型;
常见的JVM调优方法有哪些?可以具体到调整哪个参数,调成什么值?
JVM虚拟机内存划分、类加载器、垃圾收集算法、垃圾收集器、class文件结构是如何解析的;

Java扩展篇

红黑树的实现原理和应用场景;
NIO是什么?适用于何种场景?
Java9比Java8改进了什么;
HashMap内部的数据结构是什么?底层是怎么实现的?(还可能会延伸考察
ConcurrentHashMap与HashMap、HashTable等,考察对技术细节的深入了解程度);
说说反射的用途及实现,反射是不是很慢,我们在项目中是否要避免使用反射;
说说自定义注解的场景及实现;
List 和 Map 区别,Arraylist 与 LinkedList 区别,ArrayList 与 Vector 区别;

Spring相关

Spring AOP的实现原理和场景?
Spring bean的作用域和生命周期;
Spring Boot比Spring做了哪些改进? Spring 5比Spring4做了哪些改进;
如何自定义一个Spring Boot Starter?
Spring IOC是什么?优点是什么?
SpringMVC、动态代理、反射、AOP原理、事务隔离级别;

中间件篇

Dubbo完整的一次调用链路介绍;
Dubbo支持几种负载均衡策略?
Dubbo Provider服务提供者要控制执行并发请求上限,具体怎么做?
Dubbo启动的时候支持几种配置方式?
了解几种消息中间件产品?各产品的优缺点介绍;
消息中间件如何保证消息的一致性和如何进行消息的重试机制?
Spring Cloud熔断机制介绍;
Spring Cloud对比下Dubbo,什么场景下该使用Spring Cloud?

数据库篇

锁机制介绍:行锁、表锁、排他锁、共享锁;
乐观锁的业务场景及实现方式;
事务介绍,分布式事物的理解,常见的解决方案有哪些,什么事两阶段提交、三阶段提交;
MySQL记录binlog的方式主要包括三种模式?每种模式的优缺点是什么?
MySQL锁,悲观锁、乐观锁、排它锁、共享锁、表级锁、行级锁;
分布式事务的原理2阶段提交,同步异步阻塞非阻塞;
数据库事务隔离级别,MySQL默认的隔离级别、Spring如何实现事务、JDBC如何实现事务、嵌
套事务实现、分布式事务实现;
SQL的整个解析、执行过程原理、SQL行转列;

Redis

Redis为什么这么快?redis采用多线程会有哪些问题?
Redis支持哪几种数据结构;
Redis跳跃表的问题;
Redis单进程单线程的Redis如何能够高并发?
Redis如何使用Redis实现分布式锁?
Redis分布式锁操作的原子性,Redis内部是如何实现的?

源码系列

看过哪些源代码?

Java多线程

1、线程池的原理,为什么要创建线程池?创建线程池的方式;

JAVA线程池原理详解一 :
线程池的优点:
1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

线程池的创建:

1 public ThreadPoolExecutor(int corePoolSize,
2                               int maximumPoolSize,
3                               long keepAliveTime,
4                               TimeUnit unit,
5                               BlockingQueue<Runnable> workQueue,
6                               RejectedExecutionHandler handler) 

corePoolSize:线程池核心线程数量

maximumPoolSize:线程池最大线程数量

keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

unit:存活时间的单位

workQueue:存放任务的队列

handler:超出线程范围和队列容量的任务的处理程序

线程池的实现原理:
提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

线程池的源码解读:
1、ThreadPoolExecutor的execute()方法

 1 public void execute(Runnable command) {
 2         if (command == null)
 3             throw new NullPointerException();
       //如果线程数大于等于基本线程数或者线程创建失败,将任务加入队列
 4         if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
          //线程池处于运行状态并且加入队列成功
 5             if (runState == RUNNING && workQueue.offer(command)) {
 6                 if (runState != RUNNING || poolSize == 0)
 7                     ensureQueuedTaskHandled(command);
 8             }
         //线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
 9             else if (!addIfUnderMaximumPoolSize(command))
           //创建线程失败,则采取阻塞处理的方式
10                 reject(command); // is shutdown or saturated
11         }
12     }

2、创建线程的方法:addIfUnderCorePoolSize(command)

 1 private boolean addIfUnderCorePoolSize(Runnable firstTask) {
 2         Thread t = null;
 3         final ReentrantLock mainLock = this.mainLock;
 4         mainLock.lock();
 5         try {
 6             if (poolSize < corePoolSize && runState == RUNNING)
 7                 t = addThread(firstTask);
 8         } finally {
 9             mainLock.unlock();
10         }
11         if (t == null)
12             return false;
13         t.start();
14         return true;
15     }

我们重点来看第7行:

 1 private Thread addThread(Runnable firstTask) {
 2         Worker w = new Worker(firstTask);
 3         Thread t = threadFactory.newThread(w);
 4         if (t != null) {
 5             w.thread = t;
 6             workers.add(w);
 7             int nt = ++poolSize;
 8             if (nt > largestPoolSize)
 9                 largestPoolSize = nt;
10         }
11         return t;
12     }

这里将线程封装成工作线程worker,并放入工作线程组里,worker类的方法run方法:

 public void run() {
            try {
                Runnable task = firstTask;
                firstTask = null;
                while (task != null || (task = getTask()) != null) {
                    runTask(task);
                    task = null;
                }
            } finally {
                workerDone(this);
            }
        }

worker在执行完任务后,还会通过getTask方法循环获取工作队里里的任务来执行。

我们通过一个程序来观察线程池的工作原理:

1、创建一个线程

 1 public class ThreadPoolTest implements Runnable
 2 {
 3     @Override
 4     public void run()
 5     {
 6         try
 7         {
 8             Thread.sleep(300);
 9         }
10         catch (InterruptedException e)
11         {
12             e.printStackTrace();
13         }
14     }
15 }

2、线程池循环运行16个线程:

 1 public static void main(String[] args)
 2     {
 3         LinkedBlockingQueue<Runnable> queue =
 4             new LinkedBlockingQueue<Runnable>(5);
 5         ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);
 6         for (int i = 0; i < 16 ; i++)
 7         {
 8             threadPool.execute(
 9                 new Thread(new ThreadPoolTest(), "Thread".concat(i + "")));
10             System.out.println("线程池中活跃的线程数: " + threadPool.getPoolSize());
11             if (queue.size() > 0)
12             {
13                 System.out.println("----------------队列中阻塞的线程数" + queue.size());
14             }
15         }
16         threadPool.shutdown();
17     }

执行结果:

线程池中活跃的线程数: 1
线程池中活跃的线程数: 2
线程池中活跃的线程数: 3
线程池中活跃的线程数: 4
线程池中活跃的线程数: 5
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数1
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数2
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数3
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数4
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 6
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 7
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 8
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 9
----------------队列中阻塞的线程数5
线程池中活跃的线程数: 10
----------------队列中阻塞的线程数5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@232204a1[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
    at test.ThreadTest.main(ThreadTest.java:17)

从结果可以观察出:

1、创建的线程池具体配置为:核心线程数量为5个;全部线程数量为10个;工作队列的长度为5。

2、我们通过queue.size()的方法来获取工作队列中的任务数。

3、运行原理:

  刚开始都是在创建新的线程,达到核心线程数量5个后,新的任务进来后不再创建新的线程,而是将任务加入工作队列,任务队列到达上线5个后,新的任务又会创建新的普通线程,直到达到线程池最大的线程数量10个,后面的任务则根据配置的饱和策略来处理。我们这里没有具体配置,使用的是默认的配置AbortPolicy:直接抛出异常。

当然,为了达到我需要的效果,上述线程处理的任务都是利用休眠导致线程没有释放!!!

RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:

1、AbortPolicy:直接抛出异常

2、CallerRunsPolicy:只用调用所在的线程运行任务

3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

4、DiscardPolicy:不处理,丢弃掉。

我们现在用第四种策略来处理上面的程序:

 1 public static void main(String[] args)
 2     {
 3         LinkedBlockingQueue<Runnable> queue =
 4             new LinkedBlockingQueue<Runnable>(3);
 5         RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
 6 
 7         ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, queue,handler);
 8         for (int i = 0; i < 9 ; i++)
 9         {
10             threadPool.execute(
11                 new Thread(new ThreadPoolTest(), "Thread".concat(i + "")));
12             System.out.println("线程池中活跃的线程数: " + threadPool.getPoolSize());
13             if (queue.size() > 0)
14             {
15                 System.out.println("----------------队列中阻塞的线程数" + queue.size());
16             }
17         }
18         threadPool.shutdown();
19     }

执行结果:

线程池中活跃的线程数: 1
线程池中活跃的线程数: 2
线程池中活跃的线程数: 2
----------------队列中阻塞的线程数1
线程池中活跃的线程数: 2
----------------队列中阻塞的线程数2
线程池中活跃的线程数: 2
----------------队列中阻塞的线程数3
线程池中活跃的线程数: 3
----------------队列中阻塞的线程数3
线程池中活跃的线程数: 4
----------------队列中阻塞的线程数3
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数3
线程池中活跃的线程数: 5
----------------队列中阻塞的线程数3

这里采用了丢弃策略后,就没有再抛出异常,而是直接丢弃。在某些重要的场景下,可以采用记录日志或者存储到数据库中,而不应该直接丢弃。

设置策略有两种方式:

1RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, queue,handler);
2ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, queue);
  threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

JAVA线程池原理详解二:
Executor框架的两级调度模型:
在HotSpot VM的模型中,JAVA线程被一对一映射为本地操作系统线程。JAVA线程启动时会创建一个本地操作系统线程,当JAVA线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程并将它们分配给可用的CPU。

在上层,JAVA程序会将应用分解为多个任务,然后使用应用级的调度器(Executor)将这些任务映射成固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

Executor框架类图:
在这里插入图片描述

JAVA线程既是工作单元,也是执行机制。而在Executor框架中,我们将工作单元与执行机制分离开来。Runnable和Callable是工作单元(也就是俗称的任务),而执行机制由Executor来提供。这样一来Executor是基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相当于消费者。

1、从类图上看,Executor接口是异步任务执行框架的基础,该框架能够支持多种不同类型的任务执行策略。

public interface Executor {

    void execute(Runnable command);
}

Executor接口就提供了一个执行方法,任务是Runnbale类型,不支持Callable类型。

2、ExecutorService接口实现了Executor接口,主要提供了关闭线程池和submit方法:

public interface ExecutorService extends Executor {

    List<Runnable> shutdownNow();


    boolean isTerminated();


    <T> Future<T> submit(Callable<T> task);

 }

另外该接口有两个重要的实现类:ThreadPoolExecutor与ScheduledThreadPoolExecutor。

其中ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务;而ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行任务,或者定期执行命令。

之前是使用ThreadPoolExecutor来通过给定不同的参数从而创建自己所需的线程池,但是在后面的工作中不建议这种方式,推荐使用Exectuors工厂方法来创建线程池

这里先来区别线程池和线程组(ThreadGroup与ThreadPoolExecutor)这两个概念:

a、线程组就表示一个线程的集合。

b、线程池是为线程的生命周期开销问题和资源不足问题提供解决方案,主要是用来管理线程。

Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadExecutor和CachedThreadPool

a、SingleThreadExecutor:单线程线程池

ExecutorService threadPool = Executors.newSingleThreadExecutor();

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

我们从源码来看可以知道,单线程线程池的创建也是通过ThreadPoolExecutor,里面的核心线程数和线程数都是1,并且工作队列使用的是无界队列。由于是单线程工作,每次只能处理一个任务,所以后面所有的任务都被阻塞在工作队列中,只能一个个任务执行。

b、FixedThreadExecutor:固定大小线程池

ExecutorService threadPool = Executors.newFixedThreadPool(5);
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

这个与单线程类似,只是创建了固定大小的线程数量。

c、CachedThreadPool:无界线程池

ExecutorService threadPool = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

无界线程池意味着没有工作队列,任务进来就执行,线程数量不够就创建,与前面两个的区别是:空闲的线程会被回收掉,空闲的时间是60s。这个适用于执行很多短期异步的小程序或者负载较轻的服务器。

Callable、Future、FutureTash详解:
Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接口的类与实现Runnable的类都是可以被线程执行的任务。

三者之间的关系:

Callable是Runnable封装的异步运算任务。

Future用来保存Callable异步运算的结果

FutureTask封装Future的实体类

1、Callable与Runnbale的区别

a、Callable定义的方法是call,而Runnable定义的方法是run。

b、call方法有返回值,而run方法是没有返回值的。

c、call方法可以抛出异常,而run方法不能抛出异常。

2、Future

Future表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结果

 1 public interface Future<V> {
 2 
 3     boolean cancel(boolean mayInterruptIfRunning);
 4 
 5     boolean isCancelled();
 6 
 7     boolean isDone();
 8 
 9     V get() throws InterruptedException, ExecutionException;
10 
11     V get(long timeout, TimeUnit unit)
12         throws InterruptedException, ExecutionException, TimeoutException;
13 }

3、FutureTask

可取消的异步计算,此类提供了对Future的基本实现,仅在计算完成时才能获取结果,如果计算尚未完成,则阻塞get方法。

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>

FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。

Callable与FutureTask:
定义一个callable的任务:

 1 public class MyCallableTask implements Callable<Integer>
 2 {
 3     @Override
 4     public Integer call()
 5         throws Exception
 6     {
 7         System.out.println("callable do somothing");
 8         Thread.sleep(5000);
 9         return new Random().nextInt(100);
10     }
11 }
 1 public class CallableTest
 2 {
 3     public static void main(String[] args) throws Exception
 4     {
 5         Callable<Integer> callable = new MyCallableTask();
 6         FutureTask<Integer> future = new FutureTask<Integer>(callable);
 7         Thread thread = new Thread(future);
 8         thread.start();
 9         Thread.sleep(100);
10         //尝试取消对此任务的执行
11         future.cancel(true);
12         //判断是否在任务正常完成前取消
13         System.out.println("future is cancel:" + future.isCancelled());
14         if(!future.isCancelled())
15         {
16             System.out.println("future is cancelled");
17         }
18         //判断任务是否已完成
19         System.out.println("future is done:" + future.isDone());
20         if(!future.isDone())
21         {
22             System.out.println("future get=" + future.get());
23         }
24         else
25         {
26             //任务已完成
27             System.out.println("task is done");
28         }
29     }
30 }

执行结果:

callable do somothing
future is cancel:true
future is done:true
task is done

这个DEMO主要是通过调用FutureTask的状态设置的方法,演示了状态的变迁。

a、第11行,尝试取消对任务的执行,该方法如果由于任务已完成、已取消则返回false,如果能够取消还未完成的任务,则返回true,该DEMO中由于任务还在休眠状态,所以可以取消成功。
future.cancel(true);

b、第13行,判断任务取消是否成功:如果在任务正常完成前将其取消,则返回true
System.out.println(“future is cancel:” + future.isCancelled());

c、第19行,判断任务是否完成:如果任务完成,则返回true,以下几种情况都属于任务完成:正常终止、异常或者取消而完成。
我们的DEMO中,任务是由于取消而导致完成。
System.out.println(“future is done:” + future.isDone());

d、在第22行,获取异步线程执行的结果,我这个DEMO中没有执行到这里,需要注意的是,future.get方法会阻塞当前线程, 直到任务执行完成返回结果为止。
System.out.println(“future get=” + future.get());
Callable与Future

public class CallableThread implements Callable<String>
{
    @Override
    public String call()
        throws Exception
    {
        System.out.println("进入Call方法,开始休眠,休眠时间为:" + System.currentTimeMillis());
        Thread.sleep(10000);
        return "今天停电";
    }
    
    public static void main(String[] args) throws Exception
    {
        ExecutorService es = Executors.newSingleThreadExecutor();
        Callable<String> call = new CallableThread();
        Future<String> fu = es.submit(call);
        es.shutdown();
        Thread.sleep(5000);
        System.out.println("主线程休眠5秒,当前时间" + System.currentTimeMillis());
        String str = fu.get();
        System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" + System.currentTimeMillis());
    }
}

执行结果:

进入Call方法,开始休眠,休眠时间为:1478606602676
主线程休眠5秒,当前时间1478606608676
Future已拿到数据,str=今天停电;当前时间为:1478606612677
这里的future是直接扔到线程池里面去执行的。由于要打印任务的执行结果,所以从执行结果来看,主线程虽然休眠了5s,但是从Call方法执行到拿到任务的结果,这中间的时间差正好是10s,说明get方法会阻塞当前线程直到任务完成。

通过FutureTask也可以达到同样的效果:

public static void main(String[] args) throws Exception
    {
      ExecutorService es = Executors.newSingleThreadExecutor();
      Callable<String> call = new CallableThread();
      FutureTask<String> task = new FutureTask<String>(call);
      es.submit(task);
      es.shutdown();
      Thread.sleep(5000);
      System.out.println("主线程等待5秒,当前时间为:" + System.currentTimeMillis());
      String str = task.get();
      System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" + System.currentTimeMillis());
    }

以上的组合可以给我们带来这样的一些变化:

如有一种场景中,方法A返回一个数据需要10s,A方法后面的代码运行需要20s,但是这20s的执行过程中,只有后面10s依赖于方法A执行的结果。如果与以往一样采用同步的方式,势必会有10s的时间被浪费,如果采用前面两种组合,则效率会提高:

1、先把A方法的内容放到Callable实现类的call()方法中

2、在主线程中通过线程池执行A任务

3、执行后面方法中10秒不依赖方法A运行结果的代码

4、获取方法A的运行结果,执行后面方法中10秒依赖方法A运行结果的代码

这样代码执行效率一下子就提高了,程序不必卡在A方法处。

创建线程池的几种方式:
ThreadPoolExecutor、ThreadScheduledExecutor、ForkJoinPool

在这里插入图片描述

2、线程的生命周期,什么时候会出现僵死进程;

在这里插入图片描述
僵死进程是指子进程退出时,父进程并未对其发出的SIGCHLD信号进行适当处理,导致子进程停留在僵死状态等待其父进程为其收尸,这个状态下的子进程就是僵死进程。

3、说说线程安全问题,什么是线程安全,如何实现线程安全;

线程安全 - 如果线程执行过程中不会产生共享资源的冲突,则线程安全。
线程不安全 - 如果有多个线程同时在操作主内存中的变量,则线程不安全
实现线程安全的三种方式
1)互斥同步
临界区:syncronized、ReentrantLock
信号量 semaphore
互斥量 mutex
2)非阻塞同步
CAS(Compare And Swap)
3)无同步方案
可重入代码
使用Threadlocal 类来包装共享变量,做到每个线程有自己的copy
线程本地存储

4、创建线程池有哪几个核心参数? 如何合理配置线程池的大小?

1)核心参数

public ThreadPoolExecutor(int corePoolSize, // 核心线程数量大小
int maximumPoolSize, // 线程池最大容纳线程数
long keepAliveTime, // 线程空闲后的存活时长
TimeUnit unit,
//缓存异步任务的队列 //用来构造线程池里的worker线程
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
//线程池任务满载后采取的任务拒绝策略
RejectedExecutionHandler handler)
  1. 核心说明
    1、当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
    2、当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池中的核心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取任务并处理。
    3 、当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目达到 maximumPoolSize(最大线程数量设置值)。
    4、如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任务拒绝处理。

3)线程池大小分配
线程池究竟设置多大要看你的线程池执行的什么任务了,CPU密集型、IO密集型、混合型,任务类型不同,设置的方式也不一样。
任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。

3.1)CPU密集型
尽量使用较小的线程池,一般Cpu核心数+1
3.2)IO密集型
方法一:可以使用较大的线程池,一般CPU核心数 * 2
方法二:(线程等待时间与线程CPU时间之比 + 1)* CPU数目
3.3)混合型
可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定

5、volatile、ThreadLocal的使用场景和原理;

volatile原理
volatile变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓
存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其
后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内
存屏障这句指令时,在它前面的操作已经全部完成。

在这里插入图片描述
volatile的适用场景:
1)状态标志,如:初始化或请求停机
2)一次性安全发布,如:单列模式
3)独立观察,如:定期更新某个值
4)“volatile bean” 模式
5) 开销较低的“读-写锁”策略,如:计数器

ThreadLocal原理:
ThreadLocal是用来维护本线程的变量的,并不能解决共享变量的并发问题。ThreadLocal是各线程将值存入该线程的map中,以ThreadLocal自身作为key,需要用时获得的是该线程之前存入的值。如果存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。

在这里插入图片描述

ThreadLocal的适用场景:
场景:数据库连接、Session管理

6、ThreadLocal什么时候会出现OOM的情况?为什么?

ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap,Thread调用exit方法如下:
在这里插入图片描述
ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。

7、synchronized、volatile区别

  1. volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作。

  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢synchronized锁对象时,会出现阻塞。

  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到执行。

  4. volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。

8、synchronized锁粒度、模拟死锁场景;

synchronized:具有原子性,有序性和可见性

1.三大性质简介
在并发编程中分析线程安全的问题时往往需要切入点,那就是两大核心:JMM抽象内存模型以及happens-before规则,三条性质:原子性,有序性和可见性。关于synchronized和volatile已经讨论过了,就想着将并发编程中这两大神器在 原子性,有性和可见性上做一个比较,当然这也是面试中的高频考点,值得注意。

2.原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象:

int a = 10; //1
a++; //2
int b=a; //3
a = a+1; //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,java内存模型中定义了8中操作都是原子的,不可再分的。

lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。那么如何理解这些指令了?比如,把一个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行store,write操作。注意的是:java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的。也就是说read和load之间可以插入其他指令,store和writer可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:read a,read b, load b,load a

由原子性变量操作read,load,use,assign,store,write,可以大致认为基本数据类型的访问读写具备原子性(例外就是long和double的非原子性协定)

synchronized:
上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是—synchronized关键字,也就是说synchronized满足原子性。

volatile:
我们先来看这样一个例子:

public class VolatileExample {
    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该就是:10*10000 = 100000;可是运行多次都是小于100000的结果,问题在于 volatile并不能保证原子性,在前面说过counter++这并不是一个原子操作,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将新值赋值给变量counter。如果线程A读取counter到工作内存后,其他线程对这个值已经做了自增操作后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。

如果让volatile保证原子性,必须符合以下两条规则:
1、运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
2、变量不需要与其他的状态变量共同参与不变约束

3.有序性
synchronized:
synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性。

volatile:
在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下:

public class Singleton {
    private Singleton() { }
    private volatile static Singleton instance;
    public Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里为什么要加volatile了?我们先来分析一下不加volatile的情况,有问题的语句是这条:

instance = new Singleton();

这条语句实际上包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的内存地址。但由于存在重排序的问题,可能有以下的执行顺序:

在这里插入图片描述

如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会是错得。而用volatile修饰的话就可以禁止2和3操作重排序,从而避免这种情况。volatile包含禁止指令重排序的语义,其具有有序性。

4.可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性。

粒度:对象锁、类锁
分为对象锁和类锁:

public class T {
 
	public void test1() throws Exception{
     synchronized(this){ //对象锁
    	 System.out.println(Thread.currentThread().getName()+"---test1  Doing");
    	 Thread.currentThread().sleep(2000);
    	 test2();
     };
	}
	
	public synchronized void test2() throws Exception{ //同test1方法
	    System.out.println(Thread.currentThread().getName()+"--- test2 Doing");
	    Thread.currentThread().sleep(2000);
	    test1();
	}
	
	public void testclass1() throws Exception{
	     synchronized(T.class){//类锁
	    	 System.out.println(Thread.currentThread().getName()+"---testclass1  Doing");
	    	 Thread.currentThread().sleep(2000);
	    	 
	     };
	}
	
	public static synchronized void testclass2() throws Exception{ //同testclass1方法
    	 System.out.println(Thread.currentThread().getName()+"---testclass2  Doing");
    	 Thread.currentThread().sleep(2000);
    	 testclass3();
	}
	
	public static synchronized void testclass3() throws Exception{ //同testclass1方法
   	  System.out.println(Thread.currentThread().getName()+"---testclass3  Doing");
   	  Thread.currentThread().sleep(2000);
   	  testclass2();
	}
}

不同对象 访问类锁测试:

public class clent {
 
	public static void main(String[] args) throws Exception {
		final CountDownLatch c=new CountDownLatch(1);
		new Thread(new Runnable(){
			@Override
			public void run() {
				T t1=new T();
				try {
					System.out.println(Thread.currentThread().getName()+"启动");
					c.await();
					t1.testclass3();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
	   new Thread(new Runnable(){
					@Override
					public void run() {
						T t1=new T();
						try {
							System.out.println(Thread.currentThread().getName()+"启动");
							c.await();
							t1.testclass2();
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}).start();
	   c.countDown();
	   /* 可以发现 类的synchronized一个线程获得不释放,其他线程就不能访问 通过Jconsole可以看到Thread0 一直在等待Thread1
	   Thread-1启动
	   Thread-1---testclass2  Doing
	   Thread-0启动
	   Thread-1---testclass3  Doing
	   Thread-1---testclass2  Doing
	   Thread-1---testclass2  Doing
	   Thread-1---testclass3  Doing*/
	}

不同对象访问对象锁测试:

public class clent1 {
 
	public static void main(String[] args) {
		final CountDownLatch c=new CountDownLatch(1);
		new Thread(new Runnable(){
			@Override
			public void run() {
				T t1=new T();
				try {
					System.out.println(Thread.currentThread().getName()+"启动");
					c.await();
					t1.test1();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
	   new Thread(new Runnable(){
					@Override
					public void run() {
						T t1=new T();
						try {
							System.out.println(Thread.currentThread().getName()+"启动");
							c.await();
							t1.test2();
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}).start();
	   c.countDown();
	  /* Thread-0启动
	   Thread-0---test1  Doing
	   Thread-1启动
	   Thread-1--- test2 Doing
      */
	}

同一个对象,多个线程访问对象锁测试:

public class client2 {
	public static void main(String[] args) {
		final CountDownLatch c=new CountDownLatch(1);
		final T t1=new T();
		new Thread(new Runnable(){
			@Override
			public void run() {
				
				try {
					System.out.println(Thread.currentThread().getName()+"启动");
					c.await();
					t1.test1();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}).start();
	   new Thread(new Runnable(){
					@Override
					public void run() {
						
						try {
							System.out.println(Thread.currentThread().getName()+"启动");
							c.await();
							t1.test2();
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}).start();
	   c.countDown();
	}
	
	/*结果 同一个对象,当其中一个线程没有释放锁时,另一个会一直等待。
	Thread-1启动
	Thread-1--- test2 Doing
	Thread-0启动
	Thread-1---test1  Doing
	Thread-1--- test2 Doing
	Thread-1---test1  Doing
	Thread-1--- test2 Doing*/
 
}

以上演示的是阻塞等待,下面演示死锁。

public class E {
	public static synchronized void testclass() throws Exception{ 
   	 System.out.println(Thread.currentThread().getName()+"---E.method  Doing");
   	 Thread.currentThread().sleep(2000);
   	 E1.testclass();
	}
}
public class E1 {
	public static synchronized void testclass() throws Exception{ 
	   	 System.out.println(Thread.currentThread().getName()+"---E1.method  Doing");
	   	Thread.currentThread().sleep(1000);
	   	 E.testclass();
		}
}
public class SiSuo {
 
	public static void main(String[] args) {
		final CountDownLatch c=new CountDownLatch(1);
		new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					c.await();
					E.testclass();
					E1.testclass();
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
		}).start();
		
		new Thread(new Runnable(){
			@Override
			public void run() {
				try {
					c.await();
					E1.testclass();
					E.testclass();
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
		}).start();
		c.countDown();
	}
	/*执行结果,一直等待
	Thread-1---E1.method  Doing
	Thread-0---E.method  Doing*/
 
}

下一章:JVM相关===================================================


网站公告

今日签到

点亮在社区的每一天
去签到