java多线程知识合集

java多线程知识合集

原文:https://blog.csdn.net/boker_han/article/details/82918881

 https://blog.csdn.net/boker_han/article/details/79466524

线程的生命周期

线程是一个动态执行的过程,它也有一个从生产到死亡的过程。

下图显示了一个线程完整的生命周期。

在这里插入图片描述

新建状态:

使用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()这个线程

就绪状态:

当线程对象调用了start方法之后,该线程就进入就绪状态,就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

运行状态:

如果就绪状态的线程获取CPU资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态

阻塞状态:

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占有资源之后,该线程就从运行状态进入阻塞状态。睡眠时间已到或获得设备资源后可以重新进入就绪转态。可分为三种:

  • 等待阻塞:运行状态中的线程执行wait()方法,使线程进入等待阻塞状态。
  • 同步阻塞:线程在获取synchronized同步锁失败(因为同步锁线程占用)。
  • 其他阻塞:通过调用线程的sleep()获取join()发出了I/O请求时,线程就会进入到阻塞状态,当sleep状态超时,join()等待线程终止或超时,或者I/O处理完毕,线程重新转入就绪状态。

死亡状态:

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态

1、多线程的使用场景

  • 程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行
  • 程序中出现可分解的大任务,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间
  • 程序中出现需要后台运行的任务,比如一些监测任务、定时任务,可以利用多线程来完成

2、自定义线程的实现

jdk提供了三种自定义线程的方式:

1、继承Thread类
//通过继承Thread类实现自定义线程类
public class ThreadDemo extends Thread{
    //线程体
    @Override
    public void run() {
        System.out.println("重写Thread类的run方法创建多线程");
    }
    public static void main(String[] args) {
        System.out.println("当前正在执行的线程:"+Thread.currentThread());
        //实例化自定义线程类实例
        Thread thread = new ThreadDemo();
        //调用start()实例方法启动线程
        thread.start();
        System.out.println("继承Thread类创建的线程" + thread.getName());
    }
}

优点:实现简单,只需实例化继承类的实例,即可使用线程

缺点:可扩展性低,java是单继承的语言,如果一个类已经继承了其他类,就无法通过这种方式实现自定义线程

2、实现Runnable接口
//通过实现Runnable接口,实现自定义线程类
public class RunnableDemo implements Runnable {
    //线程体
    @Override
    public void run() {
        System.out.println("实现Runnable接口并重写其run方法来创建多线程");
    }
    public static void main(String[] args) {
        System.out.println("当前正在执行的线程:"+Thread.currentThread());
        //线程的执行目标对象
        Runnable runnable = new RunnableDemo();
        //实际的线程对象
        Thread thread = new Thread(runnable);
        //设置线程的名称
        thread.setName("Thread - runnable");
        //启动一个线程
        thread.start();
        System.out.println("继承Thread类创建的线程名:" + thread.getName());
    }
}

优点:

  • 扩展性好,可以在此基础上基础其他类,实现其他必需的功能
  • 对于多线程共享资源的场景,具有天然的支持,适用于多线程处理一份资源的场景

缺点:构造线程实例的过程相对繁琐一点

3、实现Callable接口
//通过实现Callable接口来,实现自定义线程类
public class CallableDemo implements Callable<Integer> {
    //线程体
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i=0;i<=10;i++){
            sum = sum + i;
        }
        return sum;
    }
    public static void main(String[] args) {
        //线程执行目标
        Callable callable = new CallableDemo();
        //包装线程执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //传入线程执行目标,实例化线程对象
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();
        Integer result = null;
        try {
            //获取线程执行结果
            result = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("线程的实现结果:"+result);
    }
}

优点:

  • 扩展性好
  • 支持多线程处理同一份资源
  • 具有返回值以及可抛出受检查异常

缺点:

  • 相对于实现Runnable接口的方式,较为繁琐
4、小结

我们对这三种方式进行分析,可以发现:方式一和方式二本质上都是通过实现Runnable接口并重写run()方法,将接口实现类的实例传递给Thread线程类来执行线程体(run()方法中的实现),这里将Runnable接口实现类的实例作为线程执行目标,供线程Thread实例执行;所以需要对Callable接口的实现类进行包装,包装成Runnable接口的实现类(通过实现Runnable接口的FutureTask;类进行包装),从而使得Thread类能够接收Callable接口实现类的实例,可见这里使用了适配器模式!

