面试题

jvm问题及调优

java跨平台怎么实现的?(一次编写到处运行)

java文件通过javac编译成class文件后,通过在不同平台的jvm(java虚拟机)生成不同平台的机器码,就能在不同平台运行。

图片

 

jvm虚拟机整体架构

jvm虚拟机    类加载过程(加载、验证、准备、解析、初始化、使用和卸载

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。

其中验证、准备、解析3个部分统称为连接

加载:类加载器加载唯一的class文件进入jvm中。

验证:验证class文件中内容是否符合规范,不规范jvm无法正常运行

准备:给类在中的局部变量表中的变量   赋予内存空间和初始值(零值)

解析:动态链接把符号引用替换为直接引用,在方法区找到调用方法的位置

初始化:给栈中的变量赋值     与     给堆中创建实例对象执行静态代码块

 

jvm虚拟机    4种类加载器(启动,扩展,应用,自定义)

什么时候加载类:什么时候使用到了这个类,它就去class字节码文件中去加载这个类。

类加载器作用:确定唯一类进行加载,JVM将指定的class文件读取到内存里,并运行该class文件里的Java程序,

启动类加载器:加载jvm核⼼类库,如java.lang.*等,由C++语言编写,无法通过java代码.parent()得到

扩展类加载器:(ext)从jre/lib/ext下加载类库,基本API之外的一些拓展类,包括一些与安全性能相关的类。

应用程序加载器:保存在项目路径下,自己写的java代码中使用的类

自定义类加载器:这是开发人员通过拓展ClassLoader类定义的自定义加载类,加载程序员定义的一些类。

用途:1.用于加载解密(被加密的class文件),2.加载未在项目路径下的class文件,3.将非class文件转为class文件并加载

自定义一个类加载器,需要继承ClassLoader类,并实现findClass方法。在findClass方法中使用defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class并返回(只要二进制字节流的内容符合Class文件规范)。

 

类加载器的双亲委派机制

启动类加载器《——扩展类加载器《——应用程序加载器《——自定义类加载器

例如:创建String类,赋予方法,如果先发现没有自定义类加载器,应用程序类加载器会先收到类加载请求,委托给上层的父加载器扩展类

扩展类委托给它的上层的父加载器启动类来加载,启动类加载器发现自己有一个java源码其中有String类,直接就会使用java中最原始的String类,而你自己创建的String类并不会加载。

如果启动类加载器检查到无法加载该类,会抛出异常,通知下一层,下一层进行同样的操作,直到有加载器加载类

 

(1):最下层的(自定义类加载器)(没有自定义加载器就使用应用程序加载器)收到类加载的请求
(2):把这个请求委托给⽗加载器去完成,⼀直向上委托,直到启动类加载器
(3):启动器加载器检查能不能加载,能就加载(结束);否则,抛出异常,通知⼦加载器进⾏加载
(4):保障类的唯⼀性和安全性以及保证JDK核⼼类的优先加载

 

jvm沙箱安全机制

沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离防止对本地系统造成破坏

沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

组成沙箱的组件:

类装载器双亲委托机制,从最上层启动类加载器开始类的加载,避免了恶意代码篡改java核心类,由于包的访问权限限制,外部恶意代码没有权限修改内层代码

字节码校验器:在编译过程完成后产生class文件检查class文件是否符合java语言规范,帮助java程序实现内存保护,java核心类(java.lang.*等)不会进行字节码校验,因为核心代码经过验证

存取控制器:存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

安全管理器:是核心API和操作系统之间的主要接口。实现权限控制比存取控制器优先级高

安全软件包:java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者,消息摘要,数字签名(keytools,https),加密,鉴别

 

 

jvm虚拟机  栈(一个线程对应一部分栈,一个方法对应栈中一部分栈帧内存区域,栈先进后出特性与调用嵌套方法先后的需求一致

存放内容

每有一个线程,jvm虚拟机就分配   一部分栈区域    给线程

每有一个方法,当前栈就会分配   一部分栈帧内存区域(存放在各个   给这个方法   存放方法中的局部变量表,操作数栈,动态链接,方法出口

栈的特性先进后出,调用嵌套方法时,最里层的方法需要先结束,最外层最后结束。

栈满了会报错:StackOverflowError

 

栈帧中:

局部变量表存放方法中的局部变量(堆中对象的内存地址,数值)的空间————图片

操作数栈临时存放需要做操作(加减乘除,赋值)的值(1,2,3。。)的空间,最后会将值   出该栈    放入局部变量表————局部变量与操作数栈图片

动态链接:用于调用方法时,将常量池中该方法的符号引用转换为直接引用,在方法区内部中找到该调用方法的代码位置————图片

方法出口保存调用方法的返回信息,通过该信息知道外层方法的执行位置,继续执行外层方法。————图片

图片

 

jvm虚拟机  程序计数器(存放当前线程代码运行位置)

存放内容

和栈一样,每有一个线程,jvm虚拟机就分配   一部分程序计数器区域    给线程(每个线程的程序计数器是独立的)

用于存放代码的执行到哪的位置,当线程挂起时,能知道要从哪继续执行

字节码执行引擎执行方法区中加载的class文件的同时修改程序计数器,使其线程知道执行到哪了

图片

修改程序计数器图片

 

jvm虚拟机   方法区(jdk6之前使用永久代(虚拟机内存),7-8使用的永久代而8后没有永久代了使用元空间(本地内存)作为具体实现)(存放class,static,final,常量池(常量值,类方法的符号引用))

存放内容

存放class文件的类的相关内容(常量,静态变量,类信息),运行时常量池——运行时常量池图片

静态变量:存储的是的对象地址————静态变量图片

由于方法区存放类的相关信息,所以对于动态生成类的情况比较容易出现方法区的内存溢出

jdk7之前使用没在堆的单独永久代,使用jvm内存空间,会导致永久区OOM(OutOfMemoryError: PermGen space

jdk7-8可能导致的OOM(OutOfMemoryError: java heap space

jdk8之后使用元空间并不在虚拟机中,而是使用本地内存。会导致元空间OOM(OutOfMemoryError: Metaspace

 

jvm虚拟机    堆(存放对象,minor gc处理年轻代垃圾对象,

老年代进入方法(生存区未满,满了),full gc处理老年代垃圾对象)

存放内容

存放创建的对象,       可供中的局部变量表引用,方法区中的静态变量引用,本地方法栈中的变量引用————图片

占堆内存的比例,年轻代:老年代  =  1:2

年轻代各个区的比例,eden:s0:s1 = 8:1:1  

老年代进入方式

生存区某个区(s0或s1)    空间未满    每次minor  gc 分代年龄+1      分代年龄=15时

生存区某个区(s0或s1)    空间不够存放大的对象    这个对象直接进入老年代

生存区某个区(s0或s1)    相同年龄所有对象的大小总和大于 Survivor 空间(s0或s1)的一半,大于等于该年龄的对象直接进入老年代(容易将对象放入老年代,产生老年代的full  gc,解决方法:将老年代内存区域调给生存区,避免幸存对象大于占生存区的一半

 

GC Roots根节点中的局部变量表中引用堆的变量,方法区中引用堆的静态变量,本地方法栈的引用的变量————图片

查看对象是否可回收算法

引用计数法(缺点明显:遇到循环引用会内存溢出

如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用(P=null),那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收

致命缺点:在遇到循环引用时,删除了最初的引用,其内部的循环引用会导致引用计数一直为1,内存回收不了内存溢出——图片

 

可达性分析算法(实用)

可达性分析算法:从GC Roots根节点(jvm各个区中引用堆的变量对象节点)出发,搜索向下的节点(对象中的成员变量中的对象),直到没有向下的节点了,

找到的都标记为非垃圾对象,其他的对象就是垃圾对象

 

GC回收对象算法

复制算法(使用在年轻代)—————图片

minor gc处理年轻代垃圾对象(伊甸园—>s0区—>s1区)

最开始对象进入堆中的年轻代空间分代年龄<15中的伊甸园区

中的伊甸园(eden)区满了,存放不了其他的对象了,会启用字节码执行引擎的垃圾收集线程,进行对象垃圾回收堆中垃圾对象所占的内存(minor  gc      可达性分析算法)

(每进行一次minor  gc,分代年龄+1

垃圾回收清空伊甸区和(s0区或者s1区)

幸存对象进入生存区(survivor区)中的s0区,之后伊甸区又满了进行minor  gc 所有的幸存对象(伊甸区的,s0的)进入生存区中的s1区

之后伊甸园空间满了每进行minor gc,对象会在s0区与s1区之间转移,分代年龄为15会进入堆中的老年代空间

优点:没有内存碎片

缺点:始终有一个生存区的一个区(s0或s1)没有存放数据,浪费了内存

 

标记清除算法(使用在老年代)(两次扫描,内存碎片问题)————图片

第一次扫描标记非垃圾对象,第二次扫描清除未标记的对象

优点:所有内存空间都能使用

缺点:扫描两次浪费时间,会产生内存碎片

 

标记压缩算法(使用在老年代)(标记清除的优化版)(三次扫描,无内存碎片问题)————图片

第一次扫描标记非垃圾对象,第二次扫描清除未标记的对象————第三次扫描向一边移动幸存的对象

优点:无内存碎片

缺点:比清除法多了移动的步骤

 

分代收集算法(根据年轻代和老年代特性使用不同的GC算法)

年轻代使用复制算法,

老年代使用标记清除算法,标记压缩算法,清除几次后再压缩

 

jvm虚拟机    本地方法栈(存放本地方法)

存放内容

本地方法:用native修饰图片)的方法,由非java语言实现(c/c++语言)的方法,

常用于java自身实现困难或效率不高,而采用c/c++语言与操作系统或硬件底层的交互方法

本地方法栈:当线程使用到本地方法时,jvm会分配    一部分本地方法栈内存空间    用于存放本地方法,

之后会调用JNI(java native interface)本地方法接口,JNI作用:加载本地方法库的方法,融合不同的编程语言(c/c++)运行java程序

 

JMM(java memory model)java 内存模型

作用:缓存一致性协议,用于定义数据读写的规则(遵守,找到这个规则)。

JMM定义了线程工作内存和主内存之间的抽象关系∶

线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,先将共享变量缓存到本地内存中,在本地内存中操作完成后,更新到主内存。

Java内存模型只保证了基本读取和赋值是原子性操作(i = 10    其他的都不是原子性),要保证更大的原子性,必须使用lock和synchronized

 

 

jvm调优工具

java自带的    jvisualvm(杰维u ven)

Arthas(阿尔萨斯)

jpfiler

Arthas使用

需要先下载arthas的压缩包,之后解压有个arthas-boot.jar

开启arthas

java -jar arthas-boot.jar

 

进入监控中心

dashboard

 

查看所有线程详情

thread

 

查看线程的错误堆栈(可以直接定位到具体行,cpu占用过高)

thread 线程ID

 

查看线程死锁

thread -b

 

反编译(将程序转为代码)程序(可以检查线上是否是正确版本的代码)

jad  类全名(com.demo.projectname)

 

 jpfiler使用

下载jpfiler9.2版本,写入注册码(网上有)

idea下载插件(plugins)jpfiler

更改idea  setting  ——》 jprofiler   的启动exe

 

 

 

 

设置jvm参数,产生堆内存OOM错误信息文件(放在项目的最外层)(可以使用jpfiler打开查看报错信息)

-XX:+HeapDumpOnOutOfMemoryError

使用jpfiler   打开文件查看大对象,可以快速定位到

 

 

 

 

这个地方可以查看到哪里产生堆OOM报错

 

 

 

 

 

jvm参数设置

占堆内存的比例,年轻代:老年代  =  1:2

年轻代各个区的比例,eden:s0:s1 = 8:1:1  

-Xms8m     (设置堆初始分配内存大小    默认物理内存的1/64)

-Xmx8m     (设置堆最大分配内存大小               默认1/4)

-Xmn8m     (设置堆年轻代分配内存大小               默认1/4)

将堆的最小值-Xms 参数与最大值-Xmx 参数设置为一样即可避免堆自动扩展(堆内存会被垃圾回收器回收)

-XX:NewRatio=4,则表示新生代:老年代=1:4

-XX:SurvivorRatio=6,则表示Survivor区中的一个区:Eden区=1:6

 

-XX:+HeapDumpOnOutOfMemoryError     (存储堆内存OOM错误信息为文件(文件在项目最外层)(可以使用jpfiler软件打开))

-XX:+PrintGCDetails      (在idea中打印GC详情信息)

-Xloggc:log/gc.log(参数中gc.log就是外部文件的名称)(记录运行中GC日志到外部文件)

 

jvm调优

减少老年代full gc,因为full gc 会导致STW(stop the word),而且full gc 的STW时间较长将用户线程停止专心进行垃圾回收,用户使用就会卡顿

STW存在的意义:如果用户线程不停止,继续执行代码,在某个方法执行完成前正在执行gc,

但可能在gc过程中该方法执行完成,其方法中的局部变量变成垃圾了,

但是gc处理该方法中的变量标记却为非垃圾,gc处理失败,会再次进行处理,命中非垃圾对象的几率低,垃圾回收效率差

 

情况1:并发高的情况下,堆中的线程最后一两秒还幸存的对象大小大于生存区一半,直接进入老年代,容易产生full gc

生存区某个区(s0或s1)    相同年龄所有对象的大小总和大于 Survivor 空间(s0或s1)的一半,大于等于该年龄的对象直接进入老年代(容易将对象放入老年代,产生老年代的full  gc)

解决方法将老年代内存区域调给(伊甸区和生存区,这两个区的大小比例最好不要变),避免幸存对象大于占生存区的一半

 

情况2:机器的内存大,堆中的伊甸园区内存比较大(30G),当存满伊甸园区时,需要回收(minor gc)大约30G的垃圾STW时间长,用户使用卡

解决方法使用G1垃圾收集器,设置垃圾收集的最大停滞时间(默认200ms),其垃圾收集器将回收机制的回收阈值降低(即当伊甸园区的内存装有对象需要回收达到最大停滞时间回收100ms,就进行回收),回收3G垃圾较轻松,STW只有ms级别,对用户就没什么影响了

 

情况3:如果函数调用太深,超过了栈的大小(-Xss   通常只有几百k),则会抛出java.lang.StackOverflowError

解决方法:通常不会修改栈的大小(即修改-Xss),而是会检查函数是否调用太深,是否使用递归,递归是否有出口

 

情况4OOM错误(OutOfMemoryError):存放对象过多释放不了,年轻代和老年代空间都被占满,报堆内存溢出错误

解决方法:1.尝试扩大堆的内存空间    2.使用jvm工具分析线程运行情况定位代码错误位置

 

并发编程

守护线程和用户线程区别

守护线程(daemon):是为用户线程提供服务的,当守护线程没有可服务用户线程时(即用户线程都结束了),jvm会关闭清除所有守护线程。例如:jvm垃圾回收线程

用户线程:是程序创建的线程,当主线程结束后,用户线程不会结束会接着执行,jvm存活。

线程与进程的区别(一个jvm就是一个进程,进程中各个线程共享进程资源协同处理任务)

进程是操作系统分配资源的基本单元,线程是cpu处理器任务调度和执行的基本单位

一个应用程序只对应着一个进程,Java应用程序不存在多进程

一个程序至少有一个进程,一个进程至少有一个线程。

并发与并行的区别(并发:一个cpu切换运行不同的任务,并行:多个cpu同时进行不同的任务)

并发:进程会相互抢夺cpu资源

一个cpu多个核心也可以做到并行

 

 

 

什么是多线程中的上下文切换(多线程不停抢夺cpu使用权,让cpu切换到自己线程)

上下文就是一个释放处理器的使用权,另外一个线程获取处理器的使用权,自发和非自发的调用操作,都会导致上下文切换,会导致系统资源开销

如何避免多线程的上下文切换,资源浪费

1、简单任务使用单线程,避免多线程的上下文切换

2、在使用锁时,多线程会抢夺cpu资源导致上下文切换,

最好使用无锁hash算法并发并发编程,在多线程处理数据时,可以通过给数据取模分段(hash算法),均匀的把不同段数据的任务分给不同的线程

3、无锁CAS算法,就是等同于乐观锁(版本控制),多线程不会争夺cpu资源上锁,CAS算法采用    V现值,E期望现值,N修改后现值    参数控制是哪个线程获取资源

CAS算法(比较设置算法):通过比较V,E是否相同(线程版本号与资源版本号 是否相同),设置V值

相同就    使用N修改V(修改资源版本号),同时获取资源。

不同就    修改线程的  E期望现值,再次进入获取资源的循环

CAS图片

CAS可能产生ABA问题(简单的比较数值,可能产生V值变化1->2->1,但是最后的   1  已经不是之前的版本V了,无法控制版本)

需要添加版本号控制(version)

4、使用单线程协程多任务编程,通过在单线程中实现多任务的调度执行。

 

 

死锁与活锁与饥饿的区别?

死锁

是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁必要原因:

1.互斥条件,资源只能有一个线程占用                                                            2.请求保持条件,在自己已有一个资源的情况下,请求其他资源被阻塞,会一直等待不释放已有资源

3.不剥夺条件,其他线程不能抢夺线程中的独占资源,只能自己主动释放      4.循环等待条件,线程间互相等待对方释放资源(乱序获取资源的锁,A线程A资源,B线程B资源  都要对方的资源)

解决办法:破坏其中一种产生原因就能解决

1.破坏互斥条件,使用CAS算法(乐观锁),不产生锁,资源不会被占用。

2.破坏请求保持条件,线程不使用   synchronized   使用   Lock锁  ,其中可以设置 获取锁的等待时长,等待超时就释放自己已有资源,重试线程。(可能导致活锁

3.破坏循环等待条件,将获取资源的锁排序,必须按照顺序获取锁(先获取A资源再获取B资源),A线程获取A资源,B线程需要按照顺序  获取A资源阻塞,A线程能顺利获取B资源

检测死锁方法:

使用java自带的    jvisualvm(杰维u ven)检测定位发生死锁的代码行

 

活锁

因为某些原因,线程一直尝试,失败,尝试,失败、、导致线程一直重试

例如:使用   Lock锁  ,其中可以设置 获取锁的等待时长,等待超时就释放自己已有资源,重试线程

可能一直获取锁失败,一直重试,导致活锁

解决办法:

可以通过给重试随机设置延迟时间,在不同的时间里重试,能大大降低线程冲突的情况

 

饥饿(运气差和优先级低    导致   线程等待cpu运行时间长)

高优先级线程吞噬所有的低优先级线程的CPU时间。

线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问

线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒

解决方法:

使用   Lock接口   代替   synchronized代码块   ,其中有公平锁,优先排队等待线程,不需要抢夺资源,不存在线程抢不到资源饥饿的情况

 

缓存一致性问题(两个线程本地缓存的数据不一致)

两个线程对共享变量 i   进行 i  +1   本来预期值是2

初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存(有两个缓存其中的  i=0)当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。

此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

但是主存的值是1

解决办法

1.总线锁机制(将共享变量的内存锁住,只能由一个cpu操作,缺点:锁住内存期间,其他cpu无法访问内存,效率低下

2.缓存一致性协议(当一个cpu在写共享变量内存时,会通知其他cpu将自己的缓存设为无效的,其他cpu在发现缓存无效后会重新读取内存并缓存)

 解决以下三个特性(原子性,可见性,有序性)就能解决缓存一致性问题

 

原子性(线程中操作全部一起执行,不能被其他线程中断,要么就全部还没执行)

两个操作不是原子性的,就会发生执行了一个操作,另一个操作可能被中断,或者不执行,例如:i++

可见性(一个线程修改了共享变量的值,其他线程能够立即看得到修改的值。)

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,

那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

有序性(程序执行的顺序按照代码的先后顺序执行)

指令重排序(不会影响单线程执行结果,会影响多线程):

因为只会考虑线程中数据依赖关系,对代码的执行顺序进行优化,即对没有数据依赖关系的面的代码先执行,前面的代码后执行

多线程因为指令重排序不会考虑线程之间的数据依赖关系,会将线程中后面执行的代码先执行,可能导致其他有依赖关系的线程执行失败

 

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2先执行,唤醒线程2,但是context并没有初始化完成,会导致线程2执行失败
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

 

 

Lock与synchronized的区别

1)Lock是一个,而synchronized是Java中的关键字,synchronized是内置的语言(JVM)实现

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

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

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

5)Lock可以多个线程同时进行读操作(读写锁)

 

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,

而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:

基于线程的分配,而不是基于方法调用的分配。

举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,

而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

可中断锁

在Java中,synchronized就不是可中断锁,而Lock是可中断锁

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁

可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁(设为true)

ReentrantLock lock = new ReentrantLock(true);

读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突(可以多个线程同时进行读操作)。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

 

volatile关键字(不会执行加锁操作。保证了可见性,有序性,无法保证原子性,使用场景:状态标记量,单例模式的双重if检验锁

volatile bool flag = false;

有volatile修饰的变量在生成字节码文件时会产生内存屏障   

作用:

1.保证了不同线程对该变量操作的内存可见性,当一个线程在自己的工作内存中修改了这个变量的值,volatile 保证了新值能立即同步到主内存

2.在有线程作了写操作时,其他线程将自己的缓存设为无效的,其他线程在发现缓存无效后会重新读取主内存并缓存最新变量值

3.禁止指令重排序,有volatile修饰的变量,相隔两边的代码顺序不能越过内存屏障到另一边,一边可以随便换顺序

使用条件:必须对该变量的操作是原子性

使用场景:

1.状态标记量

volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

2.懒汉式单例模式的双重 if 检验锁(用于创建供多线程使用的单例对象(类只能创建一个对象),因为创建对象不是原子性的,需要使用synchronized

懒汉式:使用时才创建对象

public class Singleton{
    private static volatile Singleton singleton;
    private Singleton(){}
    public static Singleton newInstance(){
        //第一次if是用来提高创建后的效率,当对象已经创建了,多线程不会抢夺锁,浪费资源
        if(singleton==null){
            //获取对象的锁
            synchronized(Singleton.class){
                //第二次if是判断有没有创建对象
                if(singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}

 

posted @ 2021-09-06 21:24  低调的。。。  阅读(123)  评论(0编辑  收藏  举报