Java面试题总结 | Java面试题总结4-多线程模块(持续更新)

多线程

线程互斥和线程同步

线程互斥:当多个线程需要访问同一资源时,要求在一个时间段内只能允许一个线程来操作共享资源,操作完毕后别的线程才能读取该资源,这叫线程的互斥。我们需要使用synchronized来给共享区域加锁,确保共享资源安全。

线程同步:多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问。

线程和进程的区别?

(1)一个线程从属于一个进程;一个进程可以包含多个线程。

(2)一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。

(3)进程是系统资源调度的最小单位;线程CPU调度的最小单位。

(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。

(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。

(6)进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。

(7)通信方式不一样。

(8)进程适应于多核、多机分布;线程适用于多核

什么是上下文切换

CPU通过分配时间片来执行任务,当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态。
——任务从保存到再加载的过程就是一次上下文切换

切出: 一个线程被剥夺处理器的使用权而被暂停运行
切入: 一个线程被系统选中占用处理器开始或继续运行

通过调用下列方法会导致自发性上下文切换:

  • Thread.sleep()
  • Object.wait()
  • Thread.yeild()
  • Thread.join()
  • LockSupport.park()

请你说说多线程

线程是程序执行的基本单元,一个进程中会拥有多个线程,他可以使进程并发的处理多个任务。每个线程有自己独立的程序计数器,本地方法栈和虚拟机栈,线程之间共享进程的堆和方法区。 多线程的好处:当一个线程进入阻塞或者等待状态时,其他的线程可以获取CPU的执行权,提高了CPU的利用率。 多线程的缺点:可能产生死锁;频繁的上下文切换可能会造成资源的浪费、线程不安全问题。

说说怎么保证线程安全

可以通过原子类、volatile、锁的方式来实现线程安全

  • 原子类:是JUC包下的一系列原子类,使用该类可以简化线程同步,原子类主要是通过CAS的方式来实现线程安全的更新共享变量 【CAS:通过预期值与内存值的比较来判断是否修改】
  • vloatile关键字:被volatile修饰的关键字保证了可见性,禁止了指令重排,保证了单个变量读写的安全性
    • 可见性:被volatile修饰的变量,当发生写操作时,JVM会把线程本地内存中的变量强制刷新到主内存中,同时会使其他线程中volatile变量失效。
    • 禁止指令重拍:是通过内存屏障实现的,当程序执行到volatile变量的时候,在其前面的操作已经结束了,并且结果是对后面可见的,并且后面的操作肯定还没有执行;在进行指令优化时,不可以将volatile变量前面的语句拿到后面执行,同样也不能把后面的语句拿到前面执行。
  • 锁:锁则可以保证临界区内的多个共享变量线程安全。Java中常用的锁有两种:synchronized+juc包下的lock锁。
    • synchronized锁是互斥锁,可作用于方法和代码块上,在1.6之后引入轻量级锁、偏向锁等优化
    • lock锁是一个接口,通过lock和unlock 方法进行加锁和解锁,基于AQS实现,其加锁解锁就是操作AQS的state变量,并且将阻塞队列存在AQS的双向队列中
  • 还可以使用本地存储的方式:通过使用ThreadLocal类定义线程的局部变量,ThreadLocal为变量在每一个线程创建了一个副本,那么每个线程可以通过访问自己内部的局部变量。

说一说java并发

并发是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。。Java并发则由多线程实现的。

为什么要使用多线程?

线程可以比作是轻量级的进程,是程序执行的最小单元,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 也意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

使用多线程可能会带来什么问题?

可能产生死锁;频繁的上下文切换可能会造成资源的浪费、线程不安全问题。

创建线程的三种方法

自定义类继承 Thread 类;重写 run 方法编写线程执行体;创建线程对象,调用 start 方法启动线程

实现 Runnable 接口;实现 run 方法编写线程执行体;创建 Runnable 接口的实现类对象;创建线程对象,调用 start 方法启动线程

实现 Callable接口;实现 call方法编写线程执行体;创建 Callable接口的实现类对象,将对象作为参数创建FurtureTask对象,再将task对象作为参数创建thread对象,调用start方法开启线程,还可以使用task对象的get方法获取返回值。

实现接口更推荐使用:同一个对象可以被多个线程使用

线程三种创建方式的区别

(1)继承Thread类与实现Runnable接口:相比继承Thread类,由于Java类是单继承,实现Runnable接口的方法更显灵活,实用。

(2)Callable与其它两种的区别:主要是在于Callable是有返回值的,至于接不接收由主线程决定。而主线程可以通过FutureTask类的get()方法去接收,若是调用get()方法接收返回值,那么主线程就不再会和创建的线程并发执行,而是会等待该线程执行完获取到返回值之后再继续执行,这就有别于前两种方式。

线程池如何执行线程

execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。

execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。

execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

线程安全的理解

当过个线程访问某个方法的时候,不论通过怎么样的调用方式或者这些线程如何的交替执行,我们在主程序中都不需要进行任何的同步操作,得到的结果也是预期想要的,这就是线程安全

有三个线程T1,T2,T3,如何保证顺序执行

要保证T1、T2、T3三个线程顺序执行,可以利用Thread类的join方法。你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

public class JoinTest2 {
    
    
 
    // 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
 
    public static void main(String[] args) {
    
    
 
        final Thread t1 = new Thread(new Runnable() {
    
    
 
            @Override
            public void run() {
    
    
                System.out.println("t1");
            }
        });
        final Thread t2 = new Thread(new Runnable() {
    
    
 
            @Override
            public void run() {
    
    
                try {
    
    
                    // 引用t1线程,等待t1线程执行完
                    t1.join();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("t2");
            }
        });
        Thread t3 = new Thread(new Runnable() {
    
    
 
            @Override
            public void run() {
    
    
                try {
    
    
                    // 引用t2线程,等待t2线程执行完
                    t2.join();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("t3");
            }
        });
        t3.start();//这里三个线程的启动顺序可以任意,大家可以试下!
        t2.start();
        t1.start();
    }
}

如何创建线程池

  • 使用ThreadPoolExecutor
  • 通过 Executor 框架的工具类 Executors 来实现
    • FixedThreadPool : 该方法返回一个固定线程数量的线程池
    • SingleThreadExecutor: 方法返回一个只有一个线程的线程池
    • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池
    • newScheduledThreadPool:定义以及周期执行线程的线程池

线程池如何执行线程

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完

join方法的作用?

答:Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

(8条消息) 多线程面试题(一):现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?_夏至&未至的博客-CSDN博客_现在有t1t2t3三个线程

Thread类的join方法

  • wait是object类的方法
  • join是Thread类的方法
  • Wait的用法:当一个线程调用wait的时候,会释放同步锁,然后该线程进入等待状态。其他挂起的线程会竞争这个锁,得到锁的继续执行。
  • join的用法:一个线程A运行中调用线程B.join()方法,则A线程停止执行,一直等到B线程执行完毕,A线程才会继续执行!!
  • join方法的实现,利用了wait()和notifyAll()方法。

请你说说死锁定义及发生的条件

死锁:两个线程互相抢夺对方的资源并且不释放自己的资源,从而导致死锁的产生。死锁产生的条件:

  • 互斥:一个资源在同一个时刻只能由一个线程执行

  • 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放

  • 循环等待:发生死锁时所有的线程都会形成一个死循环,一直阻塞。

  • 不可剥夺条件:线程对所获得的资源在未使用完时不能被其他线程剥夺,只能自己释放。

避免死锁的方法就是破坏死锁产生的条件。

如何解决死锁

1.破坏互斥条件:破坏不了,临界资源本来就用于互斥,所以就不从这儿下手了

2.破坏占有并等待条件:一次性申请完所有的资源,这样就不会说导致占有了一个或多个,等待着另一个的情况了

3.破坏不剥夺条件:让持有一部分资源的线程,再申请不到资源的时候主动放弃自己已经获得了的资源

4.破坏循环等待条件:获取临界资源的时候,按照统一的顺序来,比如上述例子,让两个线程都先争夺资源s1,再争夺资源s2.

线程的周期

新建-就绪-运行-阻塞-死亡

新建:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态

就绪:当线程对象 调用了start()方法之后,该线程处于就绪状态,至于该线程何时开始运行,取决于JVM里线程调度器的调度。

运行:如果处于就绪状态的线程获得了处理器的使用权,开始执行run()方法的线程执行体,则该线程处于运行状态

阻塞:系统会给每个可执行的线程一个小时间段来处理任务。当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会,线程将会进入阻塞状态,或者当线程吊用了sleep、wait等方法时主动放弃使用权;或者调用阻塞IO的方式

死亡:

  • run()或call()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁,通常不推荐使用。

线程的通信方式

线程通信主要可以分为三种方式,分别为共享内存消息传递管道流。每种方式有不同的方法来实现

  • 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。

volatile共享内存

  • 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。

wait/notify等待通知方式
join方式

  • 管道流

管道输入/输出流的形式

共享内存

在java中,所有堆内存中的所有的数据(实例域、静态域和数组元素)存放在主内存中可以在线程之间共享,一些局部变量、方法中定义的参数存放在本地内存中不会在线程间共享。线程之间的共享变量存储在主内存中,本地内存存储了共享变量的副本。如果线程A要和线程B通信,则需要经过以下步骤

①线程A把本地内存A更新过的共享变量刷新到主内存中
②线程B到内存中去读取线程A之前已更新过的共享变量。

这保证了线程间的通信必须经过主内存。下面引出我们要学习的关键字volatile

volatile有一个关键的特性:保证内存可见性,即多个线程访问内存中的同一个被volatile关键字修饰的变量时,当某一个线程修改完该变量后,需要先将这个最新修改的值写回到主内存,从而保证下一个读取该变量的线程取得的就是主内存中该数据的最新值,这样就保证线程之间的透明性,便于线程通信。

消息传递

wait/notify等待通知方式

从字面上理解,等待通知机制就是将处于等待状态的线程将由其它线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。最典型的例子生产者–消费者模式

等待/通知机制提供了三个方法用于线程间的通信

  • wait()当前线程释放锁并进入等待(阻塞)状态;
  • notify()唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后继续竞争锁;
  • notifyAll()唤醒所有正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后继续竞争锁

等待/通知机制是指一个线程A调用了对象Object的wait()方法进入等待状态,而另一线程B调用了对象Object的notify()或者notifyAll()方法,当线程A收到通知后就可以从对象Object的wait()方法返回,进而执行后序的操作。线程间的通信需要对象Object来完成,对象中的wait()、notify()、notifyAll()方法就如同开关信号,用来完成等待方和通知方的交互。

使用wait()、notify()和notifyAll()需要注意以下细节

  • 使用wait()、notify()和notifyAll()需要先调用对象加锁
  • 调用wait()方法后,线程状态由Running变成Waiting,并将当前线程放置到对象的等待队列
  • notify()和notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()和notifyAll()的线程释放锁之后等待线程才有机会从wait()返回
  • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部转移到同步队列,被移到的线程状态由Waiting变为Blocked。
  • 从wait()方法返回的前提是获得调用对象的锁

join方式

join()方法的作用是:在当前线程A调用线程B的join()方法后,会让当前线程A阻塞,直到线程B的逻辑执行完成,A线程才会解除阻塞,然后继续执行自己的业务逻辑,这样做可以节省计算机中资源。

每个线程的终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join方法返回,实际上,这里涉及了等待/通知机制,即下一个线程的执行需要接受前驱线程结束的通知。

管道输入/输出流

管道流是是一种使用比较少的线程间通信方式,管道输入/输出流和普通文件输入/输出流或者网络输出/输出流不同之处在于,它主要用于线程之间的数据传输,传输的媒介为管道。

管道输入/输出流主要包括4种具体的实现:PipedOutputStrean、PipedInputStrean、PipedReader和PipedWriter,前两种面向字节,后两种面向字符。

java的管道的输入和输出实际上使用的是一个循环缓冲数组来实现的,默认为1024,输入流从这个数组中读取数据,输出流从这个数组中写入数据,当这个缓冲数组已满的时候,输出流所在的线程就会被阻塞,当向这个缓冲数组为空时,输入流所在的线程就会被阻塞。

img

buffer:缓冲数组,默认为1024
out:从缓冲数组中读数据
in:从缓冲数组中写数据

线程的通信机制

说一说Java多线程之间的通信方式

在Java中线程通信主要有以下三种方式:

  1. wait()、notify()、notifyAll()

    如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。这三个方法都不是Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作。并且因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。另外,这三个方法都是本地方法,并且被final修饰,无法被重写。

    wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

    每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

  2. await()、signal()、signalAll()

    如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。

    Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,await()/signal()/signalAll() 与 wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

  3. BlockingQueue

    Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

    程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

线程之间的状态转换需要通信机制

多个线程并发执行时在默认情况下是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

何保证线程间通信有效利用资源?

多个线程在处理同一个资源,并且任务不同时(如一个生产,一个消费),需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制

什么是等待唤醒机制

在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用notifyAll()来唤醒所有的等待线程。

等待与唤醒机制涉及的三个方法

重点强调:以下三个方法wait、notify、notifyAll都是Object的方法

  • 调用wait方法,线程主动放弃CPU的执行权,即使CPU空闲也不会被调度,会进入WaitSet中(关于WaitSet见下文),最重要的是同时会释放锁

  • wait()会让线程进入无限等待状态(WAITING),只能等待notify或notifyAll来唤醒,即从WaitSet中释放出来,重新进入到调度队列中

  • wait(long)或wait(long,int)会让线程进入计时等待状态(TIMED_WAITING),在时间未到时,它可能被notify或notifyAll唤醒,否则,时间到了,它会自动苏醒,进入调度队列

  • 同wait一样,notify和notifyAll必须由持有锁的对象来调用,即也只能在synchronized代码块或方法中使用

sleep和wait的区别

wait和sleep方法的联系:

都会让程序暂停执行,放弃CPU执行权,都可以进入有超时时间的等待状态(TIMED_WAITING)wait和sleep方法的区别:

1.sleep是Thread类的方法,wait是Object类中定义的方法

2.sleep()方法可以在任何地方使用;而wait()方法只能在synchronized方法或synchronized块中使用

3.最核心的区别:Thread.sleep只会让出CPU,不会导致锁行为的改变;Object.wait不仅让出CPU,还会释放已经占有的同步资源锁,sleep不会释放锁、wait会自动释放锁

4.sleep在调用的时候必须传入时间,时间到之后,线程会自动苏醒;而wait有两种方法:无限等待的wait()和有超时时间的等待wait(long),wait()被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法,wait(long)超时后线程会自动苏醒。

5.wait 通常被用于线程间通信,sleep 通常被用于程序暂停执行

公平锁和非公平锁的实现

Java语言中有许多原生线程安全的数据结构,比如ArrayBlockingQueue、CopyOnWriteArrayList、LinkedBlockingQueue,它们线程安全的实现方式并非通过synchronized关键字,而是通过java.util.concurrent.locks.ReentrantLock来实现。

ReentrantLock的实现是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的。 其可重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。

ReentrantLock类里一共由三部分组成。

  • ReentrantLock类中有三个内部类。
  • Sync:继承AQS。
  • NonfairSync:继承Sync。
  • FairSync:继承Sync。

公平锁和非公平锁在锁的获取上都使用到了 volatile 关键字修饰的state字段, 这是保证多线程环境下锁的获取与否的核心。但是当并发情况下多个线程都读取到 state == 0时,则必须用到CAS技术,一门CPU的原子锁技术,可通过CPU对共享变量加锁的形式,实现数据变更的原子操作。volatile 和 CAS的结合是并发抢占的关键。

第二种解释

在Java中实现锁的方式有两种,一种是使用Java自带的关键字synchronized对相应的类或者方法以及代码块进行加锁,另一种是ReentrantLock,前者只能是非公平锁,而后者是默认非公平但可实现公平的一把锁。

ReentrantLock是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的,并且它的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS),AQS使用一个整形的volatile变量state来维护同步状态,这个volatile变量是实现ReentrantLock的关键。我们来看一下ReentrantLock的类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZ6PHXID-1681702198241)(D:/学习/JAVA/面经/面试题整理版本.assets/reentrantlock-2.jpg)]

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • tryAcquire 是一个抽象方法,是公平与非公平的实现原理所在。
  • addWaiter 是将当前线程结点加入等待队列之中。公平锁在锁释放后会严格按照等到队列去取后续值,而非公平锁在对于新晋线程有很大优势。
  • acquireQueued 在多次循环中尝试获取到锁或者将当前线程阻塞。
  • selfInterrupt 如果线程在阻塞期间发生了中断,调用 Thread.currentThread().interrupt() 中断当前线程。

公平锁和非公平锁在说的获取上都使用到了 volatile 关键字修饰的state字段, 这是保证多线程环境下锁的获取与否的核心。但是当并发情况下多个线程都读取到 state == 0时,则必须用到CAS技术,一门CPU的原子锁技术,可通过CPU对共享变量加锁的形式,实现数据变更的原子操作。volatile 和 CAS的结合是并发抢占的关键。

  • 公平锁FairSync

    公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,如果有, 当前线程会执行如下步骤:

    if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
          
                   
        setExclusiveOwnerThread(current);
        return true; 
    }
    

    其中hasQueuedPredecessors是用于检查是否有等待队列的:

    public final boolean hasQueuedPredecessors() {
          
          
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    
  • 非公平锁NonfairSync

    非公平锁在实现的时候多次强调随机抢占:

    if (c == 0) {
          
          
        if (compareAndSetState(0, acquires)) {
          
          
            setExclusiveOwnerThread(current);
            return true; 
        }
    } 
    

    与公平锁的区别在于新晋获取锁的进程会有多次机会去抢占锁,被加入了等待队列后则跟公平锁没有区别。

公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

AQS

AQS 使用一个 int 成员变量来表示同步状态,这个变量被volatile所修饰,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

状态信息通过 protected 类型的getState()setState()compareAndSetState() 进行操作

//返回同步状态的当前值
protected final int getState() {
    
    
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) {
    
    
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    
    
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 定义两种资源共享方式

1)Exclusive(独占)

只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:

  • 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。

ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。

总结:公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

2)Share(共享)

