目录
AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
AQS(abstractQueuedSynchronized)
AQS(AbstractQueuedSynchronizer)
包
定义包用package关键字
1.对类文件进行分类管理。
2.给类文件提供多层名称空间。
一般在定义包名时,因为包的出现是为了区分重名的类。所以包名要尽量唯一。怎么保证唯一性呢?可以使用url域名来进行包名称的定义。
package pack; //定义了一个包,名称为pack。注意:包名的写法规范:所有字母都是小写。
类的全名称是包名.类名
包是一种封装形式,用户封装类,想要被包意外的程序访问,该类必须public;类中的成员,如果被包意外访问,也必须是public;
包与包之间访问可以使用的权限有两种:
1.pulibc
2.protected:只能是不同包中的子类可以使用的权限。
java四种权限
范围 | public | protected | default | private |
同一个类中 | ok | ok | ok | ok |
同一个包中 | ok | ok | ok | |
子类 | ok | |||
不同包中 | ok |
import 导入:类名称变长,写起来很麻烦。为了简化,使用了一个关键字:import可以使用这个关键字导入指定包中类。实际开发时,用哪个类导入那个类,不见使用*
import packa.*; //这个仅仅是导入了packa 当前目录下的所有的类。不包含子包。
如果导入两个包中存在相同名称的类,这时候如果用到该类,必须在代码中指定包名。
常见的软件包:
java.lang | language java 的核心包 |
java.awt | 定义的都是用于java图形界面开发的对象 |
java.net | 用于java网络编程方面的对象都在该包中 |
java.io | input output 用于操作设备上数据的对象都在该包中。比如:读硬盘数据,往硬盘写入数据 |
java.util | 工具包,时间对象、集合框架 |
jar: java的压缩包,主要用于存储类文件,或者配置文件等。
命令格式:jar -cf 包名.jar 包目录
解压缩: jar -xvf 包名.jar
将jar包目录列表重定向到一个文件中:jar -tf 包名.jar >c:/1.txt
多线程
进程是cpu分配资源的基本单位(CPU时间、内存等)
线程是cpu执行单位,一个进程中可以并发多个线程
总结: ①进程比线程健壮不容易死
②进程比线程更耗费系统资源
③进程之间通信比线程麻烦
并行:两个及两个以上的作业同一时刻内执行。
什么情况下用多线程
1、程序包含复杂的计算任务时( 主要是利用多线程获取更多的CPU时间(资源))。
2、处理速度较慢的外围设备 (比如:打印时。再比如网络程序,涉及数据包的收发,时间因素不定。使用独立的线程处理这些任务,可使程序无需专门等待结果)。
3、程序设计自身的需要 (WINDOWS系统是基于消息循环的抢占式多任务系统,为使消息循环系统不至于阻塞)
返回当前线程名称:Thread.currentThread.getName()
线程的名称的由:Thread的编号定义,编号从0开始。线程要运行的代码都统一存在run方法中。
线程要运行必须通过类中指定的方法开启。start方法:1.启动了线程 2.让jvm调用run方法。
并发的三大特性
原子性,可见性,有序性。
原子性:指在一个操作中,cpu不能在中途暂停然后再调度,即不被中断操作,要么全部执行完成,要么全部不执行。
可见性 :当一个线程修改了共享变量的值,其他线程能够看到修改的值。
可以使用volatile、synchronized、final解决。
并发的三大特性 -- java面试_滕青山YYDS的博客-CSDN博客
解决并发问题
主要可以分为两大类,锁机制和无锁机制
无锁
- 局部变量仅仅存在于每个线程的工作内存中,不存在共享的情况,自然就没有并发安全问题。
- 不可变对象一旦创建就不会改变,无论多个线程对他操作,他都是不可变的,自然也没有并发问题
- ThreadLocal的本质是每个线程都有自己的副本,每个线程的副本是互不影响的,自然就没有并发问题
- cas 在Java中的实现则通常是指以英文Atomic为前缀的一系列类,它们都采用了CAS的思想。Atomic使用的是Unsafe类提供硬件级别的原子操作。来看看Unsafe的方法通过v获取一个旧的值,接着CAS操作来对数据进行比较并置换,操作失败就进入while循环,直到成功为止。
有锁
Synchronized和ReentrantLock都是采用了悲观锁的策略。他们的实现非常类似,只不过一种是通过语言层面来实现 (Synchronized),另一种是通过编程方式实现(ReentrantLock)。
生命周期
- New (新创建):未启动的线程;
- Runnable (可运行):可运行的线程,需要等待操作系统资源;
- Blocked (被阻塞):等待监视器锁而被阻塞的线程;
- Waiting (等待):等待唤醒状态,无限期地等待另一个线程唤醒;
- Timed waiting (计时等待):在指定的等待时间内等待另一个线程执行操作的线程;
- Terminated (被终止):已退出的线程。 [ˈtɜːmɪneɪtɪd]
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前。 新建状态下,线程压根就没有启动,仅仅只是存在一个线程对象而已.Thread t = new Thread();
此时t就属于新建状态当新建状态下的线程对象调用了start方法,此时从新建状态进入可运行状态.线程对象的start方法只能调用一次,否则报错:IllegalThreadStateException.
例如
Thread t1=new Thread();
可运行(runnable)
分成两种子状态,ready和running。分别表示就绪状态和运行状态。
就绪状态
线程对象调用start方法之后,等待JVM的调度(此时该线程并没有运行),这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行,换句话说线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。
运行状态
线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行
堵塞(blocked)
有一个线程获取了锁未释放,其他线程也来获取,但发现获取不到锁也进入了被阻塞状态。
被阻塞状态只存在于多线程并发访问下,区别于后面两种因线程自己进入”等待“而导致的阻塞。
进入状态:1.进入synchronized 代码块/方法。2.未获取到锁
退出状态:获取到监视器锁
等待状态waiting
(等待状态只能被其他线程唤醒):
此时使用的无参数的wait方法,
当线程处于运行过程时,调用了wait()方法,此时JVM把当前线程存在对象等待池中.
计时等待状态(timed waiting)
(使用了带参数的wait方法或者sleep方法)
当线程处于运行过程时,调用了wait(long time)方法,此时JVM把当前线程存在对象等待池中.
:当前线程执行了sleep(long time)方法.
终止状态(terminated)
通常称为死亡状态,表示线程终止.
正常执行完run方法而退出(正常死亡).
遇到异常而退出(出现异常之后,程序就会中断)(意外死亡).
JVM 异常结束,所有的线程生命周期均被结束。
线程一旦终止,就不能再重启启动,否则报错(IllegalThreadStateException).
等待阻塞和锁阻塞有什么区别
等待是主动的等待,是一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。例如调用:
Object.wait() 的线程处于WAITING状态,直到另一个线程在该对象上调用 Object.notify() 或 Object.notifyAll() 。
调用 Thread.join() 的线程处于WAITING状态,等待指定线程终止
- LockSupport#park()
阻塞是被动的阻塞,一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。
线程的 blocked状态往往是无法进入同步方法/代码块来完成的。这是因为无法获取到与同步方法/代码块相关联的锁。
Java线程状态(生命周期)--一篇入魂 - 渊渟岳 - 博客园
阻塞的情况又分为三种: 最重要
- 等待阻塞:运行的线程执行wait()方法, 会释放占用的锁,JVM会把该线程放入”等待池“中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或者notifyAll()方法才能被唤醒。
- synchronized。 [ˈsɪŋkrənaɪzd]
java线程的五大状态,阻塞状态详解 - Life_Goes_On - 博客园
创建线程方式
- 继承 Thread 类,并重写它的 run 方法
- 实现Runnable接口
- 实现Callable接口 并结合 Future 实现 [ˈfjuːtʃə(r)]
- 通过线程池创建线程 JDK 自带的 Executors 来创建线程池对象
不重写 run() 或 call() 方法直接实例化Thread类创建的线程没有实际意义;
只有Callable方式创建的线程可以获取线程的返回值。
终止线程方式:
- while(flag)标志位, volatile 标记位的停止方法
2. interrupt[ˌɪntəˈrʌpt] 中断
3. stop() 方法强行终止线程,不推荐使用这个方法,该方法已被弃用
Runnable和Callable的区别:
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
start()和run()的区别
- start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
- run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)
run() 方法和start() 方法区别
方法性质不同:run 是一个普通方法,而 start 是开启新线程的方法。
执行速度不同:调用 run 方法会立即执行任务,调用 start 方法是将线程的状态改为就绪状态,不会立即执行
参考链接: cloud.tencent.com/developer/article/1997292
Wait、Sleep和Yield方法的区别
Wait [weɪt] yield [jiːld]
wait()和sleep()的关键的区别在于,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。
当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。
sleep来自Thread类,和wait来自Object类。
yield()与wait()和sleep()方法有一些区别,它仅仅释放线程所占有的CPU资源,从而让其他线程有机会运行,但是并不能保证某个特定的线程能够获得CPU资源。谁能获得CPU完全取决于调度器,在有些情况下调用yield方法的线程甚至会再次得到CPU资源。所以,依赖于yield方法是不可靠的,它只能尽力而为
链接:https://www.jianshu.com/p/25e959037eed
当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?为什么?
是不能的,其他线程只能访问该对象的非同步方法,同步方法则不能进入;
因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法,说明对象锁已经被取。
线程输出ABC ,怎么保持顺序
答:要保证T1、T2、T3三个线程顺序执行,可以利用Thread类的join方法。
- join写法(两种写法) 主线程join,子线程join
- 线程池写法
线程池shutdown方法和shutdownNow有区别,shutdown要等所有线程执行完后再关闭,shutdownNow将线程池内正在执行的线程强制停掉。
- wait、notify写法
- Condition写法
- CountDownLatch写法
- CyclicBarrier写法
- Thread.sleep写法
- CompletableFuture写法
https://juejin.cn/post/7053828761359220773
问:join方法的作用?
答: Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
问:join方法传参和不传参的区别?
答:join方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
https://blog.csdn.net/qq_35571554/article/details/82834486
- join方法将挂起调用线程的执行,直到被调用的对象完成它的执行。
join方法的使用
join在线程里面意味着“插队”,哪个线程调用join代表哪个线程插队先执行——但是插谁的队是有讲究了,不是说你可以插到队头去做第一个吃螃蟹的人,而是插到在当前运行线程的前面,比如系统目前运行线程A,在线程A里面调用了线程B.join方法,则接下来线程B会抢先在线程A面前执行,等到线程B全部执行完后才继续执行线程A。
join()方法的底层是利用wait()方法实现的
多线程中Thread的join方法_*吴聪聪*的博客-CSDN博客_thread.join
锁
想让同一时刻只有一个线程在执行某段代码。
死锁
多个进程由于竞争资源而造成阻塞的现象,如果无外力作用,这种局面就会一直持续下去。
两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
锁粗化和锁消除
锁粗化:虚拟机对低效的操作而进行的一个优化。JIT编译时,发现一段代码中频繁的加锁释放锁,会将前后的锁合并为一个锁,避免频繁加锁释放锁。
锁消除:即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。
死锁产生的原因
1.竞争资源 (进程已获得的资源,在末使用完之前,不能强行剥夺)
2.进程推进顺序冲突
预防死锁
预防死锁的角度主要是从破坏死锁产生的必要条件入手。
1.一次性分配所有资源,要什么给什么,直接给完。
2.只要有一个资源不分配,其他资源也不给了。
3.可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源
4.资源有序:系统给每类资源进行有序放号,进程则按照这些号码去请求获取资源,释放锁的时候则相反
有什么方案可以进行死锁的预防?
方案一:超时释放(破坏不可剥夺条件)
synchronized直接PASS,这玩意请求不到就阻塞。因此我们需要的是可以手动释放锁的LOCK进行释放锁。可以直接使用tryLock中的超时时限用来释放锁。
方案二:按计划好的顺序获取锁(破坏环路等待条件)
就是避免上述的第二个例子,避免首尾相接,按照规划好的获取锁的顺序去获取资源,需要按照具体场景去策划方案。
死锁检测
1、Jstack命令
链接:https://juejin.cn/post/7070386492324970532
java 各种锁
悲观锁/乐观锁
乐观锁:乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。
实现方式:CAS机制、版本号机制,java中JUC.atomic下的类, 底层CAS就是乐观锁。
悲观锁:悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改。所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。
比如Java里面的synchronized关键字的实现就是悲观锁。实现方式:就是加锁。 (synchronized | ReentrantLock 就是悲观锁的一种实现)
场景
Synchronized 和ReentrantLock
实现ReentrantReadWriteLock是读写锁的实现。(ReadLock、WriteLock)
Lock lock = new ReentrantLock();
try{
lock.lock();//加锁操作
}finally{
lock.unlock();
}
Synchronized VS ReentrantLock
ReentrantLock手动加锁,synchronized自动加锁。(发生异常自动解锁)
底层ReentrantLock cas,synchronized锁升级。
ReentrantLock 可以被打断Interruptibly,synchronized
都是可重入的,synchronize 对于线程自身可重入,对于其他线程仍然是不可重入阻塞的。
(子类实现父类,锁2位,)发生异常会自己解锁。
ReentrantLock 默认非公平 ,传入TRUE 变成公平锁。
Synchronized和Lock比较
synchronized是关键字,Lock是接口;
synchronized是隐式的加锁,lock是显式的加锁;
synchronized可以作用于方法上,lock只能作用于方法块;
synchronized底层采用的是objectMonitor,lock采用的CAS (AQS)
synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;
synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;
synchronized只支持非公平锁,lock支持非公平锁和公平锁;
synchronized使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal);
lock支持个性化定制, 使用了模板方法模式,可以自行实现lock方法;
原文链接:Lock与Synchronized区别_油头怪的博客-CSDN博客_synchronized和lock的区别
AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
锁的释放
synchronized: 1、以获取锁的线程执行完同步代码,释放锁
2、线程执行发生异常,jvm会让线程释放锁
Lock: 在finally中必须释放锁,不然容易造成线程死锁
锁的获取
synchronized: 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
锁的释放(死锁产生)
synchronized: 在发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock: 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
锁的状态
synchronized: 无法判断,Lock: 可以判断
锁的类型
synchronized: 可重入(针对自己线程,其他线程不可重入阻塞)不可中断 非公平
Lock: 可重入 可判断 可公平(两者皆可)
性能
synchronized: 少量同步,Lock: 大量同步
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
调度
synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制
Lock: 可以使用Condition进行线程之间的调度
用法
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
底层实现
synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。
Synchronize
Java中的每一个对象都可以作为锁。
具体表现为:1.普通同步方法,锁是当前实例对象
2.静态同步方法,锁是当前类的Class对象
3.同步方法块,锁是Synchronized括号里匹配的对象
如何实现
1. java 代码synchronize
2. synchronized经过编译之后,会在同步块的前后生成 monitorenter 和monitorexit这两个字节码指令。
3. jvm执行的时候锁自动升级。
4. lock cmpxchg
缺点
- 效率低
锁的释放情况少,只在程序正常执行完成和抛出异常时释放锁;
试图获得锁是不能设置超时;
不能中断一个正在试图获得锁的线程;
- 无法知道是否成功获取到锁;
原文链接:java 关于锁常见面试题_曦酆的博客-CSDN博客_java锁面试题
CPU三级缓存
CPU Cache缓存有3级 L1、L2 、L3 (L3是多个核心共享的)
程序执行时,会先将内存中的数据加载到共享的 L3 Cache 中,再加载到每个核心独有的 L2 Cache,最后 进入到最快的 L1 Cache,之后才会被 CPU 读取,越靠近 CPU 核心的缓存其访问速度越快。
缓存行
一行8字节
zhuanlan.zhihu.com/p/461548456
系统底层如何实现数据一致性
- 缓存一致性协议
- 如果不行,锁总线(只能一个cpu访问)
内存屏障是什么
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
Markword
8位,64字节(锁标记位(低3位)、分代年龄、hashCode)
偏向锁/轻量级锁/重量级锁
JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
偏向锁: 无实际竞争,且将来只有第一个申请锁的线程贴个标签(线程ID)。 1
重量级锁:1.6后竞争比较激烈jvm自己判断,自适应。(自旋超过10次或者超过cpu2分之一自动升级;) 10
独享锁(互斥锁)共享锁(读写锁)
共享锁:该锁可以被多个线程所持有
举例:
Synchronized ,ReentrantLock是独享锁; [riːˈɛntrənt]
读写锁ReentrantReadWriteLock中的读锁ReadLock是共享锁,写锁WriteLock是独享锁。
独享锁与共享锁通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享。
可重入锁
定义:对于同一个线程在外层方法获取锁的时候,在进入内层方法时也会自动获取锁。
优点:避免死锁
举例:ReentrantLock、synchronized(对于线程自身可重入,对于其他线程仍是不可重入阻塞)
公平锁 & 非公平锁
公平锁:多个线程相互竞争时要排队,多个线程按照申请锁的顺序来获取锁。(上来排队)
非公平锁:多个线程相互竞争时,先尝试插队,插队失败再排队。(插队)
比如:synchronized、ReentrantLock默认非公平的,实例化传TRUE公平的。底层cas
自旋锁 cas
如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会一上来就被挂起(阻塞), 而是采用循环的方式去尝试获取锁, 这样的好处是减少线程切换的消耗,
缺点是如果锁持有者持有时间过长, 长时间的自旋会消耗CPU
自旋锁比较适用于锁使用者保持锁时间比较短的情况, 这种情况下自旋锁的效率要远高于互斥锁
参考:https://www.jianshu.com/p/36eedeb3f912
- 有序性 (禁止指令重排序)。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。
Cas(期望值=内存的值,更新值)
CAS(Compare and Swap)即比较并替换, [kəmˈpeə(r)] [swɒp]。
思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false
经典ABA问题
解决:版本号(比较的时候也比较版本号)或者flag
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。 [əˈtɒmɪk] [stæmpt]
底层实现 lock cmpxchg 指令【 native 方法,调用c++ unsafe方法】
https://www.jianshu.com/p/6247f1e13706
https://www.jianshu.com/p/24ffe531e9ee
用户态 ,内核态
一般程序先调用用户态。
轻量级锁是在用户态,(自旋 while)
重量级:不消耗cpu,轮到你了才能给你解冻。
Synchronized、volatile、CAS 比较
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 乐观锁(非阻塞)
volatile、synchronized的区别?
synchronized主要是解决多个线程访问资源的同步性。
volatile作用于变量,synchronized作用于代码块或者方法。
volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。
volatile仅可以保证数据的可见性,不能保证数据的原子性。synchronized可以保证数据的可见性和原子性。
Volatile 修饰引用new T() 不管用。
JVM底层volatile是采用“内存屏障”来实现禁止特定类型的处理器重排序。加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
JMM具备一些先天的有序性,通过Happens-Before原则就可以保证的一定的有序性。
cpu 乱序执行,简单说就是程序里面的代码的执行顺序,有可能会被编译器、CPU 根据某种策略调整顺序(俗称,“打乱”)——虽然从单线程的角度看,乱序执行不影响执行结果。
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) { //1
if (singleton == null) { //2
singleton = new Singleton(); //3
}
}
}
return singleton;
}
}
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。
执行命令时虚拟机可能会对以上3个步骤交换位置 最后可能是132这种 分配内存并修改指针后未初始化 多线程获取时可能会出现问题。
当线程A进入同步方法执行singleton = new Singleton();代码时,恰好这三个步骤重排序后为1 3 2,
如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的,同时还会禁止指令重排序
所以使用volatile关键字会禁止指令重排序,可以避免这种问题。使用volatile关键字后使得 singleton = new Singleton();语句一定会按照上面拆分的步骤123来执行。
原文链接:volatile关键字在单例模式(双重校验锁)中的作用_指月小筑的博客-CSDN博客_双重校验锁volatile关键字的作用
原子操作即一条或者一系列不可以被中断的指令。
锁就是当多线程并发访问同一个资源时,用于保证数据一致性的一种机制,当给资源加锁时,只有一个线程能够访问资源,其他线程将阻塞。
原文链接:操作系统-原子性与锁机制_张火油的博客-CSDN博客_操作系统原子性
AQS(abstractQueuedSynchronized)
并发基类 内部定义了很多锁相关的方法,熟知的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等都是基于AQS来实现的。
用的设计模式 模板 templatemethod,父类默认实现,子类具体实现。
CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,同步队列
原文链接:JUC面试题_changyeyu_的博客-CSDN博客_juc面试题
Exclusive [ɪkˈskluːsɪv] 独占的
多线程安全问题的原因:一个线程在执行多条语句时,并运算同一个数据时,在执行过程中,其他线程参与进来,并操作了这个数据,会导致错误数据的产生。
juc常用的类
atomic 原子变量类
[əˈtɒmɪk] AtomicBoolean、AtomicInteger、AtomicReference等原子变量类
原子类底层原理无锁技术,内部调用 Unsafe API中的CAS(Compare and Swap)方法:
- Unsafe API - Compare-And-Swap
- CPU硬件指令支持: CAS指令
C++的 lock cmpchg指令
两个要点:
- volatile的value变量保证可见性
- CAS操作保证写入不冲突
链接:Java并发面试题整理(JUC、集合与并发)(附答案,持续更新) - 掘金
locks包
ReentrantLock 默认非公平锁 [riːˈɛntrənt],ReentrantLock是Lock的默认实现
ReentrantLock和关键字Synchronized都是可重入锁。
synchronized是不可中断的,ReentrantLock是可中断的
公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁
关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock对象的newContition()方法返回Condition实例,Condition类也可以实现等待/通知模式。
Condition 通知与等待:
await() :会使当前线程等待,同时会释放锁,当其他线程调用signal()或signalAll()时,线程会重新获得锁并继续执行。
signal() :唤醒一个等待的线程。如果任何线程正在等待此条件,则选择一个线程进行唤醒,那个线程必须在从 await 之前重新获取锁。
signalAll() :唤醒所有等待的线程。如果任何线程正在等待此条件,那么它们都被唤醒,每个线程必须在从 await 之前重新获取锁
链接:JUC并发编程——Synchronized锁和 Lock 锁 - 掘金
ReentrantReadWriteLock- 读写锁
ReentrantReadWriteLock.ReadLock:读锁
ReentrantReadWriteLock.WriteLock:写锁
Synchronized 和 Lock 的区别
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取到锁,Lock可以判断是否获取到锁;
3.synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁,Lock需在finally中手动释放锁;
4.用synchronized关键字,如果当前线A程阻塞,线程B会一直等待下去,而Lock锁不会等待下去,使用tryLock()尝试获取锁,如果获取不到,线程可以不用一直等待;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
7.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
链接:JUC并发编程——Synchronized锁和 Lock 锁 - 掘金
并发容器类
ConcurrentHashMap
ConcurrentHashMap 的键和值不能为空,而HashMap却可以
- Node数组+链表(红黑树)结构,可以实现快速的存储和检索,但是数据是无序的,非线程安全,默认容量为16,允许有空的键和值。
- 0.75,链表的深度大于等于8,数组容量大于等于64时,扩容的时候会把链表转成红黑树,时间复杂度从O(n)变成O(logN);当红黑树的节点深度小于等于6时,红黑树会转为链表结构。
- :如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
链表查询的时间复杂度为O(n),红黑树查询的时间理想情况o(1),最坏复杂度为O(logn)
有序的ConcunrrentSkipListMap,
CopyOnWriteArrayList
实现线程安全的动态数组(读写分离)
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
synchronized 更加高级的各种同步结构
CountDownLatch(门栓) [lætʃ]
可以看作一个只能做减法的计数器,可以让一个或多个线程等待执行。
场景: Master 线程等待 Worker 线程把任务执行完
示例:等所有人干完手上的活,包工头宣布下班休息。
重要方法:
public CountDownLatch(int count) // 构造方法(总数)
void await() throws InterruptedException // 阻塞并等待数量为0
0boolean await(long timeout, TimeUnit unit) // 限时等待
void countDown() // 等待数减1
long getCount() // 返回剩余数量
特点:
采用减法计数,
各个子线程内countdown,
调用线程/主线程里await,作为聚合点,一直到计数为0
CountDownLatch 的不足
CountDownLatch是一次性的,不可能重新初始化或者修改其内部计数器的值,当CountDownLatch使用完毕后,它不能再次被使用。
CyclicBarrier(栅栏)【满人,发车】
[ˈsaɪklɪk] [ˈbæriə(r)]可以让一组线程等待满足某个条件后同时执行。
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
解析:
parties 是参与线程的个数
第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
使用场景: 任务执行到一定阶段, 等待其他任务对齐
示例:组团去旅游, 到一个景点需要点名报数, 等人员到齐了才一起进场; 离开一个景点时也需要报数, 所有人到齐之后才前往下一个景点。
特点:采用加法计数;
各个子线程内await, 已经到达栅栏。
可以给CyclicBarrier加一个回调作为聚合点,此回调由前面的多个线程中的某个执行;
可以复用CyclicBarrier。
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。
链接:https://www.jianshu.com/p/de11b2e5798f
Phaser 多阶段栅栏
可以途中注册/注销参与者。
Exchanger 交换器
两个线程
Semaphore信号量【流量】
用于资源数有限制的并发访问场景,用在流量控制。 [ˈseməfɔː(r)]
acquire() 方法,阻塞方式获取一个许可。
release() 方法,变为0。
允许多线程运行semaphore获取锁,默认非公平,实例化传true公平锁
场景:
Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。只允许10个连接操作。
Java并发面试题整理(JUC、集合与并发)(附答案,持续更新) - 掘金
并发队列如各种 BlockedQueue 实现
1)ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
2)LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
存/取数据的操作分别拥有独立的锁,可实现存/取并行执行。
3)PriorityBlockingQueue:支持优先级排序的无界阻塞队列。VIP排队购票 底层使用堆实现
[praɪˈɒrəti] [ˈblɒkɪŋ]
4)DelayQueue:延迟队列。订单超时取消功能
[dɪˈleɪ]
SynchronousQueue:不存储元素的阻塞队列。相当于是交换通道,不存储任何元素
ConcurrentLinkedQueue
无界非阻塞队列,底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。由于使用非阻塞CAS算法,没有加锁,所以在计算size时有可能进行了offer、poll等操作,导致计算的元素个数不精确,所以在并发情况下size函数不是很有用
链接:https://www.jianshu.com/p/154387c239b0
AQS(AbstractQueuedSynchronizer)
并发基类,内部定义了很多锁相关的方法,熟知的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等都是基于AQS来实现的。
用的设计模式 模板 templatemethod,父类默认实现,子类具体实现。
AQS实现原理
AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(cas多线程争用资源被阻塞时会进入此队列)。fifo双向链表队 ,列里面加东西通过comporeAndSetState
AQS 中提供了很多关于锁的实现方法,
- getState():获取锁的标志state值
- setState():设置锁的标志state值
- tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。
场景分析
线程一加锁成功
如果同时有三个线程并发抢占锁,此时线程一抢占锁成功,线程二和线程三抢占锁失败,具体执行流程如下:
Executor框架
可以创建各种不同类型的线程池调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器
ThreadPoolExecutor线程池
java.util.concurrent.Executor : 负责线程的使用与调度的根接口
ThreadPoolExecutor 线程池的实现类
ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor,实现 ScheduledExecutorService
线程池的好处
- 降低资源消耗:通过复用已创建的线程来降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,不需要等待线程创建即可立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
线程池7个参数
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 空闲线程存活时间 |
unit | 时间单位 (keepAliveTime 的单位) |
workQueue | 任务队列 |
threadFactory | 线程工厂( 一般用默认的即可) |
handler | 线程拒绝策略 |
(多余空闲线程的存活时间。当池中线程数大于 corePoolSize,且空闲线程的存活时间达到keepAliveTime时,多余的空闲线程将被销毁,直到只剩下corePoolSize个)
处理任务的优先级
- 核心线程corePoolSize
- 任务队列workQueue
- 最大线程 maximumPoolSize
- 如果以上三者都满了,使用handler处理被拒绝的任务。
线程池拒绝策略
默认 AbortPolicy [ˈpɒləsi]
Reject策略有四种:
(1).AbortPolicy策略,是默认的策略,拒绝请求并抛出异常RejectedExecutionException。
(必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行)
(2)CallerRunsPolicy策略 ,由调用线程执行任务.
(3)DiscardPolicy策略,拒绝请求但不抛出异常.
(4)DiscardOldestPolicy策略,丢弃最早进入队列的任务.
一般要自己实现防止任务处理不了,要动态监控线程数或者队列里面的数量
ThreadPoolExecutor()
线程池实现
线程池种类 | 特点 |
newFixedThreadPool() | 创建固定大小的线程池,核心线程数和最大线程数大小一样, keepAliveTime为0,阻塞队列是LinkedBlockingQueue,处理CPU密集型的任务。 |
newCachedThreadPool() | 核心线程数为0,最大线程数为Integer.MAX_VALUE,keepAliveTime为60s, 阻塞队列是SynchronousQueue,并发执行大量短期的小任务。 |
newSingleThreadExecutor() | 创建单个线程池。核心线程数和最大线程数大小一样且都是1,keepAliveTime为0, 阻塞队列是LinkedBlockingQueue,按添加顺序串行执行任务。 |
newScheduledThreadPool() | 创建周期性任务,最大线程数为Integer.MAX_VALU,阻塞队列是DelayedWorkQueue |
SingleThreadScheduledExecutor | 单例的定时线程池 |
自定义 | 通过 ThreadPoolExecutor 的 7 个参数,自定义线程池 |
项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是 FixedThreadPool 和 SingleThreadExecutor 底层都是用LinkedBlockingQueue 实现的,这个队列最大长度为 Integer.MAX_VALUE,容易导致 OOM。所以实际生产一般自己通过 ThreadPoolExecutor 的 7 个参数,自定义线程池;
队列
7个阻塞队列
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
存/取数据的操作分别拥有独立的锁,可实现存/取并行执行。
3)PriorityBlockingQueue:支持优先级排序的无界阻塞队列。VIP排队购票
[praɪˈɒrəti] [ˈblɒkɪŋ]
- DelayQueue:延迟队列。订单超时取消功能
[dɪˈleɪ]
- SynchronousQueue:不存储元素的阻塞队列。相当于是交换通道,不存储任何元素
6)LinkedTransferQueue:由链表结构组成的无界阻塞队列
7)LinkedBlockingQueue:由链表结构组成的双向阻塞队列
JUC阻塞队列BlockingQueue竟然有8种类型? - 掘金
非阻塞队列
ConcurrentLinkedQueue 非阻塞无界链表队列
Java-BlockingQueue 接口5大实现类的使用场景 - 腾讯云开发者社区-腾讯云
阻塞队列有界和无界
无界队列意味着里面可以容纳非常多的元素,例如 LinkedBlockingQueue 的上限是 Integer.MAX_VALUE,约为 2 的 31 次方。
有的阻塞队列是有界的,例如 ArrayBlockingQueue 如果容量满了,也不会扩容,所以一旦满了就无法再往里放数据了。
原文链接:https://blog.csdn.net/qq_29860591/article/details/112211657
线程池的关闭
关闭线程池可以调用shutdownNow和shutdown两个方法来实现
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
java线程池 面试题(精简)_Linias的博客-CSDN博客_线程池面试题
final修饰的类对多线程有什么作用
Final 变量在并发当中,原理是通过禁止cpu的指令集重排序
多线程之 Final变量 详解 - alex_lo - 博客园
IO和CPU密集度
设置多了出现线程上下文切换,压测(jmeter)找平衡点
可以使用 Runtime.getRuntime().availableProcessor() 方法来获取 [ˈprəʊsesə(r)]
cpu密集型的任务 一般设置 线程数 =核心数N + 1
实际应用中 线程数 = ((线程CPU时间+线程等待时间)/ 线程CPU时间 ) * 核心数N
java中常见的六种线程池详解 - AnonyStar - 博客园
多线程面试题(2021最新版) - 腾讯云开发者社区-腾讯云
什么是可重入锁
所谓可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。(同一个加锁线程自己调用自己不会发生死锁情况)
通过为每个锁关联一个请求计数和一个占有它的线程。当计数为 0 时,认为锁是未被占有的。线程请求一个未被占有的锁时,jvm 将记录锁的占有者,并且将请求计数器置为 1 。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
synchronized 和 ReentrantLock 都是可重入锁。 [riːˈɛntrənt]
ReentrantLock 表现为 API 层面的互斥锁(lock() 和 unlock() 方法配合 try/finally 语句块来完成),synchronized 表现为原生语法层面的互斥锁。
synchronized(修饰方法和代码块) - 希希里之海 - 博客园
原文链接:synchronized作用于实例方法、静态方法、代码块的三种作用方式_一只小猛子的博客-CSDN博客_synchronized作用于静态方法
controller 单例是安全的吗? 类里面设置变量
默认单例,并发请求调用Controller生成的是同一个对象。从线程安全的角度来说,这些线程共享Controller的实例对象。
spring bean作用域有以下5个
- :原型模式,每次通过getBean获取该bean就会新产生一个实例,创建后spring将不再对其管理;
- :搞web的大家都应该明白的域了吧,就是每次请求都新产生一个实例,和prototype不同就是创建后,接下来的管理,spring依然在监听;
https://www.jianshu.com/p/8173cbebb4d5
ThreadLocal
ThreadLocal 叫做本地线程变量,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
线程池是对线程进行复用的,如果没有及时的清理,那么之前对该线程的使用,就会影响到后面的线程了,造成数据不准确。显示remove
场景
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。 ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。 在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
ThreadLocal类
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,
set()用来设置当前线程中变量的副本,
remove()用来移除当前线程中变量的副本,
initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
其实引起线程不安全最根本的原因 :线程对于共享数据的更改会引起程序结果错误。
线程安全的解决策略就是:保护共享数据在多线程的情况下,保持正确的取值。
什么是线程安全?如何保证线程安全?_凡尘炼心的博客-CSDN博客_什么是线程安全,怎样实现线程安全
内存泄漏问题
如果 key threadlocal 为 null 了,这个 entry 就可以清除了。ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收 。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
为什么key使用弱引用?
如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了,ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key ,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收, 可以认为这导致Entry内存泄漏。
面试题
读源码