【JUC】线程部分的知识梳理,用于强化记忆

17 篇文章 3 订阅
5 篇文章 0 订阅

1.进程/线程

进程:进程就是一段程序的执行过程。

线程:线程是独立调度和分派的基本单位。

进程和线程的区别

1)一个程序至少有一个进程,一个进程至少有一个线程.

2)线程的划分尺度小于进程,使得多线程程序的并发性高。

3)另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

2.实现一个线程的方式

  1. 继承Thread
  2. 实现Runnable方法
  3. 实现Callable方法

三种实现方式分别有什么特点,或者说怎么选择?

继承Thread代码实现比较简单,缺点是:由于是继承实现的,那么就不能继承其他类。代码扩展性太差。

实现Runnable接口:通过实现接口重写run()方法的方式创建,线程和资源类的耦合度较低,拓展性强。

实现Callable接口:通过实现接口重写call()方法的方式创建,线程和资源类的耦合度较低,拓展性强,并且支持返回值。

如何选择,一般建议使用实现接口的方式实现多线程。

3.怎么实现多线程同步

多线程的实现方式就是加锁;加锁可以选择synchronized锁(JVM层面的锁)和lock锁(JDK层面的锁)。

加synchronized锁的方式有两种,同步方法和同步代码块,同步方法又分为静态同步方法和非静态同步方法,其中静态同步方法的锁对象是Class对象,而非静态同步方法的锁对象是当前实例对象,同步代码块的锁对象既可以是当前实例对象也可以是Class对象。

或者使用lock锁的方式也能实现多线程同步。可以使用ReentrantLock(同步锁)来实现多线程的同步,注意lock锁的实现必须在方法内部,通过显式的加锁和解锁实现特别注意不要将lock的加锁过程放在try代码块中,因为我们为了保证lock锁一定能够释放,但是如果将lock的锁放在try代码块中,假如在获取锁的过程中出错(就是没能获取到锁),然后执行finally语句块时就会有IllegalMonitorStateException异常。

4.synchronized和Lock锁的区别

  1. synchronized是隐式锁,可以定义在方法上和代码块中,而lock锁是显式锁,只能放在代码中
  2. synchronized是JCM层面的锁,具有多种锁优化机制,比如偏向锁,轻量级锁,以及锁升级,锁消除等机制,而lock是JDK层面的锁,是有java代码实现的。
  3. lock是隐式锁,不能捕获到线程的状态,而Lock锁可以尝试去获取锁,并可以实现超时时间。
  4. synchronized是非公平锁,而Lock默认是非公平的锁,但是可以设置为公平锁。

5.在多线程中如何安全的使用集合,他们有什么区别?

集合在多线程中使用可能是不安全的。

多线程安全的情况下使用集合类有三种思路:

  1. 直接使用JDK提供的线程安全的集合类,比如Vector,Hashtable等
  2. 使用synchronized包下的线程安全的集合,比如synchronizedArrayList,synchronizedSet等
  3. 第三种是使用JUC包下的线程安全的集合,比如ConReentrantHashMap、CopyOnWriteArrayList等线程安全的集合。

至于具体的集合的内部实现是不同的,要分开讨论,可以具体看我的博客[JUC]集合类多线程操作不安全的三种解决方案

6.锁是什么?锁的对象如何判断?

锁其实就是Java中对于资源的一种控制方式,通过加锁和解锁使用多线程下的同步安全。

锁的对象:

synchronized锁的方式有两种,同步方法和同步代码块,同步方法又分为静态同步方法和非静态同步方法,其中静态同步方法的锁对象是Class对象,而非静态同步方法的锁对象是当前实例对象,同步代码块的锁对象既可以是当前实例对象也可以是Class对象。

7.什么是虚假唤醒?

什么是虚假唤醒?

虚假唤醒指的是:一般情况下线程的唤醒只能在notify()或者notifyAll()返回,而虚假唤醒指的是线程通过其他的方式进行了返回。

这里不得说一下wait方法:

wait()其实分为三步:1.释放锁并阻塞 2.等待条件cond发生 3.获取通知后 ,竞争获取锁。

举个例子:

线程A,C线程想要买票,线程A获取到锁调用wait方法进入等待队列,线程C买票时发现线程B正在退票获取锁失败,而B线程获取到锁资源,并进行了退票操作,此时剩余票为1,也就是这时满足了线程A唤醒的条件(也就是满足了wait方法的第二步),但是A、C线程竞争,C获取到了锁,并执行了买票,释放锁,A线程获取到了锁资源,而此时是没有余票的。

为什么会导致虚假唤醒?

虚假的唤醒造成的原因在维基百科上这样说:是由于wait方法在循环中而造成的,而解决wait方法虚假唤醒的将wait()方法置于循环中。其实也就是说置于if判断中的wait()方法在wait方法执行的第二步虽然触发了条件2,但是没竞争到锁,而当其再次竞争到锁的时候,cond条件已经改变了。而解决方式就是将wait()方法放在while循环中,为什么while方法能够保证不会造成虚假唤醒呢,因为while循环其实本质上是一个自旋的锁,也就是说其会一直判断cond条件是否满足,不管wait()方法第二次获取到cond条件是否满足,都会再次判断条件是否满足,保证线程安全性。

8.synchronized和Lock锁的使用场景

从性能方面谈:synchronized是一个同步锁,读读,写写,读写都会互斥(性能不高),而Lock锁的实现类可以实现读写分离

从功能方面谈:synchronized锁是JVM层面的是自动的,不能获取到锁的状态,而且不能去尝试去获取锁,也就是说当一个线程A获取到锁但是执行出错时,B线程只能傻傻的等,而lock锁是能够去尝试获取锁,并可以设置超时时间的。

synchronized锁是有一个锁膨胀的过程,从无锁->偏向锁->轻量级锁->重量级锁的转换的过程,如果开始的时候就知道使用场景中不太会出现并发的情况,可以考虑synchronized锁,但是如果一开始就知道多线程的并发程度很高,那么可以直接选择Lock锁,避免synchronized锁在锁转换过程中的开销。

总结一下锁的选择:

  1. 场景的不同
  2. 并发程度的高低。

9.锁升级(锁膨胀机制)?

注意锁升级也是对于synchronized锁来讲的,是JVM对其进行了优化。

整个锁升级的过程大概分为:

new–>偏向锁–>轻量级锁(无锁,自旋锁,自适应自旋)–>重量级锁

image-20201206204846923

锁升级机制是对应于synchronized锁的,锁存储在对象头中

无锁

无锁指的是对资源没有锁定,所有的线程都能同时访问并修改共享资源,但是只有一个线程能够修改成功。

无锁的特点就是:修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并返回,否则就继续循环尝试。如果多个线程修改同一个值,那么必定会有一个线程修改成功,而其他修改失败的线程会循环尝试直到修改成功。

偏向锁

偏向锁指的是如果一段同步代码自始至终都只有一个线程访问,即不存在多线程的竞争,那么该线程在后续的访问中会自动获取锁(其实是没有释放锁),所以效率极高。

初次访问到synchronized代码块时,无锁状态改变为偏向锁(通过CAS修改对象头里面的标志位),偏向锁字面理解就是:“偏向于第一个获得它线程的锁”。当偏向锁执行完毕之后,并不会立即释放偏向锁。当第二次到达同步代码块的时候,线程会判断此时持有锁的线程是不是自己(持有锁的线程ID也在对象头中),如果是则正常往下执行。由于之前没有释放锁,所以这里不需要重新获取锁。如果自始至终使用锁的线程只有一个,那么偏向锁没有额外的开销,效率极高

当一个线程访问同步代码块并获取锁时,会在对象头的Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储这指向当前线程的偏向锁。轻量级锁的获取以及释放依赖多次CAS原子指令,而偏向锁只需要在置换Thread ID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

轻量级锁(自旋锁)

image-20201206215410843

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,线程不会阻塞,从而提高性能。

轻量级锁的获取主要由两种情况:

  1. 当关闭偏向锁功能时
  2. 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。所谓的锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续竞争,没有抢到锁的线程将自旋,即不停的循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋是消耗CPU的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任务,这种状态称为忙等,不能让其一直忙等(如果一个线程一致持有资源,其他线程一直忙等,迟早会撑爆CPU)。

如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

重量级锁

重量级锁指的是:一个锁获取线程后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁:一个线程持有轻量级锁时,其他线程会自旋尝试获取锁,进行忙等,但是不能让忙等一直发生,会大量消耗CPU资源。忙等是有限度的(有个计数器记录自旋次数,默认是10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但是不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,而不是忙等,等待将来被唤醒。

也就是说讲线程间的调度交给了操作系统,而频繁的对线程运行状态的切换,线程的挂起和唤醒,会消耗大量的系统资源,时间成本也更高。

image-20201206222154556

10.锁分类

image-20201207000651955

11.ReentrantLock的实现?

12.死锁是什么?产生条件是什么?

死锁是指两个或者多个进程执行过程中,因争夺资源而造成的一种循环等待的现象。

产生条件:

  1. 请求和保持:一个进程请求资源而阻塞,保持已经获取的资源不释放
  2. 不剥夺条件:已经获取的资源,在使用完之前不能被强行剥夺
  3. 循环等待条件:若干资源形成了头尾相接的循环等待资源关系
  4. 互斥:某一时间内独占资源

13.怎么避免死锁?

破坏死锁产生条件的任意一个或者多个条件都可以破除死锁。

14.怎么在程序中检查死锁?

1.使用jps -l获取当前运行的进程id列表

image-20201206234403452

2.使用jstack 进程id找到死锁问题

image-20201206234429913

image-20201206234826280

15.保证线程安全的思路?

  1. 对非多线程安全的代码加锁
  2. 使用线程安全的类
  3. 多线程并发情况下,线程共享的变量改为方法级的局部变量
  • 38
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

炒冷饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值