Java并发编程-认识Java里的线程

  • Post author:
  • Post category:java



上一篇

我们已经了解进程和线程的相关概念CPU线程调度的基本原理,这回我们来看看java是怎么支持多线程的。

任何一个程序都必须要创建线程,特别是Java。不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等都离不开线程。



1.Java程序天生就是多线程的

下面用一个例子来看看启动一个main方法,jvm会创建多少个线程

public static void main(String[] args) {
        //Java 虚拟机线程系统的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //获取线程和线程堆栈信息
        ThreadInfo[] threadInfoArray = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfoArray) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }
}
    
    输出结果:
    [6] Monitor Ctrl-Break //监控Ctrl-Break中断信号的
	[5] Attach Listener //内存dump,线程dump,类信息统计,获取系统属性等
	[4] Signal Dispatcher // 分发处理发送给JVM信号的线程
	[3] Finalizer   // 调用对象finalize方法的线程
	[2] Reference Handler //清除Reference的线程
	[1] main //主线程,用户程序入口



2.创建和启动线程

创建启动线程是离不开两个类的,一个java.lang.Thread类,一个是java.lang.Runnable接口

  1. Runnable是对任务(业务逻辑)的抽象
  2. Thread是Java里对线程的唯一抽象

在日常开发中我们有两种方法可以创建和启动新的线程


1.实现Runnable接口

  • 第一步:创建一个类,实现Runnable接口,重写Runnable的run方法
  • 第二步:将这个类的实例对象作为参数传递到Tread的构造方法中,实例化出Thread对象。
  • 第三步:后调用Thread对象的实例方法start()启动线程。
 private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("我实现了Runnable接口");
        }
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }

还有这种匿名内部类的简化写法

public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是匿名内部类版本,我实现了Runnable接口");
            }
        }).start();
 }

jdk1.8以后,Lambda表达式写法

 
 new Thread(() -> System.out.println("我是Lambda表达式版本,我实现了Runnable接口"))


2.继承Thread类

  • 第一步:创建一个类,继承Thread类,重写其run方法
  • 第二步:实例化此类,直接调用此类start()方法启动线程
private static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("我继承了Thread类");
        }
 }
    
 public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
 }

上面不是说到,创建和启动一个线程,都离不开这两个类吗,但是继承Thread这种方式,并没有用到Runnable接口?

实际上在Thread类中已经实现了Runnable接口,我们重写其实还是Runnable的run方法。



3.带有返回值的线程

不难看出run方法是没有返回值的,如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call(),这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

在这里插入图片描述

FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

    private static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程在进行计算");
            Thread.sleep(3000);
            return 1000;
        }
    }

    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println("主线程在执行任务");
        try {
            System.out.println("运行结果" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任务执行完毕");
    }



4.线程的状态

Java线程在运行的生命周期中可能处于下表所示的6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。

初始状态(new) 线程被实例化但还没有但还没有调用start()方法
运行(runnable) 就绪(ready) Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)
运行中(running)
阻塞状态(blocked) 线程因为某种原因放弃了cpu 使用权,也即让出了cpu 时间片,暂时停止运行。
等待状态(waiting) 表示当前线程需要等待其他线程做出一些特定的动作(通知或中断)
超时等待(time_waiting) 该状态不同于waiting,它可以在指定时间后自行返回
终止状态(terminater) 表示当前线程已执行完毕


线程的状态转换


在这里插入图片描述



run()

run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。



start()

Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。

start()方法让一个线程进入就绪队列等待分配CPU,分到CPU后才调用实现的run()方法start()方法不能重复调用,如果重复调用会抛出异常。



yield()

使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。

所有执行yield()的线程有可能在进入到就绪(ready)状态后会被操作系统再次选中马上又被执行。



join()

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程A中调用了线程B的Join()方法,A线程进入等待(waiting)状态,直到线程B执行完毕后,才会继续执行线程A。



sleep()

Thread.sleep(long millis)和Thread.sleep(long millis,int nanos)静态方法强制当前正在执行的线程休眠(即暂停执行)进入超时等待(time-waiting)状态,该方法不会使当前线程释放锁。

sleep()是一个静态方法,一般直接用Thread类名.直接调用,sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。

在苏醒之后是不会返回到running状态,会返回到就绪(ready)状态,还需要等待CPU调度执行。所以线程实际等待时间往往大于sleep设置的时间。



wait()和sleep()的区别

Java中调用wait方法或者sleep方法都可以让线程进入waiting或者time-waiting状态,但是

wait是Object中的方法,而sleep则是Thread中的方法。

sleep可以在任何地方使用,而wait只可以在synchronized方法或synchronized块中使用。

sleep方法只会让出当前线程持有的时间片,而wait方法除了让出时间片还会让出当前线程持有的锁。



wait()

wait()和notify()系列都是Object对象的方法,调用该方法的线程进入 等待(waiting)状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。



wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回



wait (long,int)

对于超时时间更细粒度的控制,可以达到纳秒



notify()

通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入 等待(waiting)状态。调用notify()系列方法后,对锁无影响,线程只有在synchronized同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是synchronized同步代码的最后一行。



notifyAll()

通知所有等待在该对象上的线程,在实际开发中尽可能用notifyAll(),谨慎使用notify(),因为notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。



等待/通知机制

是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。


等待和通知的标准范式


等待方遵循如下原则。

  1. 获取对象的锁。
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
  3. 条件满足则执行对应的逻辑。
 synchronized(对象){
      while (条件不满足){
               对象.wait()
      } 
         对应的处理逻辑
 }

通知方遵循如下原则。

  1. 获得对象的锁。
  2. 改变条件。
  3. 通知所有等待在对象上的线程。
synchronized(对象){
     改变条件
     对象.notifyAll();
}

在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,进入wait()方法后,当前线程释放锁,在从wait()返回前,线程与其他线程竞争重新获得锁, 执行notify()系列方法的线程退出调用了notifyAll()的synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。



终止线程


  1. 自然终止

    要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

  2. 暂停、恢复和停止

    操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

  3. 中断

    安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。

    如果一个线程处于了等待状态(如线程调用了thread.sleep、thread.join、thread.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些等待方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

    不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,

    一、一般的阻塞方法,如sleep等本身就支持中断的检查,

    二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

    注意:处于死锁状态的线程无法被中断



版权声明:本文为yuanchengmm原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。