Java并发 --- 线程创建、状态与方法等

简介: Java并发 --- 线程创建、状态与方法等

线程的创建方式?三者之间区别是什么?如何进行选择?


java天生就是多线程的编程语言,创建新的线程有三种实现方式(实现并发编程),分别是:


  • 继承Thread,实现Runable,实现Callable<T>


创建线程的三种基本方式:


  • 继承 Thread 类并重写 run 方法,有单继承的局限性。但不符合里氏替换原则,不可以继承其他类。
  • 实现 Runnable 接口并重写 run 方法,任务和线程分开,不能返回执行结果。
  • 实现 Callable 接口并重写 call 方法,利用FutureTask执行任务,能通过futureTask.get()取到执行结果,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  • 使用 Executors 工具类创建线程池。实际开发中一般使用线程池创建线程,具体见这里

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadPoolTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程
        Thread a = new A();
        Thread b = new Thread(new B());
        FutureTask<String> futureTask = new FutureTask<>(new C());
        Thread c = new Thread(futureTask);
        // 启动线程
        a.start();
        b.start();
        c.start();
        System.out.println(futureTask.get());
    }
}
class A extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread的线程任务");
    }
}
class B implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable的线程任务");
    }
}
class C implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "实现Callable的线程任务";
    }
}


Thread 与 Runnable区别:


两者没有本质的区别,就是接口和类的区别。简言之,Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。(典型问题两个线程卖票)


ps:Thread实现了Runnable接口,提供了更多的可用方法和成员而已(在Runnable基础上进行了扩展,提供了更多的功能)。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。


Runnable 与callable 异同:


  • 相同点:都是接口,都可编写多线程程序,都采用Thread.start()启动线程
  • 主要区别
  • 有无返回值:Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  • 是否可以捕获异常:Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息


什么是Future和 FutureTask?


  • Future接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。
  • FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。


为什么启动线程要用start()方法?Thread 调用 start() 方法和调用 run() 方法的区别?


当我们new 一个 Thread,线程进入了新建状态。


  • 调用 start()方法,会启动一个新的线程并使线程进入了就绪(可运行)状态,当分配到cpu时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容(run()方法由JVM直接调用)。
  • 直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它(运行在run()方法的调用方所在的线程),即不会创建新线程来执行,所以直接执行 run() 方法的话不会以多线程的方式执行。


简言之,调用 start() 方法方可启动一个新的线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。


ps:上下文切换(进程切换):当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换


上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。


线程类的一些常用方法(阻塞方法下面给出)


  • start()方法,启动一个新的线程并进入就绪状态;
  • stop()方法,调用该方法强制结束该线程执行;
  • join方法,调用该方法等待该线程结束。
  • sleep()方法,调用该方法该线程进入等待。
  • run()方法,调用该方法直接执行线程的run()方法,但是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!
  • isAlive(): 判断一个线程是否存活。
  • activeCount(): 程序中活跃的线程数。
  • enumerate(): 枚举程序中的线程。
  • currentThread(): 得到当前线程。
  • isDaemon(): 一个线程是否为守护线程。(例如后台记录操作日志,监控内存,垃圾回收等,用户也可以自定义)
  • setDaemon(): 设置一个线程为守护线程。(分为用户线程和守护线程,JVM必须保证用户线程执行完毕,而守护线程则不用)
  • setName(): 为线程设置一个名称。
  • setPriority(): 设置一个线程的优先级。


要注意,其实wait()与notify()方法是Object的方法,不是Thread的方法!!同时,wait()与notify()会配合使用,分别表示线程挂起和线程恢复,是线程通信的一种方式。


线程中wait() 和 sleep() 方法的区别


两者均会暂停线程的执行,使线程进入阻塞状态,区别是:


  • 所属类不同:sleep()是Thread类特有的静态方法,wait()是Object类的方法。
  • 是否释放同步锁(关键):都释放cpu执行权,但sleep()不释放同步锁,wait()释放同步锁,然后线程进入等待池。
  • 使用范围不同:sleep()可以在任何地方使用,wait()只能在synchronized同步方法或者同步代码块中使用,否则会抛 IllegalMonitorStateException。
  • 阻塞与恢复方式不同:sleep不需要被唤醒(休眠之后退出阻塞);wait()方法使线程进入等待队列(线程挂起/等待)wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • 应用场景不同:wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。


ps:wait(1000)与sleep(1000)的区别


  • Thread.Sleep(1000) :意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束。
  • wait(1000):表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。


锁池与等待池


  • 锁池/Entry Set:所有需要竞争同步锁的线程都会放在锁池当中(BLOCKED状态),比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配
  • 等待池/Wait Set:当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁(WAITING状态)。只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。


注意:某个线程B想要获得对象锁,一般情况有两个先决条件:


  • 对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等)
  • 线程B已处于RUNNABLE状态。ps:对于锁池,当对象锁释放的时候,JVM就会唤醒其中一个线程转为RUNNABLE状态;对于等待池,当对象的notify方法被调用,JVM会唤醒处于等待池中的线程进入BLOCKED状态,获得锁之后进入RUNNABLE状态。

