【3】线程同步机制和锁

247 阅读35分钟

同步机制

  • 导致线程安全问题的因素时更多的是侧重其根源,包括硬件(如写缓冲器)和软件(编译器)。但是从应用程序的角度来看,线程安全问题的产生是由于多线程应用程序缺乏某种东西——-线程同步机制。线程同步机制是一套用于协调线程间的数据访问( Data access )及活动(Activity )的机制,该机制用于保障线程安全以及实现这些线程的共同目标。如果把线程比作在公路上行驶的车辆,那么线程同步机制就好比是任何车辆都需要遵循的交通规则。公路上行驶的车辆只有遵守交通规则才能够达到其目的——安全地到达目的地。
  • 从广义上来说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相关的API,如Object.wait()/Object.notify()等。

锁概述

我们知道线程安全问题的产生前提是多个线程并发访问共享变量、共享资源(以下统称为共享数据)。于是,我们很容易想到一种保障线程安全的方法——将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,该线程访问结束后其他线程才能对其进行访问。锁(Lock)就是利用这种思路以保障线程安全的线程同步机制。

按照上述思路,锁可以理解为对共享数据进行保护的许可证。对于同一个许可证所保护的共享数据而言,任何线程访问这些共享数据前必须先持有该许可证。一个线程只有在持有许可证的情况下才能够对这些共享数据进行访问;并且,一个许可证一次只能够被一个线程持有﹔许可证的持有线程在其结束对这些共享数据的访问后必须让出(释放)其持有的许可证,以便其他线程能够对这些共享数据进行访问。

一个线程在访问共享数据前必须申请相应的锁(许可证),线程的这个动作被称为锁的获得(Acquire )。一个线程获得某个锁(持有许可证),我们就称该线程为相应锁的持有线程(线程持有许可证),一个锁一次只能被一个线程持有。锁的持有线程可以对该锁所保护的共享数据进行访间,访回结束后该线程必须释放(Release)相应的锁。锁的持有线程在其获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区 ( CriticalSection )。因此,共享数据只允许在临界区内进行访问,临界区一次只能被一个线程执行。

约定:

如果有多个线程访问同一个锁所保护的共享数据,那么我们就称这些线程同步在这个锁上,或者称我们对这些线程所进行的共享数据访问进行加锁;相应地,这些线程所执行的临界区就被称为这个锁所引导的临界区。

特性

  1. 排他性
  2. 原子性
  3. 有序性

按照Java虚拟机对锁的实现方式划分,Java平台中的锁包括内部锁( Intrinsic Lock )和显式锁( Explicit Lock )。内部锁是通过synchronized 关键字实现的;显式锁是通过java.concurrent.locks.Lock接口的实现类(如java.concurrent.locks.ReentrantLock类)实现的。

image.png

1.Synchronized锁

synchronized 是 Java平台中的任何一个对象都有唯一一个与之关联的锁。这种锁被称为监视器( Monitor)或者内部锁( Intrinsic Lock )。Synchronized 锁是一种排他锁,它能够保障原子性、可见性和有序性。

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在Java 早期版本中,synchronized 属于重量级锁,效率低下。为什么呢?因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

所以,你会发现目前的话,不论是各种开源框架还是JDK源码都大量使用了synchronized 关键字。

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用 的对象是调用这个方法的对象; o 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的 所有对象;
  4. 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主 的对象是这个类的所有对象。

双重校验锁实现对象单例(线程安全)

单线程版

public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用getlnstance()的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

改进:

为了解决上面的问题,最简单的方法是将整个getInstance()方法设为同步(synchronized) 。

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static synchronized Singleton getInstance() {//封死了
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用getInstance()方法。

但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。

最终版:

这就引出了双重检验锁。双重检验锁模式(double checked locking pattern) ,是—种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验—次?因为可能会有多个线程一起进入同步块外的if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {//Single Checked
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {//Double Checked
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

双重检验锁+volatile

另外,需要注意uniqueInstance采用volatile 关键字修饰也是很有必要。uniqueInstance 采用volatile关键字修饰也是很有必要的, uniquelnstance =new Singleton();这段代码其实是分为三步执行: 1.为uniquelnstance分配内存空间 2.初始化 uniqueInstance 3.将uniquelnstance 指向分配的内存地址

但是由于JVM具有指令重排的特性,执行顺序有可能变成1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程T1执行了1和3,此时T2调用getUniqueInstance()后发现uniqueInstance不为空,因此返回uniquelnstance,但此时 uniquelnstance还未被初始化。

使用volatile可以禁止JVM的指令重排,保证在多线程环境下也能正常运行。

双重校验锁为什么要判断两次

今天写synchronized用例的时候,两个线程共享一个对象数据,当操作i的时候,在同步代码块外面判断了一次i<100,但是每一次跑,都会出现i=100,的情况,此时我想起了单例模式的双重校验锁,为什么要判断两次呢?因为可能出现线程1和线程2,在i=99的时候,同时判断了一次,都进到了for循环里面,此时线程1进入同步代码块,线程2进如阻塞队列,当线程1跑出代码块后,线程2进入同步代码块,线程1对i进行加—操作后,i变成了100,所以线程2就输出了100,所以要在同步代码块中再加一次判断,判断i的值

synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步

import java.util.ArrayList;
 
public class SynchronizedTest2 {
    public static void main(String[] args) throws InterruptedException {
        SychronizedTest2 synchronized2 = new SychronizedTest2();
        Data data = synchronized2.new Data();
        new Thread(new Runnable() {
            @Override
            public void run() {
                data.insert();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                data.insert();
            }
        }).start();
        Thread.currentThread().sleep(3000);
    }
    class Data{
        private ArrayList<Integer> arr = new ArrayList<>();
        private int i = 0;
        public void insert(){
            for(;i<100;i ++)
                synchronized(this){
                    if(i<100){
                        if(!arr.contains(i)){
                            System.out.println(Thread.currentThread().getName()+" 正在插入"+i);
                            arr.add(i);
                        }
                    }
                }
            }
        }
    }

synchronized关键字的底层原理

synchronized关键字底层原理属于JVM层面。

synchronized同步语句块的情况

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过JDK自带的javap命令查看SynchronizedDemo类的相关字节码信息: 1.首先切换到类的对应目录执行javac SynchronizedDemo.java命令生成编译后 的.class文件 2.然后执行javap -c -s -v -l SynchronizedDemo.class。

image.png

从上面我们可以看出: synchronized同步语句块的实现使用的是monitorenter和 monitorexit指令,其中:

  • monitorenter指令指向同步代码块的开始位置
  • monitorexit指令则指明同步代码块的结束位置。

当执行monitorenter 指令时,线程试图获取锁也就是获取对象监视器monitor的持有权。

在Java 虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块nteS或者方法中才能调用wait/notify等方法,否则会抛java.lang.IllegalMonitorStateException的异常的原因。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为О则表示锁可以被获取,获取后将锁计数器设为1也就是加1。

在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外—个线程释放为止。

synchronized修师方法的的情况

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

image.png

synchronized修饰的方法并没有monitorenter指令和monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。

JVM通过该ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized原理总结

  1. synchronized同步语句块的实现使用的是monitorenter和monitorexit 指 令,其中 monitorenter指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
  2. synchronized修饰的方法并没有monitorenter 指令和monitorexit 指令,取 得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。 3.不过两者的本质都是对对象监视器monitor 的获取。

synchronized的阻塞通知方法

object的wait方法

Object的notify方法

加锁口诀

  • 判断(等待)
  • 业务
  • 通知

/**
 * 线程之间的通信问题:生产者和消费者问题!  等待唤醒,通知唤醒
 * 线程交替执行  A   B 操作同一个变量   num = 0
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();


        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

// 判断等待,业务,通知
class Data{ // 数字 资源类

    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if (number!=0){  //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (number==0){ // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }

}

我们同步代码块当中的等待方法用的是wait,那wait/sleep有什么区别呢?

来自不同的类

sleep是Thread 的静态方法,

wait是Object的方法,任何对象实例都能调用。

关于锁的释放

sleep不会释放锁,它也不需要占用锁。

wait 会释放锁,但调用它的前提是当前线程占有锁,即代码要在synchronized中。

虚假唤醒问题

A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
C=>1
A=>2
C=>3
B=>2
B=>1
B=>0
C=>1
A=>2
C=>3
D=>2
D=>1
D=>0
B=>-1
B=>-2
B=>-3
D=>-4
D=>-5
D=>-6
D=>-7
D=>-8
D=>-9
D=>-10
C=>-9
A=>-8
C=>-7
A=>-6
C=>-5
A=>-4
C=>-3
A=>-2
C=>-1

image.png

拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。

所以如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。

image.png

虚假唤醒修复

if 改为 while 判断


/**
 * 线程之间的通信问题:生产者和消费者问题!  等待唤醒,通知唤醒
 * 线程交替执行  A   B 操作同一个变量   num = 0
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();


        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

// 判断等待,业务,通知
class Data{ // 数字 资源类

    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while (number!=0){  //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number==0){ // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }

}

售票案例

   class Ticket {
   //票数
   private int number = 30;
   //操作方法:卖票
   public synchronized void sale() {
   //判断:是否有票
   if(number > 0) {
   System.out.println(Thread.currentThread().getName()+" :
   "+(number--)+" "+number);
   }
   }
   }

如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有; 2)线程执行发生异常,此时 JVM 会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。

2.Lock锁

Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。 Lock 与的 Synchronized 区别

  • Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
  • Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如没有主动释放锁,就有可能导致出现死锁现象。

2.1 Lock 接口

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

下面来逐个讲述 Lock 接口中每个方法的使用

2.2 lock

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}

2.3 newCondition

关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。

用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:

  • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
  • signal()用于唤醒一个等待的线程。

注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

2.4 ReentrantLock

ReentrantLock,意思是“可重入锁”。

  1. 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
  2. 可中断锁:可中断锁时子线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
  3. 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁

ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。下面通过一些实例看具体看一下如何使用。

public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock(); //注意这个地方
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}

synchronized的局限性

synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:

  1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
  2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待

JDK1.5之后发布,加入了Doug Lea实现的java.util.concurrent包。包内提供了Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。

synchronized方式

public class Demo2 {
    private static int num = 0;
    private static synchronized void add() {
        num++;
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo2.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo2.num);
    }
}

ReentrantLock方式

import java.util.concurrent.locks.ReentrantLock;

public class Demo3 {
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    private static void add() {
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
        }
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo3.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo3.num);
    }
}

ReentrantLock的使用过程:

  1. 创建锁:ReentrantLock lock = new ReentrantLock();
  2. 获取锁:lock.lock()
  3. 释放锁:lock.unlock();

对比上面的代码,与关键字synchronized相比,ReentrantLock锁有明显的操作过程,开发人员必须手动的指定何时加锁,何时释放锁,正是因为这样手动控制,ReentrantLock对逻辑控制的灵活度要远远胜于关键字synchronized,上面代码需要注意lock.unlock()一定要放在finally中,否则,若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。

ReentrantLock获取锁的过程是可中断的

对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

  1. 要么获取到锁然后继续后面的操作
  2. 要么一直等待,直到其他线程释放锁为止

而ReentrantLock提供了另外一种可能,就是在等待获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。有些使用这个操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小时朋友还没到,突然你接到一个电话,朋友由于突发状况,不能来了,那么你一定达到回府。中断操作正是提供了一套类似的机制,如果一个线程正在等待获取锁,那么它依然可以收到一个通知,被告知无需等待,可以停止工作了。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Demo6 {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    private static ReentrantLock lock2 = new ReentrantLock(false);
    public static class T extends Thread {
        int lock;
        public T(String name, int lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                if (this.lock == 1) {
                    lock1.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lockInterruptibly();
                }
            } catch (InterruptedException e) {
                System.out.println("中断标志:" + this.isInterrupted());
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1", 1);
        T t2 = new T("t2", 2);
        t1.start();
        t2.start();
    }
}

先运行一下上面代码,发现程序无法结束,使用jstack查看线程堆栈信息,发现2个线程死锁了。

Found one Java-level deadlock:
=============================
"t2":
  waiting for ownable synchronizer 0x0000000717380c20, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t1"
"t1":
  waiting for ownable synchronizer 0x0000000717380c50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t2

lock1被线程t1占用,lock2被线程t2占用,线程t1在等待获取lock2,线程t2在等待获取lock1,都在相互等待获取对方持有的锁,最终产生了死锁,如果是在synchronized关键字情况下发生了死锁现象,程序是无法结束的。

我们对上面代码改造一下,线程t2一直无法获取到lock1,那么等待5秒之后,我们中断获取锁的操作。主要修改一下main方法,如下:

T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
t2.interrupt();

新增了2行代码TimeUnit.SECONDS.sleep(5);t2.interrupt();,程序可以结束了,运行结果:

java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.itsoku.chat06.Demo6$T.run(Demo6.java:31)
中断标志:false

从上面信息中可以看出,代码的31行触发了异常,中断标志输出:false

image.png t2在31行一直获取不到lock1的锁,主线程中等待了5秒之后,t2线程调用了interrupt()方法,将线程的中断标志置为true,此时31行会触发InterruptedException异常,然后线程t2可以继续向下执行,释放了lock2的锁,然后线程t1可以正常获取锁,程序得以继续进行。线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。

关于获取锁的过程中被中断,注意几点:

  1. ReentrankLock中必须使用实例方法lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发InterruptedException异常
  2. 线程调用interrupt()之后,线程的中断标志会被置为true
  3. 触发InterruptedException异常之后,线程的中断标志会被清空,即置为false
  4. 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false

ReentrantLock锁申请等待限时

申请锁等待限时是什么意思?一般情况下,获取锁的时间我们是不知道的,synchronized关键字获取锁的过程中,只能等待其他线程把锁释放之后才能够有机会获取到锁。所以获取锁的时间有长有短。如果获取锁的时间能够设置超时时间,那就非常好了。

ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。 tryLock无参方法

看一下源码中tryLock方法:

public boolean tryLock()

返回boolean类型的值,此方法会立即返回,结果表示获取锁是否成功,示例:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Demo8 {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(System.currentTimeMillis() + ":" + this.getName() + "开始获取锁!");
                //获取锁超时时间设置为3秒,3秒内是否能否获取锁都会返回
                if (lock1.tryLock()) {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "获取到了锁!");
                    //获取到锁之后,休眠5秒
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能获取到锁!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();
    }
}

代码中获取锁成功之后,休眠5秒,会导致另外一个线程获取锁失败,运行代码,输出:

1563356291081:t2开始获取锁!
1563356291081:t2获取到了锁!
1563356291081:t1开始获取锁!
1563356291081:t1未能获取到锁!

tryLock有参方法

可以明确设置获取锁的超时时间,该方法签名:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

该方法在指定的时间内不管是否可以获取锁,都会返回结果,返回true,表示获取锁成功,返回false表示获取失败。此方法有2个参数,第一个参数是时间类型,是一个枚举,可以表示时、分、秒、毫秒等待,使用比较方便,第1个参数表示在时间类型上的时间长短。此方法在执行的过程中,如果调用了线程的中断interrupt()方法,会触发InterruptedException异常。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Demo7 {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(System.currentTimeMillis() + ":" + this.getName() + "开始获取锁!");
                //获取锁超时时间设置为3秒,3秒内是否能否获取锁都会返回
                if (lock1.tryLock(3, TimeUnit.SECONDS)) {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "获取到了锁!");
                    //获取到锁之后,休眠5秒
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能获取到锁!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();
    }
}

程序中调用了ReentrantLock的实例方法tryLock(3, TimeUnit.SECONDS),表示获取锁的超时时间是3秒,3秒后不管是否能否获取锁,该方法都会有返回值,获取到锁之后,内部休眠了5秒,会导致另外一个线程获取锁失败。

运行程序,输出:

1563355512901:t2开始获取锁!
1563355512901:t1开始获取锁!
1563355512902:t2获取到了锁!
1563355515904:t1未能获取到锁!

输出结果中分析,t2获取到锁了,然后休眠了5秒,t1获取锁失败,t1打印了2条信息,时间相差3秒左右。

关于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,说明一下:

  1. 都会返回boolean值,结果表示获取锁是否成功
  2. tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果
  3. tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而触发InterruptedException异常,这个从2个方法的声明上可以可以看出来

ReentrantLock其他常用的方法

  1. isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁,上面代码中有使用过。

获取锁的4种方法对比

获取锁的方法是否立即响应(不会阻塞)是否响应中断
lock()××
lockInterruptibly()×
tryLock()×
tryLock(long timeout, TimeUnit unit)×

总结

  1. ReentrantLock可以实现公平锁和非公平锁
  2. ReentrantLock默认实现的是非公平锁
  3. ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
  4. 释放锁的操作必须放在finally中执行
  5. lockInterruptibly()实例方法可以相应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发InterruptedException异常
  6. 关于InterruptedException异常说一下,看到方法声明上带有 throws InterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用interrupt()方法而触发InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
  7. 实例方法tryLock()会尝试获取锁,会立即返回,返回值表示是否获取成功
  8. 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断

2.5 ReadWriteLock

ReadWriteLock 也是一个接口,在它里面只定义了两个方法:

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

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock 实现了 ReadWriteLock 接口。

ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。

下面通过几个例子来看一下 ReentrantReadWriteLock 具体用法。 假如有多个线程要同时进行读操作的话,先看一下 synchronized 达到的效果:

  public class Test {
  private ReentrantReadWriteLock rwl = new
  ReentrantReadWriteLock();

public static void main(String[] args) {
final Test test = new Test();

new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();

new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();

}

public synchronized void get(Thread thread) {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
}
}
而改成用读写锁的话:
public class Test {
private ReentrantReadWriteLock rwl = new
ReentrantReadWriteLock();

public static void main(String[] args) {
final Test test = new Test();

new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();

new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();

}

public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();

while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}

说明 thread1 和 thread2 在同时进行读操作。这样就大大提升了读操作的效率。

注意:

  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

2.6 悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized关键字和Lock的实现类都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确。

显式的锁定之后再操作同步资源

//=============悲观锁的调用方式
public synchronized void m1()
{
    //加锁后的业务逻辑......
}

// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
    lock.lock();
    try {
        // 操作同步资源
    }finally {
        lock.unlock();
    }
}

2.7 乐观锁

//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

​ 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢

乐观锁一般有两种实现方式:

  1. 采用版本号机制
  2. CAS(Compare-and-Swap,即比较并替换)算法实现

2.8案例

1、JDK源码(notify方法)

image.png

2、八种锁的案例实际体现在3个地方

  1. 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
  2. 作用于代码块,对括号里配置的对象加锁。
  3. 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
1、标准访问有ab两个线程,请问先打印邮件还是短信
class Phone //资源类
{
    public synchronized void sendEmail()
    {
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();

    }
}

-------sendEmail
-------sendSMS

2、sendEmail方法暂停3秒钟,请问先打印邮件还是短信
{
    public synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();

    }
}

class Phone //资源类
{
    public synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();

    }
}

-------sendEmail
-------sendSMS
1-2结论

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了, 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

3、新增一个普通的hello方法,请问先打印邮件还是hello
class Phone //资源类
{
    public synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.hello();
        },"b").start();

    }
}

-------hello
-------sendEmail
4、有两部手机,请问先打印邮件还是短信
class Phone //资源类
{
    public synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1
        Phone phone2 = new Phone();//资源类2

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone2.sendSMS();
        },"b").start();
    }
}

-------sendSMS
-------sendEmail
3-4结论

加个普通方法后发现和同步锁无关,hello 换成两个对象后,不是同一把锁了,情况立刻变化。

5、两个静态同步方法,同1部手机,请问先打印邮件还是短信
class Phone //资源类
{
    public static synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public static synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

-------sendEmail
-------sendSMS

6、两个静态同步方法, 2部手机,请问先打印邮件还是短信
class Phone //资源类
{
    public static synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public static synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1
        Phone phone2 = new Phone();//资源类2

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone2.sendSMS();
        },"b").start();
    }
}

-------sendEmail
-------sendSMS
5-6结论

都换成静态同步方法后,情况又变化 三种 synchronized 锁的内容有一些差别: 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方>法用的都是同一把锁——实例对象本身, 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板 对于同步方法块,锁的是 synchronized 括号内的对象

7、1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
class Phone //资源类
{
    public static synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

class Phone //资源类
{
    public static synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone.sendSMS();
        },"b").start();
    }
}

-------sendSMS
-------sendEmail

8、1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
class Phone //资源类
{
    public static synchronized void sendEmail()
    {
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS()
    {
        System.out.println("-------sendSMS");
    }

    public void hello()
    {
        System.out.println("-------hello");
    }
}

public class Lock8Demo
{
    public static void main(String[] args)//一切程序的入口,主线程
    {
        Phone phone = new Phone();//资源类1
        Phone phone2 = new Phone();//资源类2

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            phone2.sendSMS();
        },"b").start();
    }
}

-------sendSMS
-------sendEmail
7-8结论
当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。

所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。

所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

2.9 公平锁和非公平锁

在大多数情况下,锁的申请都是非公平的,也就是说,线程1首先请求锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可获得锁还是线程2可获得锁呢?这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。这就好比买票不排队,大家都围在售票窗口前,售票员忙的焦头烂额,也顾及不上谁先谁后,随便找个人出票就完事了,最终导致的结果是,有些人可能一直买不到票。而公平锁,则不是这样,它会按照到达的先后顺序获得资源。公平锁的一大特点是:它不会产生饥饿现象,只要你排队,最终还是可以等到资源的;synchronized关键字默认是有jvm内部实现控制的,是非公平锁。而ReentrantLock运行开发者自己设置锁的公平性。

看一下jdk中ReentrantLock的源码,2个构造方法:

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

默认构造方法创建的是非公平锁。

第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。

公平锁和非公平锁在程序调度上是很不一样,来一个公平锁示例看一下:

import java.util.concurrent.locks.ReentrantLock;

public class Demo5 {
    private static int num = 0;
    private static ReentrantLock fairLock = new ReentrantLock(true);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                fairLock.lock();
                try {
                    System.out.println(this.getName() + "获得锁!");
                } finally {
                    fairLock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
    }
}

看一下输出的结果,锁是按照先后顺序获得的。

修改一下上面代码,改为非公平锁试试,如下:

ReentrantLock fairLock = new ReentrantLock(false);

从ReentrantLock卖票编码演示公平和非公平现象

import java.util.concurrent.locks.ReentrantLock;

class Ticket
{
    private int number = 30;
    ReentrantLock lock = new ReentrantLock();

    public void sale()
    {
        lock.lock();
        try
        {
            if(number > 0)
            {
                System.out.println(Thread.currentThread().getName()+"卖出第:\t"+(number--)+"\t 还剩下:"+number);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

public class SaleTicketDemo
{
    public static void main(String[] args)
    {
        Ticket ticket = new Ticket();

        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"a").start();
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"b").start();
        new Thread(() -> { for (int i = 0; i <35; i++)  ticket.sale(); },"c").start();
    }
}

生活中,排队讲求先来后到视为公平。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为不公平

1、源码解读

按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗?),如果没有先驱节点才能获取锁;先占先得非公平锁,是不管这个事的,只要能抢获到同步状态就可以

image.png

2、为什么会有公平锁/非公平锁的设计为什么默认非公平?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

3、使⽤公平锁会有什么问题

公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”

4、什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

2.10 可重入锁(又名递归锁)

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

1、“可重入锁”这四个字分开来解释:

可:可以。
重:再次。
入:进入。
锁:同步锁。

进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。
自己可以获取自己的内部锁

2、可重入锁种类

1、隐式锁(即synchronized关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。 简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

同步块

public class ReEntryLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println("-----外层调用");
                synchronized (objectLockA){
                    System.out.println("-----中层调用");
                    synchronized (objectLockA){
                        System.out.println("-----内层调用");
                    }
                }
            }
        },"a").start();
    }
}

同步方法

public class ReEntryLockDemo{
    public synchronized void m1(){
        System.out.println("-----m1");
        m2();
    }
    public synchronized void m2(){
        System.out.println("-----m2");
        m3();
    }
    public synchronized void m3(){
        System.out.println("-----m3");
    }

    public static void main(String[] args){
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

        reEntryLockDemo.m1();
    }
}

2、显式锁(即Lock)也有ReentrantLock这样的可重入锁。
public class Demo4 {
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    private static void add() {
        lock.lock();
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                Demo4.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(Demo4.num);
    }
}

上面代码中add()方法中,当一个线程进入的时候,会执行2次获取锁的操作,运行程序可以正常结束,并输出和期望值一样的30000,假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一样,可重入锁。

代码中还有几点需要注意:

  1. lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
  2. unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
/**
 * @create 2020-05-14 11:59
 * 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
 */
public class ReEntryLockDemo{
    static Lock lock = new ReentrantLock();

    public static void main(String[] args){
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("----外层调用lock");
                lock.lock();
                try
                {
                    System.out.println("----内层调用lock");
                }finally {
                    // 这里故意注释,实现加锁次数和释放次数不一样
                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                    lock.unlock(); // 正常情况,加锁几次就要解锁几次
                }
            }finally {
                lock.unlock();
            }
        },"a").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println("b thread----外层调用lock");
            }finally {
                lock.unlock();
            }
        },"b").start();

    }
}