多个线程可同时执行,如 Semaphore/CountDownLatchSemaphoreCountDownLatChCyclicBarrierReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。

AQS的设计模式为模板方法

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:

protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。

各个锁的实现,都回去重写这些钩子方法来达到不同的效果

ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock()state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。

再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown() 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

CountDownLatch (倒计时器)和CyclicBarrier(循环栅栏)

CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用

对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。

CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

请你说说乐观锁和悲观锁

悲观锁:悲观锁总是假设最坏的情况,每次去拿数据是都认为别人会修改,所以每次在拿数据时都会上锁,这样别人想拿这个数据时会阻塞直到拿到锁。mysql数据库的共享锁和排他锁都是悲观锁的实现。

乐观锁:乐观锁总是假设最好的情况,每次去拿数据的时候默认别人不会修改,所以不会上锁,只有当更新的时候会判断一下在此期间有没有人更新了这个数据。适用于多读,可以使用版本号机制进行控制,使用CAS和版本号的方式实现

介绍一下java的锁

乐观锁、悲观锁

根据线程是否要锁住同步资源,分为悲观锁(锁)和乐观锁(不锁)

**悲观锁:**认为自己再使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数

据不会被别的线程修改。

锁实现方式:关键字 Synchornized,接口 Lock 的实现类