综上所述,三种实现方式都存在着一个使用范式,即首先实现线程执行目标对象(包含线程所需执行的任务),然后将目标对象作为构造参数以实例化Thread实例,来获得线程!本质上都是实现一个线程体,由Thread来执行线程体,达到开启线程执行任务的效果!但是三种方式各有优缺点,使用时,应该结合具体需求来选用合适的实现方式进行开发!

Thread类中给出的关于线程状态的说明:

/**
     * 线程生命周期中的的六种状态
     * NEW:还没有调用start()的线程实例所处的状态
     * RUNNABLE:正在虚拟机中执行的线程所处的状态
     * BLOCKED:等待在监视器锁上的线程所处的状态
     * WAITING:等待其它线程执行特定操作的线程所处的状态
     * TIMED_WAITING:等待其它线程执行超时操作的线程所处的状态
     * TERMINATED:退出的线程所处的状态
     * 给定时间点,一个线程只会处于以下状态中的一个,这些状态仅仅是虚拟机层面的线程状态,并不能反映任何操作系统中线程的状态
     */
    public enum State {
        //还没有调用start()开启的线程实例所处的状态
        NEW, 
        //正在虚拟机中执行或者等待被执行的线程所处的状态,但是这种状态也包含线程正在等待处理器资源这种情况
        RUNNABLE,
        // 等待在监视器锁上的线程所处的状态,比如进入synchronized同步代码块或同步方法失败
        BLOCKED,
        // 等待其它线程执行特定操作的线程所处的状态;比如线程执行了以下方法: Object.wait with no timeout、Thread.join with no timeout、 LockSupport.park
        WAITING,
       // 等待其它线程执行超时操作的线程所处的状态;比如线程执行了以下方法: Thread.sleep、Object.wait with timeout
       //Thread.join with timeout、LockSupport.parkNanos、LockSupport.parkUntil
        TIMED_WAITING,
        //退出的线程所处的状态
        TERMINATED;
    }
  • 新建(New):当线程实例被new出来之后,调用start()方法之前,线程实例处于新建状态
  • 可运行(Runnable):当线程实例调用start()方法之后,线程调度器分配处理器资源之前,线程实例处于可运行状态或者线程调度器分配处理器资源给线程之后,线程实例处于运行中状态,这两种情况都属于可运行状态
  • 等待(Waitting):当线程处于运行状态时,线程执行了obj.wait()或Thread.join()方法、Thread.join、LockSupport.park以及Thread.sleep()时,线程处于等待状态
  • 超时等待(Timed Waitting):当线程处于运行状态时,线程执行了obj.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil以及Thread.sleep(long)方法时,线程处于超时等待状态
  • 阻塞(Blocked):当线程处于运行状态时,获取锁失败,线程实例进入等待队列,同时状态变为阻塞
  • 终止(Terminated):当线程执行完毕或出现异常提前结束时,线程进入终止状态

线程的状态转换

上面也提到了,某一个时间点线程的状态只能是上述6个状态其中的一个;但是,线程在程序运行过程中的状态是会发生变化的,由一个状态变为另一个状态,那么下面给出线程状态转换图帮助我们清晰地理解线程的状态转变过程:

在这里插入图片描述

上面我们已经对线程的实现以及线程的状态有了较为清晰的认识,那么通过上述内容,我们也可以发现其实有很多方法,我们并没有详细地介绍,比如start()、yield()、wait()、notify()、notifyAll()、sleep()、join()等等,这些方法大多来源于JDK中Thread类这一关键的线程类中,下面结合Thread类的源码看一下,多线程编程中经常遇到的方法有哪些,以及这些方法的用途;

线程类Thread源码

实例同步方法:join()

