Java多线程,线程池,并发线程安全面试题

1.多线程

1)什么是多线程

多线程就是在同一时刻干多件事情

实现在同一时刻干多件事情的方式有两种(CPU) :

  1. 并行 : 多个核心在同一时刻"同时"干多件事情

  2. 并发 : 单个核心在同一时刻"快速交替"干多件事情

快速交替 : 其实就是cpu执行过程中并不是一个程序执行完之后cpu才切换, 而是cpu时间片用完, 就会切换到下个线程执行, 给人一种多个程序同时执行的感觉

2)多线程的好处

多线程可以在"单位时间内"处理"复杂且庞大"的数据或业务时提升效率

  1. 在多核cpu中, 利用多线程可以实现并行执行

  2. 同步处理的流程容易发送阻塞, 可以用线程来实现异步处理, 提高程序处理实时性

  3. 线程可以认为是轻量级的进程, 所以线程的创建、销毁比进程更快(性能开销更小)

(举例说明A服务需要B C D的数据 如果是单线程的话只能一个一个的拿, 使用多线程可以同时拿到所有需要的数据)

3)多线程的弊端

  1. 如果并发的线程数量很多, 并且每个线程都是执行一个世纪很短的任务就结束了, 这样频繁创建线程就会大大降低系统的效率, 因为频繁创建线程和销毁线程需要时间

  2. 如果大量线程在执行, 会涉及到线程间上下文切换, 会极大的消耗cpu运算资源

4)如何创建线程?

  1. 继承Thread

  2. 实现Runnable接口

  3. 使用Callable接口(可以使用CompletableFuture)

注意 :

我们项目中使用多线程编程一定要使用线程池, 否则可能会导致线程池创建过多发生异常

5)线程安全

多个线程在对共享数据进行读改写的时候, 可能导致的数据错乱就是线程的安全问题了

如何判断当前程序中是否存在线程安全问题?

  1. 是否存在多线程环境

  2. 在多线程环境下是否存在共享变量

  3. 在多线程环境下是否存在对共享变量"写"操作

2. 线程的生命周期? 线程有几种状态

  1. 新建状态(New):新创建了一个线程对象。

  2. 就绪状态(Runnable):线程对象创 建后,其他线程调用了该对象的start方法。该状态的线程位于 可运行线程池中,变得可运行,等待获取CPU的使用权。

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

  5. 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

阻塞的情况又分为三种 :

  1. 等待阻塞 : 运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待 池”中。进入这个状态后,是不能自动唤醒的,必依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法

  2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。

  3. 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 sleep是Thread类的方法

3. wait和sleep的区别

共同点 :

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

不同点 :

  • 方法归属不同

    • sleep(long)是Thread的静态方法

    • wait(), wait(long)都是Object的成员方法, 每个对象都有

  • 醒来时机不同

    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来

    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去

    • 它们都可以被打断唤醒

  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制

    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)

    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

4.volatile的作用和原理

1) JVM内存模型

JVM让java程序与硬件指令进行了隔离

由于JVM运行程序的实体是线程,创建每个线程时,java 内存模型会为其创建一个工作内存(我们一般称为栈),工作内存是每个线程的私有数据区域。

Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。

2) java并发编程要解决的三个问题(三大特征)

原子性

一个线程在CPU中操作不可暂定,也不可中断,要不执行完成,要不不执行

内存可见性

默认情况下变量,当一个线程修改内存中某个变量时,主内存值发生了变化,并不会主动通知其他线程,即其他线程并不可见

有序性

程序执行的顺序按照代码的先后顺序执行。

3) Volatile

volatile关键字, 保证内存可见性, 永远都从内存中去读, 不要使用JIT优化成true 它并不能保证线程安全

5. 为什么用线程池? 解释下线程池参数

  1. 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。

  2. 提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。

  3. 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。

/*
corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会 
消除,而是一种常驻线程 
​
maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程 
数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但 
是线程池内线程总数不会超过最大线程数 
​
keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会 
消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过keepAliveTime 、
unit 表示超出核心线程数之外的线程的空闲存活时间,
也就是核心线程不会 setKeepAliveTime 来设置空闲时间 
​
​
workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放 
入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程 
​
ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建 
工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择 
自定义线程工厂,一般我们会根据业务来制定不同的线程工厂 
​
Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这 
时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程 
池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提 
交的任务时,这是也就拒绝
​
​
*/ 
  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  1. java中常见的几种线程池

// 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,
//若无可回收,则新建线程。
        Executors.newCachedThreadPool();//
​
        //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
        Executors.newFixedThreadPool(10);
​
        //创建一个定长线程池,支持定时及周期性任务执行。
        Executors.newScheduledThreadPool(10);// 核心线程数10
​
        //创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
        //保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
        Executors.newSingleThreadExecutor();
  1. 线程池执行顺序

6. 项目中线程池的使用

  1. tomcat自带线程池

  2. CompletableFuture 创建线程时指定线程池,防止创建线程过多

7. synchronized锁释放时机

● 当前线程的同步方法、代码块执行结束的时候释放

1) 正常结束

2) 异常结束出现未处理的error或者exception导致异常结束的时候释放

● 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁

8.Sychronized和ReentrantLock的区别

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类

  2. sychronized的底层是JVM层⾯的锁(底层由C++ 编写实现),ReentrantLock是API层⾯的锁 (java 内部的一个类对象)

  3. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁

  4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁

注: 假设多个线程都要获取锁对象,满足先等待的线程先获得锁则是公平锁,否则是非公平锁

  1. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态

  1. sychronized底层有⼀个锁升级的过程(访问对象线程数由少到多,竞争由不激烈到激烈,底层会通过一种锁升级机制 无锁->偏向锁->轻量级锁->重量级锁,保证性能) ,会使用自旋 线程频繁等待唤醒会浪费性能,特别是锁的获取也许只需要很短的时间 ,不限于等待,直接执行简单代码while(true)执行完抢锁 来优化性能

9. 悲观锁 VS 乐观锁

  • 悲观锁的代表是 synchronized 和 Lock 锁

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】

    • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能

    • 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会

  • 乐观锁的代表是 AtomicInteger AtomicStampReference,使用 cas 来保证原子性

    • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】

    • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换

    • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数

10. ConcurrentHashMap的原理

1) JDK1.7时

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突

  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了(默认16,可以指定)

  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍

  • 其他Segment首次创建小数组时,会以Segment[0] 为原型为依据,数组长度,扩容因子都会以原型为准

2) JDK1.8时

  • 数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突

  • 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容

  • 扩容条件:Node 数组满 3/4 时就会扩容(0.75 扩容因子)

  • 扩容时并发 get

    • 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞

  • 扩容时并发 put

    • 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞

11. Hashtable 和 ConcurrentHashMap 有什么区别?其底层实现是什么?

  • Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合

  • Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它

  • ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突

12. ThreadLocal

  • 线程本地对象, 数据保存在当前咸成忠, 属于线程安全

  • 应用场景 : 接受请求后, 请求头中包含用户信息, 可以在拦截器中将数据存放到ThreaLocal中, 这样后续service和controller都可以获取到这个数据, 保证线程安全

  • 原理 : 底层数据结构是一个ThreadLocalMap, 每个线程都持有一个, key是ThreadLocal对象, value是他的值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值