JUC三连问

2023-11-07

1、进程和线程的区别

1、进程是资源分配的基本单位,线程是程序执行的最小单位

2、一个进程包括多个线程

3、每个进程都有自己的内存和资源,一个进程中的线程会共享这些内存和资源,每个线程都有单独的栈内存,和寄存器

2、并行和并发的区别

  • 并行指两个或多个事件在同一时刻发生,是在不同实体上的多个事件
  • 并发指两个或多个事件在同一时间间隔内发生,是在同一实体上的多个事件

3、创建线程的方式

1、继承Thread类,重写run()方法,无返回值,不抛出异常

2、实现Runnable接口,重写run()方法,无返回值,不抛出异常

3、实现Callable接口,重写call()方法,有返回值,可以抛出异常,再当做参数传给FutureTask对象,FutureTask对象再作为参数再传递给Thread对象来启动线程

4、通过线程池来创建线程

4、sleep()和wait()的区别

1、sleep()不会释放锁,wait()会释放锁。(sleep()睡眠完之后重新回到运行状态,wait()等待完成后,回到就绪状态)

2、sleep()是Thread的方法,wait()是Object的方法

3、sleep()可以在方法的任何地方使用,wait()只能在同步代码块中使用

5、synchronized和Lock的区别

1、synchronized会自动释放锁,Lock不会自动,需手动加锁、解锁

2、synchronized锁之后,等待线程不能获取锁的状态,会一直等下去,而Lock锁之后,等待线程可以获取锁的状态,tryLock()尝试获取到锁。

3、synchronized可以锁方法和代码块,一般用于锁少量代码,Lock锁只能锁代码块,一般用于锁大量代码块

4、synchronized是java关键字,lock是java接口

6、什么是死锁?死锁产生的条件?如何避免死锁?

  • 死锁就是两个或多个线程各自占有对方想要的资源,而对自己手中的资源不放,而造成循环等待,形成死锁。

死锁产生的四个条件:

  • 互斥条件:一个资源同一时间只能被一个线程使用

  • 不可剥夺条件:线程占有的资源,在释放之前不会被其他线程占用

  • 请求和保持条件:线程等待过程中,对以占有资源保持不放

  • 循环等待条件:多个线程互相等待对方释放资源

如何避免死锁,就是破坏这四个必要条件:

破坏不可剥夺条件,一个线程在等待时,将它占有的资源重新加入到资源分配列表中,等到该线程重新获得要等待资源及原有资源才执行下去

破坏请求和保持条件:

1、静态分配即每个线程在开始执行时就申请他所需要的全部资源

2、动态分配即每个线程在申请所需要的资源时,他本身不占用系统资源

破坏循环等待条件:

采用资源有序分配,其基本思想就是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

7、线程池有哪些?

1、newCacheThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

2、newFixedThredPool()创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待。

3、newScheduleThreadPool()创建一个定长线程池,支持定时及周期性任务执行。

4、newSingleThreadExecutor()创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。

5、newSingleThreadScheduledExecutor()创建一个单线程化的线程池,支持定时及周期性任务执行。

6、new ThreadPoolExecutor()方法创建线程池时,要设置的7大参数:

image-20201128203707821

(阻塞队列这里有点错误,正确的是当线程数超过核心线程池大小,然后再阻塞队列中存储线程。)

创建线程流程:

如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
如果此时线程池中的线程数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的线程数量小于maximumPoolSize,建新的线程来处理被添加的任务。

    如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的线程数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。 

也就是:处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

当线程数超过最大线程池的大小和阻塞队列中的线程数时,就会有拒绝策略拒绝多余的线程。其中有4种拒绝策略。

image-20201128204732448

8、线程池的状态?

Running
线程池处于Running状态时,能够接收新任务以及对已添加的任务进行处理。
线程池的初始状态为Running,换句话说线程池一旦被创建,就处于Running状态,且线程池中的任务数为0
Shutdown
线程池处于SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
调用线程池的shutdown()方法时,线程池由Running->Shutdown
Stop
线程池处于Stop状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务的线程。
调用线程池的shutdownNow()方法时,线程池由Running->stop
Tidying
当所有的任务已终止,ctl记录的任务数为0,线程池的状态就会变为tidying。
当线程池中的任务为空,且阻塞队列中的任务数也为空时,状态就会由shutdown->tidying
当线程池中的任务为空时,状态由 stop -> tidying
Terminated
线程池处于tidying状态时,调用terminated()就会由tidying-> terminated