注意:join()会让主线程等待它执行完毕,才会继续往下执行

 /**
     * 等待调用此方法的线程执行结束
     * @throws  InterruptedException 如果任何线程中断了当前线程,将会抛出此异常,同时将中断标志位清除
     */
    public final void join() throws InterruptedException {
        join(0);
    }
    /**
     * 最多等待millis毫秒,时间一到无论是否执行完毕,都会返回
     * 如果millis为0,那么意味着一直等到线程执行完毕才会返回
     * 此方法的实现是基于循环检测当前线程是否存活来判断是否调用当前实例的wait方法来实现的
     * @param  millis 等待时间
     * @throws  IllegalArgumentException 非法参数异常
     * @throws  InterruptedException 如果任何线程中断了当前线程,将会抛出此异常,同时将中断标志位清除
     */
    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);	//主线程等待,下面的wait()方法也一样
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    /**
     * 线程执行结束之前最多等待millis毫秒nanos纳秒
     * 此方法基于循环判断isAlive返回值来决定是否调用wait方法来实现
     * 随着一个线程终止,将会调用notifyAll方法 
     * 所以建议不要在当前实例上调用 wait、 notify、 notifyAll
     */
    public final synchronized void join(long millis, int nanos)
            throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                    "nanosecond timeout value out of range");
        }
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
        join(millis);
    }

中断方法以及检测中断方法和判活方法

 /**
     * 中断当前线程
     * 如果当前线程阻塞在Object的wait()、wait(long)、wait(long, int),或者
     * join()、join(long)、join(long, int)以及sleep(long)、sleep(long, int)等方法
     * 那么将会清除中断标志位并受到一个中断异常
     * 非静态方法
     */
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
    /**
     * 检测当前线程是否已经被中断,此方法会清除当前线程的中断标志
     * 也就是说,如果这个方法被连续调用两次,并且第一次调用之前,线程被中断过,那么第一次调用返回true,第二次返回false
     * @return  <code>true</code> 如果当前线程已经被中断,返回true,否则返回false     
     * 静态方法
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    /**
     * 检测当前线程是否已经被中断,此方法不会清除当前线程的中断标志
     * 非静态方法
     */
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
    /**
     * 根据参数值决定是否在判断中断标志位之后清除标志位
     * 实例方法
     */
    private native boolean isInterrupted(boolean ClearInterrupted);
    /**
     * 检测一个线程是否还存活,存活指的是已经启动但还没有终止
     * 实例方法
     */
    public final native boolean isAlive();

线程调度

java线程的实现:java线程模型是基于操作系统原生线程模型来实现的;

线程模型只对线程的并发规模和操作成本产生影响,对java程序的编写和运行过程来说,并没有什么不同;

线程优先级

时分形式是现代操作系统采用的基本线程调度形式,操作系统将CPU资源分为一个个的时间片,并分配给线程,线程使用获取的时间片执行任务,时间片使用完之后,操作系统进行线程调度,其他获得时间片的线程开始执行;那么,一个线程能够分配得到的时间片的多少决定了线程使用多少的处理器资源,线程优先级则是决定线程可以获得多或少的处理器资源的线程属性;

可以通过设置线程的优先级,使得线程获得处理器执行时间的长短有所不同,但采用这种方式来实现线程获取处理器执行时间的长短并不可靠(因为系统的优先级和Java中的优先级不是一一对应的,有可能Java中多个线程优先级对应于系统中同一个优先级);Java中有10个线程优先级,从1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认优先级为5;
因此,程序的正确性不能够依赖线程优先级的高低来判断;

线程调度

线程调度是指系统为线程分配处理器使用权的过程;主要调度方式有:抢占式线程调度、协同式线程调度;

抢占式线程调度

每个线程由系统来分配执行时间,线程的切换不由线程本身来决定;java默认使用的线程调度方式时抢占式线程调度;我们可以通过Thread.yield()使当前正在执行的线程让出执行时间,但是,却没有办法使线程去获取执行时间;

协同式线程调度

每个线程的执行时间由线程本身来控制,线程执行任务后主动通知系统,切换到另外一个线程上;

两种线程调度方式的优缺点

协同式的优点:实现简单,可以通过对线程的切换控制避免线程安全问题;

协同式的缺点:一旦当前线程出现问题,将有可能影响到其他线程的执行,最终可能导致系统崩溃;

抢占式的优点:一个线程出现问题不会影响到其他线程的执行(线程的执行时间是由系统分配的,因此,系统可以将处理器执行时间分配给其他线程从而避免一个线程出现故障而导致整个系统崩溃的现象发生)

结论:

在java中,线程的调度策略主要是抢占式调度策略,正是因为抢占式调度策略,导致多线程程序执行过程中,实际的运行过程与我们逻辑上理解的顺序存在较大的区别,也就是多线程程序的执行具有不确定性,从而会导致一些线程安全性问题的发生;那么,什么是线程安全呢?

线程安全

