侧边栏壁纸
博主头像
霖祥的小破站博主等级

欢迎来到霖祥的小破站,这里是一个汇聚编程者共同探索、分享和成长的地方。我们致力于为广大程序员提供最新的编程技术动态、深度的技术文章和实用的开发经验。无论你是前端开发、后端工程师、数据科学家还是人工智能研究者,这里都将是你不可或缺的技术指南。 在霖祥的小破站,你将发现: 热门编程语言与框架的深度解读与比较,助你选择最适合的工具。 实用的编码技巧与项目经验分享,让你的代码更加优雅高效。 最新科技趋势与前沿技术的解析,助你把握行业发展脉搏。 共享精彩项目案例,启发你的创造力与思维方式。 加入我们,一起踏上无尽可能的编程之路,让技术之花在你手中绽放!

  • 累计撰写 12 篇文章
  • 累计创建 8 个标签
  • 累计收到 14 条评论

目 录CONTENT

文章目录

java并发编程的艺术

霖祥的小破站
2024-06-20 / 1 评论 / 3 点赞 / 611 阅读 / 6,621 字

java并发编程艺术电子版链接

java并发编程的艺术

资料1:并发思维导图(JUC 并发)
资料2:https://www.yuque.com/hollis666/wty0im/dye7p0b0it112wcy

线程

基础

1. 什么是线程和进程?

  • 进程具有独立的执行环境。比如微信,qq可以看作一个进程集合。线程是存在进程中的,线程是最小的执行单位。
  • 比如main就是一个进程,包括了main执行线程和gc线程。

2. 什么是多线程的上下文切换?

  • 指的cpu在一个线程切换到另一个线程时,保存当前线程的执行状态,程序计数器,指针等。保证切换会原线程时能够正常的运行。
    如何降低上下文切换?
    • 使用线程池减少线程的创建和销毁。无锁编程。使用cas算法。

3. 用户态和内核态 如何切换?

  • 用户态指的是应用程序运行在非特权模式下的状态。内核态指的是操作系统运行在特权模式下的状态。这样区分的目的是为了保证系统的安全运行。
  • 当应用程序需要操作底层硬件资源的时候,就会从用户态切换内核态取读取资源,操作完之后再切换回来,内核态时线程时阻塞暂停的。

4. cpu和线程的关系?

  • 同一时刻一个cpu只能执行一个线程。

5. 创建线程的几种方式?

  • 继承thread接口,重写里面的run方法
  • 实现runnable接口 run方法
  • 实现callable接口 重写里面的call方法

6.runnable和callable的区别?

  • runnable的run方法无返回值 callable的call方法有返回值 object
  • callable方法还可以抛出异常 runable不行

线程池 线程池

7.创建线程池的方式?

  • 1.new ThreadPoolExecutor(参数,,)实现 常用 项目中确实用到了。
/**
创建出票线程工厂
 */
ThreadFactory issueThreadFactory = ThreadUtil.newNamedThreadFactory("trade_order_thread_factory", new ThreadGroup("issue"), true);

/**
查询票号任务线程池
 */
ThreadPoolExecutor queryTicketNoThreadPool = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(), issueThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
    1. Executors.方式创建 固定线程数量大小的线程池,单线程的线程池,可缓存的线程池,定时执行任务的线程池 不常用
  • 图中就可以看到为什么不常用了,因为本质还是new threadpoolExecutor(),只不过是将参数帮我们写好了的,但生产中需要我们根据具体的业务需求压测之后填写参数。所以推荐自己new threadPoolExecutor
    image-1718875386277

8.线程池的参数有哪些?

  • corePoolSize:核心线程数量,可以类比正式员工数量,常驻线程数量
  • maximumPoolSize:最大的线程数量,公司最多雇佣员工数量。常驻+临时线程数量。
  • workQueue:多余任务等待队列,再多的人都处理不过来了,需要等着,在这个地方等。
  • keepAliveTime:非核心线程空闲时间,就是外包人员等了多久,如果果还没有活干,解雇了。
  • threadFactory:创建线程的工厂,在这个地方可以统一处理创建的线程的属性。每个公司对员工的要求不一
  • 样,恩,在这里设置员工的属性。
  • handler:线程池拒绝策略,什么意思呢?就是当任务实在是太多,人也不够,需求池也排满了,还有任务咋
  • 办?默认是不处理,抛出异常告诉任务提交者,我这忙不过来了了。