9、线程池为何要构建空任务的非核心线程?

防止阻塞队列中还有任务存在,但此时可能核心线程数为零,要执行的话只能等待下一次添加新任务到队列中。所以在线程快结束的时候,创建空任务的非核心线程就是为了防止这种情况发生。

10、线程池使用完毕为何必须要shutdown方法?

如果没有及时shutdown,那么线程池中的核心线程永远不会被gc回收,那就有可能造成内存泄露问题。

11、CAS是什么?

CAS(compare and swap),比较并交换。可以解决多线程并行情况下使用锁造成的性能损耗的一种机制。CAS操作包含三个操作数–内存位置(V),预期原值(A),新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做处理。

CAS产生:
在修饰共享变量的时候经常使用volatile关键字,但是volatile值有可见性和禁止指令重拍(有序性),无法保证原子性。虽然在单线程中没有问题,但是多线程就会出现各种问题,造成现场不安全的现象。所以jdk1.5后产生了CAS利用CPU原语(不可分割,连续不中断)保证现场操作原子性。
CAS应用:
在JDK1.5 中新增java.util.concurrent(JUC)就是建立在CAS之上的。相对于对于synchronized这种锁机制,CAS是非阻塞算法的一种常见实现。所以JUC在性能上有了很大的提升。

比如AtomicInteger类,AtomicInteger是线程安全的的,下面是源码:

img

img

进入unsafe看到do while自循环,这里的自循环,就是在 判断预期原值 如果与原来的值不符合,会再循环取原值,再走CAS流程,直到能够把新值赋值成功。

CAS优点:

cas是一种乐观锁的思想,而且是一种非阻塞的轻量级的乐观锁,非阻塞式是指一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

CAS 缺点:
1、循环时间长开销大,占用CPU资源。如果自旋锁长时间不成功,会给CPU带来很大的开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
2、只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
3、ABA问题

解决ABA问题:

  1. 添加版本号
  2. AtomicStampedReference

12、对volatile的理解,volatile的可见性和禁止指令重排是如何实现的?

在代码中加入volatile关键字时,生成的汇编代码会出现一个Lock前缀指令,实际上就是一个内存屏障。它有3个功能:

  • 它能确保指定重排序时不会把后面的指定排到内存屏障之前的位置,也不会把前面的指定排序到内存屏障后面。即在执行到内存屏障时,它前面的操作已经完成。

  • 它会强制将对缓存的修改操作写入到主内存中。

  • 如果是写操作,它会导致其他CPU中对应的缓存无效。

1、volatile的可见性:

volatile的功能就是,将被修改的变量,在被修改后可以立即同步到主内存,被修改的变量在每次被使用之前都从主内存刷新,其实本质是通过MESI缓存一致性协议来实现可见性。

可见性原理:
1、首先cpu会根据共享变量是否带有Volatile字段,来决定是否使用MESI协议保证缓存一致性。
2、如果有Volatile,汇编层面会对变量加上Lock前缀,当一个线程修改变量的值后,会马上经过store、write等原子操作修改主内存的值(如果不加Lock前缀不会马上同步),为什么监听到修改会马上同步呢?就是为了触发cpu的嗅探机制,及时失效其他线程变量副本。

具体看这里 volatile详情

2、volatile禁止指定重排:

volatile关键字的另外一个作用就是禁止指定重排优化,从而避免多线程下程序出现乱序执行的现象。以下单例模式的双重检索就是一个好的实例:

public class Singleton {
 