线程安全的定义

简单来说,线程安全就是对于多个线程并发执行的操作不需要进行任何外部的控制,也不需要进行任何的协调,就能够保证程序的执行结果与开发人员的预期结果保持一致,那么这个多线程程序就是线程安全的;

注意:

线程安全问题一定是基于多个线程之间存在访问共享数据这一前提下的;如果多个线程之间不会访问同一个变量,那么就不存在线程安全的问题;

线程安全的分类

线程安全这一概念并不仅仅分为线程安全和非线程安全,按照线程安全的强弱程度可以将各种共享变量的操作分为:不可变、绝对线程安全、相对线程安全、线程兼容以及线程对立这五种情况;

  • 不可变:如果共享变量是不可变的对象,那么对该共享变量的多线程操作一定是线程安全的,因为对象时不可变的,所以任何线程都不可以改变共享变量的状态,也就不会出现脏读的现象;
    • 如果共享变量是一个基本数据类型的变量,那么可以使用final关键字保证其是不可变的;
    • 如果共享变量是一个对象,那么就需要保证对象的行为不会改变该对象的状态,可已将一个类的所有字段使用final关键字修饰,那么就可以保证该类的对象是不可变的,如java.lang.String类;
  • 绝对线程安全:不需要再调用端进行任何同步处理,就能保证代码在多线程并发的场景下保证线程安全的,即多线程并发执行的结果符合预期的记过;Java API中标注为线程安全的类,大多数都不是绝对线程安全;
  • 相对线程安全:java api中标注为线程安全的类,大多数都是相对线程安全,也就是通常意义上的线程安全,保证对共享变量单独操作时是线程安全的,调用时可以不用额外的保障措施;例如Vector、HashTable、或通过Collections的synchronizedCollection()方法包装的集合等;
  • 线程兼容:线程兼容指对象本身并不是线程安全的但是可以通过在调用端正确采用同步手段来保证对象在并发环境中可以安全地使用,是通常意义上的非线程安全;Java api 中的大部分类都是线程兼容的,例如ArrayList、HashMap等;
  • 线程对立:无论调用端采用什么同步措施都不能保证多线程环境中的线程安全;线程对立很少出现;

线程安全问题的解决方法

介绍了线程的调度原理之后,其实可以分析出线程安全问题的起因在于多线程的执行顺序具有不确定性,那么当多个线程同时操作一份资源就会出现意想不到的情况,而编译器和处理器会对执行的指令进行重排序,这些因素导致了线程安全问题;

那么,在实际开发中,我们一般需要解决的都是上述的相对线程安全以及线程兼容这两种线程安全性问题;那么,对于这两类问题,又可以细分为可见性、原子性以及有序性这三类问题;这里暂且先不进行细分,就线程安全问题,我们给出常用解决措施;

线程安全问题重现

下面结合具体的代码来看一下使用多线程编程时可能会出现的线程安全问题:

public class ThreadSecurity implements Runnable {
    //静态变量,所有对象共享
    private static int count = 0;
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count = count + 1;
            System.out.println(Thread.currentThread().getName() + "执行" + i + "次的结果:" + count);
        }
    }
    public static void main(String[] args) {
        Runnable runnable1 = new ThreadSecurity();
        Runnable runnable2 = new ThreadSecurity();
        Thread thread1 = new Thread(runnable1, "Thread - 1");
        Thread thread2 = new Thread(runnable2, "Thread - 2");
        //获取当前时间到1970年的毫秒数
        long start = System.currentTimeMillis();
        thread1.start();
        thread2.start();
        //join()方法会使主线程等待,直到加入它的线程执行完成或等待时间超时
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("线程执行所需时间:"+(end-start));
    }
}

运行结果:
在这里插入图片描述

上述代码的预期结果应该是输出的结果应该是20,显然和我们预期的结果不同,这就是线程安全问题;我们分析一下,为什么会出现这样的情况,之前提到过,多线程执行的时候代码执行的顺序具有不确定性,那么就可能会出现,线程1(thread1)在获取到count的值之后,cpu执行权被分配到了线程2(thread2),线程2获取到的值与线程1获取到的相同,那么两个线程累加操作执行后,相当于只累加来一次,这样就会导致线程不安全问题;(从java的内存模型的角度更好理解)那么,如何解决这个问题,我们可以利用java中的synchronized关键字对线程体进行同步,代码如下:

public class ThreadSecurity implements Runnable {
    //静态变量,所有对象共享
    private static int count = 0;

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //对线程体进行同步
            synchronized (ThreadSecurity.class) {
                count = count + 1;
            }
            System.out.println(Thread.currentThread().getName() + "执行" + i + "次的结果:" + count);
        }
    }

    public static void main(String[] args) {
        Runnable runnable1 = new ThreadSecurity();
        Runnable runnable2 = new ThreadSecurity();
        Thread thread1 = new Thread(runnable1, "Thread - 1");
        Thread thread2 = new Thread(runnable2, "Thread - 2");
        //获取当前时间到1970年的毫秒数
        long start = System.currentTimeMillis();
        thread1.start();
        thread2.start();
        //join()方法会使主线程等待,直到加入它的线程执行完成或等待时间超时
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("线程执行所需时间:" + (end - start));
    }
}

同步处理后代码执行的结果如下:
在这里插入图片描述

显然,经过同步后的代码,就可以保证多线程并发执行的情况下,结果依然符合预期结果;

synchronized关键字的使用方式

synchronized同步代码块:锁的对象为指定的对象

synchronized同步实例方法:锁的对象为当前实例

synchronized同步静态方法:锁的对象为Class对象

synchronized关键字的应用实例

线程安全的单例模式实现

public class SingleTonThreadSafe {
    	//属性私有化,volatile实现内存可见性、禁止指令重排序
    	private volatile static SingleTonThreadSafe singleTonThreadSafe = null;
    	//无参构造函数私有化
    	private SingleTonThreadSafe(){}
    	//静态方法外部使用,获取对象实例
    	public static SingleTonThreadSafe getInstance(){
        	//第一次判断,避免不必要的加锁
        	if(singleTonThreadSafe == null){
            	//同步实例化代码块
            	synchronized(SingleTonThreadSafe.class){
                	//再次检测,避免其它线程已经实例化
                	if(singleTonThreadSafe == null){
                    	//实例化,其他线程立即可见
                    	singleTonThreadSafe = new SingleTonThreadSafe();
                	}
            	}
        	}
        	return singleTonThreadSafe;
    	}
	}

synchronized同步锁的使用注意点

  • 死锁
    • 定义:多个线程相互等待已被对方占有的锁,同时都不释放自己已经占有的锁,导致线程之间陷入僵持,致使系统不可用
    • 形成条件:互斥锁,锁只能主动释放、循环等待
    • 避免策略:顺序加锁、超时获取自动放弃、死锁检测
  • 活锁
    • 定义:线程等待被其它线程唤醒,但是实际没有线程来唤醒,导致线程一致无法恢复到运行状态
    • 避免策略:编程时有等待,就必有对应的唤醒

线程间通信

如果你的多线程程序仅仅是每个线程独立完成各自的任务,相互之间并没有交互和协作,那么,你的程序是无法发挥出多线程的优势的,只有有交互的多线程程序才是有意义的程序,否则,还不如使用单线程执行多个方法实现程序来的简单。易懂、有效!

那么,线程间进行交互通信的手段有哪些呢?下面,将给出常用的多线程通信的实现手段以及相应的代码示例,并结合具体的代码进行分析,对其中需要注意的地方进行突出提示;

等待通知机制

我们先看这样一个场景:线程A修改了对象O的值,线程B感知到对象O的变化,执行相应的操作,这样就是一个线程交互的场景;可以看出,这种方式,相当于线程A是发送了消息,线程B接收到消息,进行后续操作,是不是很像生产者与消费者的关系?我们都知道,生产者与消费者模式可以实现解耦,使得程序结构上具备伸缩性;那么java中如何实现这种功能呢?

一种简单的方式是,线程B每隔一段时间就轮询对象O是否发生变化,如果发生变化,就结束轮询,执行后续操作;

但是这种方式不能保证对象O的变更及时被线程B感知,同时,不断地轮询也会造成较大的开销;分析这些问题的根本在哪?其实,可以发现状态的感知是拉取的而不是推送的因此才会导致这样的问题产生;

那么,我们就会思考,如何将拉取变为推送来实现这样的功能呢?