9.工作中如何使用线程池的?

  • 工作用使用new threadPoolExecutor()方法创建线程,指定核心线程数,最大线程数,非核心线程空闲时间,时间单位,阻塞队列,线程工厂,拒绝策略。拒绝策略一般使用的是new ThreadPoolExecutor.CallerRunsPolicy()。
  • 注:为什么与CountDownLatch进行组合使用呢?

10.使用线程池的好处和问题?

  • 线程可以复用
  • 减少创建和销毁线程的开销

11.线程提交任务的流程?

public void execute (Runnable command){
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();

    if (workerCountOf(c) < corePoolSize) {
        // 如果核心线程未全部启动启动核心线程,执行本次提交任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    // 核心线程已占满
    // 线程池未关闭,则将本次任务添加到任务队列
    if (isRunning(c) && workQueue.offer(command)) {
        // 双重校验,如果线程池关闭了,那么将本次任务移出任务队列。
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command))
            // 成功移出任务队列,执行决绝策略
            reject(command);
        else if (workerCountOf(recheck) == 0)
            // 如果没有可运行线程,启动一个非核心线程执行本次任务
            addWorker(null, false);
    }
    // 无法添加到任务队列,那么启动非核心线程
    else if (!addWorker(command, false))
        // 线程数已满,执行决绝策略
        reject(command);
}
  • 执行execute方法之后,
    • 先判断当前执行线程是否小于核心线程数,小于则将任务放到addwoker方法创建核心线程执行并结束。
    • 如果不满足,则判断线程池是否关闭,没关闭时放入到等待队列中。
    • 如果等待队列也放不下就启动非核心线程执行,直到最大线程数。
    • 如果最大线程数还不够,则执行拒绝策略。
    • 我在生产中拒绝策略使用的时callrunspolicy() 也就是交给提交任务的线程去执行。其次还可以选择直接舍弃任务

12.线程池的work机制?todo

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

13.为什么不建议直接使用Spring的@Async?https://www.yuque.com/hollis666/wty0im/naw927g44ywpxw4e

  • @Async注解表示异步执行,在方法上使用。当一个线程调用该方法的时候,就会创建一个新的线程异步执行这个方法。一般配合@EableAsnc 开启异步执行,但是不建议使用默认的@Async,因为没有最大线程数的设置,在大量并发会造成严重性能问题。
  • 所以建议自定义异步线程池:
