线程基础面试题

线程面试题

这是一些基础的线程面试题,对于后面相对难理解部分有参考csdn写的好的博客,有些也是自己的理解,我也只是浅层次的做了笔记,想要深层次理解,还需要阅读相关书籍

创建线程的四种方法:

  • Thread:继承Thread类
  • Runnable:实现Runnable接口
  • Callable:实现Callable接口
  • Executor框架:通过线程池创建线程

并行和并发有什么区别?

  • 并发:又称共行性,是指能处理多个同时性活动的能力,并发事件之间不一定要同一时刻发生
  • 并行:是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行

线程和进程的区别?

  • 进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
  • 进程间相互独立进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。
  • 线程的创建和切换开销比进程小。

守护线程是什么?

用户线程即运行在前台的线程,而守护线程是运行在后台的线程。

守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。
虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。
另外有几点需要注意:

  • setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。
  • 在守护线程中产生的新线程也是守护线程。
  • 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。

说一下 runnable 和 callable 有什么区别?

  • Runnable执行方法是run(),Callable是call()
  • 实现Runnable接口的任务线程无返回值;实现Callable接口的任务线程能返回执行结果
  • call方法可以抛出异常,run方法若有异常只能在内部消化

线程有哪些状态?

新建、就绪、运行、阻塞、死亡五个状态

sleep() 和 wait() 有什么区别?

  1. 这两个方法来自不同的类分别是Thread和Object
    注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程
  2. 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
synchronized(x){
      x.notify()
     //或者wait()
 }
  1. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。

notify()和 notifyAll()有什么区别?

Java提供了两个方法notify和notifyAll来唤醒在某些条件下等待的线程,你可以使用它们中的任何 一个,但是Java中的notify和notifyAll之间存在细微差别,这使得它成为Java中流行的多线程面试问题之一。当你调用notify时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用notifyAll方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定,这就是为什么在循环上调用wait,因为如果多个线程被唤醒,那么线程是将获得锁定将首先执行,它可能会重置等待条件,这将迫使后续线程等待。因此,notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。
何时在Java中使用notify和notifyAll
如果所有线程都在等待相同的条件,并且一次只有一个线程可以从条件变为true,则可以使用notify over notifyAll。
在这种情况下,notify是优于notifyAll 因为唤醒所有这些因为我们知道只有一个线程会受益而所有其他线程将再次等待,所以调用notifyAll方法只是浪费CPU。
虽然这看起来很合理,但仍有一个警告,即无意中的接收者吞下了关键通知。通过使用notifyAll,我们确保所有收件人都会收到通知

线程的 run()和 start()有什么区别?

线程的run()方法是由java虚拟机直接调用的,如果我们没有启动线程(没有调用线程的start()方法)而是在应用代码中直接调用run()方法,那么这个线程的run()方法其实运行在当前线程(即run()方法的调用方所在的线程)之中,而不是运行在其自身的线程中,从而违背了创建线程的初衷;

创建线程池有哪几种方式?

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

