挑灯夜读——Java锁:最全锁介绍
作者:互联网
Java最全的线程锁的介绍(不全打我)
乐观锁和悲观锁
乐观锁
:
- 顾名思义就是很乐观的意思。
在线程操作某一个资源时,总是认为该资源不会被其它的线程占有,只有本线程对其操作,所以不需要加锁的过程。
- 而不加锁的好处就是减少了上锁耗费的时间
- 通过版本号和CAS实现乐观锁,而这种机制是Java.util.concurrent.atomic包下面的原子类就是通过CAS乐观锁来实现的。
- 每次只需要进行一次对比操作,查看数据是否发生变化,而后该线程操作该资源。
悲观锁
:
- 对资源持有悲观态度
悲观锁,线程每次操作某一资源时,都会对其进行加锁,以防止其它线程抢夺锁。其它线程拿不到锁就会处于被阻塞的状态。
- 加锁的坏处是耗费了加锁解锁的时间,但是安全性增加了。
- synchronized和ReentrantLock就是很典型的悲观锁。当然hashtable也是悲观锁。
两者对比
:
区别点 | 乐观锁 | 悲观锁 |
---|---|---|
锁 | 不加锁,减少耗费时间 | 加锁,保证安全 |
应用场景 | 写操作极少(避免冲突) | 写操作多(保证安全) |
实现方式 | 原子类通过CAS乐观锁实现 | synchronized和ReentrantLock等 |
独占锁和共享锁
独占锁
:
- 独占当然意味着该线程会独自占有该资源的操作,其它线程无法获取锁,只能阻塞等待。
获得独占锁的线程,既能修改数据,又能读取数据,操作不受限制
- ==synchronized和JUC中的Lock实现类就是独占锁
共享锁
:
- 共享意味着,该数据资源不仅仅能被一个线程获得锁,还能被其它线程获得锁,也就是操作的权限。当然这种操作只能是读操作
获得共享锁的线程只能对其进行读操作,当线程已经拿到数据共享锁后,只能增加共享锁,
独占锁
无法增加。当然独占锁优先级大于共享锁。有了独占锁,共享锁无法添加。
- ReentrantReadWriteLock就是一种共享锁。
互斥锁和读写锁
互斥锁
:
- 互斥锁其实就是独占锁的一种普通实现,意味着一个线程获取互斥锁后,具有唯一性和排他性。
互斥锁一次只能有一个线程拥有,其它线程只有等待。
读写锁
:
- 读写锁是共享锁的一种体现,读写锁管理一组锁,分别是读锁和写锁。
读锁可以在没有写锁的情况下被多个线程同时持有,而写锁就是独占的。
相当于读锁是共享锁,而写锁是独占锁。
写锁的优先级高于读锁,并且每次读锁获取锁时,必须看到前一个写锁释放时更改的内容。
- 读写锁的并发程度高于互斥锁,因为可以管理不同的锁类别,从而使用诸如多个读线程来持有锁等情况。
- ReadWirteLock就是读写锁的接口。
公平锁和非公平锁
公平锁
:
- 顾名思义就是很公平的意思,前一个线程释放锁后,下一个获取锁的线程为阻塞队列中的第一个线程,很公平,先来后到体现的淋漓尽致。
多线程按照申请获取锁的顺序持有锁,好似银行排队办理事务的感觉。
- 我们可以在创建ReentrantLock锁时,手动定义构造函数的参数为true,从而实现公平锁。
- 缺点:可能出现,排队的第一个线程需要执行十分钟,第二个线程只需要执行五毫秒且很急迫,这时仍需要执行第一个线程完后再执行第二个,很浪费时间。
非公平锁
:
- 也就是前一个线程释放锁后,下一个获取锁的线程可能并不是阻塞队列的第一个线程,而是其中的某个具有更高优先级的线程。
该锁并不会按照先来后到的顺序获取锁,而是共同竞争,谁优先级高或者抢的快,就能获得锁。
- synchronized和ReentrantLock都是默认的非公平锁。
- 可能出现某个优先级不高的线程一直未被执行。
可重入锁
- 可重入锁意味着:锁可以再次进入该同步块的内部同步块,而不需要获取锁。
public static synchronized void methodA() throws Exception{
methodB()
}
public static synchronized void methodB() throws Exception{
......//方法
}
- 假如没有可重入锁,那么在获取方法A的锁后,就无法再获取方法B的锁,这样就会陷入:
无法放弃A,但是不放弃A由无法获得B锁,陷入左右为难,从而死锁自己的情况。宛如一个脚踏两只船的渣男/渣女。
- 但是有了可重入锁机制后,只需要获取A方法的锁,便可以直接走到B方法无需关心锁。
自旋锁
- 自旋锁是为了防止在某个线程未获取到锁的时候,不被直接挂起,也就是丢入阻塞线程。因为挂起和唤醒也需要消耗资源。
通过让未获取得锁得线程产生一个自旋动作,有机会便获取锁。
- 自旋锁只适合前一个线程持有锁时间不长得时候操作,如果持有时间很长,那么自旋锁反而会消耗更多得资源。
- 自JDK1.6后,便加入了自适应自旋锁,也就是根据前面线程自旋得次数和获得锁得情况来变化到最佳得自旋次数。
分段锁
- 分段锁,不是一种锁,而是一种锁操作得概念和方式。
分段所,就是将很大一块同步块分割为很多小部分,我们无需对整个部分加锁,只需加锁需要使用得那一小块。
- 诸如上面得concurrentHashMap就使用了segment得思想对小分段进行加锁。
锁升级
- 在JDK1.6之后,为了提高性能,减少加锁和释放锁带来的性能消耗。引入了四种不同的锁状态:无锁、偏向锁、轻量级锁、重量级锁,随着线程的竞争升级,锁也开始升级了。
无锁
:
- 就是一个乐观锁的状态,并未对线程进行加锁,前面已经提到过乐观锁,这里不做赘述。
偏向锁
:
- 比乐观锁更悲观一点,偏向锁会对第一次操作该数据的线程进行加偏向锁的操作,下一个该线程进行操作时,只需判断是否为该线程即可进入操作。
而保存是否为偏向锁的区域为被操作对象头中的mark word中,在是否为偏向锁中添加是,偏向锁线程的ID号等,倘若有线程来操作该数据,只需要对比即可,倘若是,则直接进入。倘若不是,那么就存在竞争,进行锁升级。
轻量级锁
:
- 轻量级锁是在竞争变得更加激烈的情况下升级得来。此时认为竞争存在,但是程度不高,于是在加锁时,如若线程失败,并不会并阻塞,而是进入自旋等待的状态。
- 倘若自旋的次数超过了一定的量,或者一个线程持有锁,一个线程在自旋,此时又来了第三个线程访问,此时轻量级锁就会膨胀为重量级锁。
重量级锁
:
- 重量级锁由轻量级锁升级而来。该锁就为互斥锁,也就是说一个线程拿到锁后,其它线程都必须进入阻塞等待状态。
- 其中synchronized内部就是这样一种锁升级的过程。
锁优化
锁粗化
:
- 通俗来说:就是前面定义的锁太细腻,从而使操作耗费了大量时间。
Lock lock = new ReentrantLock();
public void menthod(){
for(int i=0;i<100;i++){
synchronized(lock)
//......操作
}
}
- 上面操作虽然不会出错,但是0到100需要不断加锁解锁,耗费资源,此时,将锁放在其它位置,不仅也能达到目的,还能减少加解锁。
Lock lock = new ReentrantLock();
public void menthod(){
synchronized(lock)
for(int i=0;i<100;i++){
//......操作
}
}
- 上面就是锁粗化,意味着不再那么细腻,当然粗化后更加高效。
锁消除
:
- 通俗来讲:在不需要锁的时候,加入了锁。
- 比如我们操作一个局部变量,仍将局部变量定义为线程安全类。而局部变量会放入线程栈中操作,本就是线程独占且安全的,没有必要使用安全类,比如StringBuffer类来作为局部变量。
- 此时内部就需要对该类的锁进行消除,从而提高效率。
参考:
- 本次学习目录参考苏三说技术公众号,内容为自己理解和总结。
- 总结:
希望此次学习有所收获!
标签:加锁,Java,独占,夜读,互斥,线程,自旋,操作,挑灯 来源: https://blog.csdn.net/qq_45746068/article/details/119134527