@Configuration
@EnableAsync
public class AsyncExecutorConfig {
@Bean("registerSuccessExecutor")
public Executor caseStartFinishExecutor() {
    ThreadFactory namedThreadFactory = new ThreadFactobryBuilder
.setNameFormat("registerSuccessExecutor-%d").build();
ExecutorService executorService = new ThreadPoolExecutor(100,200,
0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy())
return executorService;


//使用
@Async("registerSuccessExecutor")
public void onApplicationEvent(RegisterSuccessEvvent event){
RegisterInfo registerinfo = (Registerinfo) event.getsource();
}

14.submit和execute方法的区别?

  • 两者都是向线程池提交任务。
  • 不同点:execute提交后无返回值,用法比较简单。而submit提交后会有返回值 futrure ,通过future.get()捕获异常。

15.线程异常处理方式?

  • 手动trycatch,缺点是主线程中不能捕获子线程异常。
  • 使用submit提交任务后返回future类,就可以让主线程捕获子线程异常。

线程池的状态转换

  • Running、ShutDown、Stop、Tidying、Terminated
  • 线程池一旦被创建就会进入runing状态,等待接受任务。
  • 调用线程池的shutdown方法就进入shutdown状态,但是仍然会处理已经添加的任务。
  • 当调用shutDownNow方法时候就进入stop状态,并且会中止已经添加的任务。
  • 当所有的任务数量为0的时候,就会进入tiding。
  • 调用terminated方法就会进入terminated状态。

线程的生命周期

  • 1.调用了start方法创建一个线程(并没有真正执行)此时进入runnable状态。
  • 2.当cpu分片执行该线程后就进入了running状态。
  • 3.当调用sleep和wait方法时就会进入blocked状态。
  • 4.当线程执行结束就会进入terminated状态,结束线程的生命周期。
    image-1718875588494

线程池体现的设计模式

  • 模板方法模式

核心线程和非核心线程的区别?

  • 本质上都是一样的,线程池中也不会标记那些线程是核心和非核心线程。唯一的区别在于非核心线程有一个最大的空闲时间,如果达到这个空闲时间就会销毁,而核心线程是一种存在线程池中的。

核心线程数如何设置的理论公式

  • 1.判断是cpu密集型任务还是io密集型任务。cpu:任务一进来需要cpu进行计算。io任务更多是调用接口和数据库。
  • cpu密集型任务:n(多少核)+1
  • io密集型任务:2n(cpu)

什么是守护线程和普通线程?

  • 守护线程指的是后台运行的线程,比如gc线程。普通线程指的就是我们主动创建的线程。
  • 两者唯一的区别在于:jvm在执行完所有的普通线程之后就会结束,而不会等待守护线程。
  • 我们可以通过 线程对象.setDaemon(true)设置为守护线程。
    run/start方法,sleep/wait,notify/notifyAll区别?
  • 调用start方法是创建一个线程,并不是马上执行线程,是由cpu决定什么时候执行。调用两次start方法报错
  • sleep和wait的区别:
    1. wait() 方法属于 Object 类的一部分,而 sleep() 方法属于 Thread 类。因此,wait() 方法用于线程间的同步和通信,而 sleep() 方法用于线程的暂停。
    2. wait() 方法必须在同步代码块中调用(即在使用 synchronized 关键字修饰的方法或代码块中),并且调用 wait() 方法后会释放对象的锁,从而允许其他线程获取该对象的锁。而 sleep() 方法可以在任何地方调用,它不会释放任何锁。
    3. 当调用 wait() 方法时,线程会进入等待状态,并且需要通过 notify/notifyAll唤醒一个线程或者全部等待的线程。

面试常问?

  • 线程执行任务什么时候才会让出cpu?
    • 答:当线程的时间片用完时;当线程执行I/O时;当线程调用yield()主动让出cpu。

文章推荐:https://tech.meituan.com/2018/11/15/java-lock.html

同步和并发理解?

  • 同步指的是控制多个线程对一个资源进行访问顺序,保证数据的一致性和安全性的一种机制。
  • 并发指的是一段时间多个任务同时执行。

java中按照某些特性将锁进行分类如下图:

image-1718875633616

线程要不要锁住同步资源?

  • 悲观锁:
    • 当对一个对象并发操作时,悲观锁认为别人一定会修改数据,所以先加锁,后修改数据。比如lock和synchronized就是悲观锁。
    • 适合写多读少的场景
  • 乐观锁:
    • 对一个对象资源并发操作时,只有在更新操作时先判断其他线程有误修改,如果没有则更新,如果另一个线程正在修改则通过自旋或者报错操作。比如cas算法
    • 适合读多写少的场景。

数据库中的悲观锁和乐观锁如何实现的?

  • 悲观锁:select …for update
START TRANSACTION;
SELECT * FROM table_name WHERE condition FOR UPDATE;
-- 对查询结果进行操作
COMMIT;
  • 乐观锁:通过版本号实现乐观锁。
START TRANSACTION;
--获取 id和version
select * from t_point where point_name =#{name}
-- where条件中比较version,且更新条件中将version+1
update t_point set point_name=#{pointName},point_type =#{pointType},version=version+1 where id =#{id} and version =#{version}
COMMIT;
  • 自旋锁:线程获取同步资源被占用时,通过自旋等待锁的释放,避免cpu的阻塞和唤醒。
  • 自适应锁:自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
  • 重入锁和非重入锁:
    • ReentrantLock和synchronized都是重入锁。非可重入锁NonReentrantLock。
    • 当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
  • 共享锁和排它锁:
    • 排它锁:同悲观锁一样。只有一个线程可以读写
    • 共享锁:多个线程可以共读,但是不能修改数据。

synchronized

https://www.bilibili.com/video/BV1aJ411V763?p=11&vd_source=d895293f5f20ef79bd6ffbeb4865aae9

预备知识

  • 对象的结构?
    • 对象是由对象头和实例数据和对齐填充组成。其中对象头最重要的就是markword 里面记录年龄信息,锁标志,hashcode等。
      image-1718875682851

synchronized重量级锁的底层实现原理?

  1. 当一个线程尝试获取对象的锁时,它会先尝试获取与该对象关联的mutex变量。如果mutex变量已经被其他线程锁定,那么当前线程将进入阻塞状态,直到mutex变量被释放。
  2. 一旦线程成功获取了对象的锁(即成功获取了mutex变量),它就可以执行synchronized代码块中的内容。在执行完毕后,线程会释放这个锁,同时释放对应的mutex变量

为什么要有锁升级?原因是最开始的synchronized是重量级锁,特点是频繁的上下文切换效率低。

image-1718875700274

  • 无锁:存储对象的hashcode,年龄。
  • 偏向锁:一个线程获取对象时,通过将threadId存入到对象头中markword实现加锁。
  • 轻量级锁:
    • markword中存储的是64bit的指针(指向线程栈中的lock record的指针)
    • 原理将对象的markword中的信息复制到线程栈中lock record中,然后markword存在一个指针指向线程lock record。
    • 这么做的原因是 一个指针栈64bit存不下,所以将markword中的信息移动到调用的线程栈中。
  • 重量级锁:
    1. markword存储指针(指向mutex互斥变量的指针)。

锁升级过程:

锁状态只能升级不能降级。

  • 当一个线程A获取对象的时候,会将threadId存储到对象头中的markword中。
  • 如果另一个线程B又来获取该对象,则会在全局安全点的时候,判断线程A是否存活或者是否依旧持有锁,如果没有则替换threadId。
  • 如果依旧持有锁,则升级会轻量级锁,线程B并将该对象的markword信息复制到线程A的线程栈的lockreword中,对象只存在指向该lock reword的指针。
  • 之后再次进行cas获取锁,如果获取成功则将指针指向线程B的线程栈的lockword中。如果仍然失败,将会默认最多10次自旋获取锁,之后则将对象升级为重量级锁,如果线程C也来竞争轻量级锁则会直接生成重量级锁。
  • 此时对象的markword存储的指针指向一个monitor(底层关联这mutex互斥变量)。此时线程B尝试获取monitor(监视器)获取加锁的对象,如果获取了其他线程只能阻塞等待。

锁粗化?

  • 锁粗化是一种优化手段,当编译器检查到连续的加锁解锁操作时,将它们合并成一个范围更大的锁操作,减少不必要的锁竞争。

锁消除?

  • 当编译器执行代码时,发现某些加锁的资源并不会被多线程竞争,所以就直接去掉加锁。

ReentrantLock

文章推荐: https://juejin.cn/post/6896278031317663751#heading-1
文章推荐:https://juejin.cn/post/7165029692574007326

- synchronized 关键字后为什么还需要 ReentrantLock?

  • synchronized使用过操作系统的mutex互斥变量实现,线程的阻塞和唤醒需要内核态到用户态的切换,线程上下文的切换带来开销。所以引入和lock锁(cas+自旋锁)提升加锁的性能。

- ReentrantLock是什么?

  • ReentrantLock是可重入锁,是基于AQS实现的。AQS又叫做队列同步器,是一个使用模板方法的抽象类。而ReentrantLock则是实现AQS的其中一种锁。

- ReentrantLock 如何去使用?

  • 通过new ReentrantLock(),调用lock()方式去直接尝试获取锁。
  • 调用trylock()尝试获取锁,成功返回true,失败立刻返回false。而lock方法在拿不到锁之后就会尝试阻塞当前线程。
  • 通过unlock()主动释放锁,相对于synchronized 自动释放锁
  • isHeldByCurrentThread():判断当前线程是否持有锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
            System.out.println("Incremented: " + count);
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();
    }
}