3、Synchronized的重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

  • ​ 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

  • ​ 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

  • ​ 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

2.11 小结(重点)

Lock 和 synchronized 有以下几点不同:

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  5. Lock 可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized。

2.12 死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

image.png

1、产生死锁主要原因

  1. 系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当
public class DeadLockDemo{
    public static void main(String[] args){
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();

        new Thread(() -> {
            synchronized (objectLockA){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockB)
                {
                    System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
                }
            }
        },"A").start();

        new Thread(() -> {
            synchronized (objectLockB){
                System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
                //暂停几秒钟线程
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                synchronized (objectLockA){
                    System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
                }
            }
        },"B").start();

    }
}

2、如何排查死锁

  1. 纯命令
ps -l
jstack 进程编号
  1. 图形化
jconsole

3、死锁互斥的四个条件

  • 互斥条件
    临界资源是独占资源,进程应互斥且排他的使用这些资源。
  • 占有和等待条件
    进程在请求资源得不到满足而等待时,不释放已占有资源。
  • 不剥夺条件
    又称不可抢占,已获资源只能由进程自愿释放,不允许被其他进程剥夺。
  • 循环等待条件
    又称环路条件,存在循环等待链,其中,每个进程都在等待链中等待下一个进程所持有的资源,造成这组进程处于永远等待状态。

