2024春招面试题:Java并发相关知识_threadlocal经典的应 场景就是连接管理

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

1.2.线程解决了什么问题?

单位时间内处理复杂且庞大的数据或业务时提升效率

1)如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创

建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

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

1.3 如何创建线程?

  1. 继承Thread

2)实现Runnable接口

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

注意:

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

1.4 线程安全

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

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

  1. 是否存在多线程环境

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

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

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

1.线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。

  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于

可运行线程池中,变得可运行,等待获取CPU的使用权。

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

  1. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进

入就绪状态,才有机会转到运行状态。

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

2.阻塞的情况又分为三种:

(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的作用和原理

4.1 JMM内存模型

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

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

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

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

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

原子性

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

内存可见性

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

有序性

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

4.3 Volatile

volatile帮我们解决了:

内存可见性问题

指令重排序问题

不能保证变量操作的原子性(Atomic)

  1. 被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修

饰共享变量的值,新值总是可以被其他线程立即得知。(会主动通知)

我们可以通过如下案例验证

import java.util.Date;

public class MyData {

    private boolean flag=false;
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public boolean isFlag() {
        return flag;
    }

    public static void main(String[] args) throws Exception {
        MyData myData = new MyData();
        // 线程1 修改值
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 子线程3s 后 修改为true
                myData.setFlag(true);
            }
        }).start();

        System.out.println(new Date());
        while (!myData.isFlag()){
            // (如果不用 volatile)  理论上 3s  这里的死循环会结束,但是 实际上3s 后主线程一直在死循环
            // 如果不用 volatile 主线程并没有感知到 子线程修改了变量
        }
        System.out.println("已经被修改了"+new Date());
    }
}

注意: volatile 并不保证线程安全的,即多个线程同时操作某个变量依旧会出现线程安全问题

如下案例

public class MyData {

       private volatile int number=1;
       public void addNum(){
           number++;
       }

    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.addNum();
                }
            }).start();
        }

        // 程序运行时,有主线程和垃圾回收线程也在运行。如果超过2个线程在运行,那就说明上面的20个线程还有没执行完的,就需要等待
     
        while (Thread.activeCount()>2){
            Thread.currentThread().getThreadGroup().activeCount();
            Thread.yield();// 交出CPU 执行权
        }
        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
    }

}
  1. 禁止指令重排序优化。
int a = 0; 
bool flag = false; 
public void write() { 
    a = 2; //1 
    flag = true; //2 
}
public void multiply() { 
        if (flag) { //3 
        int ret = a * a;//4 
    } 
} 

write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,

再到线程1,这时候a才赋值为2,很明显迟了一步。

但是用 flag 使用 volatile修饰之后就变得不一样了

使用volatile关键字修饰后,底层执行时会禁止指令重新排序,按照顺序指令

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;
    }

java 中常见的几种线程池

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

        //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
        Executors.newFixedThreadPool(10);

        //创建一个定长线程池,支持定时及周期性任务执行。
        Executors.newScheduledThreadPool(10);// 核心线程数10

        //创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
        //保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
        Executors.newSingleThreadExecutor();

//看源码,解释一下这三个创建线程池方法的作用
Executors.newFixedThreadPool(1);
Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor();

6.项目中线程池的使用?

  1. tomcat 自带线程池
  2. CompletableFuture 创建线程时指定线程池,防止创建线程过多
  CompletableFuture<Integer> task1 =  CompletableFuture.supplyAsync(()->{
                result.setCluesNum(reportMpper.getCluesNum(beginCreateTime, endCreateTime, username));
                return null;
            },指定线程池);

案例:CompletableFuture异步和线程池讲解 - 不懒人 - 博客园

7 synchronized

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)执行完抢锁 来优化性能

代码演示

可重入演示

public static void main(String[] args) {
        // 可重入锁演示
            save();
    }
    public synchronized static void save() {
        System.out.println("save");


![img](https://img-blog.csdnimg.cn/img_convert/333b03079ae20aa2c8f3ee44d7d1d165.png)
![img](https://img-blog.csdnimg.cn/img_convert/d6dbe291273ff4b67eff4c6ac7dd3ccf.png)
![img](https://img-blog.csdnimg.cn/img_convert/946815f324f657853055b88a856df64a.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**

代码演示


可重入演示



public static void main(String[] args) {
// 可重入锁演示
save();
}
public synchronized static void save() {
System.out.println(“save”);

[外链图片转存中…(img-CYJHl4Ch-1715665367687)]
[外链图片转存中…(img-ly0YutLE-1715665367687)]
[外链图片转存中…(img-7iIUe9T6-1715665367687)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值