在解释什么是多线程之前,我们先来了解了解什么是进程和线程
进程是计算机操作系统分配资源的最小单位,线程是操作系统执行任务调度的基本单位
一个程序其实就相当于一进程,这个进程里面最少有一个线程来对程序中的各个功能来进行处理
单核CPU执行指令都是一条一条执行的,但是并不代表不支持多任务同时执行
场景:在播放视频的同时,也可以打字,但是这并不能说它们一定是并行的,其实这种并行还是依赖于操作系统对多任务的调度,A进程执行0.001秒,B进程0.001秒,由于速度非常快在我们使用者看来就相当于这些任务是并行的
我就用我们Java来举例,当我们启动一个Java程序,实际上就是启动了一个JVM进程,在线程中我们使用一个主线程来执行main
方法,在这个main
方法中我们又可以启动多个线程来执行我们其它的方法,这样就做到了我们对多线程的使用,对于多线程是我们Java必须要学习的一个基础
多线程编程会增加程序性能,但也相应的提高了系统复杂度,线程的上下文切换等消耗CPU资源的行为,所以对于多线程的开发还有很多我们值得研究的
Thread类本质上是实现了Runnable接口的一个实例
class Thread_A extends Thread{
@Override
public void run() {
System.out.println("我是线程A");
}
}
创建、启动线程
public static void main(String[] args) {
Thread thread_a = new Thread_A();
thread_a.start();
}
执行结果
这种方式有一个缺点,因为类是单继承的,继承了Thread类了之后就不能继承其它类了,所以这种方式用的很少,下面这种方式使用的比较多
我们还可以直接选择实现Runnable接口来创建线程
class Runnable_B implements Runnable{
@Override
public void run() {
System.out.println("我是线程B");
}
}
创建、启动线程
public static void main(String[] args) {
Thread thread_b = new Thread(new Runnable_B());
thread_b.start();
}
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("使用Lambda表达式创建线程");
});
thread.start();
}
具体怎么使用线程池来创建线程可以看看我的这篇文章
怎么使用线程池创建线程
相比这种方式创建线程,前面两种大家可能会比较熟悉,所以我会给大家详细讲解这种创建线程方式
那要怎么判断到底是使用Callable
还是Runnable
呢?当你的任务需要返回值时,就需要实现Callable
接口,不需要返回值直接实现Runnable
创建线程即可
1.首先我们定义一个类来实现Callable
接口,重写call()
class Callable_A implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("模拟计算过程相当复杂,不能立马返回===");
Random random = new Random();
int a = random.nextInt(1000);
return a*2;
}
}
2.创建FutureTask
对象,并将实现Callable
接口的类传入FutureTask
构造方法中
FutureTask<Integer> task_a = new FutureTask<>(new Callable_A());
3.创建Thread
对象,并将FutureTask
传入Thread
构造方法中,启动线程
Thread thread_a = new Thread(task_a);
thread_a.start();
4.调用**get()**方法获取返回结果
Integer result = task_a.get();
System.out.println(result);
输出结果
这里有一个点要注意,当我们调用**get()去获取结果时,若是结果还没计算完成,则会一直阻塞在get方法中,就比如我们不调用thread_a.start();
这个启动线程的方法,去调用get()**就会一直阻塞线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个线程池,核心线程数量为10个
ExecutorService executorService = Executors.newFixedThreadPool(10);
//创建一个返回值任务的列表,方便后面获取返回值
List<FutureTask> futureTaskList = new ArrayList<>();
for(int i = 0;i<20;i++){
//新建一个futrueTask对象,未来获取返回值使用,调用submit方法就相当于开始执行线程
FutureTask<Integer> futureTask = (FutureTask<Integer>) executorService.submit(new Callable_A(i));
futureTaskList.add(futureTask);
}
//别忘了将线程池关闭,要不然线程池一直处于运行状态程序不会结束
executorService.shutdown();
//调用方法获取返回值
for(FutureTask futureTask:futureTaskList){
Integer result = (Integer) futureTask.get();
System.out.println(result);
}
}
class Callable_A implements Callable<Integer>{
private int a;
public Callable_A(int a) {
this.a = a;
}
@Override
public Integer call() throws Exception {
return a*2;
}
}
执行结果
对于怎么实现Callable
接口来创建线程我们已经清楚了,但是对于上面的FutureTask还是有些模糊,在这里给大家扩展一下FutureTask的相关知识
我们来看看Java文档对FutureTask
的描述
大致意思就是一个可取消的异步计算任务,此类提供的基本实现Future
,包括启动和取消计算的方法,查询以查看计算是否完成以及检索计算结果的方法。只有在计算完成后才能检索结果;调用get()
如果计算尚未完成,则这些方法将阻塞。一旦计算完成,就不能重新启动或取消计算(除非使用调用计算 runAndReset()
)。
使用FutureTask
的好处就是我们可以异步的去获取任务的返回值,不用一直在那等到结果返回,也就是相当于我们点外卖时,付完钱会给我们创建一个订单,这个订单就相当于我们FutureTask
对象,等外卖的过程中我们可以处理别的事,等到外卖好了调用get()
就可以得到我们的食物,这就实现了我们异步获取结果
FutureTask
其实就是实现了Future
接口,我们先来看看Future
有哪些方法
FutureTask
可用于包装一个Callable
或 Runnable
对象。由于FutureTask
实现了 Runnable
接口,所以FutureTask
可以将提交给线程池执行
我们来看看FutureTask
的构造器源码
上面两个构造方法就是在创建FutureTask
对象时,需要包装一个Callable
或 Runnable
对象
通过实现Callable接口创建的Thread对象,在执行start()
时,就相当于创建一个新的线程来执行FutureTask
的run()
,通过run
方法执行的状态,可以分为未执行、正在执行和已完成
未执行:处在未执行状态的FutureTask
调用get()
会阻塞该线程,调用cancel()
会取消执行run()
正在执行:调用get()
方法也会阻塞线程,调用cancel(true)
会取消正在执行的任务,调用cancel(false)
则不会取消正在执行的任务
已完成:调用get()
得到返回结果,调用cancel()
则会抛出异常
FutureTask
的状态使用了一个(int)state
来表示,使用了volatile
关键字来保证了可见性,当state
修改时会立即刷新到内存,保证了每个线程对这个变量都能取到最新值
/**
* The run state of this task, initially NEW. The run state
* transitions to a terminal state only in methods set,
* setException, and cancel. During completion, state may take on
* transient values of COMPLETING (while outcome is being set) or
* INTERRUPTING (only while interrupting the runner to satisfy a
* cancel(true)). Transitions from these intermediate to final
* states use cheaper ordered/lazy writes because values are unique
* and cannot be further modified.
*
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;//新任务,初始状态
private static final int COMPLETING = 1;//任务有返回结果时,这是一个中间状态
private static final int NORMAL = 2;//任务正常结束的状态
private static final int EXCEPTIONAL = 3;//任务因出现异常而结束
private static final int CANCELLED = 4;//任务未执行时,调用cancel取消了任务
private static final int INTERRUPTING = 5;//任务正在执行,调用cancel(true)取消了任务,这个状态表示任务正在取消
private static final int INTERRUPTED = 6;//表示线程已中断了运行
FutureTask<Integer> futureTask = new FutureTask<>(() ->{
Random random = new Random();
int result = random.nextInt(1000);
return result;
});
Thread thread = new Thread(futureTask);
thread.start();
Thread类中定义了一个枚举类来表示线程的状态
public enum State {
//新建状态
NEW,
//可运行状态(就绪状态)
RUNNABLE,
//阻塞状态
BLOCKED,
//等待状态
WAITING,
//限时等待状态
TIMED_WAITING,
//终止状态
TERMINATED;
}
其实看到上面的枚举类,大家可能会疑惑,为什么没有RUNNING
这个状态
其实JVM是将线程的调度委托给了操作系统去执行,我们在JVM看到的的线程切换其实就是对底层状态的包装,因为现在的操作系统底层(操作系统区分ready、Running,waiting等状态)都是多任务执行的,每个任务执行个10ms左右,在我们看来就像是多个任务一起执行
如果JVM对这些上下文切换不进行包装一下的话,我们通过JVM在观察线程执行状态时,就会看到线程在ready、Running、waiting状态不断的闪烁,因为切换的太快了,1s可能就能切换个几十次,把底层的这些状态映射上来也没有什么实际的意义,所以JVM就把ready、Running,部分waiting状态包装成了RUNNABLE
是个能表达线程此刻状态的一个很好的选择
当我们使用new Thread()
新建一个Thread对象时,该线程就处于新建状态
当我们调用了start()
时,线程会直接进入到就绪状态JVM会为其创建程序计数器和方法栈,争取到CPU时间片就会运行
线程进入阻塞状态的条件:
1获取锁:当线程A要执行方法A时,线程B先通过synchronized关键字或Lock等锁将此方法锁上时,只有线程B才能执行此方法,这样线程A就转换为阻塞状态,等待线程B执行完线程A才可以执行,这时JVM会将线程A会放入锁池,线程A会和锁池中的线程去竞争锁资源
场景:线程A、B互相持有对方的锁,同时进入了BLOCKED(阻塞状态),也就造成了我们常见的死锁
2.主动调用sleep()
,会让出CPU时间片,但不会让出锁资源,在sleep的过程中会一直占用锁资源
class Runnable_B implements Runnable{
@Override
public void run() {
try{
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("我是线程B");
}
}
3.主动调用suspend()
thread.suspend();//容易造成死锁,主动停止且不释放锁资源
4.使用了阻塞式的IO,例如Scanner输入等
5.等待某个触发事件
线程从阻塞中脱离的条件:
1.线程竞争到锁资源
2.sleep()
时间结束
3.主动调用resume()
thread.resume();
4.获取到输入数据就进入到RUNNABLE(就绪状态)
5.等待到事件发出通知进入到RUNNABLE(就绪状态)
进入此状态主要有下列3种情况
Object.wait
Thread.join
LockSupport.park(Object)
离开此状态的3种方法
Object.notify/Object.notifyAll()
线程被终止
LockSupport.unpark(Object)
当线程调用了wait()
时会进入等待状态,这时线程会让出CPU和锁资源,唤醒线程只能再调用notify()/notifyAll()
来唤醒,这里就体现BLOCKED(阻塞状态)和WAITING(等待状态)的区别,当WAITING线程被其他线程调用了notify()/notifyAll()
唤醒了之后,可以进入到两个状态
当线程的执行不需要获取锁资源的时候,会直接进入RUNNABLE(就绪状态)
当线程需要获取锁时进入到的是BLOCKED(阻塞状态),这是为什么呢?因为调用了wait()
时释放的是CPU和锁资源,当线程想要进入到RUNNABLE(就绪状态),就要拿到锁才能继续往下执行
直接给出结论:等待是主动的,阻塞是被动的
看看Java源码,进入此状态主要有下列5种情况
Thread.sleep(long time)
Object.wait(long time)
Thread.join(long time)
LockSupport.parkNanos
LockSupport.parkUntil
离开此状态的方式
等待超时
进入此状态的条件
run()
或call()
执行结束使用此方法来停止线程主要分两种情况
1.线程未处在阻塞状态:使用isInterrupt()
来判断中断标志来结束线程,当我们调用interrupt()
时,中断标志就会置为true,随即结束线程
2.线程处在阻塞状态:调用interrupt()
时,方法会抛出InterruptedException
,通过捕获该异常,跳出循环从而让我们有机会结束线程,所以当interrupt()
并不是直接结束线程,而是先捕获InterruptedException
跳出循环,然后结束run方法
stop()
来结束线程当一个线程使用 stop()
暴力停止时候,他会立即释放所有他锁住对象上的锁。这会导致对象处于不一致的状态(不同步)
场景转账,当一个线程从这个账户扣款时,调用stop()
来暴力终止操作,由于他会立刻释放对这个账户上面的所有锁,这样的话,别的线程来对这个账户进行修改操作,严重危害安全,所以不建议使用stop()
来停止线程
sleep属于Thread类中的方法,而wait是属于Object类的方法
调用sleep方法会释放CPU时间片但不会释放监视器(锁)
调用wait方法会释放CPU时间片和监视器
与线程相关的基本方法有(start()
、wait()
、sleep()
、join()
、notify()
、notifyAll()
等),start我就不说了,下面我会给大家说其他的
使用该方法的线程进入WAITING状态,加入等待队列,会释放当前对象持有的锁,并且只有等到其它线程的通知或中断线程才会返回
使用该方法的线程进入休眠,且不会释放当前线程持有锁,使用sleep(long time)进入TIMED_WAITING状态
在线程B中调用线程A的join(),线程B要等到线程A执行完才可以执行
场景:
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread threadA = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("使用Lambda表达式创建线程A");
});
Thread threadB = new Thread(() -> {
try {
//等待线程A执行完线程B才可以执行
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("使用Lambda表达式创建线程A");
});
threadA.start();
threadB.start();
}
}
join()
有什么用,比如线程B需要用到线程A的计算结果,这样线程B内调用线程A的join()
就可保证线程A计算完成后才去执行线程B
当有一个线程调用了此方法,会从等待队列中随机唤醒一个线程执行
threadA.notify();
当有线程调用了此方法,会唤醒等待队列的所有进程
threadA.notifyAll();
isAlive()
: 判断一个线程是否生存
activeCount()
: 程序中活跃的线程数
enumerate()
: 枚举中的线程数
currentThread()
: 得到当前线程
isDaemon()
: 判断是否为守护线程
setDaemon()
: 设置为守护线程
我们正常创建的线程称为非守护线程,可以通过threadA.setDaemon()
将线程A设为守护线程,判断一个线程是否为守护线程则通过isDaemon()
命令
JVM什么时候停止运行:
当JVM中的非守护线程都停止运行了之后,JVM才会停止运行
守护线程是在JVM的其它非守护线程都运行完了之后会自动关闭的一种特殊线程,所以我们一般用守护线程来做一种后台线程,也就是你希望JVM关闭时,这个线程自动关闭
就比如垃圾回收线程,平时正常运行,关闭JVM会自动销毁
是指某一个时间点占用CPU寄存器和程序计数器的内容,因为我们知道操作系统是通过分配时间片来调度任务的,线程只有得到CPU资源的才可以执行任务
1.当执行任务的线程所分配的CPU时间片用完,线程挂起等待下一次时间片的分配
2.抢占锁资源,没抢占到所资源的线程挂起
3.碰到IO阻塞任务,线程挂起
4.主动挂起线程,让出CPU时间片
这里说明一点,线程的上下文切换是耗费资源的,首先程序要先保存线程执行的状态,分配到时间片后再把上一次保存的信息再加载出来,这个动作是耗费性能和时间的,举个例子吧,单线程执行10000次+1操作和多线程执行10000次+1操作,哪个更快?这里直接给出答案,感兴趣的小伙伴可以自己动手去复现一下,因为对于一些简单的业务逻辑,多线程的上下文切换时间是损耗非常大的
从这条分界线开始我们将进入到中高级(上流)的部分啦,上面给你们打好了线程相关的基础,接下来我们要面对的是线程安全的问题,当我们使用多线程来处理业务时,我们的资源是有限的,比如有500部手机,有5000个用户在抢,每个用户代表一个线程,如果这5000个线程一股脑的去扣减库存的话,很有可能会造成超卖的情况,这种损失可是很可怕的,所以我们下面要研究的是怎么保证多线程的情况下,保证资源的安全
进入上流专题
高并发:系统中突然新增了非常非常多的请求,比正常情况下还要多,例如TB、JD双十一等这种场景我们可以称为高并发
在这里和大家说明白了,多线程≠高并发,多线程是解决高并发问题的一个方案,这么说大家明白了吧,例如你高并发的场景为5000个请求/s,这种情况我们就不能使用单线程的方式来处理这些请求了,这样我们就可以启动多个线程来处理这么高并发所产生的多个请求
悲观锁在并发量高,读少写多的场景使用多,如synchronized
乐观锁在并发量低,读多写少的场景用的比较多,如CAS
Lock的ReentrantLock、写锁为独占锁
ReentrantReadWriterLock中读锁为共享锁
公平锁需要维护一个队列,等待时间越久的线程获取锁的几率就越大
非公平锁则是直接尝试获取锁
还有一些偏向锁、轻量级锁等知识我们在后面会给大家介绍
获取该锁的线程,可以重复获取此锁而不会造成死锁
我们了解了锁知识后要知道怎么使用以及在什么场景使用,用锁的目的就是为了保护线程的安全,下面我们来看看影响线程安全的因素
在了解怎么保护线程安全之前,我们了解并发编程的三个特性,只有保证了下面三点,才有可能保证我们的线程安全
保证一个操作要么不做,要么做到完
数据对每个线程都是可见的,因为一般线程执行任务之前,会把需要用到的数据拷贝到线程自己的缓冲区进行操作,然后再刷回内存,这样一来就容易让其它线程对读取到老数据,导致最终结果不一致的问题,我们一般使用volatile
来对变量进行修饰,保证该变量对所有线程的可见性
保证多个线程之间的执行顺序
synchronized是Java的一个关键字,是一种同步锁,它是通过对象内部的**监视器锁(monitor)**来实现的
这里注意两个点:
synchronized是可重入锁,也是个独占锁
synchronized保证原子性、可见性和有序性
我们用代码来给大家展示展示synchronized怎么使用,主要是用来修饰方法、代码块和静态方法
public class SynchronizedDemo {
//修饰方法
//锁住的是对象的实例
public synchronized void getA(){
//do something...
}
//修饰代码块
public void getB(){
synchronized (this){
//do something...
}
}
//修饰静态方法
//锁住的是Class实例,因为Class的相关数据是存储在永久代里的,永久代的数据又是全局共享
//因此静态方法锁也相当于类的全局锁,会锁住所有调用该方法的线程,不管创建多少个实例
public synchronized static void test2(){
//do something...
}
}
synchronized修饰方法会得到ACC_SYNCHRONIZED;synchronized修饰代码块的会看到monitorenter和monitorexit指令
在JDK1.6之前,synchronized它是依赖底层的Mutex Lock来实现的,Mutex Lock它是一个重量级锁,操作系统到java线程获取锁与释放锁的过程涉及到用户态到内核态的切换,这个转换的成本其实是非常高的,这也是synchronized效率低下的原因之一,下面所作的优化都是为了减少获得锁和释放锁所带来的性能消耗
对不同的场景引入了不同的锁,加入了自旋锁、偏向锁和轻量级锁来对synchronized进行优化,且加入了锁消除和锁粗化的操作
编译过程中进行逃逸分析,若对象不会逃逸到方法外,则进行锁消除,也就是不对此对象上锁
public class SynchronizedDemo {
public void test(){
Object o = new Object();
synchronized (o){
//do something...
}
}
}
class Object{
}
//进行了锁消除之后
public void test(){
Object o = new Object();
//do something...
}
看着上面的代码,可以看出每进入一次方法都会创建一个新对象,所以没必要加锁
通常情况下,为了保证多线程的有效并发,会要求每个线程持有锁的时间尽量短,即使用完资源后应及时释放,不断地重入锁,会不断消耗系统宝贵的资源,所以为了避免重入锁次数过多,提出了锁粗化,例如一个方法内对同一个对象上了两次锁,经过锁粗化,就会对这个对象只上一次锁
public class SynchronizedDemo {
public void test(){
synchronized (this){
System.out.println("1");
}
synchronized (this){
System.out.println("2");
}
synchronized (this){
System.out.println("3");
}
}
}
上面这串代码就是对该对象不断地重入,经过锁粗化后的代码是下面这样的
public class SynchronizedDemo {
public void test(){
synchronized (this){
System.out.println("1");
System.out.println("2");
System.out.println("3");
}
}
}
有效减少了锁重入次数减少了对资源的损耗
若是没有线程竞争锁的情况,synchronized此时访问上锁的对象方法速度与非上锁对象方法几乎相同,但是若产生轻度竞争则膨胀为轻量级锁,重度竞争直接膨胀为重量级锁
竞争的线程不会阻塞,提高响应速度,得不到此锁的线程会启动自旋旋来防止自己被挂起
加锁方式:CAS设置对象头
它与互斥锁类似,但是与互斥锁不同的是,当线程去获取锁的时候,如果该锁被占用了,一般来说共享资源被占用的时间不会那么长,所以线程不会真正的在操作系统层面被挂起,而是占用CPU循环的去询问该锁能不能获取,使用不当还会出现一个问题,自旋锁实际上是一个不公平锁,不是等待时间越长的线程,获取锁的几率就越大
JDK 1.6使用 -XX: +UseSpinning 开启自旋锁,-XX:PreBlockSpin = 5 为自旋5次
JDK 1.7 后去掉此参数,为JVM控制
优点:
不会引起频繁的线程上下文切换,使线程一直处于用户态
竞争此锁的进程不会自旋,线程直接阻塞
加锁方式:作用在方法上时:ACC_SYNCHRONIZED
所用在代码块上时:monitorenter->执行代码块->monitorexit
首先线程访问锁对象,判断对象头内的锁类型,若为00则为轻量级锁,尝试CAS去获取锁,若失败则自旋等待,若自旋次数(可自行设置)超过设定次数则升级为重量级锁,对象头锁类型更改为10也就是重量级锁,这时再去竞争锁还竞争不到的话,线程阻塞,加入等待队列
ReentrantLock中文译为重入锁,和它的名字一样,它是可以重入的,在介绍它的剧痛发之前,我们先来看看它所实现的Lock接口
ReentrantLock为java.util.concurrent.locks包下的Lock接口的一个实现类,我们来看看Lock接口下的主要方法
void lock()
:获取锁,若锁空闲则直接获取;若锁被占用则阻塞当前线程,直到可以获取锁
boolean tryLock()
:尝试去获取,获取成功返回true,失败返回false,与lock()不同的是,tryLock()不会阻塞当前线程,会接着执行下面的方法
void unlock()
:释放当前线程持有的锁
Condition newCondition()
:返回绑定到此Lock上的Condition
实例
Condition实例需要通过Lock接口下的newCondition()
来获取,Condition需要配合Lock一起使用,更多的使用在线程间相互交互的场景
我们来看看Condition下的方法
这里我们主要说的是上面红框的方法
当线程调用此方法时,会将线程加入等待队列进入等待状态释放持有锁,直到其它线程调用signal()
来尝试唤醒线程
我们来看看Java文档是怎么描述这个方法的
唤醒一个等待线程,究竟是唤醒哪个线程呢?说的再多都不如用代码去实践证明一下
public class ReentrantLockDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
for(int i = 0;i<=20;i++){
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "被进入等待队列");
condition.await();
System.out.println(Thread.currentThread().getName() + "被唤醒");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
},"线程" + i).start();
}
System.out.println("测试线程唤醒顺序");
for(int i = 0;i<=20;i++){
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}).start();
}
}
}
我把结果截图给大家看看
我经过多次测试,得出的结论是先进入等待队列的线程就越快被唤醒
唤醒等待队列中的所有线程去竞争锁
我们来看看LeetCode上的一道笔试题
题目链接:H2O 生成
这道题的意思就是当一个线程打印完O字符之后,另一个线程需要打出两个H来与它形成一个水分子(HHO),这道题就可以使用Condition + Lock来完成,我的具体思路就是一个线程打印一个O字符后必须调用await()
来挂起线程,然后调用signalAll()
来唤醒其它的线程打印H字符,当打印出两个H字符后,线程执行await()
挂起,再调用signalAll()
唤醒其它线程打印O字符,如此往复…这里我直接上代码
class H2O {
public H2O() {
}
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//记录H的打印数
volatile int count = 0;
public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
//首先尝试获取锁,而且必须在finally块中释放锁
lock.lock();
try{
while(count == 2){
//调用await()阻塞线程
condition.await();
}
// releaseHydrogen.run() outputs "H". Do not change or remove this line.
releaseHydrogen.run();
count++;
condition.signalAll();
}finally{
lock.unlock();
}
}
public void oxygen(Runnable releaseOxygen) throws InterruptedException {
lock.lock();
try{
while(count < 2){
condition.await();
}
// releaseOxygen.run() outputs "O". Do not change or remove this line.
releaseOxygen.run();
count = 0;
condition.signalAll();
}finally{
lock.unlock();
}
// releaseOxygen.run() outputs "O". Do not change or remove this line.
}
}
源码
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
通过源码可以看到,创建公平锁还是非公平锁是根据我们传进去的fair
来决定的,传入true
则创建公平锁,false
则创建非公平锁,默认创建非公平锁,创建和使用方法如下,千万别忘了最后要释放锁
public class ReentrantLockDemo2 {
public static void main(String[] args) {
//实现公平锁
Lock fairSync = new ReentrantLock(true);
//实现非公平锁
Lock nonFairSync = new ReentrantLock(false);
new Thread(new Runnable() {
@Override
public void run() {
nonFairSync.lock();
try {
//do something...
System.out.println("获取锁要做的事...");
}catch (Exception e){
e.printStackTrace();
}finally {
nonFairSync.unlock();
}
}
}).start();
}
}
我们先来看看new一个公平锁的过程
首先执行ReentrantLock构造方法,并传入一个true
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
就得到一个FairSync
对象,下面就是该对象的源码
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
接下来看看上锁的过程,当我们调用lock()
也就是尝试去获取锁的方法,会发生什么呢?首先会执行FairSync
的lock()
也就是下面这段
final void lock() {
acquire(1);
}
该方法执行了acquire(1)
,为什么是1,表示加锁一次,继续往下走
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面这段代码的意思是如果**!tryAcquire(arg)
(尝试获取锁失败了)**,则会执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)
也就是,将该线程放入等待队列并且将线程挂起,如果加入等待队列还是失败的话,就会执行selfInterrupt()
,也就是抛出异常
我们来看看!tryAcquire(arg)
的源码
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state值,如果是零代表不是重入,若>0表示重入
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//看看当前线程是不是重入线程,不是的话直接返回false
else if (current == getExclusiveOwnerThread()) {
//重入次数= state + 1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//将state的值更新
setState(nextc);
//返回true
return true;
}
return false;
}
继续深入hasQueuedPredecessors()
来看看,这个方法就是判断该线程是不是位于等待队列的第一位
public final boolean hasQueuedPredecessors() {
//t节点为队列最后一个节点
Node t = tail;
//h节点为头节点
Node h = head;
Node s;
//第一个条件就是如果头节点!=尾节点继续下面的判断,若头节点==尾节点就证明该队列只有一个线程了,直接返回false
//第二个条件是一个组合条件
//(令s节点等于头节点的下一个节点,如果为null返回true,不为null返回false)
//(s节点代表的线程不等于该线程返回true,等于该线程返回false)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
上面的源码其实很好理解,当头节点不等于尾节点时,s节点就不可能为null,只需要判断s节点对应的线程是不是本线程就行,若是的话就有机会竞争锁,若不是直接bey bey,继续在等待队列中候着吧
当我们判断了该线程为等待队列中的第一个线程,则CAS的尝试获取锁,成功的话将当前获取锁的线程置为排他性锁拥有者线程,也就是我们常说的独占锁线程
接下来我们来看看若是获取锁失败会怎么办呢?看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
的源码
我们先来看看addWaiter(Node.EXCLUSIVE)
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private Node addWaiter(Node mode) {
//将该线程封装成一个节点
Node node = new Node(Thread.currentThread(), mode);
//得到队列中的最后一个节点
Node pred = tail;
//若得到的尾节点不为null
if (pred != null) {
//将线程的前置节点设为尾节点
node.prev = pred;
//使用CAS去将pred节点设为node节点
if (compareAndSetTail(pred, node)) {
//将pred.next节点设置为node节点
pred.next = node;
//返回节点
return node;
}
}
//得到的尾节点为null则进行初始化
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//若尾节点为空,则说明队列没初始化
if (t == null) {
//CAS初始化设置头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//不为空则node节点的前置节点为原tail节点
node.prev = t;
//设置新的尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
}
上面这串代码的意思就是,将没获取到锁的线程封装成一个Node节点,加入队列的尾部,具体流程我在上面的源代码中都写出来了
下面来看看看acquireQueued(final Node node, int arg)
的源码
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//主要还是这段代码
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()
这段代码的主要作用还是判断是否将线程挂起
//询问当前线程是否要挂起,保障在挂起线程之前的最后一次询问
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//将线程挂起
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
上面这串代码的意思就是,将线程挂起,具体流程我在上面的源代码中都写出来了
以上就是公平锁的实现原理,这里面还涉及到了AQS框架的原理,这里我就不深入了,感兴趣的家人们可以评论区给我留言
非公平锁的实现就比较直接了,没公平锁那样需要做判断
我们先来看看new一个非公平锁的过程
首先执行ReentrantLock构造方法,并传入一个false
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
得到一个NonfairSync
对象,下面是该对象的源码
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
调用lock()
后,直接CAS尝试获取锁,成功则将该线程设置为独占锁线程,失败则进入我们熟悉的acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
公平锁和非公平锁调用的是同一个acquire(1)
,区别就是在这个tryAcquire(arg)
上,我们来看看非公平锁的tryAcquire(arg)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
内部调用了nonfairTryAcquire(acquires)
,继续深入看看
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
源码内部也是直接的CAS的尝试获取锁,无论线程是在等待队列的哪个位置都可以尝试获取锁,成功返回true
,失败返回false
,后面失败了继续加入等待队列的过程和公平锁一样,这里就不在赘述了
上述就是非公平锁的实现原理
两种锁没有谁好谁坏,但是非公平锁的效率是公平锁所达不到的,看看上面两种锁的源码就知道了
我们主要从4个方面来说明
Synchronized是Java的一个关键字,通过JVM的监视器(monitor)来对资源进行控制,JDK 1.6之后Synchronized涉及到偏向锁、轻量级锁、自旋锁的锁升级、锁消除操作优化,还涉及原本从操作系统映射上来的重量级锁,操作系统到java线程获取锁与释放锁的过程涉及到用户态到内核态的切换
ReentrantLock是JDK 1.5之后,存在于java.util.concurrent.locks包下Locks接口的一个实现类,主要还是通过CAS原子操作、自旋来获取锁
Synchronized为非公平锁;ReentrantLock有公平锁/非公平锁两种实现
Synchronized不需要手动释放,由JVM来进行自动解锁;ReentrantLock需要在finally语句块内调用unlock()
来对锁进行主动释放
由于Synchronized为JVM层面的锁,不可手动中断,只有在代码运行时抛出异常时才可中断,代码正常执行结束Synchronized会自动解锁
ReentrantLock可以支持中断
当我们调用了lock()
去获取锁时,如果长时间获取不到锁,就会一直阻塞在该方法,而我们想中断这个过程要怎么办呢?我们还可以使用tryLock(long timeout, TimeUnit unit)
尝试去获取锁,,我们来看看java文档是怎么描述的
当超时直接返回false
在等待时间内,被中断了直接抛出InterruptedException,不会等待
在等待时间内,无锁资源可用则直接返回false,不会等待
又或者直接调用lockInterruptibly()
来进行中断
也就是说当线程A得到锁,线程B没获取锁,则线程B可以调用interrupt()
来进行中断,示例代码
public class ReentrantLockDemo2 {
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo2 reentrantLock = new ReentrantLockDemo2();
//创建线程A
Thread thread_A = new Thread(new Runnable() {
@Override
public void run() {
try {
reentrantLock.test();
} catch (Exception e) {
e.printStackTrace();
}
}
});
//创建线程B
Thread thread_B = new Thread(new Runnable() {
@Override
public void run() {
try {
reentrantLock.test();
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread_A.start();
System.out.println("等一会才启动B");
Thread.sleep(2000);
thread_B.start();
//模拟线程B最长等待时间
Thread.sleep(5000);
//主动中断B的等待
thread_B.interrupt();
}
public void test() throws Exception{
try {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "得到锁");
System.out.println("办正事...");
Thread.sleep(10000);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("释放锁");
}
}
}
执行结果
ReadWriteLock就是共享锁/独占锁的一个例子,
引用Java文档的一句话:
读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。 它利用了这样一个事实:一次只有一个线程( 写线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(读线程)。从理论上讲,通过使用读写锁允许的并发性增加将导致性能改进超过使用互斥锁。 实际上,并发性的增加只能在多处理器上完全实现,然后只有在共享数据的访问模式是合适的时才可以。
总结来说就一句话,读锁允许多个线程持有,写锁只允许一个线程持有,使用读写锁的原因也是为了提高系统的并发度,具体使用方法如下
public class ReentrantReadWriteLockDemo {
//得到一个读写锁对象
ReadWriteLock lock = new ReentrantReadWriteLock();
//得到读锁
Lock readLock = lock.readLock();
//得到写锁
Lock writeLock = lock.writeLock();
Map<String,String> map = new HashMap<>();
public String get(){
readLock.lock();
String res = null;
try {
res = map.get("ALiangX");
}catch (Exception e){
e.printStackTrace();
}finally {
readLock.unlock();
}
return res;
}
public void put(String key,String value){
writeLock.lock();
String res = null;
try {
map.put(key,value);
}catch (Exception e){
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
}
CAS是compare and swap/compare and set的缩写,CAS是一种基于锁且是原子性的操作,它也是一种乐观锁。它的原理就是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
悲观锁场景:多个线程并发,争夺一个共享资源,要使用ReentrantLock或synchronized对共享资源进行加锁,只有进程互斥访问,其它线程自旋,虽然现在基本都是用线程池来尽可能的降低不断创建线程造成系统性能损耗,但是若一个线程占用锁的时间过长,容易造成其它线程挂起阻塞,效率太低
CAS场景:多个线程并发去对共享资源进行CAS修改,只有一个线程能修改成功,其它线程疯狂尝试修改也不能修改成功
1.ABA问题
场景:变量K的初始值为5,线程A将变量K的值CAS修改为10,过了一会又CAS把变量K值修改为5,线程B CAS取到变量K的值这时线程B是不知道变量K修改过,这就导致了数据不一致的问题
解决方法:每次修改值之后加上版本号,例如1K—>2K—>3K,加上版本号后,之后的线程就知道变量是否经过修改了
2.只能保证一个变量的原子性操作
3.CPU开销也不小
高并发情况下,多线程都去尝试,CPU的性能损耗是不小的
该关键字主要有两个作用
1.保证了修饰的元素对每个线程可见,使用该关键字修饰的变量,线程对其修改之后会立即刷新到内存,保证下一个取到它的线程取到最新的数据
2.禁止指令重排序
它是比Synchronized更轻量的同步**“锁”**,线程去尝试修改volatile修饰的变量的时候不会加锁,而是直接修改主存中存储的变量,所以当变量一修改,其它线程取到的就是最新值
private volatile int a = 0;
文章链接:Semaphore、CyclicBarrier和CountDownLatch三者的区别
结合笔试题讲解:力扣多线程笔试题(Leetcode 1115、1116、1117、1195)
i++ 和 ++i本身就不是一个线程安全的操作,在多线程的环境下来对其操作只能对其加锁,而且使用 Synchronized的话有可能会升级为重量级锁,严重影响效率,所以就出现了我们的java.util.concurrent.atomic下的AtomicInteger、AtomicBoolean等原子操作类,其方法指令具有原子性不会被打断,里面是通过CAS来对值进行修改,主要用于在高并发环境下的高效程序处理
这里我们只展示AtomicInteger的使用方式,和部分源码解析
当我们执行下面这个语句
AtomicInteger atomicInteger = new AtomicInteger();
就会调用构造器帮我们创建一个AtomicInteger
对象,内部帮我们初始化一个value值,初始值为0
下面是该对象的方法,我就展示三个经常用的,这下面的方法都是线程安全的
个共享资源,要使用ReentrantLock或synchronized对共享资源进行加锁,只有进程互斥访问,其它线程自旋,虽然现在基本都是用线程池来尽可能的降低不断创建线程造成系统性能损耗,但是若一个线程占用锁的时间过长,容易造成其它线程挂起阻塞,效率太低
CAS场景:多个线程并发去对共享资源进行CAS修改,只有一个线程能修改成功,其它线程疯狂尝试修改也不能修改成功
原理:先对原ArrayList进行拷贝,再进行修改操作,随后将原ArrayList的指针指向这个修改后的ArrayList
缺点:只能保证最终结果的一致,在拷贝过程中其它线程有可能取到假数据
且每次修改都需要复制一次数组,内存消耗大,系统性能几乎都用在了复制原数组上了
其原理就是将数据拷贝一份到ThreadLocal去自己维护,在多线程情况下实现数据隔离,最主要的还是get()
、set()
、remove()
三个方法我们先来看看它们的源码
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//判断map对象是否为空
if (map != null) {
//获得Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//得到map的value值并返回
T result = (T)e.value;
return result;
}
}
//若map为空,则进行初始化
return setInitialValue();
}
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断对象是否为空
if (map != null)
//不为空则设置值
map.set(this, value);
else
//为空则新建一个ThreadLocalMap对象
createMap(t, value);
}
public void remove() {
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//移除map中的所有值
m.remove(this);
}
底层维护了一个ThreadLocalMap,这个ThreadLocalMap内部维护了一个Entry,存储对象的类型主要看创建ThreadLocal时传进来的类型
private static final int INITIAL_CAPACITY = 16
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
创建的ThreadLocalMap对象Entry数组默认大小为16
看着上面的源码,这个Entry的key是弱引用的,有很多小伙伴不知道什么是弱引用?我们来看看Java文档的描述
也就是说弱引用的对象,生命周期比较短,一旦发现了只具有弱引用的对象且没有与之关联的对象,则会直接回收,但是垃圾回收器作为一个后台的守护线程,不一定会立马回收这些数据
ThreadLocal使用完了之后应该被GC回收,但是创建ThreadLocal的线程(比如线程池中的线程)不一定会停止啊,也就是说value有可能还是被强引用持有,这样GC就不能回收此value,一直累计就容易造成OOM了
解决方法很简单,在使用结束后,调用remove()
来清除ThreadLocal内的信息就可以
ThreadLocal
提供了一个子类,这个类就是InheritableThreadLocal
,当想进行数据共享实现InheritableThreadLocal
就行
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("ALiangX");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
}
}).start();
}
}