    private volatile static Singleton instance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

首先,对象初始化分3个步骤:

1、给对象分配内存空间

2、初始化对象

3、对象的引用指向分配好的内存地址

如果没有加上volatile时,就可能在初始化对象的时候发生重排序,第二步和第三步互换了,先引用指向内存地址了,此时另外一个线程在外面判断对象是否为空的时候,这是对象不为空就直接返回了,这时候返回的对象没有进行初始化,会导致错误,所以加上volatile关键字禁止指令重排,确保给对象初始化完成后,再执行内存地址。

这时候volatile也是通过内存屏障来禁止指令重排,完成有序性的。具体的内存屏障策略为:storestore,storeload,loadload,loadstore

屏障类型 指令示例 说明
LoadLoad Load1; LoadLoad; Load2 保证load1的读取操作在load2及后续读取操作之前执行
StoreStore Store1; StoreStore; Store2 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore Load1; LoadStore; Store2 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad Store1; StoreLoad; Load2 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

13、as-if-serial和happens-before规则

1、as-if-serial定义:无论编译器和处理器如何进行重排序,单线程程序的执行结果不会改变。

2、happens-before关系保证正确同步的多线程的执行结果不被改变

happens-before具体定义:

(1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的顺序排在第二个之前。

(2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

happens-before一共有七项规则:

  • 程序顺序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁加锁。
  • volatile变量规则:对于一个volatile域的写,happen-before于任意后续对这个volatile的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
  • join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  • 程序中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则:一个对象的初始化完成(构造函数结束)先行发生于它的finalize()方法的开始。

as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能的提高程序执行的并行度。

14、对象的创建过程?

创建对象的一共有六个步骤:

1、检查类是否已经被加载

2、为对象分配内存空间

3、将分配到内存空间初始化默认值(int初始化为0,boolean初始化为false,string初始化为null)

4、为对象进行必要的设置(设置对象头,包括该对象所属的类,类的元数据信息,hasCode值,gc年龄)

5、初始化类信息

  • 执行Class文件中的()方法
  • 初始化对象的字段值
  • 静态代码块
  • 构造代码块
  • 构造方法

6、对象引用执行分配好内存地址

15、对象在内存中的内存布局

对象在堆内存中的布局划分为三个部分:

1、对象头(Mark Word{hasCode值,gc年龄,偏向锁线程id,偏向时间,锁的状态等等}、Class pointer(是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;))

2、实例数据

3、对齐填充

数组对象的内存分布:

1、对象头

2、实例数据

3、对齐填充

4、数组长度(4个字节)

一个比较坑的题目 Object objet = new Object()占用多少字节?

总结:所以Object obj = new Object()一个 如果默认情况下是占用20个字节(merkword8字节+classponit4字节+对齐4字节+obj4字节),如果关闭压缩指令,那么就是占用也是20个字节(merkword8字节+classponit8字节+obj4字节),因为用不到补齐了,哈哈是不是被坑了。
具体看对象内存分布

内存分布为什么要加上对齐填充,内存的字节数为什么要是8的倍数呢?

答:64位 机器下,单次可读写 64 bit(即 8 byte) 数据,因此每 8 byte 作为一 读取单元。计算机并非逐字节读取内存,而是按8的倍数的字节块读写内存,故地址必须为上述倍数,故各种数据类型需要按照一定规则在空间上排列,以提高访问效率。

16、锁的四种状态及升级过程

一、前言

锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

在synchronized最初的实现方式是“阻塞或唤醒一个Java线程需要操作系统切换CPU来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切切换的时间可能比用户执行的时间还长“,这种方式效率低下,由此,JDK1.6带来了偏向锁、轻量级锁。

四种锁的升级过程如下:

在这里插入图片描述

锁升级过程总结:

1、在一个线程没有执行到同步代码块的时候,这时锁的状态是无锁的,mark word里面存储的是对象的hashCode值,对象的分代年龄,是否偏向锁0,锁标志位01这些信息。

2、当线程执行到同步代码块边界时,且没有线程执行该代码块时,锁对象会变成偏向锁,通过CAS操作把把线程id存储到mark word里的偏向锁的id,Epoch实际上存储的是偏向时间戳,以及对象分代年龄,是否偏向锁1,锁标志位01这些信息。

3、如果此时有另外一个线程执行到同步代码块时,此时mark word里的偏向锁线程id去除掉,且锁升级为轻量级锁(自旋锁),线程会通过CAS操作不断的自旋,尝试获取到锁。而获取到锁的线程,会将线程的指向栈中锁的记录的指针存入到mark word里,且锁标志位为00。

4、而其他没有获取到锁的线程还是会通过CAS不断的尝试获取到锁,大量的消耗CPU资源。JVM默认线程自旋失败十次的话,锁再次升级为重量级锁,(不然会导致太多线程不断的自旋,导致CPU挂满),此时将获得重量级锁的指针存入到mark word中,且锁标志位为10,没有获取到锁的线程则会阻塞挂起,等待cpu调度。这些就是锁升级的大致过程。

锁对比:

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢,用户态和内核态来回切换影响性能。 追求吞吐量,同步块执行速度较慢

锁:

其实锁的信息都是存放在对象头中的,以Hotspot为例,对象头主要包括Mark word(标记字段) Klass pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

以下就是具体的存储信息

在这里插入图片描述

在这里插入图片描述

17、请你介绍下synchronized及其底层原理

Synchronized是一个隐式的可重入的不可中断的同步锁,可以同步方法(默认锁对象为this(当前实例对象)),同步代码块(自己指定锁对象)。

底层实现:

1、对象头是 synchronized 实现的关键,使用的锁对象是存储在 Java 对象头里的、及锁升级过程信息也存储在对象头中的对象头的。具体看12问、13问。

2、从java代码反编译来看,如下图所示,每当线程执行到同步代码块中就会有个monitorenter和monitorexit命令的出现,其实每个对象都存在着一个monitor与之关联,当线程执行到代码块时,会尝试使用monitorenter命令获取到该对象的monitor(监视器锁),获取成功后,该monitor便处于锁定状态。

在这里插入图片描述

monitor(监视器锁)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。Mutex Lock的切换需要CPU从用户态转换为内核态中,因此状态转换需要耗费很多的处理时间,所以Synchronized是一个重量级操作。

synchronized可重入原理:

上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗? 答案是:不需要获取该锁,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。
Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

18、请描述下AQS,为什么AQS的底层是CAS+volatile?

AQS是AbstractQueuedSynchronizer的简称,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,AQS底层实现原理用一句话总结就是:volatile + CAS + 一个虚拟的FIFO双向队列(CLH队列)。

在这里插入图片描述

AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。

使用一个volatile的int类型的state表示同步状态,通过内置的FIFO队列CLH完成资源获取的排队工作,将资源封装为Node,通过cas改变state值

AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如ReadWriteLock。

自定义资源共享方式:

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

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

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回falsetryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回falsetryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

示例:如使用Reentranlock使用非公平锁时,上锁的操作,如果使用cas操作失败的话,会进入acquire方法中,将该线程通过addWaiter方法独占方式加入等待队列。

为什么AQS的底层是CAS+volatile?

答:如上述代码所示往队列里面加thread的时候,是通过CAS的方法 compareAndSetTail() 往尾巴tail上加。而tail是volatile的。还有比如修改state的值,也是通过CAS的方法 compareAndSetState() 来修改,而state也是volatile的。

19、AQS唤醒节点时,为何从后往前找。

两个方面点:
(1)node节点在插入AQS双向链表时,是从tail中去添加的,首先是吧prve指向最后一个节点,然后将tail指向新添加的节点,他就算完成了插入。但其实还原本最后一个节点的next还没有指向新添加的节点,所以如果从前往后唤醒的话,就会导致新添加的node节点会丢失。而从后往前则不会。
(2)还是一个就是当链表中的某个节点取消,它会先去调整prev这个指针的指向,再去调整next的指向,所以prev指向优先级更好,所以从前往后可能会造成错过某个节点。

20、synchronized和reentrantlock的底层实现及重入的底层原理

synchronized的底层实现

(1)synchronized是用来保证线程同步,用的锁存在java对象头中,利用monitorenter和monitorexit指令实现,monitorenter指令是在编译后插入到同步代码块开始位置,而monitorexit是插入到方法结束后和异常处。jdk1.6之后引入了大量的优化,这其中又涉及到锁的四种升级状态:new(无锁) →偏向锁→轻量级锁(自旋锁)→重量级锁。而底层,synchrnoized是利用操作系统的Mutex Lock(互斥锁)来实现的,

(2)synchronized同步块对同一条线程来说是可以重入的,不会出现自己把自己锁死的问题。同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。由于java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,所以synchronized是java语言中一个重量级操作。

(3)重入锁实现可重入性原理或机制:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记录下锁的持有线程,并且将计数器置为1;此时其他线程请求该锁,则必须等待;而持有该锁的线程如果再次请求这个锁,就可以再次拿到这把锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器变为0,则释放锁。

reentantlock的底层实现原理

(4)ReentrantLock是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。ReentrantLock的处理逻辑,其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。

(5)reentantlock的非公平锁的实现:对于共享变量state,先通过cas尝试是否能获取到锁,先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;若state大于0,肯定有其他线程已经抢占资源,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数,其他情况,则获取失败;

(6)reentantlock的可重入公平锁的实现:公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还有比该线程排队的还久的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。

21、强、软、弱、虚引用

其实强引用、软引用、弱引用、虚引用这四个概念非常简单好记。

  • 强引用:gc时都不会回收
  • 软引用:只有在内存不够用时,gc才会回收
  • 弱引用:只要gc就会回收
  • 虚引用:是否回收都找不到引用的对象,获取永远都为null,仅用于管理堆外内存。因为JVM无法直接清理堆外内存,所以提供一个虚引用,交给垃圾回收器的回收队列,这个队列就是用来标记哪些堆外内存需要回收,再调用c++释放空间。

22、你清楚ThreadLocal吗,ThreadLocal如何解决内存泄漏问题?

ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。

很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
一、ThreadLocal设计

1、在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。
2、JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下:

  • 每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);

  • Map里面存储ThreadLocal对象(key)和线程的变量副本(value);

  • Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;

  • 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。

    JDK8之后设计的好处在于:

每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。

二、ThreadLocal内存泄露问题

内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。

使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。

解决办法:

(1)尽量使用private static final修饰ThreadLocal实例。使用private与final修饰符主要是为了尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要是为了确保ThreadLocal实例的全局唯一。

(2)ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏问题的方法。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JUC三连问 的相关文章

  • 尝试使用 Eclipse 启动 Glassfish 服务器时出现 org.apache.catalina.LifecycleException

    我一直忙于使用 angularjs 前端构建一个 REST 应用程序 使用 MAVEN jersey quickstart webapp 使用 GLASSFISH Web 服务器在 Eclipse 上开发 今天 当我开始对项目进行一些开发时
  • 在 Java Swing 中检测 JScrollPane 上的 mouseClick 事件

    如果我有这样的东西 我可以使用布尔标志 performAdjustment 控制自动滚动 static boolean performAdjustment true JTextArea textArea new JTextArea JScr
  • Android NumberPicker 带字符串

    I have customised the NumberPicker to show text The output is this 当我按 确定 时 我想将 e x 鼠标添加到我的列表 文章 中 我得到的是索引值 int 它由 array
  • JAVA - 带有特殊字符的 LDAP 密码不起作用

    我试图在我的系统上创建一个登录屏幕 在 Active Directory 中进行查询 但是当用户的密码包含一些特殊字符 如 和 时 它不会验证 我需要加密密码才能工作吗 我该怎么做 我使用 getPassword 通过 JPasswordF
  • Java/JAXB:将具有相同名称但不同属性值的 XML 元素解组到不同的类成员

    我正在尝试根据其属性之一将具有多个 Fields 元素的 XML 解析为不同的类成员 这是 XML
  • 有效地查找正则表达式的所有重叠匹配项

    这是后续与 java 正则表达式匹配的所有重叠子字符串 https stackoverflow com q 11303309 244526 有没有办法让这段代码更快 public static void allMatches String
  • 使用 jdbc 程序连接到 Open Office odb 文件

    我编写了以下代码来连接到 OpenOffice db String db C Documents and Settings hkonakanchi Desktop Test odb Class forName org hsqldb jdbc
  • @PreUpdate 不适用于 Spring Data JPA

    我有一个实体 Entity EntityListeners MyEntityListener class class MyEntity 还有听者 class MyEntityListener PrePersist PreUpdate pub
  • Java:检查给定日期是否在当前月份内

    我需要检查给定的日期是否在当前月份 我编写了以下代码 但 IDE 提醒我getMonth https docs oracle com javase 7 docs api java util Date html getMonth and ge
  • 如何在具有动态列的表中插入值 Jdbc/Mysql

    我想在具有动态列的表中添加值 我设法创建一个包含动态列的表 但我不知道如何插入数据 Create Table sql CREATE TABLE MyDB myTable level INTEGER 255 int columnNumber
  • 业务代表与服务定位器

    Business Delegate 和 Service Locator 之间有什么区别 两者都负责封装查找和创建机制 如果 Business Delegate 使用 Service Locator 来隐藏查找和创建机制 那么 Busines
  • java JFileChooser 文件大小过滤器

    我知道我可以按文件类型进行过滤 但是可以按文件大小进行过滤吗 例如 JFileChooser 仅显示 3 MB 以内的图片 简短的回答应该是 你尝试过什么 长答案是肯定的 JFileChooser fc new JFileChooser f
  • 在 Mac 上使用 JRE 打开 jar 文件

    我有一个 jar 文件 旨在通过命令行运行 我不打算在运行应用程序的机器上进行任何java开发 我的思考过程是 因此我应该只需要JRE而不是JDK 此外 JDK 大约是 JRE 的 4 倍 我不想下载它 在 Mac 上安装 JRE 时 它不
  • .class 与 .java

    class 文件和 java 文件有什么区别 我正在尝试让我的小程序工作 但目前我只能在 Eclipse 中运行它 还不能嵌入 HTML 谢谢 编辑 那么如何使用 JVM 进行编译呢 class 文件是编译后的 java 文件 java 都
  • Mule/码头设置

    我有一个正在运行的 Mule 应用程序 我想在其上设置 Jetty 来响应 http 请求 以下配置
  • Android 中的字符串加密

    我正在使用代码进行加密和加密 它没有给出字符串结果 字节数组未转换为字符串 我几乎尝试了所有方法将字节数组转换为字符 但没有给出结果 public class EncryptionTest extends Activity EditText
  • Java SE + Spring Data + Hibernate

    我正在尝试使用 Spring Data Hibernate 启动 Java SE 应用程序 并且到目前为止已经完成了以下操作 配置文件 Configuration PropertySource classpath hibernate pro
  • JDK 7 的快速调试/调试构建

    我正在寻找 JDK 的调试 或者我猜他们称之为快速调试构建 以启用在运行时生成的打印程序集以及查找性能问题时所需的其他诊断 就目前情况而言 我似乎找不到可以直接使用的 现成的 快速调试构建二进制包 有人可以帮我提供下载链接 或者至少提供有关
  • Java泛型类型

    当我有一个界面时 public interface Foo
  • 使用 Android 的 Mobile Vision API 扫描二维码

    我跟着这个tutorial http code tutsplus com tutorials reading qr codes using the mobile vision api cms 24680关于如何构建可以扫描二维码的 Andr

随机推荐

  • 服务器硬件规格常用查看命令——磁盘相关命令

    smartctl smartctl是一个能够控制和监控磁盘的SMART Self Monitoring Analysis and Reporting Technology 自我监测 分析和报告技术 命令 使用方法 提示 使用该命令只能查看到
  • Apache Hop-使用介绍【持续完善中】

    文章目录 Pipelines 管道 Pipeline Editor 管道编辑器 TOOLBAR Create a Pipeline 创建管道 How pipelines work 管道如何工作 Concepts 概念 Create a pi
  • 计算机组成原理(七)——总线BUS

    总线 BUS 总线 BUS 概念 连接计算机系统各个功能部件的信息传输线 是各个部件共享数据及信息的传输介质 用来连接计算机系统各功能部件而构成一个完整系统 实际上是一组信号线 广义地讲 任何连接两个以上电子元器件的导线都可以称为总线 计算
  • 微服务系列文章之 Redisson实现分布式锁

    一 高效分布式锁 当我们在设计分布式锁的时候 我们应该考虑分布式锁至少要满足的一些条件 同时考虑如何高效的设计分布式锁 这里我认为以下几点是必须要考虑的 1 互斥 在分布式高并发的条件下 我们最需要保证 同一时刻只能有一个线程获得锁 这是最
  • 【go】异步任务解决方案Asynq实战

    文章目录 一 Asynq介绍 二 所需工具 三 代码示例 四 Reference 一 Asynq介绍 Asynq 是一个 Go 库 一个高效的分布式任务队列 Asynq 工作原理 客户端 生产者 将任务放入队列 服务器 消费者 从队列中拉出
  • openEuler之RPM软件包管理命令

    RPM命令介绍 安装软件 rpm i xx rpm 卸载软件 rpm e xx rpm 升级形式安装 rpm U xx rpm 常用参数 v 显示详细信息 h 显示文本进度条 1 安装软件 1 下载RPM包 root localhost l
  • K8s企业版多节部署

    K8s企业版多节部署 实验步骤 K8s的单节点部署 master2节点部署 负载均衡部署 使用双机热备 k8s网站页面 实验环境 使用Nginx做负载均衡 lb01 192 168 217 136 24 CentOS 7 5 lb02 19
  • DLL+资源模块切换

    MFC程序中存在一个模块状态 Module State 的问题 也就是资源重复的问题 此处的术语模块是指一个可执行程序 或指其操作不依赖于应用程序的其余部分但使用MFC运行库的共享副本的一个DLL 或一组DLL 我们所创建的MFC DLL就
  • VS2019配置opencv详细图文教程和测试代码

    摘要 vs2019新鲜出炉 配置opencv又有哪些不一样呢 这个教程将会一步一步的教你如何配置opencv和跑动opencv一个简单的项目 测试代码请在原文找到 转发备注原文链接 https xygeng cn post 151 html
  • 机器学习数据获取与处理

    数据获取与处理 以CV任务为主 课程目的 数据的获取途径 数据处理与标注 数据预处理方法 模型训练评估 一 数据集的获取 通常 我们的数据来源于各个比赛平台 首先是AIStudio中的数据集 大部分经典数据集例如百度AI Studio Ka
  • 小程序用户隐私保护协议纯文案修改指引

    目录 一 修改缘由 二 官方指引 三 填写入口 一 修改缘由 小程序提交审核不通过 审核失败原因 存在平台未允许的服务内容 违反 微信小程序平台运营规范常见拒绝情形3 4 详情描述 你好 你的小程序涉及收集 使用和存储用户信息 请增加 用户
  • C++模板的特化(specialization)和偏特化(partial specialization)

    C 模板的特化及偏特化 类模板全特化 对类中的某个成员函数进行特化处理 类模板的偏特化 个数偏特化 范围偏特化 函数模板全特化 函数模板偏特化 模板函数和模板类有的时候可能需要对传入的不同类型进行不同的处理 比如说有的模板传入int或dou
  • 【Linux】进程间通信-命名管道FIFO

    命名管道概述 如果我们要在不相关的进程间交换数据 那么使用FIFO文件将会十分方便 FIFO文件通常也称为命名管道 named pipe 命名管道是一种特殊类型的文件 它在文件系统中以文件名的形式存在 创建命名管道 创建命名管道一般有两种方
  • Unity打包的apk在安卓4.4.2盒子上碰到的问题

    项目场景 Unity开发的项目需要在安卓4 4 2盒子上运行 问题描述 1 会出 从顶部向下滑动即可退出全屏模式 的弹框 这是android4 4的一个特性 叫做沉浸模式 Full screen Immersive Mode 当app启用该
  • 要庆幸,找到了自己-------Day73

    跟朋友聊了大半晚上 看看时间 已经要睡觉的点了 坐下来写这篇文章 只为了感念下曾经的自己 如今的自己 未来的自己 就那么迷茫了那么多年 也坚守了那么多年 如果有方法可以做到 那为何不去努力呢 如果没有人帮 那就自己去克服它 那个守望的孩子就
  • 使用Nginx+Keepalived组建高可用负载平衡Web server集群

    一 首先说明一下网络拓扑结构 1 Nginx 反向代理Server HA Nginx master 192 168 1 157 Nginx backup 192 168 1 158 虚拟IP统一为 192 168 1 110 2 web服务
  • 信号和槽的绑定

    为了更加深入的理解信号和槽的绑定 我们使用以下2种方法来实现绑定 比如我们在QT degisnger界面中添加一个label控件和horizontalScrollBar控件 我们想实现 拖动horizontalScrollBar进度条 la
  • 使用STM32组建基于LoRa的环境监测系统

    文章目录 一 前言 二 介绍 三 硬件连接 1 系统框架 2 中心网关的连接 3 传感器节点1的连接 4 传感器节点2的连接 四 网关程序 1 主程序设计 2 LoRa程序 3 串口1程序 4 LCD显示程序 五 传感器节点程序 1 传感器
  • hexo+git搭建个人博客

    前言 喜欢写 Blog 的人 会经历三个阶段 第一阶段 刚接触 Blog 觉得很新鲜 试着选择一个免费空间来写 第二阶段 发现免费空间限制太多 就自己购买域名和空间 搭建独立博客 第三阶段 觉得独立博客的管理太麻烦 最好在保留控制权的前提下
  • JUC三连问

    1 进程和线程的区别 1 进程是资源分配的基本单位 线程是程序执行的最小单位 2 一个进程包括多个线程 3 每个进程都有自己的内存和资源 一个进程中的线程会共享这些内存和资源 每个线程都有单独的栈内存 和寄存器 2 并行和并发的区别 并行指