死锁只有在这四个条件同时满足时出现。

4、死锁解决的方法

主要有一下三种方法:

  • 死锁防止
  • 死锁避免
  • 死锁检测和恢复
1 死锁防止

在程序运行之前防止发生死锁。

前面说了死锁产生的条件有四个,分别是:互斥条件、占有和等待条件、不剥夺条件、循环等待条件。

而死锁防止的策略就是至少破坏这四个条件其中一项。

2 破坏互斥条件

使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。

只读数据文件、磁盘等软硬件资源均可采用这种办法管理;
但是许多资源是独占性资源,如可写文件、键盘等只能互斥的占有;
所以这种做法在许多场合是不适用的。

3 破坏占有和等待条件

采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。

实现简单,但是严重的减低了资源利用率。
因为在每个进程占有的资源中,有些资源在运行后期使用,有些资源在例外情况下才被使用,可能会造成进程占有一些几乎用不到的资源,而使其他想使用这些资源的进程等待。

4 破坏不剥夺条件

剥夺调度能够防止死锁,但是只适用于内存和处理器资源。

方法一:占有资源的进程若要申请新资源,必须主动释放已占有资源,若需要此资源,应该向系统重新申请。

方法二:资源分配管理程序为进程分配新资源时,若有则分配;否则将剥夺此进程已占有的全部资源,并让进程进入等待资源状态,资源充足后再唤醒它重新申请所有所需资源。

5 破坏循环等待条件

给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。

采用层次分配策略,将系统中所有的资源排列到不同层次中

  • 一个进程得到某层的一个资源后,只能申请较高一层的资源
  • 当进程释放某层的一个资源时,必须先释放所占有的较高层的资源
  • 当进程获得某层的一个资源时,如果想申请同层的另一个资源,必须先释放此层中已占有的资源

5、死锁恢复

  • 资源剥夺法
    剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除。
  • 进程回退法
    根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制。
  • 进程撤销法
    • 撤销陷入死锁的所有进程,解除死锁,继续运行。
    • 逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。

可选择符合下面条件之一的先撤销:
1.CPU消耗时间最少者
2.产生的输出量最小者
3.预计剩余执行时间最长者
4.分得的资源数量最少者后优先级最低者

  • 系统重启法
    结束所有进程的执行并重新启动操作系统。这种方法很简单,但先前的工作全部作废,损失很大。