适用场景:写操作较多

**乐观锁:**认为自己再使用数据的时候不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时

候判断之前有没有别的线程更新了这个数据。

锁实现方式:CSA 算法,例如 AtomicInteger 类的原子自增是通过 CAS 自旋(如果内存中的版本与该线

程中复制到的版本号不同时,会自旋,自旋是重新去读内存中的信息到线程中,再进行修改操作,操作

完再尝试更新)实现的

适用场景:读操作较多,不加锁的特点能够使其读操作的性能大幅度提升。

读锁(共享锁)写锁(排他锁)

读锁(共享锁)(用数据库来举例)

对同一份数据,多个读操作可以同时进行而不互相影响

当一个进程对表加了读锁后:该进程和其他进程都可对该表进行读操作;

该进程不能对表进行修改会产生 error;

该进程在释放该表的读锁前也不能读取其他的表;

其他进程想对该表进行修改时,会进入阻塞状态,当锁释放后完成修改。

写锁(排它锁)

当写操作没有完成前,会阻断其他写锁和读锁。进程能够读自己上写锁的表;

进程能够写自己上写锁的表;

该进程在释放该表的写锁之前不能读取其他表;

其他进程要读这个上了写锁的表,会进入阻塞状态,等锁释放后,完成读操作。

读写锁:ReentrantReadWriteLock lock= new ReentrantReadWriteLock();

