JUC并发编程(1):Java多线程编程总结与补充

267 阅读20分钟

第一节:Java多线程编程总结与补充

目录

image.png

1、JUC概述

Java5.0 提供了java.util.concurrent包,简称 JUC 包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection实现等。

查看这个工具包,里面就含有许多接口和类

image.png

其实,JUC指的就是java.util包下的三个工具类:

  1. java.util.concurrent
  2. java.util.concurrent.atomic
  3. java.util.concurrent.locks

image.png

2、进程、线程和管程概念

进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以包含多个线程,每条线程并行执行不同的任务。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

管程:Monitor(监视器),也就是我们平时所说的锁,(Monitor其实是一种同步机制,它的义务是保证(在同一时间)只有一个线程可以访问被保护的数据和代码)。JVM中同步时基于进入和退出的监视器对象(Monitor,管程),每个对象实例都有一个Monitor对象。Monitor对象和JVM对象一起销毁,底层由C来实现。执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程。

总结来说:

  • 进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
  • 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

Java真的可以开启线程吗:开不了

我们查看start方法源码

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

	//本地方法,底层为C++
    private native void start0();

    /**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

3、线程的状态

3.1、线程的状态模型

JDK中用Thread.State枚举类定义了线程的这几种状态

public enum St就绪状态、可运行状态/ate {
    NEW, //初始状态、开始状态
    RUNNABLE, //就绪状态、可运行状态。
    BLOCKED, //阻塞状态
    WAITING, //等待状态
    TIMED_WAITING, //超时等待状态
    TERMINATED; //终止状态、结束状态
}

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建:当一个Thread类或其 子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪:处于新建状态的线程被star()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定 义了线程的操作和功能
  • 阻塞
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

线程的声明周期图如下:

image.png

3.2、wait和sleep的区别

功能都是阻塞当前线程,有什么区别?

  • sleep是Thread类中的静态方法,wait是Object类中的方法,任何对象实例都能调用,且必须在同步代码块中使用,不需要捕获异常
  • sleep不会释放锁,它也不需要占用锁,可以在任何地方使用,必须要捕获异常。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
  • wait方法可以让当前线程进入等待状态,当别的其他线程调用notify()或者notifyAll()方法时,当前线程进入就绪状态。wait方法必须在同步上下文中调用。也就是说,如果想要调用wait方法,前提是必须获取对象上的锁资源。当wait方法调用时,当前线程会释放已获取的对象锁资源,并进入等待队列,其他线程就可以尝试获取对象上的锁资源。
  • Sleep方法让当前线程休眠指定时间。休眠时间的准确性依赖于系统时钟和CPU调度机制。不释放以获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入当前同步快或者同步方法中的。可通过interrupt()方法来唤醒休眠线程。
  • 它们都可以被interrupted方法中断。

具体来说:

Thread.Sleep(1000) 意思是在未来的1000毫秒内该线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep()的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。

image.png

其实,真是开发中并不会去使用sleep方法,而是使用TimeUnitTimeUnitjava.util.concurrent包下面的一个类,TimeUnit提供了可读性更好的线程暂停操作,通常用来替换Thread.sleep( ),但是底层实现还是使用的Thread.sleep( )

字段描述
SECONDS停顿3秒
MINUTES停顿3分钟
HOURS停顿3小时
DAYS停顿三天

代码使用

//停顿3s
try { 
    TimeUnit.SECONDS.sleep(3);  
} catch (InterruptedException e) {
    e.printStackTrace();
}
//停顿3分钟
try { 
    TimeUnit.MINUTES.sleep(3);  
} catch (InterruptedException e) {
    e.printStackTrace();
}
//停顿3h
try { 
    TimeUnit.HOURS.sleep(3);  
} catch (InterruptedException e) {
    e.printStackTrace();
}
//停顿三天
try { 
    TimeUnit.DAYS.sleep(3);  
} catch (InterruptedException e) {
    e.printStackTrace();
}

4、多线程的并行和并发

4.1、串行模式

串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。串行是一次只能取得一个任务,并执行这个任务。

4.2、并行模式

并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU

System.out.println(Runtime.getRuntime().availableProcessors());//获取cpu核数

4.3、并发

并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象, 并发描述的是多进程同时运行的现象。

但实际上,对于单核心 CPU 来说,同一时刻只能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会。

要解决大并发问题,通常是将大任务分解成多个小任务,由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。

这可能会出现一些现象:

可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果。

  • 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用异步的方式,比如只有准备好产生了事件通知才执行某个任务。
  • 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率。

并发编程的本质:充分利用CPU的资源

5、用户线程和守护线程

  • 用户线程:平时用到的普通线程,自定义线程
  • 守护线程:运行在后台,是一种特殊的线程,比如垃圾回收线程,当主线程结束后,用户线程还在运行,JVM 就会一直存活,如果没有用户线程,都是守护线程,JVM 结束,进而守护线程也会退出

结论:

  • Java程序默认有两个线程,即main线程(用户线程)和GC垃圾回收线程(守护线程)
  • 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,只要任何非守护线程还在运行,守护线程就不会终止
  • 用户线程是独立存在的,不会因为其他用户线程退出而退出。
  • 如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。
  • main方法主线程执行完毕退出后,用户线程则不会消亡,用户线程会一直运行直到其运行完毕。
  • 默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置成守护线程,这个函数务必在线程启动前进行调用,否则会报java.lang.IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。

演示1:

public class Main {
    public static void main(String[] args) {
        //新建一个子线程(用户线程),并取名为aa
        Thread thread = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread() .isDaemon()); 
            //isDaemon( )方法表示这个线程是用户线程还是守护线程,
            //如果是true: 则为守护线程,反之为用户线程
            while (true){
                //让其进入一个死循环,让用户线程一直执行下去
            }
        },"aa");
        thread.start(); //调用thread的start方法,启动子线程
        System.out.println(Thread.currentThread().getName() + "over");
    }
}

打印结果,证明主线程虽然结束了,但是创建的这个用户线程还在运行,则jvm一直存活

mainover //主线程结束了
aa::false //用户线程还是在运行

演示2:

public class Main {
    public static void main(String[] args) {
        //新建一个子线程(用户线程),并取名为aa
        Thread thread = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread() .isDaemon()); 
            while (true){
                //让其进入一个死循环
            }
        },"aa");       
        thread.setDaemon(true);//将用户线程设置成守护线程
        thread.start(); //调用thread的start方法,启动子线程
        System.out.println(Thread.currentThread().getName() + "over");
    }
}

打印结果:证明主线程结束了,并且已经没有用户线程了,都是守护线程,JVM 结束

mainover
aa::true

6、实现多线程的4种方式

  1. 继承Thread类
  2. 实现Runnable接口:Runnable没有返回值、效率相比于Callable相对较低!
  3. 实现Callable接口:该接口就是属于java.util.concurrent包下的 image.png
  4. 线程池

6、传统 Synchronized 锁

6.1、概述

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

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}。括起来的代码,作用的对象是调用这个代码块的对象;

  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

    • 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字修饰的方法不能被继承。
    • 如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。
    • 当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
  • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

    @Override
    public void run() {
        while(true){
            sale(); //调用同步静态方法
        }
    }
    
    private static synchronized void sale() {//同步监视器:window.class,所以必须加static
        //private synchronized void sale() 同步监视器:t1.t2.t3,错误的方式
        if(ticket>0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //Thread.currentThread()必须加上,因为是静态方法,所以没有对象,只有类去调用
            System.out.println(Thread.currentThread().getName()+",卖票,票号为: "+ticket);
            ticket--;
        }
    }
    
  • 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。

6.2、售票案例

这一部分已经在Java基础的多线程编程中讲过,这里我们使用实现Runnable接口的方式进行,并且直接使用匿名子类的方式进行操作

按照下面格式:线程就是单独的一个资源类

//第一步  创建资源类,定义属性和和操作方法
class Ticket {
    
    //30张票,这里不需要static修饰,
    //因为全局只有一个Ticket对象,所以里面的number自动就只有一份
    private int number = 30; 
    
    //操作方法:卖票
    public synchronized void sale() {
        //判断:是否有票
        if(number > 0) {
            System.out.println(Thread.currentThread().getName()+" : 卖出:"+(number--)+" 剩下:"+number);
        }
    }
}

public class SaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket ticket = new Ticket(); //因为静态方法里面不能直接调用非静态的方法,必须使用对象调用
        //创建三个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();//调用同步方法
                }
            }
        },"AA").start();

        new Thread(new Runnable() { //函数式接口
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"BB").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"CC").start();
    }
}

结果

AA : 卖出:30 剩下:29
AA : 卖出:29 剩下:28
AA : 卖出:28 剩下:27
AA : 卖出:27 剩下:26
AA : 卖出:26 剩下:25
AA : 卖出:25 剩下:24
AA : 卖出:24 剩下:23
AA : 卖出:23 剩下:22

结论:

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

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  • 线程执行发生异常,此时 JVM 会让线程自动释放锁。
  • 线程调用wait方法,当前线程阻塞,并且会释放锁

7、Lock接口--解决线程安全问题

7.1、背景

在前面的使用 synchronized同步机制解决线程安全问题的时候,其实是有缺陷的,即如果这个获取锁的线程由于要等待 IO或者其他原因(比如调用 sleep方法)被阻塞很长时间了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。因为不管程序执行过程当中,就算获取到锁的线程由于要等待 IO或者其他原因(比如调用 sleep方法)被阻塞很长时间,最终都会在finally 块中进行释放锁的操作,从而提升效率

7.2、什么是Lock锁

从JDK 5.0开始,Java提供了更强大的线程同步机制一通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

image.png

ReentrantLock实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁。

7.3、Lock接口的常用方法

①、Lock 接口

public interface Lock {    
    //获取锁,如果锁被暂用则一直等待
    void lock();   
    //通过这种方式获取锁,会中断其他线程的锁等待。 
    void lockInterruptibly() throws InterruptedException;    
    //注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
    boolean tryLock();   
    //比起tryLock()就是给了一个时间期限,保证等待参数时间
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    
    //释放锁
    void unlock();    
    Condition newCondition();
}

下面来逐个讲述 Lock 接口中每个方法的使用,意味着Lock的实现类必须实现上述方法,直接通过实现类名.方法即可访问。

②、lock方法

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。采用 Lock,必须主动去释放锁,并且在代码发生异常时(比如Thread.sleep(100); ),不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{} 块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;  //实例化Lock,但是一般是使用其子类ReentrantLock 
lock.lock(); //加锁
//lock.tryLock(),验证一下是否加上了锁
try{
    //处理任务
}catch(Exception ex){
    e.printStackTrace();
}finally{
    lock.unlock(); //释放锁      
}

7.4、Lock锁实现窗口卖票

//第一步  创建资源类,定义属性和和操作方法
class LTicket {
    //票数量
    private int number = 30;

    //创建可重入锁,如果构造器传入true就是一个公平的锁,即先进先出的特点,不写默认是false
    private final ReentrantLock lock = new ReentrantLock(true);
    //卖票方法
    public void sale() {
        //上锁
        lock.lock();
        try {
            //判断是否有票
            if(number > 0) {
                System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
            }
        } finally {
            //解锁,不管获取锁的线程是否发生异常阻塞,最终一定会释放锁
            lock.unlock();
        }
    }
}

public class LSaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    //创建三个线程
    public static void main(String[] args) {

        LTicket ticket = new LTicket();

        new Thread(()-> { //函数式接口,lambda表达式
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"AA").start();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(()-> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        },"CC").start();
    }
}

ReentrantLock:可重入锁,如果构造器传入true就是一个公平的锁,即先进先出的特点(先来的线程就先抢到锁执行),不写默认是false

结果就是:当每一个线程进来操作票时,就加锁了,等到它执行完毕,他才会释放锁,无论中途是否发生异常,最终都会是释放锁,下一个线程才会进来继续操作

7.5、synchronized 和 lock的区别?

相同:二者都可以解决线程安全问题

不同

  • lock是一个接口,通过这个接口可以实现同步访问;而synchronized是java的一个关键字,synchronized是内置的语言实现。
  • lock可以使用interrupt方法或者设置超时时间tryLock(long timeout,TimeUnit unit)来中断一个等待锁的线程,而synchronized只能等待锁的释放,不能响应中断,除非抛出异常或者正常运行完成。【interrupt()方法可以打断一个在sleep的线程,使得返回到初始状态】
  • lock可以通过trylock方法来知道有没有获取锁,而synchronized不能。
  • Lock可以提高多个线程进行读操作的效率,可以通过readwritelock实现读写分离。
  • Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用,因此不会出现死锁;而 Lock 则必须要用户去手动开启锁和unlock来释放锁,如果没有主动释放锁,就有可能导致出现死锁现象,因此lock的unlock方法一般都是必须放在finally里面的。
  • synchronized 适合锁少量的代码同步问题,Lock锁适合锁大量的同步代码
  • synchronized 可能会让阻塞线程一直等待,Lock锁就不会,最终肯定会释放锁。

synchronized与Lock的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性( 提供更多的子类)
  4. lock是api层面的锁,主要使用ReentrantLock实现
  5. synchronized是重入锁,不可以中断的,非公平锁,ReentrantLock两者都可以,默认是非公平锁,构造方法可以传入boolean值,true为公平锁
  6. ReentrantLock用实现分组唤醒来唤醒多个线程(等待队列),可以精确唤醒,而不是像synchronized要随机唤醒一个,要么多个

优先使用顺序:

Lock----→同步代码块(已经进入了方法体,分配了相应资源)----→同步方法(在方法体之外)

8、Lock锁的Condition接口

参考链接自www.cnblogs.com/awkflf11/p/…

8.1、Condition介绍

Lock接口源码:

public interface Lock {    
    //获取锁,如果锁被暂用则一直等待
    void lock();   
    //通过这种方式获取锁,会中断其他线程的锁等待。 
    void lockInterruptibly() throws InterruptedException;    
    //注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
    boolean tryLock();   
    //比起tryLock()就是给了一个时间期限,保证等待参数时间
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    
    //释放锁
    void unlock();    
    Condition newCondition();
}

可以看到最后有一个方法newCondition(),并返回一个Condition对象

在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活。 下图是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):

image.png

Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  6. signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  7. signalAll():唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

Condition是一种广义上的条件队列(等待队列)。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作(在队列中等待),直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

8.2、Condtion的实现

获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:

public class ConditionObject implements Condition, java.io.Serializable {
}

8.3、等待队列

每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。我们看Condition的源码就明白了:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;

    //头节点
    private transient Node firstWaiter;
    //尾节点
    private transient Node lastWaiter;

    public ConditionObject() {
    }

    /** 省略方法 **/
}

