高并发的三板斧:缓存、消息、分布式
总结:
1. volatile的原理,Atomic的原理(底层),synchronize的原理(底层),ReentrantLock原理(底层)。
2. 锁升级、锁降级。
3. 线程的状态图
4. 单例模式
5. JUC、ReentrantReadWriteLock、并发List
6. 线程池
7.ThreadLocal
8.阻塞队列
9.要求线程A、B、C按照指定顺序输出
一、并发基本概念
1、(个人理解)并发和高并发是两个概念,解决的是两类问题
并发:多个线程操作相同的资源,旨在保证线程安全,合理利用资源
高并发:服务能同时处理大量请求,提高程序性能
2、并发和并行:
并发(concurrent):多个交替进行,从而“看上去”是同时。
并行(parallellism):在同一时刻进行。
3、为什么需要cpu缓存?因为cpu的频率太快了,主存跟不上,cache是为了缓解两者速度不匹配这一问题。
4、cpu重排序:处理器为提高运算速度而做出违背代码原有顺序的优化。
5、java内存模型:主要参考JVM虚拟机,同时注意栈中的内容是线程隔离的。
6、java内存模型--抽象关系:(书里第二章讲的都是这个!! )
注意JMM控制其实也就是volatile字段读写的内存语义(写对应释放锁,读对应加锁)
7、java内存模型--同步操作与规则(JVM虚拟机最后一章的关键!!!)详见JVM虚拟机
二、线程概念
1. 多线程的实现
1.1 实现多线程的三种方式
- 继承Thread类,使用run方法进行同步启动
- 实现Runnable接口,使用start方法进行异步启动
- 使用ExecutorService的exec(runnable)方法运行runnable类
- 使用ExecutorService的submit(runnable/callable),启动返回结果的线程,返回值为Future,再调用get()来获得结果。
1.2 Thread和Runable的区别和联系(看看就好)
① 联系:
- Thread类实现了Runable接口。
- 都需要重写里面Run方法。
② 不同:
- 实现Runnable的类更具有健壮性,避免了单继承的局限。
- Runnable更容易实现资源共享,能多个线程同时处理一个资源。
2. 线程的状态
yield方法:使当前线程从执行状态变为就绪状态。
sleep方法:强制当前正在执行的线程休眠,当睡眠时间到期,则返回到可运行状态。
join方法:一个线程自己join自己是没用的,一般是两个线程t1和t2,t1的运行过程中调用t2.join()的意思就是线程t1加入到线程
t2的运行中,等线程t2运行完才回到线程t1继续运行,如图所示:
线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep是不会释放持有的锁)
3. sleep和wait的区别
Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。但是sleep是不释放锁的。
三、多线程之间的通信
1.wait/notify机制
wait():该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结构。
notify():该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。
notifyAll():notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
2.同步(多个线程通过synchronized关键字这种方式来实现线程间的通信)
本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
3.while轮询的方式
尽管线程A一直在while中执行,需要占用CPU。但是,线程的调度是由JVM或者说是操作系统来负责的,并不是说线程A一直在while循环,然后线程B就占用不到CPU了。对于线程A而言,它就相当于一个“计算密集型”作业了。如果我们的while循环是不断地测试某个条件是否成立,那么这种方式就很浪费CPU。如果同步快中代码进入了死循环,则可能导致多线程无法继续执行下去。
4.管道通信(通过管道,将一个线程中的消息发送给另一个)
5.线程的调度模型:(Java使用的是抢占式调度模型)
- 分时调度模型 所有线程轮流使用 CPU ,平均分配占用 CPU 的时间片
- 抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
6.守护线程:
所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。
三点五、线程终止
第一种方法是自己定义一个volatile boolean的变量来实现终止。(了解)
private volatile boolean flag= false;
protected void stopTask() {
flag = true;
}
@Override
public void run() {
while (!flag) {
// 执行任务...
}
}
第二种方法是调用interrupt()方法:此时要分两种情况讨论
① 被中断线程处于运行状态:
interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。而isInterrupt()方法会获取到中断标记,从而实现中断。
@Override
public void run() {
while (!isInterrupted()) {
// 执行任务...
}
}
② 被中断线程处于阻塞状态:
若此时调用interrupt()方法,会将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时抛出一个InterruptedException异常。因此,通过一个try-catch来判断是否出现中断。
综合两种情况,解决方法如下:在while循环的外面加一层try-catch。
@Override
public void run() {
try {
// 1. isInterrupted()保证,只要中断标记为true就终止线程。
while (!isInterrupted()) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
}
}
注:一定要在外层!!如果在内层,会出现死循环!(因为阻塞的话调用interrupt会将中断标记清除,而try-catch会将异常处理,然后接着进行下一次循环)
四、Atomic和Volatile
线程安全性包含了原子性、可见性以及有序性,
1. 原子性:同一时刻只有一个线程能对它进行操作。
2. 可见性:当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。
3. 有序性:因为在并发时,重排序可能会影响到结果,所以要使用一些方法来确保其有序性。
其中Atomic类能保证原子性,volatile能保证可见性和有序性。
1. Atomic
①(手写一个同步一个原子操作的代码)
代码见链接:https://github.com/bintoYu/concurrent-learn/blob/master/src/other/Atomic.java
② Atomic的相关源码:
(重点)AtomicX类型之所以能实现原子性,是因为其源码中实现了unsafe类,而unsafe类的方法里会使用CAS方法(拿当前值与底层的值进行对比,如果相同则进行swap操作)
AtomicInteger类:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe类:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
从最下面一行可以看到CAS方法是native的,意思就是用的是底层的hotspot的c/c++源码,如果去细究底层的话,最后最后会到这么一行代码,这是一行cpu硬件的指令:
lock cmpxchg (compare and exchange的意思)
实际上,compare and exchange并没有原子性,因此需要使用lock,lock的意思是:在执行这个指令时,不允许其他cpu打断我,这样就能保证原子性。
附:AtomicLong和LongAdder类:
补充知识:对于64位的long及double类型,jvm允许将64位的读/写操作 拆分成 两次32位的读/写操作。
LongAdder:经过一系列方法确保效率高,高并发时优先使用,但精度可能会下降。
AtomicLong:序列号生成这一类需要准确的数值时,使用AtomicLong
③ Atomic的ABA问题:
- 线程 1 从内存位置V中取出A。
- 线程 2 从位置V中取出A。
- 线程 2 进行了一些操作,将B写入位置V。
- 线程 2 将A再次写入位置V。
- 线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。
尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。
解决方法:用AtomicStampedReference/AtomicMarkableReference (维护了一个“状态戳
”)
e)总结:
synchronize:适合竞争不激烈,可读性好
lock:竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,且比lock性能好,但只能同步一个值
2. Volatile
volatile有三个功能:保证可见性、保证有序性以及内存屏障
①:保证可见性:
1. volatile写操作时会强制将修改后的值刷新到主内存中
2. 写操作后会导致其他线程工作内存中对应的变量值失效,因此,再读取该变量值的时候就需要重新从读取主内存中的值。
通过这两个操作,就可以解决volatile变量的可见性问题。
注1:synchronize也能保证可见性:解锁前,把共享变量写入主内存。加锁时,清空工作内存中共享变量,确保从主内存中读到最新的值。(写对应释放锁,读对应加锁)
注2:!!!!(注意:对于volatile int count,在多线程环境下count++ 并不能保证线程安全)即volatile不能保证原子性!!!!
不具备的原因:volatile为什么不能保证原子性_十一月上的博客-CSDN博客_volatile为什么不能保证原子性
实际上,volatile不适合计数,但很适合作为状态量的标识
②:保证有序性:
JVM会对代码进行优化,导致代码段内的代码出现重排序,在并发环境下重排序可能会影响到结果。
volatile会保证有序性,禁止重排序,volatile标识的部分不允许代码段内出现重排序,从而保证有序性。
静止重排序是通过内存屏障来实现的:
JSR(JavaSpecification)规范将内存屏障分为以下四种:
1. LoadLoad屏障
2. StoreStore屏障
3. LoadStore屏障
4. StoreLoad屏障
其实很简单,以LoadLoad为例,就是上面的Load语句和下面的Load语句,这哥俩不能重排序。其他的也是一样的道理。
对于volatile而言,JVM层面要求volatile读和写操作前加屏障:
StoreStoreBarrier | LoadLoadBarrier
volatile写 | volatile读
StoreLoadBarrier | LoadStoreBarrier
也很简单,StoreStoreBarrier要求必须上面的写完才能进行下面的volatile写,StoreLoadBarrier要求必须上面的volatile写完才能进行下面的读操作。 读操作同理。
附有序性的部分规则(了解):
happens-before原则:
①程序次序规则:按照代码顺序(单线程)
②锁定规则:unlock在lock的前面
③volatile规则:对一个变量的写 先行发生于 对这个变量的读
④传递规则:A -> B , B ->C 则 A->C
四点五、单例模式:
a)懒汉模式:(可能多个线程同时访问到null,然后都new了对象)。
双重检测机制:不是线程安全,因为可能会出现重排序(2和3可以交换顺序)而导致不安全。
1.给instance分配空间
2.调用 Singleton 的构造函数来初始化、
3.将instance对象指向分配的内存空间(instance指向分配的内存空间后就不为null了);
因此不加volatile的话可能会有两种情况:
1、 先init ,再instance - > O
2、先instance -> O ,再 init
如果线程1 先instance -> O, 此时线程2进行了if(instance == null) ,这时 instance 已经是非 null 了(但却没有初始化),所以线程2会获得没有初始化的instance然后去操作,就会出错。
解决方案:双重检测+volatile。
b)饿汉模式(直接new):线程安全
五、synchronize(可重入)
1、修饰的对象:
第一类(实例锁):代码块(中括号括起来的),方法(如果有两个对象的话,两者相互不影响,即test1和test2的0-9是乱序的)
第二类(全局锁):静态方法块,类:(两个对象也没用,会锁住,即test1:0到9 再 test2:0到9)
2、举例:
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
注意:(以下得是不同线程之间的访问,同一个线程的话是一定可以访问的)
(01) 线程1:x.isSyncA(), 线程2: x.isSyncB() : 不能被同时访问,因为isSyncA()和isSyncB()都是访问同一个对象(对象x)的同步锁!
(02) 线程1: x.isSyncA(), 线程2:y.isSyncA():可以同时被访问。因为访问的不是同一个对象的同步锁。
(03) 线程1:x.cSyncA(), 线程2:y.cSyncB(): 不能被同时访问。x.cSyncA()和y.cSyncB()都相当于Something.cSyncA()。
(04) 线程1:x.isSyncA(), 线程2:Something.cSyncA():可以被同时访问。一个是对象的锁,一个是类的锁。
3、注:Thread类内部使用synchronized的情况:
//以下两个Thread用的是两个不同的对象,因此实例锁的时候会乱序
Thread t1 = new MyThread("t1"); // 新建“线程t1”
Thread t2 = new MyThread("t2"); // 新建“线程t2”
//以下两个new基本不用,但可以加深理解,他们使用实例锁的时候会互斥
Runnable demo = new MyRunable(); // 新建“Runnable对象”
Thread t1 = new Thread(demo, "t1"); // 新建“线程t1”, t1是基于demo这个Runnable对象
Thread t2 = new Thread(demo, "t2"); // 新建“线程t2”, t2是基于demo这个Runnable对象
4、Synchronized 原理一 :可重入-通过monitorenter和monitorexit来实现
synchronized中可重入的原理是指自己在持有锁的时候可以重复进,别人不可以
monitorenter :
每个对象都有一个monitor锁,包含线程持有者和计数器。
1.如果计数器为0,则该线程进入monitor,然后将计数器设置为1,该线程即为monitor的所有者。
2.如果线程已经占有该monitor,重新进入,则计数器加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到计数器为0。
monitorexit:
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
5、Synchronized 原理二 :偏向锁、轻量级锁、重量级锁
本小节内容参考自马士兵老师的视频:马士兵2022年最新Java多线程高并发编程详细讲解——20年架构师告诉你Java多线程与高并发应该怎么学_哔哩哔哩_bilibili
先说一下轻量级和重量级的区别:轻指的是通过CAS模拟“锁”的功能,因为CAS不需要用到内核态(不需要向OS老大申请权限),所以轻;重量级指的是你得去找操作系统申请锁了,这一过程需要经过操作系统内核,所以重。
重量级锁的锁,具体指的是内核中mutex,这是一个互斥的数据结构,同时mutex数量是有限制的。
早期的时候,JDK的synchronize是重量级锁,但因为重量级锁太笨重,在很多情况下效率太低,因此后面才慢慢改进成偏向锁-轻量级锁-重量级锁这一机制。
正式介绍:
一开始是无锁状态,不需要解释;
为什么会有偏向锁? 因为很多情况下,synchronize方法都是一个线程在运行。
以厕所为例,偏向锁就是第一个来的人,直接把自己的名字贴到厕所门上,就进去上厕所了,这样的好处就是没别人的时候我也不需要摘掉,想什么时候用就用。
当来了第二个人开始,就需要抢厕所了,抢的过程其实很简单,首先,先把门上的名字撕下来,然后谁能把自己的名字贴到厕所门上(使用CAS,不需要经过操作系统内核,具体实现是会在线程栈中生成一个自己线程的LockRecord,然后尝试把LockRecord使用CAS赋值到对象上,如下图所示),谁就算抢到了这把锁,这个锁就是轻量级锁(实际上会优先照顾拥有偏向锁的那个线程),没有抢到的话,就使用CAS看看门上的名字还在不在,还在的话就自己转一圈然后再尝试CAS,不在的话就赶紧贴上自己的名字,所以说,轻量级锁也叫自旋锁。
当抢厕所的人很多,或者某个人转圈的次数太多了(JDK1.6以前是超过1/2CPU核数,或者转圈次数超过10次,JDK1.6以后采用自适应自旋,让JVM自己来计算)。和现实中一样,就需要通过排队来维持秩序了。OS会开辟一个队列,让大家都去排队,排队的时候线程会进入等待BLOCK状态,不需要转圈(不消耗CPU资源),轮到谁OS就会去叫谁。这是锁就升级为了重量级锁,因为重量级锁需要向操作系统OS这个老大来申请并维护,所以比较重。
为什么要用到重量级锁? 因为转圈是需要消耗CPU资源的,但是排队的时候是不需要消耗资源的。
这里补一张markword的布局图,分为了几种情况:
- 无锁态:即普通对象,有hashcode、分代年龄,同时锁包含了三位。
- 偏向锁:有一个指向当前线程的指针(相当于把自己的名字贴到门上),epoch略,分代年龄,同时锁包含了三位。
- 轻量级锁:线程栈中指向当前线程的指针,锁包含了两位。
- 重量级锁:指向锁的指针,锁包含了两位。
- GC标记,标记信息,锁包含了两位。
问题:偏向锁一定优于轻量级锁?
不一定,在你明确知道会有不止一个线程时,就要直接去使用轻量级锁,因为偏向锁总是会多出“撕下名字”这一过程(锁撤销)。 因此偏向锁虽然默认启动,但是会有延迟(4s),这4s内要是有多个人抢的话,就直接轻量级锁。
6、synchronized与Lock的区别
补充:Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。
六、锁
1、锁的一些概念:
① 公平锁,非公平锁:
- 公平锁:先请求锁的一定先被满足,即FIFO
- 非公平锁则反之,容易造成一个线程连续获得锁的情况,导致线程“饥饿”。
② 悲观锁、乐观锁:
- 悲观锁:一般用于写操作,认为自己取数据的时候总是会有人在修改。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
- 共享锁:一般用于读操作,是一种乐观锁。读的时候不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据(例如CAS),可以使用版本号等机制。
③ 排他锁、共享锁:(适用场景:1. 数据库的锁 2. ReentrantReadWriteLock)
- 共享锁(读锁):读可以共享,但是读写之间互斥。
- 排他锁(写锁):只有自己能操作,其他人都不能访问。
④ 自旋锁:对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。
⑤ 偏向锁,轻量级锁,重量级锁:详见synchronize原理二部分。
2、Lock API:
Lock是一个接口,方法定义如下:
void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
void lockInterruptibly() // 和 lock()方法相似, 但阻塞的线程可中断,抛出 java.lang.InterruptedException异常
boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回true
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
void unlock() // 释放锁
3. AQS(AbstractQueueSynchronizer)(重点)
① AQS的内部结构:
(重要!背)AQS依赖一条队列(称为同步队列)(FIFO,由一个个Node组成,双向链表),且维护一个volatile int的共享资源state。
对于ReentrantLock而言,state=0表示同步状态可用(如果用于锁,则表示锁可用),state>0表示同步状态已被占用(锁被占用),因为ReentrantLock是可重入锁,所以state可以大于1。
private volatile int state
需要注意的是:不同的AQS实现,state所表达的含义是不一样的。
可以这么说,只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握。
② AQS的2种同步方式:排它锁,共享锁。
排它锁如ReentrantLock,共享锁如Semaphore和CountDownLatch。
③ AQS的动态变化(重点,背!):
当线程没有抢到锁时,会将该线程封装成一个Node结点,然后加到队尾。(tail的设置需要用到CAS)
队列中的Node节点会进行自旋(不断循环)判断,直到自己的上一个节点是头节点head(也就是第二个节点)时,才会被暂停(通过LockSupport.park()方法实现),head节点释放锁时会去唤醒第二个节点,然后第二个节点就可以去获取锁。
具体代码可见ReetrantLock的源码分析。
④ AQS的使用场景
JUC中的很多类都是基于AQS来实现的,其中最经典的就是ReentrantLock,接下来介绍ReentrantLock,并帮助加深对AQS的理解。
4、ReentrantLock (重入锁)
①ReentrantLock的原理
a)ReentrantLock中的Sync实现了AQS,所以实际上是有一条同步队列来进行控制的。
b)可重入的原理和synchronize的原理相似,“AQS中有个volatile的state,当前线程每进入一次,state就+1”
② ReentrantLock的两种同步方式:公平锁,非公平锁
公平锁由FailSync实现,非公平锁由UnFailSync实现,默认是非公平锁。
注意:无论是公平锁还是非公平锁,一旦线程抢锁失败被封装成Node节点进入同步队列后,就得按照顺序一个一个去获取锁了,此时就不是非公平的了。
不信的小伙伴可以看我的实验代码,代码中先生成了一个线程把锁抢住不放,然后每隔0.1s顺序生成线程t2至t10,在第一个线程释放锁后,可以看到,锁还是会按照顺序一个一个分配,即便用的是非公平锁,代码地址:
https://github.com/bintoYu/concurrent-learn/blob/master/src/juc/ReentrantLockUnfailTest.java
那么公平和非公平的区别在哪呢?
现在有这么一个场景:A占着厕所,B在排队,当A释放厕所,还没唤醒B时,C来了。如果是非公平锁,C就有可能会抢到厕所,因为他直接奔向厕所了。如果是公平锁,即便厕所没人,也会先看看队列里有没有人,有人的话会先老老实实去排队。
具体细节详见知乎大牛的文章:说一下公平锁和非公平锁的区别?
下面对非公平进行大概地源码分析:
③ ReentrantLock源码分析 ----以lock()为例 (感兴趣的可以看看,时间有限的背一背上面的AQS动态变化即可)
lock()时序图如图所示:
最后的addWaiter()方法会将线程封装成一个Node节点添加到队列末尾处。
这里只介绍几个重要的方法,首先是NonfailSync的lock()方法:
final void lock() {
//由于这里是非公平锁,因此需要通过对state进行cas来抢占锁
if (compareAndSetState(0, 1))
//设置当前线程为锁的拥有线程
setExclusiveOwnerThread(Thread.currentThread());
else
//没抢到的话,尝试去获取锁
acquire(1);
}
可以看到,由于这里是非公平锁,因此需要通过cas来抢占锁。
接着看真正的抢锁的实现:
//acquires指的是当前线程需要使用锁的次数(因为是重入锁)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//state ==0 :即当前锁可占有
if (c == 0) {
//使用CAS尝试获取锁
if (compareAndSetState(0, acquires)) {
//设置当前线程为锁的拥有者
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程本来就是锁的拥有者,则可以重复进入(重入锁)
else if (current == getExclusiveOwnerThread()) {
//增加state
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
addWaiter()的功能是:当前线程如果抢锁失败,则会被封装成一个Node然后加入到同步队列中,这里不列具体代码。
accquireQueued()的代码如下,不断地进行自旋,直到上一个节点是
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//不断循环(自旋)
for (;;) {
final Node p = node.predecessor();
//只有当前一个节点是head时才能尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果前一个节点是head,则会被暂停(通过LockSupport.park()来实现)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
解锁部分就不细讲了,只给出代码:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//解锁时会去唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
...
//看这里,会调用unpark()唤醒下一个线程
if (s != null)
LockSupport.unpark(s.thread); //释放许可
}
④ AQS和Condition:(重要)
AQS最经典的场景就是ReentrantLock的Condition:
Condition 实现等待的原理是:除了ReentrantLock的同步队列外,Condition自己内部还有一个等待队列。
Condition 的本质就是等待队列和同步队列的交互:
当一个持有锁的线程调用 Condition.await() 方法,那么该线程会释放锁,然后构造成一个Node节点加入到等待队列的队尾。
当一个持有锁的线程调用 Condition.signal() 时,它会执行以下操作:
将等待队列队首的节点移到同步队列,然后对其进行唤醒操作。
通过Condition可以实现线程的分组运行,具体示例:https://github.com/bintoYu/concurrent-learn/blob/master/src/juc/ReentrantLockCondition.java
除此之外,还可以通过Condition分组来实现生产者/消费者模型:
https://github.com/bintoYu/concurrent-learn/blob/master/src/juc/Depot.java
⑤Synchronize和ReentrantLock的区别(初级程序员优先使用synchronize)
性能上:
synchronized在资源竞争不是很激烈的情况下是很合适的。原因在于,编译程序通常会尽可能的优化synchronized,另外可读性非常好。 ReentrantLock: 当同步非常激烈的时候,ReentrantLock还能维持常态。
功能上:Synchronize有的ReentrantLock都有。但ReentrantLock有一些独有的功能:
a)可指定是公平锁还是非公平锁 (默认是非公平锁)
b)提供了Condition类,可分组唤醒线程
c)lock.lockInterruptibly(),允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
5、读写锁 ReentrantReadWriterLock (没有读写的情况才能写)
①分别有一个readLock和一个writeLock
②readLock是共享锁,writeLock是排它锁,也就意味着:
1、读和读之间不互斥 (与非读写锁的区别就在这)
2、写和写之间互斥
3、读和写之间互斥(这个超级重要)
③读写锁的锁降级概念:(获得读锁的特例)
- 锁降级:t0 上写锁 --> 修改值 --> 上读锁 --> 释放写锁 --> 使用数据 --> 释放读锁
- 也就是说,锁降级就是在释放写锁的前一步上读锁,因为是同一个线程,所以这两个锁不会冲突。
- 如果不使用锁降级,可能会出现以下原因:(重点看1和2)
- t0 上写锁 --> 修改值 --> 释放写锁 --> 使用数据,即使用写锁后直接使用刚刚修改数据 ,与此同时,t1在t0释放写锁后获得写锁。这样t0使用的是t1修改前的旧数据。
- t0 上写锁 --> 修改值 --> 释放写锁 -->上读锁 --> 使用数据,也就是用完写锁再去获取读锁,其缺点在于,如果有别的线程在等待获取写锁,那么上读锁时会进入等待队列进行等待,就无法立即使用刚修改的值。
- t0 上写锁 --> 修改值 --> 使用数据 --> 释放写锁,这样做虽然不会有问题,但这样是以排它锁的方式使用读写锁,就没有了使用读写锁的优势。
七、J.U.C
注:CountDownLatch、CyclicBarrier 等的底层也是借助AQS
1、J.U.C之CountDownLatch (多线程运行,确保latch中的线程都执行完后其他线程才变成resume状态)
①调用countDown() 让计数减1
②调用await() 等待计数变成0后,其他线程(本例中为主线程)变成resume。
③await()可以设置时间,达到时间就接着往下进行。
注意:以下代码中的test方法中都会等待1秒,以便实现线程的堵塞。
2、J.U.C之CyclicBarrier (多线程计算数据,最后合并结果)
当test方法里使用await()的个数达到5个(即有五个线程ready),这五个同时变成resume。
注:这里的await()方法实际上用的就是Condition.await();
常用场景:达到个数时回滚,以及多线程计算数据,最后合并结果:
考题:Java中CyclicBarrier 和 CountDownLatch有什么不同?
a)CountDownLatch 对外,CyclicBarrier对内。前者是一组线程都countDown后其他线程才能接着进行。而后者是内部多个线程相互等待,都ready后再一起执行。
b)与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。
3、J.U.C之Semaphore (控制对资源访问的线程数量)
① 调用acquire() 和 release() 方法。先在构造函数中设置好资源数,当资源不够某个线程获取时,便进入堵塞状态。
② tryAcquire():立即尝试获取资源 ,获取不到便丢弃线程并返回false。(下图中只会有3个线程执行,其余的线程因为获取不到资源被丢弃)
③ tryAcquire()还可以带时间参数,表示多久
4、JUC之LockSupport:
LockSupport定义了一组以park开头的方法来阻塞当前线程,以及unpark方法来唤醒一个被阻塞的线程。
一般来说,如果我们想暂停某个线程,不论是调用wait()还是await(),线程是得有锁才能被暂停的,而LockSupport不需要线程有锁就能暂停。另外,notify()方法无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。
public class LockSupportExample {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for(int i = 0; i < 10; i++){
System.out.println(i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(i == 5){
LockSupport.park();
}
}
});
thread.start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(thread);
}
}
5、并发List:
注意:因为ArrayList和LinkedList都不是线程安全的,所以得使用Collections的synchronizedList方法来生成线程安全的list:
List list = Collections.synchronizedList(new LinkedList<>());
//或
List list = Collections.synchronizedList(new ArrayList<>());
八、线程池
1. 为什么用线程池?
- 1.创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率。(线程复用)
- 2.线程并发数量过多,抢占系统资源从而导致阻塞。(控制并发数量)
- 3.对线程进行一些简单的管理。(管理线程)
2. 线程池的原理
(1)线程复用:实现线程复用的原理应该就是要保持线程处于存活状态(就绪,运行或阻塞)
(2)控制并发数量:(核心线程和最大线程数控制)
(3)管理线程(设置线程的状态)
3.线程池的参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
- keepAliveSeconds:空闲存活时间 (非核心线程空闲的时间超过该值就会被销毁。)
- workQueue:阻塞队列,用来保存等待被执行的任务
4. 当一个任务被添加进线程池时,执行策略:
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量闲达到了corePoolSize,则将任务移入队列,等待空线程将其取出去执行 (通过getTask()方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源,整个getTask操作在自旋下完成)
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,就会执行任务拒绝策略。
5. workQueue的几种类型:
(1)SynchronousQueue:直接提交队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行插入操作的线程就要一直等待,反之亦然。
public class ThreadPool {
private static ExecutorService pool;
public static void main( String[] args )
{
//maximumPoolSize设置为2 ,拒绝策略为AbortPolic策略,直接抛出异常
pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.execute(new ThreadTask());
}
}
}
输出结果:
pool-1-thread-1
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.hhxx.test.ThreadTask@55f96302 rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 2]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
at com.hhxx.test.ThreadPool.main(ThreadPool.java:17)
可以看到,创建了三个线程,因为创建的线程数大于maximumPoolSize时,直接执行了拒绝策略抛出异常。
(2)ArrayBlockingQueue:有界的任务队列,是一个有指定容量的队列。
(3)LinkedBlockingQueue:无界的任务队列,是一个容量无限的队列,因为容量无限,所以使用这个队列的时候,就不会去创建非核心线程,也就是说maximumPoolSize这个参数是无效的。
(4)PriorityBlockingQueue:会按照任务的优先级进行排序。
6. 线程池的任务拒绝策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
7 常见四种线程池:
(1)可缓存线程池CachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
根据源码可以看出:
这种线程池内部没有核心线程,线程的数量是有没限制的。
在创建任务时,若有空闲的线程时则复用空闲的线程,若没有则新建线程。
没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁。
适用:执行很多短期异步的小程序或者负载较轻的服务器。
(2)FixedThreadPool 定长线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
根据源码可以看出:
该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。
如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。
适用:执行长期的任务,性能好很多。
(3)SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
根据源码可以看出:
有且仅有一个工作线程执行任务,所有任务按照指定顺序执行,即遵循FIFO规则。
适用:一个任务一个任务执行的场景。
(4)ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
根据源码可以看出:
DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是CachedThreadPool和FixedThreadPool 结合了一下。
不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。
这个线程池是上述4个中唯一一个有延迟执行和周期执行任务的线程池。
适用:周期性执行任务的场景(定期的同步数据)
总结:除了new ScheduledThreadPool 的内部实现特殊一点之外,其它线程池内部都是基于ThreadPoolExecutor类实现的。
8. 在ThreadPoolExecutor类中有几个非常重要的方法:
execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
submit(),这个方法也是用来向线程池提交任务的,实际上它还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
shutdown()不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow()立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
9. 线程池中的最大线程数
一般说来,线程池的大小经验值应该这样设置:(其中N为CPU的个数)
- 如果是CPU密集型应用(CPU使用频率高,不适合频繁切换),则线程池大小设置为N+1
- 如果是IO密集型应用,则线程池大小设置为2N+1
九、ThreadLocal
1. 概念
ThreadLocal,顾名思义,就是可以给每个Thread自己用的容器。
一句话:每个线程内部都有一个自己的ThreadLocalMap,因此可以用来做线程数据隔离,保证线程安全。
map中key为ThreadLocal,value为任意对象。因此使用的时候,生成一个ThreadLocal,然后将需要的数据set进去即可。要set多个数据就new 多个ThreadLocal。
2.原理
ThreadLocal类提供的几个方法:
get() 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
remove()方法 (略)
初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量进行哈希。
3.使用方法:
new一个或多个ThreadLocal,然后调用set()和get()获取就行。
public class ThreadLocalTest {
private static ThreadLocal threadLocal = new ThreadLocal();
private static ThreadLocal threadLocal2 = new ThreadLocal();
public static void main(String[] args) {
new Thread(() ->{
//要存两个对象的话就得new两个不同的ThreadLocal
threadLocal.set(new Person("zhangsan"));
threadLocal2.set(new Person("lisi"));
System.out.println(threadLocal.get());
System.out.println(threadLocal2.get());
}).start();
new Thread(() ->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里print出来的是null,因为不同线程的ThreadLocal的数据是隔离的,
System.out.println(threadLocal.get());
System.out.println(threadLocal2.get());
}).start();
}
static class Person{
String name ;
public Person(String name) {
this.name = name;
}
}
}
4. 使用场景
最常见的ThreadLocal使用场景有两个: 管理数据库连接、管理Session(尤其是员工信息)。
4.1 管理数据库连接
我们知道,数据库的事物Transaction有下面两个需求:
1. 要求在一个方法内的所有方法都执行或都回滚,因此要确保某个线程在执行事物方法时,connection始终不变。
2. 为了保证效率,可以让不同线程使用不同的connection。
而spring借助ThreadLocal便可以完美实现这一机制。
具体源码可见我的另一篇博客spring源码系列(十) 事物Transaction的第五部分-spring Transaction 使用ThreadLocal管理Connection。
4.2 管理session(员工信息)
十、并发容器
注意:以下队列的共性:
1.put()和take()是会阻塞的,而offer()和poll()是不会阻塞的。
2.并发都是通过ReentrantLock实现的。
3.阻塞是通过两个condition实现的(notEmpty和notFull)。
1、阻塞队列:BlockingQueue
这里blocking的具体含义:
当队列中没有数据的情况下,消费者端会被自动阻塞(挂起),直到有数据放入队列。
当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
1.1 ArrayBlockingQueue:读和写共用同一个ReentrantLock,也就意味着(读读、写写、读写都互斥)。
并发是由ReentrantLock来实现,而阻塞是有lock的两个Condition来实现。
//仍然是有数组实现
final Object[] items;
//并发是由ReentrantLock来控制
final ReentrantLock lock;
//阻塞是有两个Condition来实现
private final Condition notEmpty;
private final Condition notFull;
//put方法会先加锁,操作完再解锁,如果阻塞进入await()
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
//enqueue里有signal操作
enqueue(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
//dequeue里有signal操作
return dequeue();
} finally {
lock.unlock();
}
}
最初我以为共用一个锁的话,在线程1读空队列阻塞,然后牢牢占据着锁不放 -> 线程2尝试写到该队列 会发生死锁,但自己实验后发现并不会,后来相通不会的原因在于如果进入阻塞(await)会先把锁释放!并不会占据着锁不放!
1.2 LinkedBlockingQueue:生产者端和消费者端分别采用了独立的锁来控制数据同步,也就意味着读和写之间是不互斥的!(注意这里和读写锁ReentrantReadWriteLock的区别)。
同样,阻塞是通过两个Condition实现的。
//内部使用带next的Node来实现linked结构
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
//读锁
private final ReentrantLock takeLock = new ReentrantLock();
//读阻塞
private final Condition notEmpty = takeLock.newCondition();
// 写锁
private final ReentrantLock putLock = new ReentrantLock();
//写阻塞
private final Condition notFull = putLock.newCondition();
1.3 PriorityBlockingQueue(分布式任务管理平台就用的这个):只有一个锁,内部控制线程同步的锁采用的是公平锁。queue中存储的类需要实现compare方法。
1.4 LinkedBlockingDeque
双向队列,只有一个锁,两个condition。
1.5 DelayQueue:延时获取
除此之外还有三种队列。
2、ConcurrentHashMap (详见java笔记)
3、ConcurrentLinkedQueue:(了解)
3.1 是一个单向队列,队列由Node组成
3.2 并发的确保:head和tail设置为volatile,Node中的item和next也设置为volatile。
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
private static class Node<E> {
volatile E item;
volatile Node<E> next;
...
}
3.3 Node中操作节点数据的API,都是通过Unsafe机制的CAS函数实现的;例如casNext()是通过CAS函数“比较并设置节点的下一个节点”
补充题目:
1、你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写。
关键:使用ReentrantReadWriteLock
class MyData{
//数据
private static String data = "0";
//读写锁
private static ReadWriteLock rw = new ReentrantReadWriteLock();
//读数据
public static void read(){
rw.readLock().lock();
System.out.println(Thread.currentThread()+"读取一次数据:"+data+"时间:"+new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rw.readLock().unlock();
}
}
//写数据
public static void write(String data){
rw.writeLock().lock();
System.out.println(Thread.currentThread()+"对数据进行修改一次:"+data+"时间:"+new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rw.writeLock().unlock();
}
}
}
2、你将如何使用thread dump?你将如何分析Thread dump?
kill -3 <pid>
说明: pid: Java 应用的进程 id ,也就是需要抓取 dump 文件的应用进程 id 。
当使用 kill -3 生成 dump 文件时,dump 文件会被输出到标准错误流。假如你的应用运行在 tomcat 上,dump 内容将被发送到/logs/catalina.out 文件里。
dump文件示例:
"pool-1-thread-13" prio=6 tid=0x000000000729a000 nid=0x2fb4 runnable [0x0000000007f0f000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
- locked <0x0000000780b7e688> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:167)
at java.io.BufferedReader.fill(BufferedReader.java:136)
at java.io.BufferedReader.readLine(BufferedReader.java:299)
- locked <0x0000000780b7e688> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:362)
* 线程名称:pool-1-thread-13
* jvm线程id:tid=0x000000000729a000
* 线程状态:runnable
* 起始栈地址:[0x0000000007f0f000]
3、 用java写一个死锁
public static void main(String[] args)
{
Object lock1 = new Object();
Object lock2 = new Object();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(() ->{
synchronized(lock1)
{
System.out.println("get lock1,want lock2...");
Thread.sleep(1000); //try-catch忽略
synchronized (lock2)
{
System.out.println("get lock2");
}
}
}
);
exec.execute(()->{
synchronized (lock2)
{
System.out.println("get lock2,want lock1....");
Thread.sleep(1000); //try-catch忽略
synchronized (lock1)
{
System.out.println("get lock1");
}
}
});
}
4、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
用start()来启动线程 ---> 异步执行
而如果使用run()来启动线程 ---> 同步执行
多线程就是为了并发执行,因此需要使用start
5、使用notFull和notEmpty来实现生产者/消费者模型
public class Depot
{
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
LinkedList<Integer> queue;
int limit;
public Depot(int limit)
{
queue = new LinkedList<>();
this.limit = limit;
}
public void produce(int i)
{
lock.lock();
try
{
// System.out.println("生产" + i);
//满了阻塞
if(queue.size() == limit)
// {
// System.out.println("队列已满,进入阻塞");
notFull.await();
// System.out.println(i+ "已被唤醒");
// }
queue.offer(i);
notEmpty.signal();
}catch(Exception e)
{
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
public int consume()
{
int num = -1;
lock.lock();
try
{
//空了阻塞
if(queue.size() == 0)
// {
// System.out.println("队列已空,进入阻塞");
notEmpty.await();
// System.out.println("已被唤醒");
// }
num = queue.poll();
// System.out.println("消费:"+num);
notFull.signal();
}catch(Exception e)
{
e.printStackTrace();
}
finally
{
lock.unlock();
}
return num;
}
}
public static void main(String[] args)
{
ExecutorService exec = Executors.newCachedThreadPool();
Depot depot = new Depot(5);
for(int i = 0; i < 3; i++)
{
final int num = i;
exec.execute(()->
{
depot.produce(num);
});
}
try
{
Thread.sleep(200);
}
catch (InterruptedException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i = 0; i < 7; i++)
{
final int num = i;
exec.execute(()->
{
depot.consume();
});
}
try
{
Thread.sleep(200);
}catch (InterruptedException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
for(int i = 0; i < 3; i++)
{
final int num = i;
exec.execute(()->
{
depot.produce(num);
});
}
}
6、3个任务,返回结果,如果超时200ms,返回空:
//调用三个任务,要求200ms内返回结果或者空
public class 两百秒内返回结果或者空
{
public static void main(String[] args)
{
ExecutorService exec = Executors.newCachedThreadPool();
Future<Integer> future1 = exec.submit(new MyTask());
Integer i1 = future1.get(200,TimeUnit.MILLISECONDS);
if(i1 == null)
System.out.println("null");
else
System.out.println(i1);
}
}
class MyTask implements Callable<Integer>
{
@Override
public Integer call() throws Exception
{
int sum = 0;
for(int i = 0;i < 100; i++)
{
sum+=i;
}
Thread.sleep(300);
return sum;
}
}
7、 要求线程A、B、C按照指定顺序输出:(重点)
方法一:子线程使用join()(推荐): concurrent-learn/OrderRunDemo1.java at master · bintoYu/concurrent-learn · GitHub
方法二:主线程使用join():https://github.com/bintoYu/concurrent-learn/blob/master/src/orderRun/OrderRunDemo2.java
方法三:使用单线程池(推荐):concurrent-learn/OrderRunDemo3.java at master · bintoYu/concurrent-learn · GitHub
方法四:使用CountDownLatch等JUC(推荐):concurrent-learn/OrderRunDemo4.java at master · bintoYu/concurrent-learn · GitHub
方法五:使用Condition:https://github.com/bintoYu/concurrent-learn/blob/master/src/orderRun/OrderRunDemo5.java