读写锁下面分读锁和写锁,进行写操作可以上写锁:lock.writeLock() 进行读操作可以上读锁:

lock.readLock()(读不加锁的话可能会产生脏读这些问题)

自旋锁、非自旋锁

自旋锁:当一个线程在获取锁的过程中,发现锁已经被其他线程获取,那么该线程循环等待,然后不断

等待该锁是否能够被成功获取,自旋知道获取到锁才会退出。

自旋锁的意义及使用场景:

因为阻塞与唤醒需要操作系统切换 cpu 状态(涉及到上下文切换),需要消耗一定时间。有时自旋的时间

比阻塞唤醒所需要的时间还短

**自旋锁:**固定次数自旋。自旋次数完成后还没有拿到锁,就认为更新失败

**自适应自旋锁:**假设不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据

上一次自旋的时间与结果调整下一次自旋的时间。

JDK1.6 中 可 以 通 过 -XX : -UseSpining 参 数 关 闭 自 旋 锁 优 化 , - XX:PreBlockSpin 参数修改默认

的自旋次数

JDK1.7 之后自旋锁的参数被取消,虚拟机不再支持用户配置自旋锁,自旋锁总是会被执行,并且自旋次

数也由虚拟机自动调整。

显式锁、隐式锁

隐式锁,synchronized 是基于 jvm 的内置锁,加锁与解锁的过程不需要我们在代码中人为控制,jvm 会