- ReentrantLock跟synchronized的区别?

  • 1.实现机制不同,一个是基于monitor监视器,底层mutex互斥变量实现同步。
  • 2.ReentrantLock有公平锁和非公平锁。
  • 3.双向链表队列保证同步加锁顺序。
    image-1718875766065

- ReentrantLock底层实现原理?

  • 底层是通过AQS中的 Volatile 修饰的int state变量来控制同步状态,使用双向链表的队列来实现线程的唤醒和阻塞。
  • ReentrantLock加锁过程图解
    • 如图一:需要了解更详细一下,包括node节点
      image-1718875770971
      image-1718875777214
      源码:
final void lock() {
    if (compareAndSetState(0, 1)) //cas修改state变量
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

设置当前线程为独占线程

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

如果尝试获取锁失败则调用aquire方法继续尝试获取锁。todo

ThreadLocal ThreadLocal

什么是threadLocal?作用是什么?

  • threadLocal是线程的全局变量,每个线程的threadLocal都是线程隔离的,用于解决多线程之间的同步问题(每个线程获取自己设置的变量)。
  • 通过threadLocal可以实现变量在线程之间隔离,而在类和方法中共享。

thread和threadLocal以及threadLocalMap之间的关系?

static class ThreadLocalMap {

       /**
        * The entries in this hash map extend WeakReference, using
        * its main ref field as the key (which is always a
        * ThreadLocal object).  Note that null keys (i.e. entry.get()
        * == null) mean that the key is no longer referenced, so the
        * entry can be expunged from table.  Such entries are referred to
        * as "stale entries" in the code that follows.
        */
       static class Entry extends WeakReference<ThreadLocal<?>> {
           /** The value associated with this ThreadLocal. */
           Object value;

           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
           }
       }  
   }