等待

调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。

通知

调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。

8.4、总结

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()signal()这种方式实现线程间协作更加安全和高效。简单说,他的作用是使得某些线程一起等待某个条件(Condition),只有当该**条件具备(signal 或者 signalAll方法被调用)**时,这些等待线程才会被唤醒,从而重新争夺锁。

单个 Lock 可能new多个 Condition 对象关联, 进而可以控制唤醒哪个线程。相比synchronized同步方法的notifyAll,多了多个等待队列,notifyAll所有的线程都会唤醒,notify只能唤醒一个线程,有可能生产者线程唤醒的是生产者线程。对于condition来说,我们可以实现精确唤醒,比如在生产者线程中准确唤醒某个消费者线程

使用

一个Lock实例可以绑定多个Condition,所以自然可以支持多个等待队列Condition是个接口,基本的方法就是await()signal()方法;Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition(),调用Conditionawait()signal()方法,都必须在lock保护之内,就是说必须在lock.lock()lock.unlock之间才可以使用。

- Conditon中的await()对应Object的wait();
- Condition中的signal()对应Object的notify();
- Condition中的signalAll()对应Object的notifyAll()

9、线程通信

9.1、概述

线程间通信的模型有两种:共享内存和消息传递,可以参考操作系统部分

线程间的通信具体步骤:

  1. 创建资源类,在资源类中创建属性和操作方法
  2. 在资源类操作的同步方法:判断、操作、通知
  3. 创建多个线程,调用资源类的同步方法
  4. 防止虚拟唤醒问题

