Java基础进阶——多线程与JUC(上)

最近在准备面试,把知识点复习一遍,整理出的笔记记录下,里面会穿插代码和面试例题。

内容不是原创,是总结和收集,并在理解的基础上进行一些完善,如果侵权了请联系作者,若有错误也请各位指正。因为收集的时候忘记把来源记录下来了,所以就不po出处了,请见谅(这是个坏习惯,一定改)。

Java基础进阶——多线程与JUC(上)

  • 多线程与JUC
    • JUC是什么
    • 线程的状态
      • wait和sleep的区别
    • 线程安全
      • 线程安全的级别
      • Synchronized
        • Java中的对象结构
        • Synchronized的实现原理
      • 锁的优化
        • 锁升级
        • 锁升级的过程
        • 自适应自旋锁
        • 锁粗化
        • 锁消除
      • Lock锁
        • Lock接口
        • Condition类
        • Lock中的方法
      • Synchronized与Lock的区别



本章内容主要讲Java中的多线程开发的JUC包,Java中有关多线程开发的操作基本上都在这几个包里。本章内容主要介绍JUC包内的一些概念和用法,与多线程相关的基本概念可以参考之前的内容: Java基础(十三):多线程.

本章为上篇,将分为上下两篇。上篇涉及Synchronized的实现原理,以及锁升级的介绍。

多线程与JUC


JUC是什么

JUC指的是java.util 下几个包的简称,涉及多线程开发的相关操作。

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

线程的状态