- 每个线程thread都有自己的一个threadLocalMap。

  • threadLocalMap是有一个entry对象,entry以key-value形式,key存储的就是thread私有的threadLocal实例对象,而value是我们通过set方法赋值的。

threadLocal是如何实现线程隔离的?

  • 每个线程都会有自己的ThreadLocalMap变量副本,存储于线程私有的虚拟机栈中,而不是堆中,不会被其他线程
    访问到,因此实现了线程隔离。具体操作是,首先创建一个static的全局ThreadLocal变量,创建线程后,线程内调用
    set方法时,先获取线程的ThreadLocalMap对象,若没有就创建,ThreadLocalMap类里有一个内部类Entry,他的key
    是当前创建的ThreadLocal对象存在于虚拟机栈中,值是我们指定的值,值一般是一个对象比如用户对象,存在于堆
    中。且key是弱引用的,值是强引用的。geet时,就根据ThreadLocal对象获取值。

四种引用?

  • 强引用是最常用的应用,一个对象具有强引用,gc就不会回收它。比如 Object a=new Object();
  • 软引用:当内存不足的时候,垃圾回收器才会回收软引用对象。
  • 弱引用:比如threadlocal类就是弱引用。不管内存足不足,只要发现弱引用对象就会回收。注意由于gc线程优先级低,可能未发现弱引用的对象,所以可能造成内存泄露。
  • 虚引用(gc回收的标识):并不会决定对象的生命周期,虚引用必须跟引用队列一起使用,当发现虚引用对象就会将它加入到引用队列中,我们就可以通过队列查看虚引用的对象,也就是即将被gc回收的对象。

内存泄漏

  • 内存泄漏指的是该回收的垃圾没有回收。
  • 内存溢出指的是当内存空间不足,再次产生的对象就会导致内存溢出。
    • 此图非常关键,理解该图
      image-1718875858503
  • 为什么threadlocalMap的key是虚引用?。因为为了防止内存泄漏。也就是图中方法结束之后引用1就丢失了,threadLocal应该回收,如果是强引用则会一直存在无法回收。
  • 那为什么value不是弱引用呢? 因为防止数据丢失。如果key和value都是弱引用的话,当我们get值得时候,threadLocalMap在内存不足的时候回收掉了。