9.2、synchronized实现线程通信案例--生产者/消费者模型

实例:使用两个线程对0这个值操作,一个线程加1,一个线程减1,交替实现多次,类似生产者消费者模型

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为,消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为

image.png

  • 操作线程的时候,等待线程使用wait()
  • 通知另外的线程操作用notify()、notifyAll()
//第一步 创建资源类,定义属性和操作方法
class Share {
    
    //初始值
    private int number = 0;
    
    //+1的方法
    public synchronized void incr() throws InterruptedException {
        //第二步 判断 干活 通知
        if(number != 0) { //判断number值是否是0,如果不是0,等待
            this.wait(); //阻塞当前线程,并释放锁,在哪里睡,就在哪里醒
        }
        //如果number值是0,就+1操作
        number++;
        System.out.println(Thread.currentThread().getName()+" :: "+number);
        //唤醒其他处于阻塞状态的线程
        this.notifyAll();
    }

    //-1的方法
    public synchronized void decr() throws InterruptedException {
        //判断
        if(number != 1) {
            this.wait();
        }
        //干活
        number--;
        System.out.println(Thread.currentThread().getName()+" :: "+number);
        //唤醒其他处于阻塞状态的线程
        this.notifyAll();
    }
}

public class ThreadDemo1 {
    //第三步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        Share share = new Share();
        //创建线程
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr(); //+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr(); //-1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();
    }
}