自动去加锁和解锁

显式锁,整个加锁跟解锁过程需要手动编写代码去控制,例如 ReentrantLock

可重入锁、非重入锁

可重入锁一个线程已经获得某个锁,可以再次获取锁而不会出现死锁。就是可以重复获取相同的锁。

只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。在锁设计时,不仅

判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界

资源,并把加锁次数加一。设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其

他线程才能访问。

不可重入锁当 A 方法获取 lock 锁去锁住一段需要做原子性操作的 B 方法时,如果这段 B 方法又需要

锁去做原子性操作,那么 A 方法就必定要与 B 方法出现死锁。这种会出现问题的重入一把锁的情况,叫

不可重入锁。在锁设计时, 只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实

现简单。

如何实现互斥锁(mutex)?

在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference。如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

自JDK 5起,Java类库中新提供了java.util.concurrent包(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。

ThreadLocal

Thread的核心机制

每个Thread线程内部都有一个Map。Map里面存储线程本地对象(key)和线程的变量副本(value),但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。

如何避免泄漏

为了防止此类情况的出现,我们有两种手段。

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;

既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

源码

ThreadLocal对象用于在同一个线程中传递数据,避免显式的在方法中传参。

每个线程中保存了ThreadLocalMap对象,ThreadLocalMap对象的key就是ThreadLocal对象本身,value就是当前线程的值。

看下ThreadLocal的get方法

public T get() {
    //当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //获取该ThreadLocal对象的value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //设置初始值
    return setInitialValue();
}

//获取当前线程的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

该方法首先从当前线程中获取ThreadLocalMap对象,接着从ThreadLocalMap获取该ThreadLocal锁对应的值;如果未获取到,调用setInitialValue方法,设置初始值,并返回初始值。再看下ThreadLocal的set方法

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //如果ThreadLocalMap对象存在,则直接设置key(ThreadLocal对象),value;否则创建ThreadLocalMap对象,并设置key,value
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

 void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

该方法同样获取当前线程的ThreadLocalMap对象,如果该对象不为空,那么设置key(ThreadLocal对象),value;否则创建ThreadLocalMap对象,并设置key,value

Lock和synchronized的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别

2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。

3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。

5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

ReentrantLock 和synchronized的区别

ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

划重点

相同点:

1.主要解决共享变量如何安全访问的问题

2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,

3.保证了线程安全的两大特性:可见性、原子性。

不同点:

1.ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。

2.ReentrantLock如果获取时间过长会自动释放,synchronized获取不到锁会一直等待

3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的

4.ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。

5.ReentrantLock 通过 Condition 可以绑定多个条件

6.synchronized适合于并发低的情况,因为synchronized存在锁升级,如果升级为重量级锁将会持续向cpu申请锁资源;ReentrantLock提供了阻塞队列,在高并发的情况下挂起,减少竞争,提高并发能力

Synchornized的底层实现

底层原理为:通过对象头来来存储锁信息并且还支持锁升级,在JVM中通过进入和退出moniter对象来完成代码的同步

Java 对象头

一个对象在内存中包含三部分:对象头,实例数据和对齐填充。其中 Java 对象头包含两部分:

Class Metadata Address (类型指针)。存储类的元数据的指针。虚拟机通过这个指针找到它是哪个类的实例。

MarkWord(标记字段)。存出一些对象自身运行时的数据。包括哈希码,GC 分代年龄,锁状态标志等。

JVM synchronized 的处理

如果同步的是代码块,编译时会直接在同步代码块前加上 monitorenter 指令,代码块后加上monitorexit 指令。这称为显示同步。

如果同步的是方法,虚拟机会为方法设置 ACC_SYNCHRONIZED 标志。调用的时候 JVM 根据这个标志判断是否是同步方法

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

Synchronize和lock的区别?

(1)Synchronized 是内置的 java 关键字;Lock 是一个 java 接口

(2)Synchronized 无法判断获取锁的状态;Lock 可以判断是否获取到锁

(3)Synchronized 会自动释放锁;Lock 必须要手动释放锁,如果不释放会发生死锁

(4) Synchronized 当线程拿不到锁的时候会一直等待下去,Lock可以设置一个获取锁失败的超时时间

(5) Synchronized 可重入锁,不可以中断,非公平;Lock 可重入,公平和非公平可以自己设置

(6) Synchronized 适合锁少量的代码同步问题;Lock 适合锁大量的同步代码。

lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。

(1)lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

(2)tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

(3)tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

(4)lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

Synchronized的优化

引入了锁升级机制、自旋锁和自适应自旋、锁消除、锁粗化

自旋锁与自适应自旋

在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁消除

虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

锁粗化

当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如StringBuffer 的 append 操作。

Lock底层原理

Lock的实现是基于AQS实现的,

AQS 使用一个被volatile修饰的 int 类型state变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作

AQS 定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock又可分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如 CountDownLatchSemaphoreCyclicBarrierReadWriteLock 我们都会在后面讲到。

所有通过AQS实现功能的类都是通过修改state的状态来操作线程的同步状态。比如在ReentrantLock中,一个锁中只有一个state状态,当state为0时,代表所有线程没有获取锁,当state为1时,代表有线程获取到了锁。通过是否能把state从0设置成1,当然,设置的方式是使用CAS设置,代表一个线程是否获取锁成功。

你对可重入怎么理解?

可重入就是线程已经获取到了锁,还可以下次获取到这个锁并且不会发生死锁

谈谈volatile的实现原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

谈谈ReentrantLock的实现原理

ReentrantLock是基于AQS实现的,AQSAbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。

在同步队列中,还存在2种模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。

AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQStryAcquiretryRelease方法实现的lockunlock

ReentrantLock 结构如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GS7q7qtc-1681702198242)(D:/学习/JAVA/面经/面试题整理版本.assets/image-20220225101701288.png)]

