java并发面试题

多线程基础

1、线程和进程有什么

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight    Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight   Process)
,它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的; 线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口. 顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

2、创建线程三种方式的对比

(1)采用实Runnable. Callable接口的方式创建多线程

优势

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU.代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。






劣势

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

(2)使用继承Thread类的方式创建多线程

优势

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势

线程类已经继承了Thread类,所以不能再继承其他父类。

(3)Runnable和Callable的区别

Callable规定(重写)的方法是call()Runnable规定(重写)的方法是run()Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

Call方法可以抛出异常,run方法不可以

运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的 方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任 务的执行,还可获取执行结果。

3、为什么要使用多线程呢?

从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性 能。

从计算机底层来说:

单核时代: 在单核时代多线程主要是为了提高 CPU IO 设备的综合利用率




。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单 地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100% 了。

多核时代:多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个CPU 核心被利用到,这样就提高了 CPU 的利用率。

4、线程的状态流转

Java线程具有五中基本状态:

(1)新建状态New:当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

(2)就绪状态Runnable:当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于 就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此 线程立即就会执行;

(3)运行状态Running:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行, 首先必须处于就绪状态中;

(4)阻塞状态Blocked:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  • 同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
  • 其他阻塞:通过调用线程的sleep()join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。

(5)死亡状态Dead:线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

5、什么线程死锁?避免死锁?

死锁:

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻 塞,因此程序不可能正常终止。

死锁必具备以下四个条件

互斥条件:该资源任意一个时刻只由一个线程占用。

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

不剥夺条件



线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

循环等待条件若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

只要破坏产生死锁的四个条件中的其中一个就可以了

  • 破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)

  • 破坏请求与保持条件

一次性申请所有的资源。

  • 破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  • 破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

  • 锁排序法:(必须回答出来的点)

指定获取锁的顺序,比如某个线程只有获得A锁和B



锁,才能对某资源进行操作,在多线程条件下,如何避免死锁?

通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避 免死锁。这通常被认为是解决死锁很好的一种方法。

  • 使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁

6、常见的对比

Runnable VS Callable

  • Callable仅在 Java 1.5 中引入目的就是为了来处理Runnable不支持的用例。Callable 接口可以返回结果或抛出检查异常
  • Runnable 接口不会返回结果或抛出检查异常,
  • 如果任务不需要返回结果或抛出异常推荐使用 Runnable接口,这样代码看起来会更加简洁
  • 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。Executors.callableRunnable task)或 Executors.callableRunnable taskObject resule))

shutdown() VS shutdownNow()

shutdown():关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。

shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List

shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线 程,所以无法响应中断的任务可能永远无法终止。

isTerminated() VS isShutdown()

isShutDown 当调用 shutdown() 方法后返回为 true

isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

7、sleep() 方法和 wait() 方法区和共同点?

区别

sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到 了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。

wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify 或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠 时,会释放互斥锁。

sleep 方法没有释放锁,而 wait 方法释放了锁 。

sleep 通常被用于暂停执行,wait 通常被用于线程间交互/通信。

sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long  timeout)超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者notifyAll() 方法。




相同

两者都可以暂停线程的执行。(也就是使线程进入了阻塞状态)

8、为什么调用 start() 方法时执行 run() 方法为什么们不能直调用 run() 方法

  • new 一个 Thread,线程进入了新建状态; 调用start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,(调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。)这是真正的多线程工作。
  • 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
  • 调用 start 方法方可启动线程并使线程进入就绪状态run 方法只是 thread 的一个普通方法调用还是在主线程里执行

9、Thread类中yield方法有什么作

Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法,而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在 进入到暂停状态后马上又被执行。

10、谈谈volatile使用及其原

volatile的两层语义

  1. volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者 理解为多线程环境下使用volatile修饰的变量的值一定是最新的。
  2. jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。

volatile的原:

获取JIT(即时Java编译器,把字节码解释为机器语言发送给处理器)的汇编代码,发现volatile多加了lock   addl指令,这个操作相当于一个内存屏障,使得lock指令后的指令不能重排序到内存屏障前的位置。这也是为什么JDK1.5以后可以使用双锁检测实现单例模式。