结果:

AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1

涉及到的三个方法:

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  • notify():旦执行此方法,就会唤醒被wait的一个线程。 如果有多个线程被wait,就唤醒优先级高的线程
  • notifyAll():一且执行此方法,就会唤醒所有被wait的线程。

注意:

  1. wait(), notify(), notifyAll 三个方法必须使用在同步代码块或同步方法中。
  2. 多个线程要保证是同一把锁
  3. wait(), notify(), notifyAll三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
  4. wait(), notify(), notifyALl() 三个方法是定义在java. lang. object类中。

过程总结

  • 第一次循环:numer初始值是0,假设线程AA抢到锁,线程AA首先执行加1操作,判断出numer是0,就执行加1操作,然后执行noyifyAll方法唤醒线程BB(其实刚开始也没有哪个线程处于阻塞状态),最后线程AA执行完加1 的同步方法,就释放锁。此时number=1
  • 然后线程BB抢到锁,判断出numer是1,就执行减1操作,然后执行noyifyAll方法唤醒线程AA,最后线程BB执行完加1的同步方法,就释放锁。此时number=0
  • 然后就是第二次循环,假设此时线程BB又抢到锁(因为没有线程处于阻塞状态,都在抢锁),于是执行减1操作,判断出numer不是1,于是就执行wait方法阻塞,并释放锁。
  • 此时线程AA抢到锁,执行加1操作,判断出numer是0,就执行加1操作,然后执行noyifyAll方法唤醒被阻塞的线程BB,此时number=1,最后线程AA执行完加1 的同步方法,就释放锁
  • 由于wait方法是哪里睡就哪里醒,所以线程BB被唤醒后,不会重新判断if的条件语句,会一直往下执行,于是减1 ,此时number=0
  • 最后线程BB也执行完减1 的同步方法释放锁,转而进入第三轮循环。
  • 这样就保证了线程安全问题。

