Java 面试宝典

一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅知识面试题,也是你 Java 知识点的扫盲贴。

  • CAS,CompareAndSwap,即比较并交换,它是一种无锁编程技术的核心机制。但是你确定CAS是不加锁的吗?我们先看AtomicInteger的getAndIncrement()源码:publicfinalintgetAndIncrement(){returnunsafe.getAndAddInt(this,valueOffset,1);}publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){intvar5;do{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));returnvar5;}publicfinalnativebooleancompareAndSwapInt(Objectvar1,

    2024-03-23
    阅读(100)
  • 回答CAS,CompareAndSwap,即比较并交换,它一种无锁编程技术的核心机制。其工作方式分为两步:比较:它首先会比较内存中的某个值(V)与预期的值(A)是否相等。**交换:**如果相等,那么会自动将该值(V)更新为新值(B)。如果不相等,不做任何操作。这个过程是原子操作,保证在并发环境中的数据一致性和线程安全。CAS主要存在如下三个问题:ABA问题:如果变量V的值原先是A,然后被其他线程改为B,然后又改回A,这时CAS操作会误认为自从上次读取以来V没有被修改过,从而可能产生错误的操作结果。循环时间过长问题:CAS操作如果长时间不成功,会不断进行重试,这可能会导致线程长时间处于忙等(Busy-Wait)状态,从而导致CPU长时间做无效操作。多变量原子问题:CAS只能保证一个变量的原子操作。详解CAS详解CAS,CompareAndSwap,即比较并交换。Douglea大神在同步组件中

    2024-03-23
    阅读(80)
  • synchronized和ReentrantLock两者的功能是一致的,都是Java中用于管理并发和同步机制的,但它们两者之间还是存在一些差异的。用法不同synchronized可用来修饰普通方法、静态方法和代码块publicsynchronizedvoidtest(){//...}publicvoidtest(){synchronized(this){//...}}ReentrantLock只能用在代码块上。privatefinalReentrantLocklock=newReentrantLock();publicvoidtest(){//加锁lock.lock();try{//...}finally{//释放锁lock.unlock();}}获取锁和释放锁机制不同synchronized的获取锁和释放锁是自动的,当进入synchronized修饰的方法或者方法体内,会自动获取锁,当执

    2024-03-23
    阅读(69)
  • 回答ReentrantLock是一种可重入的排它锁,主要用来解决多线程对共享资源竞争的问题。ReentrantLock提供了比synchronized更加灵活、更强大的锁机制。它的核心特性有如下几个:可重入性:ReentrantLock是一个可重入锁,也就是说同一个线程可以多次获取同一个锁。公平锁和非公平锁:ReentrantLock支持公平和非公平两种方式。公平锁意味着等待时间最长的线程将会首先获得锁非公平锁可能会让新请求的线程优先于正在等待的线程获得锁。ReentrantLock的底层实现是基于AbstractQueuedSynchronizer框架,即AQS。AQS使用一个volatile整数(state)来表示同步状态,并维护了一个等待线程的队列。在ReentrantLock中,当线程尝试获取锁时,如果锁未被占用(即state为0),该线程将成功获取锁并将state值+1。如果锁已

    2024-03-23
    阅读(64)
  • 当线程调用release()释放锁时,完成锁释放后调用unparkSuccessor()唤醒后继节点的线程,这个方法里面有一个这样的逻辑判断,当该节点的next为空,或者状态为已取消,则AQS从tail节点开始往前搜索节点:这里为什么要从tail节点开始呢?我们知道一个线程获取锁失败后,会被包装成一个Node节点加入到CLH同步队列中:如果tail没有被初始化或者设置tail节点失败都会走enq(),在这里我们考虑设置tail节点失败的情况,tail节点失败说明有多个线程在设置tail节点,走enq():privateNodeenq(finalNodenode){for(;;){Nodet=tail;if(t==null){//Mustinitializeif(compareAndSetHead(newNode()))tail=head;}else{node.prev=t;if(compa

    2024-03-23
    阅读(66)
  • 回答当线程获取锁失败后,会加入到CLH同步队列中,同时调用LockSupport.park()阻塞。当线程将锁释放完毕后,需要唤醒后继节点,调用LockSupport.unpark()唤醒线程。LockSupport用来创建锁和其他同步类的基本线程阻塞原语,相比wait()和notify()提供了一种非常灵活的线程阻塞和唤醒机制。它提供提供了两个方法:park():用于阻塞当前线程。unpark():用于唤醒一个被park()阻塞的线程。LockSupport使用一种名为“许可证”的概念。每个线程都有一个与之关联的许可证(最多一个)。park()会消耗掉这个许可证(如果存在的话),并在没有许可证的情况下阻塞线程。而unpark()则提供一个许可证。详细AQS实现线程阻塞在线程获取锁失败后,会被包装为一个Node节点,添加到CLH同步队列中,并且该节点会自旋式地不断检测是否可以获取锁,在获取

    2024-03-23
    阅读(60)
  • 回答一个线程获取锁失败后,会被包装为Node节点接入到CLH同步队列中,CLH同步队列是一个FIFO的队列,理论上来说实现一个单向的就可以了,为什么要设计为双向的呢?其实JDK注释就已经说明了答案:prev用于处理中断next用于唤醒后续阻塞线程详解CLH单向链表结构是这样的:+------+prev+-----++-----+head||<----||<----||tail+------++-----++-----+只有prev没有next。更好处理中断操作AQS提供了一个acquireInterruptibly(intarg),该方法用于获取锁过程中处理中断信号。publicfinalvoidacquireInterruptibly(intarg)throwsInterruptedException{if(Thread.interrupted())thrownewInter

    2024-03-23
    阅读(68)
  • 回答AQS是AbstractQueuedSynchronizer简称,它是J.U.C包中多个组件的底座,如ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS来实现的。AQS底层利用volatile类型的intstate来表示同步状态,该字段的含义取决于实现的同步器,如在ReentrantLock中,state表示持有锁的数量,在Semaphore中,state表示可用的许可证数量。当线程来获取同步锁时,如果status=0,说明目前没有任何线程占有锁,该线程可以获得锁并设置state=1。如果state>0,则说明有线程正在持有锁,则线程必须加入同步队列进行等待(不考虑重入)。AQS内部还维护着一个FIFO的双向同步队列,该队列通过Node的实例来构建的,每个Node代表一个等待获取资源的线程。获取锁失败的线程都会被

    2024-03-23
    阅读(66)
  • 回答SimpleDateFormat不是线程安全,当多个线程使用一个SimpleDateFormat实例(如static修饰),调用format()格式化时,会出现线程不安全的情况。其原因是SimpleDateFormat内部使用一个Calendar实例来存储解析和格式化的中间结果,但这个Calendar实例是一个成员变量且可变,当多个线程共享SimpleDateFormat时,也会共享Calendar,然而SimpleDateFormat内部没有进行任何形式的锁或者同步机制来保护这个Calendar,所以,在多线程环境下,他们之间就会互相干扰,导致日期解析或格式化错误。详细分析SimpleDateFormat线程不安全演示示例一@Testpublicvoidtest01(){SimpleDateFormatsimpleDateFormat=newSimpleDateFormat(&quo

    2024-03-23
    阅读(69)
  • 回答让当前线程主动让出CPU,触发操作系统立刻重新进行一次CPU竞争,使其他具有相同或更高优先级的线程有机会运行。分析我们都知道Thread.sleep(n)表示让线程暂停n久,同时让出CPU,等n事件过后,线程被唤醒,进入就绪状态参与CPU竞争。但是,这个sleep(0)是什么意思?休眠0?有什么意义?在操作系统中,CPU的竞争策略有多种,作为Java程序员我们以Linux为例。Linux操作系统采用时间片循环调度算法,它的目的是为了提供公平的CPU时间分配,确保每个运行的进程或线程都有相等的机会获得CPU时间片。在该调度算法下,CPU的使用时间被划分成一个个固定长度的时间片,然后按照时间片的顺序分配给各个任务。当一个任务的时间片用完后,它会被移到就绪队列的末尾,让其他等待执行的任务有机会获得CPU时间。如果进程在时间片内阻塞或结束,则CPU立即切换。当我们调用sleep()时,它其实就

    2024-03-23
    阅读(72)
  • sleep()和wait()两个方法都是用来暂停线程,具备同样的功能,但是他们之间还是存在蛮大的区别的。区别一:所属类不同sleep()是Thread类的一个静态方法。位于Thread中,强调了它是线程中的一部分,代表了线程的一种行为方式,不涉及线程间的交互。wait()是Object类的一个方法。在Java中一切皆对象,而所有对象都可以作为一个监视器(monitor),将wait()放在Object类中,体现的是面向对象设计的原则,是一种线程间通过共享对象进行通信的机制。区别二:使用语法不同sleep()是Thread类的一个静态方法,可以随时随地用。而wait()必须配合synchronized一起使用,不然在运行时就会抛出IllegalMonitorStateException的异常,如下:publicclassWaitTest{publicstaticvoidmain(String

    2024-03-23
    阅读(59)
  • 在多线程环境下i++不是线程安全的,主要原因是因为i++不是原子性的。该操作实际上是由三步组成:读取变量i的当前值执行i+1操作将新值写回变量i假如有两个线程都执行i++的操作,他们可能会发生如下情况:线程A读取变量i的当前值,比如是5。线程A计算i+1,得到6。在线程A完成写回i之前,线程B也读取变量i的当前值(仍然是5,因为线程A尚未更新它)。线程A将其计算结果6写回变量i。线程B也将其计算结果(也是6,因为它是基于i原来的值5计算的)写回变量i。i++执行被执行了两次,理论上值应该等于7,但事实其值等于6。为了防止这种情况,我们需要保证i++的操作是原子性的,目前有三种方法来实现:使用synchronized使用ReentrantLock使用AtomicInteger使用synchronized将i++操作放在synchronized代码块中,可以确保同一时间只有一个线程在执行这个操

    2024-03-23
    阅读(63)
  • 回答通常情况下,线程池中的线程在执行过程中出现了未捕获的异常时,线程会立刻终止,线程池是不会将异常抛给使用者的。如果我们不加以处理,则可能会导致一些问题,如资源未释放,错误难以诊断等等,所以为了我们系统的健壮性和可维护性,我们一般都需要对异常进行处理,方式有如下几种:使用try-catch块:在任务中直接使用try-catch来捕获和处理异常,这是最直接的方式。UncaughtExceptionHandler:实现ThreadFactory接口,我们可以创建一个自定义的线程工厂,在这个工厂里,我们创建自定义的线程,并为这些线程设置一个UncaughtExceptionHandler。该处理器会捕获线程执行过程中未被捕获的异常。使用submit()返回Future对象:使用ExecutorService.submit()提交任务,该方法会返回一个Future对象。我们可以通过Future.g

    2024-03-23
    阅读(77)
  • 回答在Java中要终止一个正在运行的线程,有三种方法:使用退出标志。使用一个标志变量来控制线程是否正常退出任务。线程的主循环不断检测这个变量,当变量标志为停止时,线程安全退出。利用中断机制。调用线程的interrupt()来控制线程中断。强制退出。调用线程的stop()强行终止线程,不推荐这种方法,详解使用退出标志当我们使用轮询或者调用第三方库失败需要不断重试时,这些类似的场景大部分都存在类似一个这样的循环体while(true){...},一般这种场景就比较适合实用退出标志来结束线程的运行。具体方法是:设置一个volatile修饰的共享变量,当变量为true时线程正常运行,当需要中断时,将该共享变量设置为false。代码如下:publicclassMyThreadextendsThread{volatileBooleanflag=true;@Overridepublicvoidrun()

    2024-03-23
    阅读(52)
  • 我们知道线程池是一种基于池化思想管理线程的工具,它带来了一系列的好处:降低资源消耗:通过重复利用现有的线程来执行任务,减少了线程创建和销毁的开销。提高响应速度:线程的创建是需要时间和资源的,利用现有线程执行任务,省去了创建线程的时间,拿到任务后可以立刻执行。提高线程的可管理性:线程是一种稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。当我们创建好一个线程池后,线程池在执行任务的过程中会不断地创建和销毁线程(非核心线程),但是核心线程(数量由corePoolSize决定)默认会一直存活在线程池中,当没有任务的时候,这些线程会“休眠等待”,当有任务提交时,会被“唤醒”执行任务。当线程池用完以后,这些核心线程会在线程池中一直休眠等待,直到调用shutdown()来关闭线程池。所以,如果我们不调用shutdown()来关闭线程池,这些

    2024-03-23
    阅读(71)
  • ConcurrentHashMap的底层结果如下:学过ConcurrentHashMap小伙伴都知道,ConcurrentHashMap开始时是链表结构,当链表长度为8时,就开始由链表转换为红黑树。这里我们要回答两个问题:1、为什么要转为红黑树主要原因还是性能。我们知道链表的查询元素时间复杂度为O(n),这就意味着随着链表长度的增加,查找效率会逐步降低。而红黑树是一种平衡二叉树,时间复杂度为O(logn),查询效率明显高于链表。2、为什么是8的时候转换为红黑树在ConcurrentHashMap中,要满足两个条件才会由链表转换为红黑树:ConcurrentHashMap总结点数大于64这是为了避免在哈希表较小的情况下就进行转换,当哈希表较小的时候,链表的查询效率通常是可以接受的。同时红黑树中的TreeNode是链表中的Node所占空间的2倍,如果过早地转换会浪费空间。同时,红黑树的数据结构

    2024-03-23
    阅读(59)
  • 回答CopyOnWriteArrayList是一个线程安全的ArrayList,其核心思想是“写时复制”,即每当我们对列表进行修改(添加、删除、更新元素)时,它都会先创建该列表的一个副本,然后在这个副本上进行修改。修改完成后,它会将原来的列表引用指向新修改过的副本。这种机制有效减少了线程之间的竞争,因为读操作可以安全地访问列表,而不需要担心写操作同时发生的情况。copy-on-Write体现的是一种读写分离的思想。CopyOnWriteArrayList实现原理原理写时复制原理图:CopyOnWriteArrayList的实现原理比较简单,源码也很简单,我们大致看下就可以了。结构CopyOnWriteArrayList使用一个volatile类型的数组来存储数据,volatile保证这个数组引用的可见性,当数组发生变化时,其他所有线程都能立即看到这个变化。privatetransientv

    2024-03-23
    阅读(65)
  • 回答CyclicBarrier和CountdownLatch都是Java并发编程中的同步辅助类。CyclicBarrier:允许一组线程互相等待,直到所有线程都达到了一个共同的屏障点。CountdownLatch:允许一个或多个线程等待,直到在其他线程中进行的一组操作完成为止。实际上两者之间还是有一点点相同,都是一组线程等待某一个触发点完成后才能进行后续的操作。其主要区别有:1、设计理念不同CyclicBarrier:用于多个线程间的互相等待,直到所有线程都达到了一个共同的屏障点。它主要用于多个线程需要等待彼此达到某个状态后才一起继续执行的场景。比如6个人约去开会,只有6个全到了才能开会,先到的需要等待后面的同学。CountdownLatch:用于一个或多个线程等待其他线程完成一定的操作。它主要用于某个线程需要等待一个任务集合完成后才开始执行的场景。2、重用性不同CyclicBarrier

    2024-03-23
    阅读(60)
  • synchronized可以在并发环境下保证原子性、可见性和有序性,而volatile只能保证可见性和有序,那为什么有了synchronized后还需要volatile呢?主要还是synchronized存在如下两个缺点:1、性能损耗虽然synchronized做了很多优化,例如自旋锁、适应性自旋、锁消除、锁粗化、偏向锁和轻量级锁等,但无论它做了多少优化,它毕竟还是一种锁,只要是锁,在获取锁和释放锁的过程中就一定存在性能损耗。volatile是一个比较轻量级的操作,只针对变量,volatile变量的读操作的性能与普通变量几乎无差别,写操作由于需要插入内存屏障所以会慢一些,但即便如此,volatile在大多数场景下也比锁的开销要低。2、产生阻塞无论是同步方法(基于ACC_SYNCHRONIZED)还是同步代码块(基于monitorenter、monitorexit)都是基于Monitor实现

    2024-03-23
    阅读(50)
  • 回答volatile是一种轻量级的同步机制,它能保证共享变量的可见性,同时禁止重排序保证了操作的有序性,但是它无法保证原子性。所以使用volatile必须要满足这两个条件:写入变量不依赖当前值。变量不参与与其他变量的不变性条件。volatile比较适合多个线程读,一个线程写的场合,典型的场景有如下几个:状态标志重检查锁定的单例模式开销较低的“读-写锁”策略详解volatile使用条件要想正确安全地使用volatile,必须要具备这两个条件:写入变量不依赖当前值:变量的新值不能依赖于之前的旧值。如果变量的当前值与新值之间存在依赖关系,那么仅使用volatile是不够的,因为它不能保证一系列操作的原子性。比如i++。变量不参与与其他变量的不变性条件:如果一个变量是与其他变量共同参与不变性条件的一部分,那么简单地声明变量为volatile是不够的。第一个条件很好理解,第二个条件这里需要解释下。“

    2024-03-23
    阅读(57)
  • DCL,Double-CheckedLocking,即双重检查锁定。很多小伙伴在单例模式中用到它,代码如下:publicclassSingleton{privatevolatilestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){//1synchronized(Singleton.class){//2if(instance==null){//3instance=newSingleton();//4}}}returninstance;}}对于这段代码其实可问的有很多,比如:为什么构造函数要使用private?为什么要进行两次if(instance==null)?synchronized(Singleton.class)在这里的作用是什么?synch

    2024-03-23
    阅读(66)
  • 回答所谓原子性,就是一个操作或者多个操作,要么完全执行,要么完全不执行,在执行过程中是不能被其他因素打断或者插入。volatile我们知道它能保证可见性和有序性的,但是对于原子性,它无法保证。详细分析原子性原子性的核心概念就两次:完整、不可分割。也就是某个线程在正在做某个业务时,要么它这个业务完整执行完,要么完全不执行,不允许处于一个中间状态。我们最最经典的原子操作是银行账户转账:从账户A向账户B转入10000元,它只允许存在两种情况:转账成功:从账户A扣掉10000元,给账户B增加10000元。转账失败:账户A、账户B均没有变化。不可能存在一个所谓的中间状态,如从账户A扣掉10000元,但是账户B不增加10000元。我们看下面这段代码:i=1;//1j=i;//2i++;//3i=j+1;//4这四句代码中哪些是原子操作,哪些不是,首先在单线程环境下,我们可以认为上面四个全部都是原子操作

    2024-03-23
    阅读(68)
  • 回答volatile是Java提供的一种轻量级的同步机制,与synchronized修饰方法、代码块不同,volatile只能用来修饰变量。当一个变量被声明为volatile后,它会确保所有线程看到该变量的值都是一致的,即一个线程更新了这个变量的值,其他线程可以立即能够看到这个更新。volatile的主要特性是:保证线程可见性和有序性,但是不保证原子性:可见性:保证一个线程对volatile变量的修改,对其他线程来说是立即可见的。有序性:禁止指令重排序。在volatile变量上的读写操作不会被编译器或处理器重排序,保证了代码的执行顺序与程序的顺序相同。非原子性:volatile不能保证复合操作的原子性。比如i++这样的操作,它涉及到读取-修改-写入的多步操作,volatile不保证其原子性。volatile的实现原理依赖于内存屏障和缓存一致性协议(MESI)。内存屏障:volatile变量

    2024-03-23
    阅读(63)
  • 回答指令重排序是指在执行程序时,为了提高性能,处理器可能会改变指令的执行顺序。volatile通过内存屏障来防止指令重排序,保证有序性。volatile提供了两种内存屏障:写内存屏障:写内存屏障设置在写volatile变量之后。它确保对该volatile变量的所有写操作在任何后续对同一变量的读操作之前完成。这就意味着,在写内存屏障之前的所有普通写操作(不仅仅是对volatile变量的写操作)都将在写入volatile变量之前完成。读内存屏障:读内存屏障设置在读volatile变量之前。它确保对该volatile变量的所有读操作在任何先前的写操作之后完成。这就意味着,所有在读内存屏障之后的普通读操作(不仅仅是对volatile变量的读操作)都将在读取volatile变量之后进行。这种内存屏障阻止了指令重排序,确保在volatile变量之前的操作不会被重排序到其之后,同时也确保在volatil

    2024-03-23
    阅读(80)
  • 什么是可见性?可见性是指一个线程对共享变量所作的修改能够被其他线程及时地看到。在单核时代,其实是不存在可见性问题的,因为所有的线程都是在一个CPU中工作的,一个线程的写操作对于其他的线程一定是可见的。但是,在多核时代,每个CPU都有自己的缓存。一个线程对共享变量的修改可能只是在它所在CPU的本地缓存中进行,而不是在主内存中进行。这就可能导致其他线程看不到这个修改,从而引发可见性问题。解决可见性的方案有两种:使用volatile修饰共享变量:一个变量被声明为volatile后,对这个变量的读写操作都是在主内存中进行的,从而保证了不同线程之间对该变量修改的可见性。使用同步机制,比如锁或者synchronized。当一个线程成功获取锁进入一个同步块时,它会看到由其他线程在相同同步块内对共享变量的修改。volatile是如何保证可见性的?这部分内容在volatile的实现原理中有,但是为了更好地阅

    2024-03-23
    阅读(76)
  • 在JDK1.6之前,synchronized是一个重量级、效率比较低下的锁,但是在JDK1.6后,JVM为了提高synchronized的性能,HotSpot虚拟机开发团队做了大量的优化工作,如自旋锁、自适应性自旋、锁消除、锁粗化、偏向锁、轻量级锁。其中偏向锁、轻量级锁已经在文章synchronized的锁升级过程是怎样的?讲解过,这里就不做说明了。自旋锁线程的阻塞和唤醒都需要依赖底层操作系统,会涉及到用户态、内核态的切换,这种操作是非常消耗资源的。如果一个同步代码块执行的时间非常短,为了这一段很短的时间去频繁阻塞和唤醒线程其实时非常不值得的。为了解决这种很短时间的任务,Java引入自旋锁。何谓自旋锁?就是当一个线程尝试去获取某个锁对象时,如果该锁对象被其他线程持有,那么该线程不会被挂起,而是一直循环检测锁是否已被释放,通过自旋而不是挂起线程,可以减少线程上下文切换的开销。。需要注意的是,

    2024-03-23
    阅读(63)
  • 在JDK1.6之前,synchronized是一个重量级、效率比较低下的锁,但是在JDK1.6后,JVM为了提高锁的获取与释放效,,对synchronized进行了优化,引入了偏向锁和轻量级锁,至此,锁的状态有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。锁升级就是无锁—>偏向锁—>轻量级锁—>重量级锁的一个过程,注意,锁只能升级,不能降级。原理详解对象头HotSpot虚拟机中,对象在内存中存储布局可以分为三块区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding):对象头:分为MarkWord和对象指针MarkWord:存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象指针:存储指向类元数据的指针,使得能够访问对象属于的类的信息。实例数据:存

    2024-03-23
    阅读(69)
  • synchronized的实现原理是怎样的?这篇面试题大明哥已经详细介绍过synchronized的实现原理了,同时大明哥也在里面简单描述了synchronized锁住的是什么,但是依然有很多同学还是不明白synchronized锁住的是什么。现在大明哥告诉你,Java一切皆对象,所以synchronized最终锁定的是对象!只不过由于synchronized的用法不同,锁住的对象不同。我们先看一个简单的例子:publicclassSynchronizedTest{publicsynchronizedvoidtest(){System.out.println(Thread.currentThread().getName()+"---testbegin...");try{TimeUnit.SECONDS.sleep(3);}catch(InterruptedExcepti

    2024-03-23
    阅读(81)
  • 回答synchronized是Java中一个重量级的关键字,它用于实现线程同步,确保多线程环境下对共享资源的安全访问。它可以修饰方法或代码块,保证一次只有一个线程可以执行同步方法或代码块内的代码。synchronized有两种形式上锁,一个是对方法上锁,一个是对代码块上锁。其实他们底层实现原理都是一样的。在进入同步代码之前先获取锁,锁计数+1,执行完同步代码后释放锁,锁计数-1,如果获取失败就阻塞式等待锁的释放。他们的不同之处在于他们在同步块的识别方式有所不同。当一个方法被synchronized修饰时,它的方法标志中会包含ACC_SYNCHRONIZED标志。当某个线程要访问方法时,会首先检查是否有ACC_SYNCHRONIZED设置,如果有,则需要先获取监视器锁,获取成功后才能执行方法,方法执行完成后再释放监视器锁。如果在该线程执行同步方法期间,有其他线程来请求执行方法,会因为无法获取

    2024-03-23
    阅读(68)
  • ThreadLocal为Java并发安全提供了一种新的思路,它采用线程隔离的机制,每个线程都有拥有自己独立的ThreadLocal副本,线程之间互不干扰,每个线程都可以独立地、安全地操作这些变量,而不会影响其他线程。ThreadLocal在实际工作中还是有比较多的应用场景,典型的有如下几个:数据库连接或会话信息:很多ORM框架,如Hibernate、Mybatis,都是使用ThreadLocal来存储和管理数据库会话的。这样就可以每个线程都有自己独立的的数据库连接,避免了多线程之间的数据库连接冲突。用户身份认证:用户登录成功后,一种常规的做法是是将用户信息存储在Session中,如果我们需要获取用户的登录信息,就需要先通过HttpServletRequest获取到Session,然后才能通过Session获取到用户信息,同时我们需要在每一个需要用户信息的接口都要加上这个参数,这种方式就显得

    2024-03-23
    阅读(75)