JAVA锁机制-可重入锁,可中断锁,公平锁,读写锁,自旋锁,

 

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

   

    在并发编程中,常常遇到多个线程访问同一个 共享资源 ,这时候做为开发者必须考虑如何维护数据一致性,在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对象上得到锁。java

 

锁的相关概念介绍

1.可重入锁算法

若是锁具有可重入性,则称做为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上代表了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,好比说method1,而在method1中会调用另一个synchronized方法method2,此时线程没必要从新去申请锁,而是能够直接执行方法method2。编程

看下面这段代码就明白了: 设计模式

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

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

而因为synchronized和Lock都具有可重入性,因此不会发生上述现象。安全

2.可中断锁多线程

可中断锁:顾名思义,就是能够相应中断的锁。并发

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

若是某一线程A正在执行锁中的代码,另外一线程B正在等待获取该锁,可能因为等待时间过长,线程B不想等待了,想先处理其余事情,咱们可让它中断本身或者在别的线程中中断它,这种就是可中断锁。

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

3.公平锁

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

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

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

而对于ReentrantLock和ReentrantReadWriteLock,它默认状况下是非公平锁,可是能够设置为公平锁。

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

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

咱们能够在建立ReentrantLock对象时,经过如下方式来设置锁的公平性:  

ReentrantLock lock = new ReentrantLock(true);

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

另外在ReentrantLock类中定义了不少方法,好比: 

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

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

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

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

 

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

4.读写锁

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

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

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

能够经过readLock()获取读锁,经过writeLock()获取写锁。

 

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

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

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();  
    }  
}

 

 

1、公平锁/非公平锁

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

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

复制代码
synchronized void setA() throws Exception{

    Thread.sleep(1000); 
    setB();

}

synchronized void setB() throws Exception{

    Thread.sleep(1000);

}
复制代码

3、独享锁/共享锁

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

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

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

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

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

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

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

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

 


    二.synchronized使用示例
    1.多窗口售票
    假设一个火车票售票系统,有若干个窗口同时售票,很显然在这里票是做为多个窗口的共享资源存在的,因为座位号是肯定的,所以票上面的号码也是肯定的,咱们用多个线程来模拟多个窗口同时售票,首先在不使用synchronized关键字的状况下测试一下售票状况。
    先将票自己做为一个共享资源放在单独的线程中,这种做为共享资源存在的线程很显然应该是实现Runnable接口,咱们将票的总数num做为一个入参传入,每次生成一个票以后将num作减法运算,直至num为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();
     }
    }

程序的输出结果以下:

  

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

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

 //方式一
    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--);
     }
    }

   再看一下加入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 单例模式(懒汉方式和饿汉方式)

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

 若是对于多窗口售票逻辑已经彻底明白了的话就能够看出这里的实现方式是有问题的,咱们能够简单的建立几个线程来获取单例输出对象的hascode值。

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

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

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

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

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

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

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

 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();
     }
     }
     }
    }

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

 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();
     }
     }
     }
    }

 测试代码以下:

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

   输出结果以下:

    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

    经过输出信息能够知道,父类Parent中会将单个线程中序列号输出完成才会执行另外一个线程中代码,可是子类Child中确是两个线程交替输出数字,因此synchronized不具备继承性。

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

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();
    }
   }

结果为:

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对象,其中一个用于读操做,而另一个用于写操做。 

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();
}

 可选择实现:

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

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

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

相关文章
相关标签/搜索