lock前缀的另一层意义是使得本线程工作内存中的volatile变量值立即写入到主内存中,并且使得其他线程共享的该volatile变量无效化,这样其他线程必须重新从主内存中读取变量值

具体原理见这篇文章:https://www.javazhiyin.com/61019.html

11、如何创线程例并运行?

Thread类本质上是实现Runnable接口的一个实例,代表一个线程的实例。创建线程实例一般有两种方法:

(1)创建Thread的子类并重写run()方法

public class MyThread extends Thread { 
    @Override
    public void run(){ 
        System.out.println("MyThread running");
    }
}

run()方法在调用调用start()方法后被执行,而且一旦线程启动后start()方法就会立即返回,而不会等到run()执行完毕后再返回。

MyThread myThread = new MyThread();
myThread.start();

(2)实现 Runnable 接口

public class MyRunnable implements Runnable{ @Override
    public void run(){ 
        System.out.println("MyRunnable running");
    }
}

在新建类时实现Runnable接口,然后在Thread类的构造函数中传入MyRunnable的实例对象,最后执行start()方法即可。

Thread thread = new Thread(new MyRunnable()); 
thread.start();

12、线程阻塞的三种情况

当线程因为某种原因放弃 CPU 使用权后,即让出了 CPU 时间片,暂时就会停止运行,直到线程进入可 运行状态( Runnable ),才有机会再次获得 CPU 时间片转入可以分为如下三种:
  • 等待阻塞(Object.wait() -> 等待队列)Running状态的线程执行Object.wait()方法后,JVM会将线程放入等待序列(waitting queue);
  • 同步阻塞(lock -> 锁池)Running状态的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM将该线程放入锁池(lock pool)中;
  • 其他阻塞(sleep/join/io)Running状态的线程执行Thread.sleep(long ms)或join()方法,或发出I/O请求时,JVM会将该线程置为阻塞状态。当sleep()状态超时,join()等待线程终止或超时,或者I/O处理完毕时,线程重新转入可运行状态(Runnable,也称为就绪状态)。

13、线程三种方式

(1)正常结束
run()或者是call()方法执行完成后,线程正常结束。
(2)异常结束
线程抛出一个未捕获的Exception或Error,导致线程异常结束。
(3)调用stop()

直接调用线程的stop()方法来结束线程,但是一般不推荐使用该方法,因为该方法通常容易导致死锁。

14、为什么调用start()方法时执行run()方法为什么们不能直调用run()方法

JVM执行start方法,会另起一条线程执行threadrun方法,这才起到多线程的效果~

如果直接调用Threadrun()方法,其方法还是运行在主线程中,没有起到多线程效果。

15、守护线程什么

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些 发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

16、了解Fork/Join框架吗

Fork/Join框架是Java7



提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架需要理解两个点,分而治之工作窃取算法

分而治之

以上Fork/Join框架的定义,就是分而治之思想的体现啦

工作窃取算法


把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的 任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算 法啦~

17、CAS解吗

(1)CAS:全称 Compare and swap ,即比较并交换,它是一条 CPU 同步原语。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。

(2)CAS 是一种无锁的非阻塞算法的实现。

(3)CAS 包含了 3 个操作数:

        需要读写的内存值 V

        旧的预期值 A

        要修改的更新值 B

(4)当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子 的。)

CAS 并发原语体现在 Java 语言中的sum.misc.Unsafe类中的各个方法。调用 Unsafe 类中的 CAS 方法, JVM 会帮助我们实现出 CAS

汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS是一种系统原语,原语属于操作系统用于范畴是由若干条指令组成的用于完成某个功能的一个过程并且原语的执行必须是连续的在执行过程中不允许被中断CAS是一条 CPU 的原子指令,不会造成数据不一致问题。

18、CAS有什么缺

(1)ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了ABB又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

可以通过AtomicStampedReference解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值 的版本来保证CAS的正确性。

(2)循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

(3)只能保证一个变量的原子操作

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

可以通过这两个方式解决这个问题

  • 使用互斥锁来保证原子性;
  • 将多个变量封装成对象,通过AtomicReference来保证原子性。

