Java中的锁分类
Java中锁分为以下几种:
- 乐观锁、悲观锁
- 独享锁、共享锁
- 公平锁、非公平锁
- 互斥锁、读写锁
- 可重入锁
- 分段锁
- 锁升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁) JDK1.6
- 自旋锁
这些锁的分类并不全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。
1、乐观锁 & 悲观锁
两种锁只是一种概念
乐观锁:乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。
实现方式:CAS机制、版本号机制
悲观锁:悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改。所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。比如Java里面的synchronized关键字的实现就是悲观锁。实现方式:就是加锁。
2、独享锁 & 共享锁
两种锁只是一种概念
独享锁:该锁一次只能被一个线程所持有
共享锁:该锁可以被多个线程所持有
举例:
synchronized是独享锁;
可重入锁ReentrantLock是独享锁;
读写锁ReentrantReadWriteLock中的读锁ReadLock是共享锁,写锁WriteLock是独享锁。
独享锁与共享锁通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享。
3、互斥锁 & 读写锁
上面讲的独享锁/共享锁就是一种概念,互斥锁/读写锁是具体的实现。
互斥锁的具体实现就是synchronized、ReentrantLock。ReentrantLock是JDK1.5的新特性,采用ReentrantLock可以完全替代替换synchronized传统的锁机制,更加灵活。
读写锁的具体实现就是读写锁ReadWriteLock。
4、可重入锁
定义:对于同一个线程在外层方法获取锁的时候,在进入内层方法时也会自动获取锁。
优点:避免死锁
举例:ReentrantLock、synchronized
5、公平锁 & 非公平锁
公平锁:多个线程相互竞争时要排队,多个线程按照申请锁的顺序来获取锁。
非公平锁:多个线程相互竞争时,先尝试插队,插队失败再排队,比如:synchronized、ReentrantLock
6、分段锁
分段锁并不是具体的一种锁,只是一种锁的设计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。CurrentHashMap底层就用了分段锁,使用Segment,就可以进行并发使用了,而HashMap确实非线程安全的,就差在了分段锁上。
7、偏向锁 & 轻量级锁 & 重量级锁
JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
8、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
守护线程和非守护线程
一、什么是守护线程
守护线程相对于正常线程来说,是比较特殊的一类线程,那么它特殊在哪里呢?别急,在了解它之前,我们需要知道一个问题,那就是:
JVM 程序在什么情况下能够正常退出?
The Java Virtual Machine exits when the only threads running are all daemon threads.
上面这句话来自 JDK 官方文档,意思是:
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。
理解起来有点拗口,看完下面的代码你就懂了 。
- ①:创建一个非守护线程;
- ②:模拟非守护线程不退出的情况;
- ③:启动线程;
- ④:主线程即将退出;
运行这段代码,猜猜看,JVM 进程是否能够正常退出呢?
可以看到因为有一个非守护线程一直在后台运行着,JVM 无法正常退出。那么,如果说正在运行的是个守护线程,结果又会怎么样呢?
①:添加一个钩子(Hook)线程, 用来监听 JVM 退出,并输出日志;
②:通过
setDaemon(true)
将该线程为守护线程;
再次运行代码,瞅瞅效果:
可以看到,当主线程退出时,JVM 会随之退出运行,守护线程同时也会被回收,即使你里面是个死循环也不碍事。
二、守护线程的作用及应用场景
通过上面的示例代码,相信你已经了解了守护线程和普通线程之间的区别,那么,我们来讨论一下为什么需要守护线程,以及何时使用,它的应用场景是什么?
上面,我们已经知道了,如果 JVM 中没有一个正在运行的非守护线程,这个时候,JVM 会退出。
换句话说,守护线程会随着主线程结束而结束,守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
JVM 中的垃圾回收线程就是典型的守护线程,如果说不具备该特性,会发生什么呢?
当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。
通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。