这就引出了java内置的经典的等待/通知机制,通过查看Object类的源码发现,该类中有三个方法,我们一般不会使用,但是在多线程编程中,这三个方法却是能够大放异彩的!那就是wait()/notify()/notifyAll();

 /**
     * 调用此方法会导致当前线程进入等待状态直到其它线程调用同一对象的notify()或者notifyAll()方法
     * 当前线程必须拥有对象O的监视器,调用了对象O的此方法会导致当前线程释放已占有的监视器,并且等待
     * 其它线程对象O的notify()或者notifyAll()方法,当其它线程执行了这两个方法中的一个之后,并且
     * 当前线程获取到处理器执行权,就可以尝试获取监视器,进而继续后续操作的执行
     * 推荐使用方式:
     *     synchronized (obj) {
     *         while (&lt;condition does not hold&gt;)
     *             obj.wait();
     *         ... // Perform action appropriate to condition
     *     }
     * @throws  IllegalMonitorStateException  如果当前线程没有获取到对象O的监视器时,抛出异常
     * @throws  InterruptedException 如果在调用了此方法之后,其他线程调用notify()或者notifyAll()
     * 方法之前,线程被中断,则会清除中断标志并抛出异常
     */
    public final void wait() throws InterruptedException {
        wait(0);
    }
    /**
     * 唤醒等待在对象O的监视器上的一个线程,如果多个线程等待在对象O的监视器上,那么将会选择其中的一个进行唤醒
     * 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
     * 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
     * 这个方法必须在拥有对象O的监视器的线程中进行调用
     * 同一个时刻,只能有一个线程拥有该对象的监视器
     * @throws  IllegalMonitorStateException  如果当前线程没有获取到对象O的监视器时,抛出异常
     */
    public final native void notify();
    /**
     * 唤醒等待在对象O的监视器上的所有线程
     * 被唤醒的线程只有在当前线程释放锁之后才能够继续执行.
     * 被唤醒的线程将会与其他线程一同竞争对象O的监视器锁
     * 这个方法必须在拥有对象O的监视器的线程中进行调用
     * 同一个时刻,只能有一个线程拥有该对象的监视器
     * @throws  IllegalMonitorStateException  如果当前线程没有获取到对象O的监视器时,抛出异常
     */
    public final native void notifyAll();

下面看一下如何通过这三个方法实现经典的等待通知机制吧! 按照JDK中推荐的使用方式实现了等待通知样例代码如下:

线程执行的顺序标在代码中

public class WaitAndNotiyf {
    //轮询标志位
    private static boolean stop = false;
    //监视器对应的对象
    private static Object monitor = new Object();

