JAVA 锁机制 - 可重入锁, 可中断锁,公平锁,读写锁,自旋锁, 解决多线程安全问题

JAVA 锁机制 - 可重入锁, 可中断锁,公平锁,读写锁,自旋锁, 解决多线程安全问题

如果需要查看具体的 synchronized 和 lock 的实现原理,请参考:解决多线程安全问题 - 无非两个方法 synchronized 和 lock 具体原理 (百度)

img

在并发编程中,经常遇到多个线程访问同一个 共享资源 ,这时候作为开发者必须考虑如何维护数据一致性,在 java 中 synchronized 关键字被常用于维护数据一致性。synchronized 机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的,因为对于共享资源属性访问是必要也是必须的,下文会有具体示例演示。
一. java 中的锁
一般在 java 中所说的锁就是指的内置锁,每个 java 对象都可以作为一个实现同步的锁,虽然说在 java 中一切皆对象, 但是锁必须是引用类型的,基本数据类型则不可以 。每一个引用类型的对象都可以隐式的扮演一个用于同步的锁的角色,执行线程进入 synchronized 块之前会自动获得锁,无论是通过正常语句退出还是执行过程中抛出了异常,线程都会在放弃对 synchronized 块的控制时自动释放锁。 获得锁的唯一途径就是进入这个内部锁保护的同步块或方法 。
正如引言中所说,对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程 A 尝试获取线程 B 的锁时,线程 A 必须等待或者阻塞,直到线程 B 释放该锁为止,否则线程 A 将一直等待下去,因此 java 内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。
根据使用方式的不同一般我们会将锁分为对象锁和类锁,两个锁是有很大差别的,对象锁是作用在实例方法或者一个对象实例上面的,而类锁是作用在静态方法或者 Class 对象上面的。一个类可以有多个实例对象,因此一个类的对象锁可能会有多个,但是每个类只有一个 Class 对象,所以类锁只有一个。 类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定的是实例方法还是静态方法区别的 。
在 java 中实现锁机制不仅仅限于使用 synchronized 关键字,还有 JDK1.5 之后提供的 Lock,Lock 不在本文讨论范围之内。一个 synchronized 块包含两个部分:锁对象的引用,以及这个锁保护的代码块。如果作用在实例方法上面,锁就是该方法所在的当前对象,静态 synchronized 方法会从 Class 对象上获得锁。

锁的相关概念介绍

1. 可重入锁

如果锁具备可重入性,则称作为可重入锁。像 synchronized 和 ReentrantLock 都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个 synchronized 方法时,比如说 method1,而在 method1 中会调用另外一个 synchronized 方法 method2,此时线程不必重新去申请锁,而是可以直接执行方法 method2。

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

