文章目录
1.进程/线程
进程:进程就是一段程序的执行过程。
线程:线程是独立调度和分派的基本单位。
进程和线程的区别
1)一个程序至少有一个进程,一个进程至少有一个线程.
2)线程的划分尺度小于进程,使得多线程程序的并发性高。
3)另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
2.实现一个线程的方式
- 继承Thread
- 实现Runnable方法
- 实现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锁的区别
- synchronized是隐式锁,可以定义在方法上和代码块中,而lock锁是显式锁,只能放在代码中
- synchronized是JCM层面的锁,具有多种锁优化机制,比如偏向锁,轻量级锁,以及锁升级,锁消除等机制,而lock是JDK层面的锁,是有java代码实现的。
- lock是隐式锁,不能捕获到线程的状态,而Lock锁可以尝试去获取锁,并可以实现超时时间。
- synchronized是非公平锁,而Lock默认是非公平的锁,但是可以设置为公平锁。
5.在多线程中如何安全的使用集合,他们有什么区别?
集合在多线程中使用可能是不安全的。
多线程安全的情况下使用集合类有三种思路:
- 直接使用JDK提供的线程安全的集合类,比如Vector,Hashtable等
- 使用synchronized包下的线程安全的集合,比如synchronizedArrayList,synchronizedSet等
- 第三种是使用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锁在锁转换过程中的开销。
总结一下锁的选择:
- 场景的不同
- 并发程度的高低。
9.锁升级(锁膨胀机制)?
注意锁升级也是对于synchronized锁来讲的,是JVM对其进行了优化。
整个锁升级的过程大概分为:
new–>偏向锁–>轻量级锁(无锁,自旋锁,自适应自旋)–>重量级锁
锁升级机制是对应于synchronized锁的,锁存储在对象头中
无锁
无锁指的是对资源没有锁定,所有的线程都能同时访问并修改共享资源,但是只有一个线程能够修改成功。
无锁的特点就是:修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并返回,否则就继续循环尝试。如果多个线程修改同一个值,那么必定会有一个线程修改成功,而其他修改失败的线程会循环尝试直到修改成功。
偏向锁
偏向锁指的是如果一段同步代码自始至终都只有一个线程访问,即不存在多线程的竞争,那么该线程在后续的访问中会自动获取锁(其实是没有释放锁),所以效率极高。
初次访问到synchronized代码块时,无锁状态改变为偏向锁(通过CAS修改对象头里面的标志位),偏向锁字面理解就是:“偏向于第一个获得它线程的锁”。当偏向锁执行完毕之后,并不会立即释放偏向锁。当第二次到达同步代码块的时候,线程会判断此时持有锁的线程是不是自己(持有锁的线程ID也在对象头中),如果是则正常往下执行。由于之前没有释放锁,所以这里不需要重新获取锁。如果自始至终使用锁的线程只有一个,那么偏向锁没有额外的开销,效率极高
当一个线程访问同步代码块并获取锁时,会在对象头的Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储这指向当前线程的偏向锁。轻量级锁的获取以及释放依赖多次CAS原子指令,而偏向锁只需要在置换Thread ID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
轻量级锁(自旋锁)
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,线程不会阻塞,从而提高性能。
轻量级锁的获取主要由两种情况:
- 当关闭偏向锁功能时
- 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。所谓的锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续竞争,没有抢到锁的线程将自旋,即不停的循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋是消耗CPU的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任务,这种状态称为忙等,不能让其一直忙等(如果一个线程一致持有资源,其他线程一直忙等,迟早会撑爆CPU)。
如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
重量级锁
重量级锁指的是:一个锁获取线程后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁:一个线程持有轻量级锁时,其他线程会自旋尝试获取锁,进行忙等,但是不能让忙等一直发生,会大量消耗CPU资源。忙等是有限度的(有个计数器记录自旋次数,默认是10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但是不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,而不是忙等,等待将来被唤醒。
也就是说讲线程间的调度交给了操作系统,而频繁的对线程运行状态的切换,线程的挂起和唤醒,会消耗大量的系统资源,时间成本也更高。
10.锁分类
11.ReentrantLock的实现?
12.死锁是什么?产生条件是什么?
死锁是指两个或者多个进程执行过程中,因争夺资源而造成的一种循环等待的现象。
产生条件:
- 请求和保持:一个进程请求资源而阻塞,保持已经获取的资源不释放
- 不剥夺条件:已经获取的资源,在使用完之前不能被强行剥夺
- 循环等待条件:若干资源形成了头尾相接的循环等待资源关系
- 互斥:某一时间内独占资源
13.怎么避免死锁?
破坏死锁产生条件的任意一个或者多个条件都可以破除死锁。
14.怎么在程序中检查死锁?
1.使用jps -l
获取当前运行的进程id列表
2.使用jstack 进程id
找到死锁问题
15.保证线程安全的思路?
- 对非多线程安全的代码加锁
- 使用线程安全的类
- 多线程并发情况下,线程共享的变量改为方法级的局部变量