- 如何导致的内存泄露?

  • 基于threadlocalmap的key和value分别是虚引用和强引用,所以当thread线程对象存活时,thread localmap是无法回收的,久而久之导致内存泄露。
如何解决内存泄露问题:手动调用remove方法

生活中如何使用threadLocal

  • 1.主要是用于用户身份拦截器中使用threadlocal存储用户信息进行透传。
    • AuthInterceptor 是个拦截器,根据请求头中的uid判断是是否存在该用户,存在则将用户信息存储到threalocal中,并透传到同一线程中的其他方法中。
@Slf4j
@AllArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Autowired
    private IAuthService authService;
    private IJpAdminUserService adminUserService;
    private IJpAdminUserConfigService adminUserConfigService;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) {
        String bearerStr = request.getHeader("Authorization");
        String token = "";
        if (bearerStr != null && bearerStr.startsWith("Bearer")) {
            token = bearerStr.replace("Bearer", "").trim();
        }
        String uid = request.getHeader("UID");
        if (!authService.verifyToken(uid,token)) {
            ResponseUtil.write(response, ResponseInfo.failed(ResponseCodeEnum.AUTH_FAILED));
            return false;
        }
        if (StrUtil.isNotBlank(uid)) {
            JpAdminUser user = adminUserService.getById(uid);
            if (Objects.isNull(user)) {
                ResponseUtil.write(response, ResponseInfo.failed("用户不存在!"));
                return false;
            }
            ContextUserInfo info = new ContextUserInfo();
            info.setUserId(user.getId());
            info.setUsername(user.getUsername());
            info.setNickname(user.getNickname());
            UserContext.add(info);
        } else {
            return false;
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

java并发面试题

1. hashCode对锁的影响? https://blog.csdn.net/nibonnn/article/details/124026493

  • 答:当在上锁之前调用对象的hashcode方法时,锁将会直接升级为轻量级锁。当在偏向锁时,调用对象的hashcode方法则会立刻升级为重量级锁。
    • 在上锁前进行hashCode方法的调用
public static void main(String[] args) {
    Object lock = new Object();
    System.out.println("初始状态:" + ClassLayout.parseInstance(lock).toPrintable());
    System.out.println(lock.hashCode());
    System.out.println("HashCode调用后状态:" + ClassLayout.parseInstance(lock).toPrintable());
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("上锁:" + ClassLayout.parseInstance(lock).toPrintable());
        }
        System.out.println("解锁:" + ClassLayout.parseInstance(lock).toPrintable());
    }).start();
}

- 在上锁后进行hashCode方法的调用:

public static void main(String[] args) {
    Object lock = new Object();
    System.out.println("初始状态:" + ClassLayout.parseInstance(lock).toPrintable());
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("上锁:" + ClassLayout.parseInstance(lock).toPrintable());
            System.out.println(lock.hashCode());
            System.out.println("HashCode调用后状态:" + ClassLayout.parseInstance(lock).toPrintable());
        }
        System.out.println("解锁:" + ClassLayout.parseInstance(lock).toPrintable());
    }).start();
}

volatile关键字的作用?

  • 禁止指令重排。在编译器对指令进行优化时,通过内存屏障禁止编译器对指令的重排,保证指令的顺序性。
  • 保证共享变量的可见性。也就是线程中对变量的操作都在主内存中进行。

volatile在哪里用到?

  • Synchronized中使用到volatile防止指令重排导致的空指针问题。
    • 比如 object=new Object() 正确步骤是:给object分配地址空间,初始化,最后指向这边空间。
    • 如果不使用volatile对象object就有可能导致 分配空间之后,直接指向这片空间,然后还没初始化时获取这个对象就会空指针异常。
  • lock中通过volatile修饰state变量,实现共享变量state的可见性。
3

评论区