首先ReentrantLock 实现了 Lock 接口,然后有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类继承自Sync,这两个类分别是用来公平锁和非公平锁的。通过Sync重写的方法tryAcquiretryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁。

CAS的原子性操作如何保证

可以想象:比较——修改这是两个动作,可能我比较的时候它是一样的,当我修改的时候它却被别的线程修改了。这就涉及到CAS本身这个操作是原子的,也就是不被其他线程所干扰的。这是利用CPU的原语来实现的。我们知道Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个桥梁,基于该类可以连接底层的操作系统直接操作特定的内存数据,Unsafe类存在sun.misc包中,其内部方法操作可以像C指针一样直接操作内存,因此Java中的CAS操作的执行依赖于Unsafe类的方法。(Unsafe类很重要!不知道的同学可以多去了解,建议自己利用反射获取unsafe对象自己写一些东西,比如自己写一个原子类)
进入compareAndSet方法我们发现他返回的是unsafe的compareAndSwapInt方法:

进入Unsafe类的native方法compareAndSwapInt,既然是native方法,那就不涉及到Java的代码了,调用UnSafe类中的CAS方法,也就是这个本地native方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。

Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务

是利用CPU原语来实现的,java的方法无法直接访问底层的系统,需要通过native方法来访问,Unsafe类里面的所有CAS方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务,JVM会帮助我们是先出CAS的汇编指令,这是完全依赖于硬件的功能,在实行的过程中不允许被中断,所以CAS是原子操作