[img](javascript:void(0)😉

class MyClass {
    public synchronized void method1() {
        method2();
    }
 
    public synchronized void method2() {
 
    }
}

[img](javascript:void(0)😉

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

而由于 synchronized 和 Lock 都具备可重入性,所以不会发生上述现象。

2. 可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。

在 Java 中,synchronized 就不是可中断锁,而 Lock 是可中断锁。

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

在前面演示 lockInterruptibly() 的用法时已经体现了 Lock 的可中断性。

3. 公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

看一下这 2 个类的源代码就清楚了:

img

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

我们可以在创建 ReentrantLock 对象时,通过以下方式来设置锁的公平性:

ReentrantLock lock = new ReentrantLock(true);

如果参数为 true 表示为公平锁,为 fasle 为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

img

另外在 ReentrantLock 类中定义了很多方法,比如:

[img](javascript:void(0)😉

isFair()        //判断锁是否是公平锁

isLocked()    //判断锁是否被任何线程获取了

isHeldByCurrentThread()   //判断锁是否被当前线程获取了

hasQueuedThreads()   //判断是否有线程在等待该锁

[img](javascript:void(0)😉

在 ReentrantReadWriteLock 中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock 并未实现 Lock 接口,它实现的是 ReadWriteLock 接口。

4. 读写锁

读写锁将对一个资源(比如文件)的访问分成了 2 个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口。

可以通过 readLock() 获取读锁,通过 writeLock() 获取写锁。

5、自旋锁
首先是一种锁,与互斥锁相似,基本作用是用于线程(进程)之间的同步。与普通锁不同的是,一个线程 A 在获得普通锁后,如果再有线程 B 试图获取锁,那么这个线程 B 将会挂起(阻塞);试想下,如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文的切换的代价高于等待资源的代价的时候(锁的已保持者保持锁时间比较短),那么线程 B 可以不放弃 CPU 时间片,而是在 “原地” 忙等,直到锁的持有者释放了该锁,这就是自旋锁的原理,可见自旋锁是一种非阻塞锁。
二、自旋锁可能引起的问题:
\1. 过多占据 CPU 时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据 cpu 时间片,导致 CPU 资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃 CPU 时间片阻塞;
\2. 死锁问题:试想一下,有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

JAVA 中一种自旋锁的实现: CAS 是 Compare And Set 的缩写

[img](javascript:void(0)😉

import java.util.concurrent.atomic.AtomicReference;  
class SpinLock {  
        //java中原子(CAS)操作  
    AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋锁的线程对象  
    private int count;  
    public void lock() {  
        Thread cur = Thread.currentThread();  
        //lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。当有第二个线程调用lock操作时由于owner值不为空,导致循环    
  
            //一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。  
        while (!owner.compareAndSet(null, cur)){  
        }  
    }  
    public void unLock() {  
        Thread cur = Thread.currentThread();  
            owner.compareAndSet(cur, null);  
        }  
    }  
}  
public class Test implements Runnable {  
    static int sum;  
    private SpinLock lock;  
      
    public Test(SpinLock lock) {  
        this.lock = lock;  
    }  
    public static void main(String[] args) throws InterruptedException {  
        SpinLock lock = new SpinLock();  
        for (int i = 0; i < 100; i++) {  
            Test test = new Test(lock);  
            Thread t = new Thread(test);  
            t.start();  
        }  
          
        Thread.currentThread().sleep(1000);  
        System.out.println(sum);  
    }  
      
    @Override  
    public void run() {  
        this.lock.lock();  
        sum++;  
        this.lock.unLock();  
    }  
}

[img](javascript:void(0)😉

一、公平锁 / 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于 ReentrantLock 而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于 Synchronized 而言,也是一种非公平锁。由于其并不像 ReentrantLock 是通过 AQS 的来实现线程调度,所以并没有任何办法使其变成公平锁。
二、可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
说的有点抽象,下面会有一个代码的示例。
对于 Java ReentrantLock 而言, 他的名字就可以看出是一个可重入锁,其名字是 Re entrant Lock 重新进入锁。
对于 Synchronized 而言, 也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

[img](javascript:void(0)😉

img

synchronized void setA() throws Exception{

    Thread.sleep(1000); 
    setB();

}

synchronized void setB() throws Exception{

    Thread.sleep(1000);

}

img

[img](javascript:void(0)😉

三、独享锁 / 共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于 Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。
对于 Synchronized 而言,当然是独享锁。
四、互斥锁 / 读写锁

上面讲的独享锁 / 共享锁就是一种广义的说法,互斥锁 / 读写锁就是具体的实现。
互斥锁在 Java 中的具体实现就是 ReentrantLock
读写锁在 Java 中的具体实现就是 ReadWriteLock
五、乐观锁 / 悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在 Java 中的使用,就是利用各种锁。
乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,典型的例子就是原子类,通过 CAS 自旋实现原子操作的更新。
六、分段锁

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

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

在 Java 中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
线程自旋和适应性自旋

我们知道,java 线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。
并且 jdk 官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置 10 次。
这样就避免了线程切换的开销,极大的提升了性能。

而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋 10 次一下。
他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

二. synchronized 使用示例
1. 多窗口售票
假设一个火车票售票系统,有若干个窗口同时售票,很显然在这里票是作为多个窗口的共享资源存在的,由于座位号是确定的,因此票上面的号码也是确定的,我们用多个线程来模拟多个窗口同时售票,首先在不使用 synchronized 关键字的情况下测试一下售票情况。
先将票本身作为一个共享资源放在单独的线程中,这种作为共享资源存在的线程很显然应该是实现 Runnable 接口,我们将票的总数 num 作为一个入参传入,每次生成一个票之后将 num 做减法运算,直至 num 为 0 即停止,说明票已经售完了,然后开启多个线程将票资源传入。

[img](javascript:void(0)😉

public class Ticket implements Runnable{
     private int num;//票数量
     private boolean flag=true;//若为false则售票停止
     public Ticket(int num){
     this.num=num;
     }
     @Override
     public void run() {
     while(flag){
     ticket();
     }
     }
     private void ticket(){
     if(num<=0){
     flag=false;
     return;
     }
     try {
     Thread.sleep(20);//模拟延时操作
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //输出当前窗口号以及出票序列号
     System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
     }
    }
    public class MainTest {
     public static void main(String[] args) {
     Ticketticket = new Ticket(5);
     Threadwindow01 = new Thread(ticket, "窗口01");
     Threadwindow02 = new Thread(ticket, "窗口02");
     Threadwindow03 = new Thread(ticket, "窗口03");
     window01.start();
     window02.start();
     window03.start();
     }
    }

[img](javascript:void(0)😉

程序的输出结果如下:

[img](javascript:void(0)😉

窗口02售出票序列号:5
    窗口03售出票序列号:4
    窗口01售出票序列号:5
    窗口02售出票序列号:3
    窗口01售出票序列号:2
    窗口03售出票序列号:2
    窗口02售出票序列号:1
    窗口03售出票序列号:0
    窗口01售出票序列号:-1

[img](javascript:void(0)😉

从上面程序运行结果可以看出不但票的序号有重号而且出票数量也不对,这种售票系统比 12306 可要烂多了,人家在繁忙的时候只是刷不到票而已,而这里的售票系统倒好了,出票比预计的多了而且会出现多个人争抢做同一个座位的风险。如果是单个售票窗口是不会出现这种问题,多窗口同时售票就会出现争抢共享资源因此紊乱的现象,解决该现象也很简单,就是在 ticket() 方法前面加上 synchronized 关键字或者将 ticket() 方法的方法体完全用 synchronized 块包括起来。

[img](javascript:void(0)😉

//方式一
    private synchronized void ticket(){
     if(num<=0){
     flag=false;
     return;
     }
     try {
     Thread.sleep(20);//模拟延时操作
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
    }
    //方式二
    private void ticket(){
     synchronized (this) {
     if (num <= 0) {
     flag = false;
     return;
     }
     try {
     Thread.sleep(20);//模拟延时操作
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName() + "售出票序列号:" + num--);
     }
    }

[img](javascript:void(0)😉

再看一下加入 synchronized 关键字的程序运行结果:

窗口01售出票序列号:5
    窗口03售出票序列号:4
    窗口03售出票序列号:3
    窗口02售出票序列号:2
    窗口02售出票序列号:1

从这里可以看出在实例方法上面加上 synchronized 关键字的实现效果跟对整个方法体加上 synchronized 效果是一样的。 另外一点需要注意加锁的时机也非常重要 ,本示例中 ticket() 方法中有两处操作容易出现紊乱,一个是在 if 语句模块,一处是在 num–,这两处操作本身都不是原子类型的操作,但是在使用运行的时候需要这两处当成一个整体操作,所以 synchronized 将整个方法体都包裹在了一起。如若不然,假设 num 当前值是 1,但是窗口 01 执行到了 num–,整个操作还没执行完成,只进行了赋值运算还没进行自减运算,但是窗口 02 已经进入到了 if 语句模块,此时 num 还是等于 1,等到窗口 02 执行到了输出语句的时候,窗口 01 的 num–也已经将自减运算执行完成,这时候窗口 02 就会输出序列号 0 的票。再者如果将 synchronized 关键字加在了 run 方法上面,这时候的操作不会出现紊乱或者错误,但是这种加锁方式无异于单窗口操作,当窗口 01 拿到锁进入 run() 方法之后, 必须等到 flag 为 false 才会将语句执行完成跳出循环,这时候的 num 就已经为 0 了,也就是说票已经被售卖完了,这种方式摒弃了多线程操作,违背了最初的设计原则 - 多窗口售票。
2. 懒汉式单例模式
创建单例模式有很多中实现方式,本文只讨论懒汉式创建。在 Android 开发过程中单例模式可以说是最常使用的一种设计模式,因为它操作简单还可以有效减少内存溢出。下面是懒汉式创建单例模式一个示例:

(懒汉式与饿汉式的区别:Singleton 单例模式(懒汉方式和饿汉方式)

[img](javascript:void(0)😉

public class Singleton {
     private static Singletoninstance;
     private Singleton() {
     }
     public static SingletongetInstance() {
     if (instance == null) {
     instance = new Singleton();
     }
     return instance;
     }
    }

[img](javascript:void(0)😉

如果对于多窗口售票逻辑已经完全明白了的话就可以看出这里的实现方式是有问题的,我们可以简单的创建几个线程来获取单例输出对象的 hascode 值。

com.sunny.singleton.Singleton@15c330aa
    com.sunny.singleton.Singleton@15c330aa
    com.sunny.singleton.Singleton@41aff40f

在多线程模式下发现会出现不同的对象,这种单例模式很显然不是我们想要的,那么根据上面多窗口售票的逻辑我们在 getInstance() 方法上面加上一个 synchronized 关键字,给该方法加上锁,加上锁之后可以避免多线程模式下生成多个不同对象,但是同样会带来一个效率问题,因为不管哪个线性进入 getInstance() 方法都会先获得锁,然后再次释放锁,这是一个方面,另一个方面就是只有在第一次调用 getInstance() 方法的时候,也就是在 if 语句块内才会出现多线程并发问题,而我们却索性将整个方法都上锁了。讨论到这里就引出了另外一个问题,究竟是 synchronized 方法好还是 synchronized 代码块好呢? 有一个原则就是锁的范围越小越好 ,加锁的目的就是将锁进去的代码作为原子性操作, 因为非原子操作都不是线程安全的,因此 synchronized 代码块应该是在开发过程中优先考虑使用的加锁方式。

[img](javascript:void(0)😉

public static SingletongetInstance() {
     if (instance == null) {
     synchronized (Singleton.class) {
     instance = new Singleton();
     }
     }
     return instance;
    }

[img](javascript:void(0)😉

这里也会遇到类似上面的问题,多线程并发下回生成多个实例,如线程 A 和线程 B 都进入 if 语句块,假设线程 A 先获得锁,线程 B 则等待,当 new 一个实例后,线程 A 释放锁,线程 B 获得锁后会再次执行 new 语句,同样不能保证单例要求,那么下面代码再来一个 null 判断,进行双重检查上锁呢?

[img](javascript:void(0)😉

public static SingletongetInstance() {
     if (instance == null) {
     synchronized (Singleton.class) {
     if(instance==null){
     instance = new Singleton();
     }
     }
     }
     return instance;
    }

[img](javascript:void(0)😉

该模式就是双重检查上锁实现的单例模式,这里在代码层面我们已经 基本 保证了线程安全了, 但是还是有问题的, 双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 java 平台内存模型。内存模型允许所谓的 “无序写入”,这也是这些习语失败的一个主要原因。 更为详细的介绍可以参考 Java 单例模式中双重检查锁的问题 。所以单例模式创建比较建议使用恶汉式创建或者静态内部类方式创建。

3.synchronized 不具有继承性
我们可以通过一个简单的 demo 验证这个问题,在一个方法中顺序的输出一系列数字,并且输出该数字所在的线程名称,在父类中加上 synchronized 关键字,子类重写父类方法测试一下加上 synchronized 关键字和不加关键字的区别即可。

[img](javascript:void(0)😉

public class Parent {
     public synchronized void test() {
     for (int i = 0; i < 5; i++) {
     System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     }
     }
    }

[img](javascript:void(0)😉

子类继承父类 Parent,重写 test() 方法.

[img](javascript:void(0)😉

public class Child extends Parent {
     @Override
     public void test() {
     for (int i = 0; i < 5; i++) {
     System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     }
     }
    }

[img](javascript:void(0)😉

测试代码如下:

[img](javascript:void(0)😉

final Child c = new Child();
    new Thread() {
     public void run() {
     c.test();
     };
    }.start();
    new Thread() {
     public void run() {
     c.test();
     };
    }.start();

[img](javascript:void(0)😉

输出结果如下:

[img](javascript:void(0)😉

Parent Thread-0:0  Child Thread-0:0
    Parent Thread-0:1  Child Thread-1:0
    Parent Thread-0:2  Child Thread-0:1
    Parent Thread-0:3  Child Thread-1:1
    Parent Thread-0:4  Child Thread-0:2
    Parent Thread-1:0  Child Thread-1:2
    Parent Thread-1:1  Child Thread-0:3
    Parent Thread-1:2  Child Thread-1:3
    Parent Thread-1:3  Child Thread-0:4
    Parent Thread-1:4  Child Thread-1:4

[img](javascript:void(0)😉

通过输出信息可以知道,父类 Parent 中会将单个线程中序列号输出完成才会执行另一个线程中代码,但是子类 Child 中确是两个线程交替输出数字,所以 synchronized 不具有继承性。

4. 死锁示例
死锁是多线程开发中比较常见的一个问题。若有多个线程访问多个资源时,相互之间存在竞争,就容易出现死锁。下面就是一个死锁的示例,当一个线程等待另一个线程持有的锁时,而另一个线程也在等待该线程锁持有的锁,这时候两个线程都会处于阻塞状态,程序便出现死锁。

[img](javascript:void(0)😉

package com.lock;
   class Thread01 extends Thread{
    private Object resource01;
    private Object resource02;
    public Thread01(Object resource01, Object resource02) {
    this.resource01 = resource01;
    this.resource02 = resource02;
    }
    @Override
    public void run() {
    synchronized(resource01){
    System.out.println("Thread01 locked resource01");
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource02) {
    System.out.println("Thread01 locked resource02");
    }
    }
    }
   }
    class Thread02 extends Thread{
    private Object resource01;
    private Object resource02;
    public Thread02(Object resource01, Object resource02) {
    this.resource01 = resource01;
    this.resource02 = resource02;
    
    }
    @Override
    public void run() {
    synchronized(resource02){
    System.out.println("Thread02 locked resource02");
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource01) {
    System.out.println("Thread02 locked resource01");
    }
    }
    }
   }
   public class deadlock {
    public static void main(String[] args) {
    final Object resource01="resource01";
    final Object resource02="resource02";
    Thread01 thread01=new Thread01(resource01, resource02);
    Thread02 thread02=new Thread02(resource01, resource02);
    thread01.start();
    thread02.start();
    }
   }

[img](javascript:void(0)😉

结果为:

Thread02 locked resource02
Thread01 locked resource01

执行上面的程序就会一直等待下去,出现死锁。当线程 Thread01 获得 resource01 的锁后,等待 500ms,然后尝试获取 resource02 的锁,但是此时 resouce02 锁已经被 Thread02 持有,同样 Thread02 也等待了 500ms 尝试获取 resouce01 锁,但是该所已经被 Thread01 持有,这样两个线程都在等待对方所有的资源,造成了死锁。

三. 其它
关键字 synchronized 具有锁重入功能,当一个线程已经持有一个对象锁后,再次请求该对象锁时是可以得到该对象的锁的,这种方式是必须的,否则在一个 synchronized 方法内部就没有办法调用该对象的另外一个 synchronized 方法了。锁重入是通过为每个所关联一个计数器和一个占有它的线程,当计数器为 0 时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM 会记录锁的占有者,并将计数器设置为 1。如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时计数器会递减,直至减为 0 时锁才会被释放。
在声明一个对象作为锁的时候要注意字符串类型锁对象,因为字符串有一个常量池,如果不同的线程持有的锁是具有相同字符的字符串锁时,两个锁实际上同一个锁。

ReentrantLock 特性

轮询锁的和定时锁

可轮询和可定时的锁请求是通过 tryLock() 方法实现的, 和无条件获取锁不一样. ReentrantLock 可以有灵活的容错机制. 死锁的很多情况是由于顺序锁引起的, 不同线程在试图获得锁的时候阻塞, 并且不释放自己已经持有的锁, 最后造成死锁. tryLock() 方法在试图获得锁的时候, 如果该锁已经被其它线程持有, 则按照设置方式立刻返回, 而不是一直阻塞等下去, 同时在返回后释放自己持有的锁. 可以根据返回的结果进行重试或者取消, 进而避免死锁的发生.

公平性

ReentrantLock 构造函数中提供公平性锁和非公平锁(默认)两种选择。所谓公平锁,线程将按照他们发出请求的顺序来获取锁,不允许插队;但在非公平锁上,则允许插队:当一个线程发生获取锁的请求的时刻,如果这个锁是可用的,那这个线程将跳过所在队列里等待线程并获得锁。我们一般希望所有锁是非公平的。因为当执行加锁操作时,公平性将讲由于线程挂起和恢复线程时开销而极大的降低性能。考虑这么一种情况:A 线程持有锁,B 线程请求这个锁,因此 B 线程被挂起;A 线程释放这个锁时,B 线程将被唤醒,因此再次尝试获取锁;与此同时,C 线程也请求获取这个锁,那么 C 线程很可能在 B 线程被完全唤醒之前获得、使用以及释放这个锁。这是种双赢的局面,B 获取锁的时刻(B 被唤醒后才能获取锁)并没有推迟,C 更早地获取了锁,并且吞吐量也获得了提高。在大多数情况下,非公平锁的性能要高于公平锁的性能。

可中断获锁获取操作

lockInterruptibly 方法能够在获取锁的同时保持对中断的响应,因此无需创建其它类型的不可中断阻塞操作。

读写锁 ReadWriteLock

ReentrantLock 是一种标准的互斥锁,每次最多只有一个线程能持有锁。读写锁不一样,暴露了两个 Lock 对象,其中一个用于读操作,而另外一个用于写操作。

[img](javascript:void(0)😉

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock(); 

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

[img](javascript:void(0)😉

可选择实现:

\1. 释放优先
2. 读线程插队
\3. 重入性
\4. 降级
\5. 升级

ReentrantReadWriteLock 实现了 ReadWriteLock 接口,构造器提供了公平锁和非公平锁两种创建方式。读写锁适用于读多写少的情况,可以实现更好的并发性。

参考:Java 并发编程之显示锁 ReentrantLock 和 ReadWriteLock 读写锁

posted @ 2020-03-17 00:08  别再闹了  阅读(403)  评论(0)    收藏  举报