线程的 sleep() 方法和 yield() 方法有什么区别?

  • sleep():Thread.sleep(millisec) 方法会休眠当前正在执行的线程,这段时间不参与CPU竞争,millisec 单位为毫秒。sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}


  • yield():对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

public void run() {
    Thread.yield();
}


两者主要区别


  • 执行后线程状态不同:线程执行 sleep() 方法后进入超时等待(TIMED_WAITING)状态,而执行 yield() 方法后进入就绪(READY)状态。
  • 运行线程优先级: sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。


Java实现阻塞的方法?


  • 线程睡眠:Thread.sleep (long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
  • 线程等待:Object类中的wait()方法,导致当前的线程等待(线程被挂起),直到其他线程调用此对象的 notify() 唤醒方法。wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用,wait()与notify()配套使用。
  • 线程礼让,Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞(线程仍处于可执行撞他),随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
  • 线程自闭,join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。


什么是守护线程?与用户线程(非守护线程的区别)


  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作(不管守护线程是否工作,直接杀死)。
  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程,main 函数所在的线程就是一个用户线程,内部同时还启动了很多守护线程(如垃圾回收线程)。


注意事项


  • 在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);  // 设置为守护线程
}


  • 在守护线程中产生的新线程也是守护线程
  • 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
  • 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。


线程与进程的区别


  • 调度(根本区别):进程是操作系统资源分配的基本单位,而线程是处理器任务调度/程序执行的基本单位。
  • 隶属关系:一个进程可以有多个线程,但至少有一个线程;而一个线程只能在一个进程的地址空间内活动。
  • 拥有资源:进程是拥有资源的一个独立单位(进程间相互独立),线程不拥有系统资源,但可以访问隶属于进程的资源(多个线程共享程序的内存空间,某个进程中的线程其他进程不可见)。
  • 调度与切换:线程上下文切换比进程上下文切换要快得多


线程状态(生命周期)有哪些?


线程的六种状态:


一个线程只能处于一种状态,并且这里的线程状态特指 Java 虚拟机的线程状态,不能反映线程在特定操作系统下的状态。以下六种:


  • NEW(新建) 状态:线程被创建(new Thead()),但未启动(未调用start()方法)
  • RUNNABLE(运行) 状态 :Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)
  • BLOCKED(阻塞) 状态:又称无限期等待,线程阻塞于锁(调用同步方法时,没有获得锁)。线程请求获取 monitor lock 从而进入 synchronized 函数或者代码块,但是其它线程已经占用了该 monitor lock。要结束该状态进入从而 RUNABLE 需要其他线程释放 monitor lock。
  • WAITING(等待) 状态:等待其他线程的显示唤醒(通知或中断),即线程执行wait()方法进入此状态(需要notify或notifyAll通知)。
  • TIME_WAITING(超时等待) 状态:又称限期等待,无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。
  • TERMINATED(终止) 状态:线程结束任务(执行完run()方法)或者产生异常结束。

image.png

总结补充:


  • 阻塞和等待都是用来描述线程状态的。阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 monitor lock。而等待是主动的,通过调用 Object.wait()等方法进入。
  • 睡眠和挂起是用来描述一个线程的行为。调用 Thread.sleep()方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用 Object.wait()方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。


Java终止线程的方式(区别线程池)?


  • 使用退出标志(比如while循环中设置flag),使线程正常退出,也就是当 run() 方法完成后线程中止。
  • 使用stop()方法,已被弃用。原因是:(1)调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。(2)调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
  • 使用 interrupt() 方法中断线程。线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。


拓展:判断某个线程是否已被发送过中断请求,请使用Thread.currentThread().isInterrupted()方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false),而不要使用thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为false)方法来判断,下面是线程在循环中时的中断方式:

while(!Thread.currentThread().isInterrupted() && more work to do){
    do more work
}


如何操作?


  • 如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。
  • 注,synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断(请参考后面的测试例子)。与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。你也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。
相关文章
|
2天前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
10 1
|
1天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
|
1天前
|
安全 Java
【JAVA进阶篇教学】第六篇:Java线程中状态
【JAVA进阶篇教学】第六篇:Java线程中状态
|
1天前
|
缓存 Java
【JAVA进阶篇教学】第五篇:Java多线程编程
【JAVA进阶篇教学】第五篇:Java多线程编程
|
1天前
|
Java
【JAVA基础篇教学】第十二篇:Java中多线程编程
【JAVA基础篇教学】第十二篇:Java中多线程编程
|
1天前
|
安全 Java
java-多线程学习记录
java-多线程学习记录
|
1天前
|
XML JavaScript Java
详解Java解析XML的四种方法
详解Java解析XML的四种方法
|
2天前
|
存储 Java API
掌握8条方法设计规则,设计优雅健壮的Java方法
掌握8条方法设计规则,设计优雅健壮的Java方法
|
2天前
|
Java C语言
详解java方法与递归
详解java方法与递归
9 3
|
2天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
12 0