9.3、虚拟唤醒问题

其实这个在Java多线程编程的时候提过一下,如下:

image.png

这里为了给上面的8.2的案例演示出虚拟唤醒的案例,这里添加额外两个线程,且操作要依次执行

new Thread(()->{
    for (int i = 1; i <=10; i++) {
        try {
            share.incr(); //+1
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"CC").start();

new Thread(()->{
    for (int i = 1; i <=10; i++) {
        try {
            share.decr(); //-1
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"DD").start();

结果:

AA :: 1
BB :: 0
CC :: 1
BB :: 0
AA :: 1
CC :: 2
AA :: 3
CC :: 4
AA :: 5
CC :: 6

主要是虚拟唤醒导致:线程可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒,换句话说,wait应该总是出现在循环中。如果一个线程因为某些原因被唤醒后,if结构的判断语句就不会判断了,就一直往下执行,所以需要将if换成while结构,每次被唤醒的线程都要重新进行number的判断。因为wait在哪里睡眠就在哪里被唤醒,结果被某个异常唤醒了后回不去了,if结构不会在判断了,需要更改为while

即出现虚假唤醒的原因是从阻塞态到就绪态再到运行态没有进行判断,我们只需要让其每次得到操作权时都进行判断就可以了

while(number != 0) { //判断number值是否是0,如果不是0,等待
    this.wait(); //在哪里睡,就在哪里醒
}
while(number != 1) { //判断number值是否是0,如果不是0,等待
    this.wait(); //在哪里睡,就在哪里醒
}

实现中断和虚假唤醒是可能的,需要将其while方法用在循环中,最后的结果:

AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
AA :: 1
BB :: 0
CC :: 1
DD :: 0
CC :: 1
DD :: 0
CC :: 1
DD :: 0
CC :: 1

注意:虚拟唤醒问题只是针对如上的编码方式产生出来的,如果换一种方式,使用if不会造成这样的问题,所以具体问题具体分析

9.4、Lock锁-ReentrantLock实现上述案例

使用 lock 先要创建锁的对象以及将通知的对象放置在资源类中

//创建Lock
private Lock lock = new ReentrantLock();//造实现类的对象
private Condition condition = lock.newCondition();//调用重写的newCondition方法,实现精确唤醒和阻塞
  • 上锁lock.lock();
  • 解锁lock.unlock();
  • 以下都为condition类的唤醒和阻塞线程方法
    • 唤醒所有等待的线程:signalAll(),通过类名调用,condition.signalAll();
    • 唤醒一个等待线程signal(),通过类名调用,condition.signal();
    • 造成当前线程在接到信号或者被中断之前一直处于等待状态await(),通过类名调用,condition.await();
//第一步 创建资源类,定义属性和操作方法
class Share {
    private int number = 0;

    //创建Lock
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1
    public void incr() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while (number != 0) {
                condition.await();
            }
            //干活
            number++;
            System.out.println(Thread.currentThread().getName()+" :: "+number);
            //通知
            condition.signalAll();
        }finally {
            //解锁
            lock.unlock();
        }
    }

    //-1
    public void decr() throws InterruptedException {
        lock.lock();
        try {
            while(number != 1) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+" :: "+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo2 {

    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"DD").start();
    }

}

实现的结果是一样的。

10、线程间定制化通信:Condition实现精准唤醒

所谓定制化通信,需要让线程进行一定的顺序操作

案列

启动三个线程,按照如下要求:AA打印5此,BB打印10次,CC打印15次,一共进行10轮

具体思路

每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程,并释放锁,让下一个线程进行打印

//创建一个可重入锁  
private Lock lock = new ReentrantLock();
//分别创建三个开锁通知,一个Lock实例可以绑定多个Condition,所以自然可以支持多个等待队列  
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();

image.png

代码实现

//第一步 创建资源类
class ShareResource {
    
    //定义标志位
    private int flag = 1;  // 1 AA     2 BB     3 CC

    //创建Lock锁
    private Lock lock = new ReentrantLock();

    //创建三个condition
    //一个Lock实例可以绑定多个Condition,所以自然可以支持多个等待队列
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //打印5次,参数第几轮
    public void print5(int loop) throws InterruptedException {
        //上锁
        lock.lock();
        try {
            //判断
            while(flag != 1) {
                //等待
                c1.await();
            }
            //干活
            for (int i = 1; i <=5; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //打印完了就通知另外一个线程进行打印
            flag = 2; //修改标志位 2
            c2.signal(); //唤醒BB线程
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    //打印10次,参数第几轮
    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while(flag != 2) {
                c2.await();
            }
            for (int i = 1; i <=10; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //修改标志位
            flag = 3;
            //通知CC线程
            c3.signal();
        }finally {
            lock.unlock();
        }
    }

    //打印15次,参数第几轮
    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while(flag != 3) {
                c3.await();
            }
            for (int i = 1; i <=15; i++) {
                System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
            }
            //修改标志位
            flag = 1;
            //通知AA线程
            c1.signal();
        }finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print5(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print10(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 1; i <=10; i++) {
                try {
                    shareResource.print15(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();
    }
}

结果

AA :: 1 :轮数:1
AA :: 2 :轮数:1
AA :: 3 :轮数:1
AA :: 4 :轮数:1
AA :: 5 :轮数:1
BB :: 1 :轮数:1
BB :: 2 :轮数:1
BB :: 3 :轮数:1
BB :: 4 :轮数:1
BB :: 5 :轮数:1
BB :: 6 :轮数:1
BB :: 7 :轮数:1
BB :: 8 :轮数:1
BB :: 9 :轮数:1
BB :: 10 :轮数:1
CC :: 1 :轮数:1
CC :: 2 :轮数:1
CC :: 3 :轮数:1
CC :: 4 :轮数:1
...