30

一网打尽Java中锁的分类

 5 years ago
source link: https://www.tuicool.com/articles/AJ3mqiZ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

来一段很常见的死锁代码,当个开胃菜:

class Deadlock {

public static String str1 = "str1";

public static String str2 = "str2";


public static void main(String[] args) {

Thread thread1 = new Thread(() -> {

try {

while (true) {

synchronized (Deadlock.str1) {

System.out.println(Thread.currentThread().getName() + "锁住 str1");

Thread.sleep(1000);

synchronized (Deadlock.str2) {

System.out.println(Thread.currentThread().getName() + "锁住 str2");

}

}

}

} catch (Exception e) {

e.printStackTrace();

}

});


Thread thread2 = new Thread(() -> {

try {

while (true) {

synchronized (Deadlock.str2) {

System.out.println(Thread.currentThread().getName() + "锁住 str2");

Thread.sleep(1000);

synchronized (Deadlock.str1) {

System.out.println(Thread.currentThread().getName() + "锁住 str1");

}

}

}

} catch (Exception e) {

e.printStackTrace();

}

});

thread1.start();

thread2.start();

}

}

猜猜上面的输出是啥?如果我将str2也赋值成”str1“,又会如何呢?

Java中锁的分类只是将锁的特性进行了归纳,可以分为:

  1. 可重入锁/不可重入锁

  2. 可中断锁

  3. 公平锁/非公平锁

  4. 独享锁(互斥锁)/共享锁(读写锁)

  5. 乐观锁/悲观锁

  6. 分段锁

  7. 偏向锁/轻量级锁/重量级锁

  8. 自旋锁

注意:ReentrantLock和ReentrantReadWriteLock虽然一些性质相同,但前者实现的是Lock接口,后者实现的是ReadWriteLock接口。

1. 可重入锁/不可重入锁

可重入锁

可重入锁是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 ReentrantLock和synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁。

看下面这段代码就明白了:

synchronized void method1() throws Exception{

Thread.sleep(1000);

method2();

}


synchronized void method2() throws Exception{

Thread.sleep(1000);

}

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronined不具备可重入性,此时线程A需要重新申请锁,这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

不可重入锁

不可重入锁是指若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到且被阻塞。我们尝试设计一个不可重入锁:

public class Lock{

private boolean isLocked = false;

public synchronized void lock() throws InterruptedException{

while(isLocked){

wait();

}

isLocked = true;

}

public synchronized void unlock(){

isLocked = false;

notify();

}

}

使用该锁:

public class Count{

Lock lock = new Lock();

public void print(){

lock.lock();

doAdd();

lock.unlock();

}

public void doAdd(){

lock.lock();

//do something

lock.unlock();

}

}

当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。

2.可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized是不可中断锁,而ReentrantLock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

可以查看ReentrantLock的lockInterruptibly(),已经充分体现了Lock的可中断性。 点击查看更多细节

3.公平锁/非公平锁

公平锁:以请求锁的顺序来获取锁。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序;而对于ReentrantLock和ReentrantReadWriteLock,默认情况下是非公平锁,但是可以在构造函数中设置为公平锁。

在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

4.独享锁(互斥锁)/共享锁(读写锁)

独享锁:该锁一次只能被一个线程所持有。
共享锁:该锁可被多个线程所持有。
对于ReentrantLock/synchronized而言,其是独享锁。但是对于ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,但读写、写读 、写写的过程是互斥的。

5. 乐观锁/悲观锁

乐观锁与悲观锁是从看待并发同步的角度来划分的。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是J.U.C下的locks包和synchronized关键字。
乐观锁在Java中的使用,(又称为无锁编程)常常采用的是CAS算法,J.U.C下Atomic包的各种实现。

6. 分段锁

分段锁其实是一种锁的设计,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它是类似于HashMap(JDK7版本及以上的HashMap实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

7. 偏向锁/轻量级锁/重量级锁

这三种锁对应synchronized锁的三种状态。JDK6通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中MarkWord的值来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

有关synchronized的这三种锁状态变化的详解, 点击查看更多细节

8. 自旋锁

在Java中,自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁存在的问题:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点:

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快

  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

在JDK6之后,自旋锁进行了优化变成自适应自旋锁了。 点击查看更多细节

FFz2Ef2.png!web

喜欢就点个“在看”呗 ^_^ UBfu6zu.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK