彻底理解Java并发:Java线程

Java
308
0
0
2022-12-20
标签   Java并发
本篇内容包括:Java线程的生命周期(新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead))、Java 线程实现/创建的4种方式、终止 Java 线程4 种方式、线程的基本方法以及对守护线程的介绍。

一、Java线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

img

1、新建状态(NEW)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

2、就绪状态(RUNNABLE)

当线程对象调用了 start() 方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

3、运行状态(RUNNING)

如果处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程处于运行状态。

4、阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因放弃了CPU使用权,也即让出了 cpu timeslice,暂时停止运行。

直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。

阻塞的情况分三种:

  • 等待阻塞( o.wait -> 等待对列):运行(running)的线程执行 o.wait() 方法,JVM 会把该线程放入等待队列(waitting queue)中。
  • 同步阻塞( lock -> 锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)
  • 其他阻塞(sleep/join):运行(running)的线程执行 thread.sleep(long ms) 或 t.join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。

5、线程死亡(DEAD)

线程会以下面三种方式结束,结束后就是死亡状态。

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

二、Java线程实现/创建方式

1、 继承 Thread 类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start() 实例方法。start() 方法是一个native方法,它将启动一个新线程,并执行 run() 方法:

public class MyThread extends Thread {
    public void run() {
    System.out.println("MyThread.run()");
    }

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.start();
    }
}

2、实现 Runnable 接口

如果自己的类已经继承另一个类,就无法直接 extends Thread,此时,可以以实现 Runnable 接口的方式实现多线程,并且实例化Thread,传入自己的Thread 实例,来调用 run() 方法 :

public class MyThread extends OtherClass implements Runnable {
    public void run() {
        System.out.println("MyThread.run()");
    }

    public static void main(String[] args) {
        //启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例: 
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
    }
}

//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run()
public void run() { 
	if (target != null) { 
	target.run(); 
	} 
}

3、Callable、ExecutorService、Future 实现有返回值线程

无返回值的任务必须实现 Runnable 接口,类似的,有返回值的任务必须实现 Callable 接口。执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了:

public class CallableTest {
    public static void main(String[] args) {
        //创建一个线程池
        ExecutorService exec= Executors.newCachedThreadPool();
        // 创建多个有返回值的任务
        List<Future<String>> results=new ArrayList<Future<String>>();
        for(int i=0;i<5;i++) {
            results.add(exec.submit(new TaskWithResult(i)));
        }
		//获取所有并发任务的运行结果 
        for(Future<String> fs :results) {
            try {
                // 从 Future 对象上获取任务的返回值,并输出到控制台
                System.out.println(fs.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}
//实现 Callable 创建有返回值的线程
class TaskWithResult implements Callable<String> {
    private int id;
    public TaskWithResult(int id) {
        this.id=id;
    }
    @Override
    public String call() throws Exception {
        return "result of TaskWithResult "+id;
    }
}

注意事项:

  • 接口:java.util.concurrent.Callable 只有一个方法,不是 run(),而是 call(),它有返回值,类型为V;
  • ExecutorService 的 submit方法会返回 Future 对象;
  • 可以用 isDone() 方法来查询 Future 是否已经完成。当任务完成时它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用 isDone() 进行检查就直接调用 get(),这种情况下 get() 将阻塞,直至结果准备就绪。你还可以在试图调用 get() 来获取结果之前,先调用具有超时的 get() 或者调用 isDone() 来查看任务是否完成。get()返回类型为V,即call方法的返回值;
  • get() 方法会抛出 InterruptedException 和 ExecutionException。

4、基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池:

public static void main(String[] args) {
        // 创建线程池 
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        while(true) {
            threadPool.execute(new Runnable() { // 提交多个线程任务,并执行 
                @Override 
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " is running ..");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } }
            });
        } }

三、Java线程终止方式

1、正常运行结束

程序运行结束,线程自动结束。

2、使用退出标志退出线程

一般 run() 方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:

最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while

循环是否退出,代码示例:

	public class ThreadSafe extends Thread {
		public volatile boolean exit = false; 
		public void run() { 
			while (!exit){
				//do something
			}
		} 
	}

定义了一个退出标志exit,当exi为true时,whil循环退出,exit的默认值为false,在定义exit时,使用了一个 Java 关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

3、Interrupt() 方法结束线程

使用interrupt()方法来中断线程有两种情况:

  1. 线程处于阻塞状态:如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
  2. 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
	public class ThreadSafe extends Thread {
		public void run() { 
			while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
				try{
					Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
				}catch(InterruptedException e){
					e.printStackTrace();
					break;//捕获到异常之后,执行 break 跳出循环
				} 
			}
		} 
	}

4、stop()方法终止线程(线程不安全)

程序中可以直接使用 thread.stop() 来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop() 调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop() 后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

四、线程的基本方法

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等方法:

img

1、线程等待(wait)

调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait() 方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

2、线程睡眠(sleep)

sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait() 方法会导致当前线程进入 WATING 状态

3、线程让步(yield)

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

4、线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  2. 若调用sleep()而使线程处于 TIMED-WATING 状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  3. 许多声明抛出InterruptedException的方法(如Thread.sleep(long mills) 方法),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据hread.isInterrupted()的值来优雅的终止线程。

5、等待其他线程终止(Join)

join()方法,等待其他线程终止,在当前线程中调用一个线程的join()方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待CPU的宠幸。

为什么要用 join()方法?

很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到join()方法。

System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread1 thread1 = new Thread1();
thread1.setName("线程 B");
thread1.join();
System.out.println("这时 thread1 执行完毕之后才能执行主线程");

6、线程唤醒(notify)

Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有notifyAll(),唤醒再次监视器上等待的所有线程。

7、其他方法

  • sleep():强迫一个线程睡眠N毫秒;
  • isAlive(): 判断一个线程是否存活;
  • join(): 等待线程终止;
  • activeCount(): 程序中活跃的线程数;
  • enumerate(): 枚举程序中的线程;
  • currentThread(): 得到当前线程;
  • isDaemon(): 一个线程是否为守护线程;
  • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) ;
  • setName(): 为线程设置一个名称;
  • wait(): 强迫一个线程等待;
  • notify(): 通知一个线程继续运行;
  • setPriority(): 设置一个线程的优先级;
  • getPriority():获得一个线程的优先级

8、sleep wait 区别

  1. 对于 sleep() 方法,我们首先要知道该方法是属于Thread类中的。而 wait() 方法,则是属于Object 类中的;
  2. sleep() 方法导致了程序暂停执行指定的时间,让出 CPU 该其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态;
  3. 在调用 sleep() 方法的过程中,线程不会释放对象锁;
  4. 而当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify() 方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

9、start 与 run区别

  1. run() 相当于线程的任务处理逻辑的入口方法,它由 Java 虚拟机在运行相应线程时直接调用,而不是由应用代码进行调用。
  2. 而 start() 的作用是启动相应的线程。启动一个线程实际是请求 Java 虚拟机运行相应的线程,而这个线程何时能够运行是由线程调度器决定的。start() 调用结束并不表示相应线程已经开始运行,这个线程可能稍后运行,也可能永远也不会运行。
  3. start() 方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
  4. 通过调用 Thread 类的 start() 方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  5. 方法 run() 称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

五、JAVA守护线程

定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开

优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务

设置:通过setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon方法。

在Daemon线程中产生的新线程也是Daemon的。

线程则是JVM级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。

example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。