父子线程

使用Threadlocal的bug

如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?比如会有以下的这种代码的实现。由于ThreadLocal的实现机制,在子线程中get时,我们拿到的Thread对象是当前子线程对象,那么他的ThreadLocalMap是null的,所以我们得到的value也是null。

InheritableThreadLocal实现

那其实很多时候我们是有子线程获得父线程ThreadLocal的需求的,要如何解决这个问题呢?这就是InheritableThreadLocal这个类所做的事情。先来看下InheritableThreadLocal所做的事情。

public class InheritableThreadLocal extends ThreadLocal {

protected T childValue(T parentValue) {
    return parentValue;
}

/**
 * 重写Threadlocal类中的getMap方法,在原Threadlocal中是返回
 *t.theadLocals,而在这么却是返回了inheritableThreadLocals,因为
 * Thread类中也有一个要保存父子传递的变量
 */
ThreadLocalMap getMap(Thread t) {
   return t.inheritableThreadLocals;
}

/**
 * 同理,在创建ThreadLocalMap的时候不是给t.threadlocal赋值
 *而是给inheritableThreadLocals变量赋值
 * 
 */
void createMap(Thread t, T firstValue) {
    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}

以上代码大致的意思就是,如果你使用InheritableThreadLocal,那么保存的所有东西都已经不在原来的t.threadLocals里面,而是在一个新的t.inheritableThreadLocals变量中了。下面是Thread类中两个变量的定义

/* ThreadLocal values pertaining to this thread. This map is maintained
    * by the ThreadLocal class. */
   ThreadLocal.ThreadLocalMap threadLocals = null;