19、synchronized volatile 的区什么?

volatile解决的是内存可见性问题,会使得所有对volatile变量的读写都直接写入主存,即保证了变量的可见性。

synchronized 解决的是执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。而且,还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存,从而 保证操作的内存可见性,同时也使得这个锁的线程的所有操作都happens-before 于随后获得这个锁的线程的操作。

两者的区别主要有:

1、volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

2、volatile 仅能使用在变量级别synchronized 则可以使用在 变量、方法和类级别的。

3、volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性。

4、volatile 不会造成线程的阻塞synchronized 可能会造成线程的阻塞

5、volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

20、synchronized Lock 有什么

  • synchronized 可以给类方法代码块加锁;而 lock 只能给代码块加锁。
  • synchronized   不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁; 而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

21、synchronized ReentrantLock 什么

1、两者都是可重入锁

可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁,两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

2、synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 synchronized

关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的

ReentrantLock JDK 层面实现的(也就是 API 层面,需要 lock() unlock() 方法配合 try/finally语句块来完成)

3、ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • 等待可中断通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁 就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,ReentrantLock类结合Condition实例可以实选择性通知”。

4、使用选择

  • 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized
  • synchronized JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

22、synchronized的用法?

  • 修饰普通方法作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  • 修饰代码块指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

特别注意:

①如果一个线程A调用一个实例对象的非静态 synchronized   方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁。

②尽量不要使用 synchronized(String s) ,因为JVM中,字符串常量池具有缓冲功能。

23、Synchronized

  1. 原子性:确保线程互斥的访问同步代码;
  2. 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 对一个变量unlock 操作之前必须要同步到主内存中如果对一个变量进行lock操作则将会清空工作内存中此变量的值在执行引擎使用此变量前需要重新从主内存中load操作或assign操作初始化变量值来保证的;
  3. 有序性:有效解决重排序问题,即   一个unlock操作先行发生(happen-before)

    于后面对同一个锁的lock操作

24、说一下 synchronized 层实

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在 执行 monitorexit 指令后,将锁计数器设为0
,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

25、多线程中 synchronized 锁升的原什么

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id
一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时 就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

26、synchronized为什么平体在哪地方

synchronized 的非公平其实在源码中应该有不少地方,因为设计者就没按公平锁来设计,核心有以下几个点:

(1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

  • 先将锁的持有者 owner 属性赋值为 null
  • 唤醒等待链表中的一个线程(假定继承者)。

在12之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。

(2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。

27、JVMsynchronized

从最近几个jdk版本中可以看出,Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给 synchronized性能带来了很大的提升。

(1)锁膨胀

上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁 --> 偏向锁 --> 量级锁 -->重量级锁,并且膨胀方向不可逆。

偏向锁

一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由 同一线程多次获得,那么此时就是偏向锁。

核心思想

如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也就变为偏向锁结构,当该线程再次请求锁时无需再做任何同步操作即获取锁的过程只需要检查 **Mark Word** 的锁标记位为偏向锁以及当前线程ID等于 **Mark Word** ThreadID即可,这样就省去了大量有关锁申请的操作。

轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其 申请锁带来的开销也就变大。

重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

(2)锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

(3)锁粗化

未完

28、synchronized  锁能降

可以的。

具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。 当锁降级时,主要进行了以下操作:

  1. 恢复锁对象的 markword 对象头;
  2. 重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。

29、ThreadLocal什么?

ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都 会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量, 从而起到线程隔离的作用,避免了线程安全问题。

//创建一个ThreadLocal变量
static ThreadLocal<String> localVariable = new ThreadLocal<>();

ThreadLocal的应用场景有:

  • 数据库连接池
  • 会话管理中使用

30、ThreadLocal的实

31、知道ThreadLocal  内存泄问题吗

32、了解ReentrantLock

ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。

ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer)

ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。

33、ReadWriteLock什么

首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、 线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

因为这个,才诞生了读写锁ReadWriteLockReadWriteLock是一个读写锁接口ReentrantReadWriteLockReadWriteLock
接口的一个具体实现,实现了读写的分离,读锁是共享的写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值