线程池都有哪些状态?

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

  1. RUNNING
    (1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
    (2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

  2. SHUTDOWN
    (1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING 变为 SHUTDOWN。

  3. STOP
    (1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) 变为STOP。

  4. TIDYING
    (1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN变为TIDYING。
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP变为 TIDYING。

  5. TERMINATED
    (1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
    (2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING变为TERMINATED。

线程池中 submit()和 execute()方法有什么区别?

  1. execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既 能提交Runnable类型任务也能提交Callable类型任务。
  2. execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
  3. execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

在 java 程序中怎么保证多线程的运行安全?

  1. 线程安全在三个方面体现

    1. 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
    2. 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
    3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
      接下来,依次分析。
  2. 原子性—atomic
    JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。它们是通过CAS完成原子性。

  3. 原子性—synchronized
    synchronized是一种同步锁,通过锁实现原子操作。
    JDK提供锁分两种:一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。
    synchronized修饰的对象有四种:
    (1)修饰代码块,作用于调用的对象;
    (2)修饰方法,作用于调用的对象;
    (3)修饰静态方法,作用于所有对象;
    (4)修饰类,作用于所有对象。

  4. 可见性—volatile
    对于可见性,JVM提供了synchronized和volatile。这里我们看volatile。
    (1)volatile的可见性是通过内存屏障和禁止重排序实现的
    volatile会在写操作时,会在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存:
    在这里插入图片描述

volatile在进行读操作时,会在读操作前加一条load指令,从内存中读取共享变量:
在这里插入图片描述

(2)但是volatile不是原子性的,进行++操作不是安全的
五、有序性
有序性是指,在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile、synchronized、lock保证有序性。另外,JMM具有先天的有序性,即不需要通过任何手段就可以得到保证的有序性。这称为happens-before原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性。虚拟机可以随意地对它们进行重排序。
happens-before原则:

1.程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行。
2.锁定规则:一个unlock操作happen—before后面对同一个锁的lock操作。
3.volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4.线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5.线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6.线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7.对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8.传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁的原因以及怎么防止死锁?

java 死锁产生的四个必要条件 :

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
    上面我们提到的这四个条件在死锁时会同时发生。也就是说,只要有一个必要条件不满足,则死锁就可以排除。

办法:解决死锁的方法分为死锁的预防,避免,检测与恢复三种(注意:死锁的检测与恢复是一个方法)。
①死锁的预防:
死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
〈1〉打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
〈2〉打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
〈3〉打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。
〈4〉打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。
②死锁的避免死锁的避免
上面我们讲到的死锁预防是排除死锁的静态策略,它使产生死锁的四个必要条件不能同时具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生。下面我们介绍排除死锁的动态策略–死锁的避免,它不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配。就是说,在资源分配过程中若预测有发生死锁的可能性,则加以避免。这种方法的关键是确定资源分配的安全性。
〈1〉安全序列
〈2〉银行家算法
③死锁的检测与恢复
一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal是使线程之间变量的隔离性
ThreadLocal中有四个主要的方法:get,set,remove,initialValue,下面主要讨论前两种。
ThreadLocal类中有一个静态内部类ThreadLocalMap(可以认为这是一个map),但是ThreadLocalMap的实例threadlocals却在Thread类中。
从源码中可知,每个线程都有一个ThreadLocalMap的实例threadlocals。在set方法中首先通过当前线程,拿到当前线程的ThreadLocalMap对象。Key为当前ThreadLocal对象,value就是需要并发操作的对象。(至于为什么用map存储数据,而不用list或其他单列集合是因为单列集合的底层是由map来实现的)。实际上,设置到ThreadLocal中的数据,其实是保存在当前线程的ThreadLocalMap这个Map中。
在这里插入图片描述
get方法就是先获取当前线程的ThreadLocalMap对象,然后将自己作为Key取得内部的实际数据。

ThreadLocal内部用的是一个Entry数组实现的,可以在ThreadLocal存取自己的一些值,如果有一些太大的对象设置到ThreadLocal中(实际是保存在线程持有的ThreadLocals Map中),此时可能会出现内存泄漏。对于ThreadLocal变量,我们可以手动的将其置为Null,比如tl =null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。
ThreadLocalMap是一个类似HashMap的东西,更准确的说,它更加类似WeakHashMap。ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱的多的引用。Java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap内部由一系列Entry构成,每一个Entry都是WeakReference
在这里插入图片描述
ThreadLocal的应用场景
Spring数据库连接:在多线程中,如果使用懒汉式的单例模式创建Connection对象,由于该对象是共享的,那么必须要使用同步方法保证线程安全,这样当一个线程在连接数据库时,那么另外一个线程只能等待。这样就造成性能降低。如果改为哪里要连接数据库就来进行连接,那么就会频繁的对数据库进行连接,性能还是不高。这时使用ThreadLocal就可以既可以保证线程安全又可以让性能不会太低。但是ThreadLocal的缺点时占用了较多的空间。

说一下 synchronized 底层实现原理?

JDK提供锁分两种:一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。
首先来说什么是monitor ?
在一个实例对象中,对象分为对象头、实力变量和填充数据组成,而对象头中有轻量级锁、重量级锁、GC标记和偏向锁、重量级锁是指向互斥量的指针(重量级锁),其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联。
从synchronized锁同步块和锁同步方法来解释synchronized实现原理

①synchronized同步块:

public class SynchronizedDemo {
     public void method() {
         synchronized (this) {
             System.out.println("Method 1 start");
         }
     }
 }

反编译结果如下:
在这里插入图片描述

  1. monitorenter :
    每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  2. monitorexit: 
    执行monitorexit的线程必须是objectref所对应的monitor的所有者。
    指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

②synchronized同步方法:

public class SynchronizedMethod {
     public synchronized void method() {
         System.out.println("Hello World!");
    }
 }

反编译结果:
在这里插入图片描述

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

synchronized 和 volatile 的区别是什么?

  1. :volatile是一个类型修饰符,用来修饰被不同线程访问和修改的变量,当值被一个线程更改后,该值会在缓存中更新,保持一致。表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
    虽说这个Volatile关键字可以解决多线程环境下的同步问题,不过这也是相对的,因为它不具有操作的原子性,也就是它不适合在对该变量的写操作依赖于变量本身自己。举个最简单的栗子:在进行计数操作时count++,实际是count=count+1;,count最终的值依赖于它本身的值。所以使用volatile修饰的变量在进行这么一系列的操作的时候,就有并发的问题
    举个栗子:因为它不具有操作的原子性,有可能1号线程在即将进行写操作时count值为4;而2号线程就恰好获取了写操作之前的值4,所以1号线程在完成它的写操作后count值就为5了,而在2号线程中count的值还为4,即使2号线程已经完成了写操作count还是为5,而我们期望的是count最终为6,所以这样就有并发的问题。而如果count换成这样:count=num+1;假设num是同步的,那么这样count就没有并发的问题的,只要最终的值不依赖自己本身。
  2. :synchronized
    synchronized叫做同步锁,是Lock的一个简化版本,由于是简化版本,那么性能肯定是不如Lock的,不过它操作起来方便,只需要在一个方法或把需要同步的代码块包装在它内部,那么这段代码就是同步的了,所有线程对这块区域的代码访问必须先持有锁才能进入,否则则拦截在外面等待正在持有锁的线程处理完毕再获取锁进入,正因为它基于这种阻塞的策略,所以它的性能不太好,但是由于操作上的优势,只需要简单的声明一下即可,而且被它声明的代码块也是具有操作的原子性。

synchronized 和 Lock 有什么区别?

可重入锁:在执行对象中所有同步方法不用再次获得锁
可中断锁:在等待获取锁过程中可中断
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

类别synchronizedLock
存在层次java的关键字,在jvm层面上是一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁。2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获取锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可)
性能少量同步大量同步

synchronized 和 ReentrantLock 区别是什么?

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
①功能区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
②性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
顺便简单介绍下ReentrantLock:
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

  1. 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
  2. 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
  3. 锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
    ReenTrantLock实现的原理:

简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值