   /*
    * InheritableThreadLocal values pertaining to this thread. This map is
    * maintained by the InheritableThreadLocal class.
    */
   ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Q:InheritableThreadLocal是如何实现在子线程中能拿到当前父线程中的值的呢?
A:一个常见的想法就是把父线程的所有的值都copy到子线程中。
下面来看看在线程new Thread的时候线程都做了些什么?

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
            // 1. 获取当前线程为父线程,其实就是创建这个线程的线程
        Thread parent = currentThread();
                // 省略代码。。。。。
        // 2. 判断inheritThreadLocals 是否==true, 父节点的inheritableThreadLocals是否不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
          //3. 符合以上的话,那么创建当前线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

1.获取当前线程为父线程,其实就是创建这个线程的线程

2.判断inheritThreadLocals 是否==true , 默认inheritThreadLocals就是为true, 通用的new Thread()方法,这个值就是true, 同时判断父节点的inheritableThreadLocals是否为空, 如果不为空,则说明需要进行传递。

3.在这个if里面,针对当前线程做了inheritableThreadLocals的初始化, 把父线程的值拷贝到这个里面来。

而且,在copy过程中是浅拷贝,key和value都是原来的引用地址

private ThreadLocalMap(ThreadLocalMap parentMap) {
           Entry[] parentTable = parentMap.table;
           int len = parentTable.length;
           setThreshold(len);
           table = new Entry[len];
           for (int j = 0; j < len; j++) {
               Entry e = parentTable[j];
               if (e != null) {
                   ThreadLocal key = e.get();
                   if (key != null) {
                       Object value = key.childValue(e.value);
                       Entry c = new Entry(key, value);
                       int h = key.threadLocalHashCode & (len - 1);
                       while (table[h] != null)
                           h = nextIndex(h, len);
                       table[h] = c;
                       size++;
                   }
               }
           }
       }

恩,到了这里,大致的解释了一下InheritableThreadLocal为什么能解决父子线程传递Threadlcoal值的问题。

在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量

在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去

因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值

猜你喜欢

转载自blog.csdn.net/qq_43167873/article/details/130196512