    public static void main(String[] args) {
        //等待线程
        Runnable waitThread = new Runnable() {
            @Override
            public void run() {
                synchronized (monitor) {
                    //循环检测标志位是否变更
                    while (!stop) {
                        try {
                            //标志位未变更,进行等待
                            monitor.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //被唤醒后获取对象的监视器执行的代码
                    System.out.println("Thread " + Thread.currentThread().getName() + " notifies the waitted thread at first time");
                    stop = false;
                }
                //休眠1秒之后,线程角色转换为唤醒角色
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //与上述代码相反的逻辑
                synchronized (monitor) {
                    while (stop) {
                        try {
                            monitor.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    monitor.notify();
                    stop = true;
                    System.out.println("Thread " + Thread.currentThread().getName() + " notifies the waitted thread at first time");
                }
            }
        };
        //通知线程
        Runnable notifyThread = new Runnable() {
            @Override
            public void run() {
                synchronized (monitor) {
                    while (stop) {
                        try {
                            monitor.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    stop = true;
                    monitor.notify();
                    System.out.println("Thread " + Thread.currentThread().getName() + " notifies the waitted thread at first time");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (monitor) {
                    while (!stop) {
                        try {
                            monitor.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Thread " + Thread.currentThread().getName() + " is awakened at first time");
                }
            }
        };
        //测试
        Thread wait = new Thread(waitThread);
        wait.setName("waitThread");
        Thread notify = new Thread(notifyThread);
        notify.setName("notifyThread");
        wait.start();
        notify.start();
    }
}

通过上述代码,可以提炼出等待通知机制的经典模式:

等待方实现步骤:

  • 加锁同步
  • 条件不满足,进入等待,被唤醒之后,继续检查条件是否满足(循环检测)
  • 条件满足,退出循环,继续执行后续代码

对应的伪代码:

synchronized(obj){
    while(condition不满足){
        obj.wait();
    }
    //后续操作
}

通知方实现步骤:

  • 加锁同步
  • 条件不满足,跳过循环检测
  • 设置条件并唤醒线程

对应的伪代码:

synchronized(obj){
    while(condition不满足){
        obj.wait();
    }
    //更新condition
    obj.notify();
    //后续操作
}

生产者消费者模式

基于等待通知机制,我们可以就很容易地写出生产者消费者模式的代码,下面给出实现样例代码:

public class ProducerAndConsumer {
    //商品库存
    private static int storeMount = 0;
    //监视器对应的对象
    private static Object monitor = new Object();

    //生产者线程
    public static void main(String[] args) {
        Runnable producerThread = new Runnable() {
            @Override
            public void run() {
                produce();
            }

            public void produce() {
                while (true) {
                    synchronized (monitor) {
                        //循环检测库存是否大于0,大于0表示还有商品可以消费,线程等待消费者消费商品
                        while (storeMount > 0) {
                            try {
                                monitor.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        //被唤醒后获取到对象的监视器之后执行的代码
                        System.out.println("Thread " + Thread.currentThread().getName() + " begin produce goods");
                        //生产商品
                        storeMount = 1;
                        //唤醒消费者
                        monitor.notify();
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        //消费者线程
        Runnable consumerThread = new Runnable() {
            @Override
            public void run() {
                consume();
            }

            public void consume() {
                while (true) {
                    synchronized (monitor) {
                        //检测库存是否不为0,如果不为0,那么商品可供消费,否者等待生产者生产商品
                        while (storeMount == 0) {
                            try {
                                monitor.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        //消费商品
                        storeMount = 0;
                        //唤醒生产者线程
                        monitor.notify();
                        System.out.println("Thread " + Thread.currentThread().getName() + " begin consume goods");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        Thread produceThread = new Thread(producerThread);
        produceThread.setName("producerThread");
        Thread consumeThread = new Thread(consumerThread);
        consumeThread.setName("consumerThread");
        produceThread.start();
        consumeThread.start();
    }
}

执行结果如下图所示:

在这里插入图片描述

总结

以上就是本文叙述的所有内容,本文首先对于java中线程调度形式,引出多线程编程中需要解决的线程安全问题,并分析线程安全问题,给出解决线程安全问题的常用手段(加锁同步),最后,结合java内置的等待通知机制,进行了样例代码的展示以及分析,给出了经典的等待通知机制的编程范式,最后,基于等待通知机制给出了生产者消费者模式的实现样例。

注意细节

  • 线程分类
    • 用户线程:大多数线程都是用户线程,用于完成业务功能
    • 守护线程:支持型线程,主要用于后台调度以及支持性工作,比如GC线程,当JVM中不存在非守护线程时JVM将会推出
      • Thread.setDaemon(true)来设置线程属性为守护线程,该操作必须在线程调用start()方法之前执行
      • 守护线程中的finally代码块不一定会执行,因此不要寄托于守护线程中的finally代码块来完成资源的释放
  • 线程的交互方式
    • join
    • sleep/interrupt
    • wait/notify
  • 启动线程的方式
    • 只能通过线程对象调用start()方法来启动线程
    • start()方法的含义是,当前线程(父进程)同步告知虚拟机,只要线程规划期空闲,就应该立即启动调用了start()方法的线程
    • 线程启动前,应该设置线程名,以便使用Jstack分析程序中线程运行状态时,起到提示性作用
  • 终止线程的方式
    • 中断检测机制
      • 线程通过调用目标线程的interrupt()方法对目标线程进行中断标志,目标线程通过检测自身的中断标志位(interrupted()或isInterrupted())来响应中断,进行资源的释放以及最后的终止操作;
      • 抛出InterruptedException异常的方法在抛出异常之前,都会将该线程的中断标志位清除,然后抛出异常
    • suspend()/resume()(弃用)
      • 调用后,线程不会释放已经占有的资源,容易引发死锁问题
    • stop()(弃用)
      • 调用之后不一定保证线程资源的释放
  • 锁释放的情况:
    • 同步方法或者同步代码块的执行结束(正常。异常结束)
    • 同步方法或同步代码块锁对象调用wait()方法
  • 锁不会释放的情况
    • 调用Thread类的静态方法yield()以及sleep()
    • 调用线程对象的suspend()(弃用)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值