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

  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");
        update();
    }
    public synchronized static void update() {
        System.out.println("update");
    }

ReentrantLock 使用演示

public class TestDemo {
    public static void main(String[] args)throws Exception {
        ReentrantLock lock = new ReentrantLock();
        // 线程1
        new Thread(()->{
            lock.lock(); // 加锁
            add();
            lock.unlock();// 解锁
        }).start();
        // 线程2
        new Thread(()->{
            lock.lock(); // 加锁
            add();
            lock.unlock();// 解锁
        }).start();
    Thread.sleep(3000);
        System.out.println(i);
    }
    static int i =0;
    public  static void add() {
        i++;
    }
}

公平/非公平锁演示

package com.huike;

import java.util.concurrent.locks.ReentrantLock;

public class TestDemo {
    public static void main(String[] args)throws Exception {
        ReentrantLock lock = new ReentrantLock(false);
        // 线程1
        new Thread(()->{
            lock.lock(); // 加锁
            add();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();// 解锁
        },"t1").start();



        // 线程2
        new Thread(()->{
            lock.lock(); // 加锁
            add();
            lock.unlock();// 解锁
        },"t2").start();

        new Thread(()->{
            lock.lock(); // 加锁
            add();
            lock.unlock();// 解锁
        },"t3").start();

        for (int j = 0; j < 100000; j++) {
            new Thread(()->{
                lock.lock(); // 加锁
                add();
                lock.unlock();// 解锁
            }).start();
        }




    Thread.sleep(30000);
        System.out.println(i);
    }
    static int i =0;
    public  static void add() {
        i++;
        System.out.println(Thread.currentThread().getName()+"获得了锁");
    }
}

10. 悲观锁 vs 乐观锁

要求

  • 掌握悲观锁和乐观锁的区别

对比悲观锁与乐观锁

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

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
      • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
      • 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
  • 乐观锁的代表是 AtomicInteger AtomicStampReference,使用 cas 来保证原子性

    • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
      • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
      • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数

12. ConcurrentHashMap的原理

12.1 JDK1.7

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了(默认16,可以指定)
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
  • 其他Segment首次创建小数组时,会以Segment[0] 为原型为依据,数组长度,扩容因子都会以原型为准

12.2 JDK1.8

img
img

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

2.1 JDK1.7

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了(默认16,可以指定)
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
  • 其他Segment首次创建小数组时,会以Segment[0] 为原型为依据,数组长度,扩容因子都会以原型为准

12.2 JDK1.8

[外链图片转存中…(img-X0ZM3Fsz-4701981442954)]
[外链图片转存中…(img-06R0Ksde-4701981442955)]

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

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值