我们看一下Thread.State的源码中的枚举,线程的生命周期内有以下6种状态,并将其注释翻译出来:

 public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,  //新生状态。 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。是线程还未调用start()方法之前的状态。

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,  //运行状态。 运行的线程状态。线程正在Java虚拟机中执行(Running),但它可能正在等待来自操作系统(如处理器)的其他资源(Ready)。包含了Ready和Running两种状态。

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,  //阻塞状态。 是正在等待锁监视器的状态。等待进入synchronized块/方法;再次进入synchronized块/方法后等待响应;调用了Object.wait()方法。

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * 
    *
  • {@link Object#wait() Object.wait} with no timeout
  • *
  • {@link #join() Thread.join} with no timeout
  • *
  • {@link LockSupport#park() LockSupport.park}
  • *
* *

A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * is waiting for a specified thread to terminate. */ WAITING, //等待状态。 线程处于无限期等待状态中。由于调用以下方法之一,线程处于等待状态:Object.wait(),没有超时时间,无限期;Thread.join(),没有超时时间,无限期;LockSupport#park()。 /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *

    *
  • {@link #sleep Thread.sleep}
  • *
  • {@link Object#wait(long) Object.wait} with timeout
  • *
  • {@link #join(long) Thread.join} with timeout
  • *
  • {@link LockSupport#parkNanos LockSupport.parkNanos}
  • *
  • {@link LockSupport#parkUntil LockSupport.parkUntil}
  • *
*/
TIMED_WAITING, //超时等待。 线程在特定的时长内等待。同样是以下方法之一导致:Thread.sleep(long)/Object.wait(long)\Thread.join(long)\LockSupport.parkNanos()\LockSupport.parkUntil()。 /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED;//终止状态。 线程已经完成操作。 }

一个线程的生命周期如下图所示:
Java基础进阶——多线程与JUC(上)_第1张图片

  • 新生状态(New)

使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。是线程还未调用start()方法之前的状态。

  • 运行状态(Runnable)

调用start()方法之后线程处于可运行状态的线程了。Runnable表示运行的线程状态,它可能正在Java虚拟机中执行,它也可能正在等待来自操作系统(如处理器)的其他资源。即包含了就绪(Ready)和运行(Running)两种状态。

  • 阻塞状态(Blocked)

是正在等待锁监视器的状态。等待进入synchronized块/方法;再次进入synchronized块/方法后等待响应;调用了Object.wait()方法。

  • 等待状态(Waiting)

线程处于无限期等待状态中。由于调用以下方法之一,线程处于等待状态:Object.wait(),没有超时时间,无限期;Thread.join(),没有超时时间,无限期;LockSupport#park()。

wait()与wait(0)同义,意思是无限期等待;sleep(0)的意思是不等待,等待时间为0。


  • 超时等待(Timed_Waiting)

线程在特定的时长内等待。同样是以下方法之一导致:Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil()。

  1. wait方法之后线程进入等待池中等待,它没有CPU的执行权,需调用notify或者notifyAll唤醒,随机唤醒。等待期间可以调用里面的同步方法,其他线程可以访问。
  2. sleep方法拥有CPU的执行权,它会在等待时间结束后自动唤醒。等待期间不释放锁,其他线程不可以访问。


  • 死亡/终止状态(Terminated)

线程已经完成操作。已经死亡的线程不能使用start()重新唤醒,死亡等于结束了。线程会以如下3种方式结束,结束后就处于死亡状态:

  1. run()或call()方法执行完成,线程正常结束。
  2. 线程抛出一个未捕获的Exception或Error。
  3. 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。


wait和sleep的区别

  1. 来自不同的类:wait属于Object类,sleep属于Thread类。
  2. 是否释放锁:wait会释放锁,sleep不会释放锁。
    wait之后线程进入等待池中等待,它没有CPU的执行权,需调用notify或者notifyAll唤醒,随机唤醒。等待期间可以调用里面的同步方法,其他线程可以访问。
    sleep方法拥有CPU的执行权,它会在等待时间结束后自动唤醒。等待期间不释放锁,其他线程不可以访问。
    若它们唤醒时没有cpu资源则需继续等待资源。
  3. 使用范围:wait需要在同步方法或者同步代码块中使用;sleep可以在任何地方使用,注意sleep是静态方法,也就是说它只对当前对象有效。通过对象名.sleep()想让该对象线程进入休眠是无效的,它只会让当前线程进入休眠。
  4. 捕获异常:wait可以不捕获异常,sleep必须捕获异常。
  • wait()的使用

wait()需要先获取对象的monitor锁,才能调用,否则报出异常java.lang.IllegalMonitorStateException,这也是wait需要在同步代码块中使用的原因。常用写法:

        synchronized(obj){
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


线程安全

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

简单来说:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

线程安全在三个方面体现:

  1. 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
  2. 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
  3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。


线程安全的级别

  • 1)不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

  • 2)绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

  • 3)相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast(快速失败)机制。

  • 4)线程非安全

ArrayList、LinkedList、HashMap等都是线程非安全的类,在多线程下操作往往因为并发导致输出容量少于实际容量,或者因为扩容的重复导致越界异常的现象。

Synchronized

线程Thread 类是一个单独的资源类,内含属性和方法,可直接使用。Thread 类是函数式接口,使用时将资源类放入线程即可。其构造方法中实现了Runnable接口,可以将Runnable当做资源类放入。

public class Test {

    public static void main(String[] args) {

        Thread t = new Thread(new Runnable() {
            public void run() {
            	System.out.println("多线程任务执行!");
            }
        });
        t.start();
        
		//Java8新特性 Lambda表达式
		new Thread(()->{System.out.println("多线程任务执行!");},"t").start();
     }
}

多线程中的并发:多线程操作同一个资源类。

一般是以下两种情况导致线程不安全:一是资源类中的数据共享(临界资源),二是多线程同时访问并改变数据。

传统的线程安全方式是用synchronized关键字,synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;
 
    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

//输出为:2000000,如果没有synchronized关键字,很可能输出小于2000000

注意

  • 使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。
  • 使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。
  • 使用使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的 synchronized方法。
  • 线程A访问实例对象的 static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

Java中的对象结构

要了解Synchronized的实现原理,要先介绍下对象头。

首先,我们要知道Java对象在内存中的布局:

已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。

  1. 对象头主要是由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)。
  2. 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐。
  3. 填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的。
    Java基础进阶——多线程与JUC(上)_第2张图片

Synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,Synchronized锁对象存在锁对象的对象头的MarkWord中。

在32位的虚拟机中MarkWord的结构:
Java基础进阶——多线程与JUC(上)_第3张图片
在64位的虚拟机中MarkWord的结构:
Java基础进阶——多线程与JUC(上)_第4张图片
图中的偏向锁和轻量级锁都是在java6以后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized关键字对应的是重量级锁,接下来对重量级锁在Hotspot JVM中的实现锁讲解。

Synchronized的实现原理

Synchronized重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在Hotspot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构如下:

ObjectMonitor() {
    _count        = 0; //用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  }

结合线程状态来说明一下Synchronized的实现原理。

  1. 当线程处于就绪状态,准备进入运行状态时,需要获取实例对象的监视器(monitor)锁,当获得了监视器后就可以进入运行状态,执行方法了。此时,ObjectMonitor对象指向了当前线程。
  2. 当线程进入等待、超时等待、阻塞状态,或者说线程运行完毕进入终止状态,则释放实例对象的监视器(monitor)锁。

在JVM规范里可以看到,不管是方法同步还是代码块同步都是基于进入和退出monitor对象来实现,然而二者在具体实现上又存在很大的区别。

  • Synchronized修饰代码块

Synchronized代码块同步在需要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。


  • Synchronized修饰方法

Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的。

如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。

简单来说就是有Synchronized关键字修饰的方法,在其方法表结构中设置了ACC_SYNCHRONIZED标志,用来标识需要调用指令获取监视器(monitor)锁。方法执行完毕之后就释放监视器(monitor)锁。


锁的优化


锁升级

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

  • 偏向锁

HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS(CompareAndSet 比较并交换)操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

偏向锁的取消:

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;

如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置。

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,就算线程执行完毕也不会释放锁,需要等待其他线程来竞争。

偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态。
  2. 撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态。


  • 轻量级锁(自旋锁)

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

  • 重量级锁

重量级锁通过对象内部的监视器(monitor)实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。重量级锁就是Synchronized锁。实现原理是mutex(互斥),任一时刻,只能有一个线程访问该对象。

注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

Java基础进阶——多线程与JUC(上)_第5张图片

锁升级的过程

  • 偏向锁升级为轻量级锁

当有线程A在执行时,B线程来竞争锁,CAS竞争锁失败,证明当前存在多线程竞争情况,将偏向锁升级为轻量锁。

  • 轻量级锁升级为重量级锁

当线程B竞争后已经升级为轻量锁的情况下,B线程不停的自旋等待锁释放,自旋次数超过定义次数,或者此时又有已升级为轻量锁的线程C同时来竞争,则升级为重量级锁(自旋超限、或轻量级锁中竞争线程较多),重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
Java基础进阶——多线程与JUC(上)_第6张图片

自适应自旋锁

JDK 1.6 引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 它怎么做呢?

线程如果自旋成功了,那么下次自旋的次数会更多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁粗化

锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。因此有了锁粗化。

锁消除

Java虚拟机在JIT编译时通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

为了提高热点代码的执行效率,在运行时会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,这就是即时编译。


Lock锁

Lock 是JDK1.5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。


Lock接口

Lock接口处于java.util.concurrent.locks包中,属于JUC范围内的一个接口。使用lock锁操作多线程,比synchronized 拥有更广泛的锁定操作,并且可以监控锁的状态,操作更加灵活。

它的实现类有ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock。我们常用的是可重入锁ReentrantLock

介绍可重入锁之前,需要知道几个概念:

  • 可重入锁。如果锁具备可重入性,则称作为可重入锁。可重入性是基于锁的分配机制实现的。当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。synchronized和Lock都具备可重入性
  • 可中断锁,就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
  • 互斥锁,任一时刻,只能有一个线程获得锁。
  • 共享锁,可以有多个线程同时获得锁。
  • 死锁,两个以上的线程在执行过程中,由于资源竞争而造成的阻塞现象,谁也无法成功获得自己所需的资源,形成的一种死循环的现象。
  • 公平锁,尽量以请求锁的顺序来获取锁,即按照FIFO先进先出的顺序进行操作。
  • 非公平锁,意味着可以插队,不一定按某种顺序进行。可重入锁ReentrantLock默认的类型是非公平锁。

非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

使用Lock锁操作:

//使用Lock锁操作: 1、获取锁对象Lock lock = new Lock();
//2、上锁 lock.lock();  3、try\catch代码块中执行真正的操作
//4、finally 中释放锁 lock.unlock();
public class MyFairLock {
    /**
     *     true 表示 ReentrantLock 的公平锁
     * 	   false 表示非公平锁,默认为false
     */
    private  ReentrantLock lock = new ReentrantLock(true);

	lock.lock();
    public void lockPrint(){
        try {
        	System.out.println(Thread.currentThread().getName() +"获得了锁");
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        MyFairLock fairLock = new MyFairLock();
        Thread[] threadArray = new Thread[10];
        for (int i=0; i<10; i++) {
            threadArray[i] = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"已启动");
            fairLock.lockPrint();
        },i).start();
        }
    }
}


Condition类

任意一个Java对象,都拥有一组监视器方法(定义在Object类中),Condition类是线程中的监视器类,Condition接口也提供了类似的Object的监视器方法,与Lock配合可以线程的精准唤醒/等待。它也是java.util.concurrent.locks包下的。

一个Condition类就对应了一个监视器。使用Lock类与Condition类可以实现指定唤醒,而Synchronized使用notify/notifyAll只能随机唤醒。

下面使用Condition类实现一个按顺序操作的线程:

//使用Condition类控制线程唤醒的顺序
public class ConditionTest {

    public static void main(String[] args){
        ThreadTest threadTest = new ThreadTest();
        new Thread(()->{
            for(int i = 0; i <5; i++){
                threadTest.printA();
            }
        },"A").start();;
        new Thread(()->{
            for(int i = 0; i <5; i++){
                threadTest.printB();
            }
        },"B").start();;
        new Thread(()->{
            for(int i = 0; i <5; i++){
                threadTest.printC();
            }
        },"C").start();;
    }
}

class ThreadTest{

    private ReentrantLock lock = new ReentrantLock();
    private Condition condA = lock.newCondition();
    private Condition condB = lock.newCondition();
    private Condition condC = lock.newCondition();
    private int count = 0;//用来标记唤醒哪个线程 0:A 1:B 2:C

    public void printA(){
        lock.lock();
        try {
            while(count != 0){
                condA.await();
            }
            System.out.println(Thread.currentThread().getName() + "-printA");
            //将标记置为1 唤醒B
            count = 1;
            condB.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    public void printB(){
        lock.lock();
        try {
            while(count != 1){
                condB.await();
            }
            System.out.println(Thread.currentThread().getName() + "-printB");
            //将标记置为2 唤醒C
            count = 2;
            condC.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    public void printC(){
        lock.lock();
        try {
            while(count != 2){
                condC.await();
            }
            System.out.println(Thread.currentThread().getName() + "-printC");
            //将标记置为0 唤醒A
            count = 0;
            condA.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}
//输出: A   B    C    A   B   C ...


Lock中的方法

  • lock()与unlock()

lock()方法是平常使用得最多的一个方法,就是用来获取锁。Lock锁不会自动释放锁,需要使用unlock()方法释放锁。一般lock()与unlock()都是成对出现的,出现的次数也必需相同,否则就会进入死锁状态。

  • tryLock() & tryLock(long time, TimeUnit unit)

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

  • lockInterruptibly()

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。


  • newCondition()

返回绑定到此 Lock 实例的新 Condition 实例。


  • ReentrantLock#isLocked()

查询此锁是否由任意线程保持。用于判断是否已上锁。


Synchronized与Lock的区别

  1. synchronized是Java内置的关键字;Lock是Java中的一个类。
  2. synchronized在线程操作完毕后由系统判断自行释放锁;Lock必须手动释放锁,lock()与unlock()都是成对出现的,出现的次数也必需相同,否则就会进入死锁状态。
  3. synchronized无法获取锁状态;Lock可判断是否获得了锁,更加灵活。
  4. synchronized不是可中断锁,会一直等待获取锁;Lock是可中断锁,可用trylock方法进行判断并中断等待,进行其他操作。
  5. synchronized是可重入锁,不可中断,是非公平锁;Lock是可重入锁,可中断锁,公平与非公平可自行设置。
  6. synchronized适合锁少量的同步代码,随机唤醒;Lock适合锁大量的同步代码,可指定唤醒,保证执行顺序。

你可能感兴趣的:(面试,Java基础,并发与多线程,java,多线程,面试)