1、线程的创建与运行
(1)、继承或直接使用Thread类
继承Thread类创建线程:
/** * 主类 */ public class ThreadTest { public static void main(String[] args) { //创建线程对象 My_Thread my_thread = new My_Thread(); //启动线程 my_thread.start(); } } /** * 继承Thread */ class My_Thread extends Thread{ @Override public void run(){ //线程的任务 System.out.println("My_Thread Running"); } }
直接使用Thread类创建线程:
class ThreadTest02 { public static void main(String[] args) { //直接使用Thread创建线程,"My_Thread"是取得线程名 Thread my_thread = new Thread("My_Thread"){ @Override public void run() { //线程的任务 System.out.println("My_Thread Running"); } }; //启动线程 my_thread.start(); } }
以上的方式都是直接使用Thread类创建线程,并通过start方法启动线程,但线程并不会立即执行,它还需要等待CPU调度,只有线程获得CPU控制权,才算是真正在执行。
直接使用Thread类的好处是:
方便传参,可在子类里添加成员变量,通过set方式设置参数或通过构造函数传参
直接使用Thread类的缺点处是:
线程的创建和任务代码冗余在一起。也可能由于继承了Thread类,故无法再继承其他类。任务无返回值。
(2)、使用Runnable接口的run方法
/** * 主类 */ public class ThreadTest03 { public static void main(String[] args) { RunnableTask task = new RunnableTask(); //创建线程,参数1 是任务对象; 参数2 是线程名字,推荐写上 Thread my_thread = new Thread(task,"My_Thread"); //启动线程 my_thread.start(); } } /** * Runable接口实现类 */ class RunnableTask implements Runnable{ @Override public void run(){ //线程的任务 System.out.println("Thread Running"); } }
以上的方式是使用Runnable接口的run方法,该方式将任务代码与线程的创建分离,这样在多个线程具有相同任务时,就可以使用同一个Runnable接口实现,同时该方式的Runnable的实现类也可以继承其他的类。该方式更灵活,故推荐使用其来创建线程。
但其缺点也是任务无返回值。
(3)、使用FutureTask的方式
//创建任务类,类似于Runnable public class CallerTask implements Callable<String> { @Override public String call() throws Exception { return "hello thread"; } public static void main(String[] args) throws ExecutionException, InterruptedException { //创建任务对象 FutureTask<String> futureTask = new FutureTask<>(new CallerTask()); //启动线程 new Thread(futureTask,"My_Thread").start(); //主线程等待"My_Thread"的任务执行完毕,并返回结果 String res = futureTask.get(); System.out.println(res); } }
上述代码实现了Callable接口的call()方法。在main函数内首先创建FutureTask对象(构造函数为CallerTask的实例)。将创建的FutureTask对象作为任务,并放到新创建的线程中启动。运行完毕后,则可以使用get方法等待线程里的任务执行完毕并返回结果。
2、Java线程的状态
Java线程在其生命周期中可能有六种状态。根据Java.lang.Thread类中的枚举类型State的定义,其状态有以下六种:
①NEW:初始状态,线程已被创建但还未调用start()方法来进行启动。
②RUNNABLE:运行状态,调用start方法后,线程处于该状态。注意,Java线程的运行状态,实际上包含了操作系统中的就绪状态(已获得除CPU外的一切运行资源,正在等待CPU调度,获得CPU控制权)和运行状态(获得CPU控制权,线程真正在执行)。因此,即使Java中的线程处于RUNNABLE状态,也并不意味着该线程就一定正在执行(获得CPU的控制权),该线程也有可能在等待CPU调度。
③BLOCKED:阻塞状态,线程阻塞于锁,即线程在锁的竞争中失败,则处于阻塞状态。
④WAITING:等待状态,该状态的线程需要等待其他线程的中断或通知。
⑤TIME-WAITING:超时等待状态,该状态下的线程也在等待通知,但若在限定时间内没有,其他线程进行通知,那么超过规定时间的线程就会自动“醒来”,继续执行run方法内的代码。
⑥TERMINATED:终止状态,线程执行完毕或者线程在执行过程中抛出异常,则线程结束,线程处于终止状态。
阻塞状态(BLOCKED),是因为其在锁竞争中失败而在等待获得锁,而等待状态(WAITING)则是在等待某一事件的发生,常见的如等待其他线程的通知或者中断。
3、Java线程Thread类常用方法
(1)、start方法
是否为static方法:否。
作用:启动一个新线程,在新线程调用run方法。
说明:线程调用start方法,进入运行状态(RUNNABLE),但并不意味着线程中的代码会立即执行,因为Java线程中的运行状态包含了操作系统层面的【就绪状态】和【运行状态】,所以只有Java线程真正获得了CPU的控制权,线程才能真正地在执行。每个线程只能调用一次start方法来启动线程,如果多次调用则会出现IllegalThreadStateException。
(2)、run方法
是否为static方法:否。
作用:线程启动后会调用的方法。
说明:
①若使用继承Thread类的方式创建线程,并重写了run方法,则线程会在启动后调用run方法,执行其中的代码。如果继承时没有重写run方法或者run方法中没有任何代码,则该线程不会进行任何操作。
②若使用实现Runnable接口的方法创建线程,则在调用start启动线程后,也会调用Runnable实现类中的run方法,如果没有重写,则默认不会进行任何操作。
那些run方法和start方法又有什么区别呢?
③start方法是真正能启动一个新线程的方法,而run方法则是线程对象中的普通方法,即使线程没有启动,也可以通过线程对象来调用run方法,run方法并不会启动一个新线程。
代码如下:
public class StartAndRun{ public static void main(String[] args) { //使用Thread创建线程 Thread t = new Thread("my_thread"){ //为线程命名为"my_thread" @Override public void run() { //Thread.currentThread().getName():获取当前线程的名字 System.out.println("【"+Thread.currentThread().getName()+"】"+"线程中的run方法被调用"); for (int i = 0; i < 3; i++) { System.out.println(i); } } }; //调用run方法 t.run(); //调用start方法 t.start(); } }
其结果如下:
【main】线程中的run方法被调用 0 1 2 【my_thread】线程中的run方法被调用 0 1 2
可以看出在my_thread线程启动前(调用start方法前),也可以调用线程对象t中的run方法,调用这个run方法的线程并不会是my_thread线程(因为还没启动呢),而是main方法所在的主线程main。这是因为run方法是作为线程对象的普通方法存在的,可以认为run方法中的代码就是新线程启动后所需要执行的任务。如果通过线程对象调用run方法,那么在哪个线程调用的run方法,就由哪个线程负责执行。
总的来说,Thread类的对象实例对应着操作系统实际存在的一个线程,该对象实例负责提供给用户去操作线程、获取线程信息。start方***调用native修饰的本地方法start0,最终在操作系统中启动一个线程,并会在本地方法中调用线程对象实例的run方法。所以,调用run方法并不会启动一个线程,它只是作为线程对象等着被调用。
(3)、join方法
是否为static方法:否。
作用:用于同步,可以使用该方法让线程之间的并行执行变为串行执行。
有代码如下:
/** * 主类 */ public class Join { public static void main(String[] args) throws InterruptedException { Task task = new Task(); Thread t1 = new Thread(task,"耗子尾汁"); //启动线程 t1.start(); //主线程打印 for(int i = 0; i < 4; i++){ if (i == 2) { //join方法:使main线程与t1线程同步执行,即t1线程执行完,main线程才会继续 t1.join(); } //Thread.currentThread().getName():获取当前线程的名称 System.out.println("【"+Thread.currentThread().getName()+"】" + i); } } } /** * Runnable接口实现类 */ class Task implements Runnable{ @Override public void run() { for(int i = 0; i < 3; i++){ System.out.println("【"+Thread.currentThread().getName()+"】"+i); } } }
其输出如下:
【main】0 【main】1 【耗子尾汁】0 【耗子尾汁】1 【耗子尾汁】2 【耗子尾汁】3 【main】2 【main】3
在上面的代码中,创建了一个命名为“耗子尾汁”的线程,并通过start方法启动。主线程和“耗子尾汁”线程都有循环打印i的任务。在“耗子尾汁”线程启动后,就会进入运行状态(Runnable),等待CPU调度,以获得CPU使用权来打印i。而主线程在执行“耗子尾汁”线程的start方法后,就会继续往下执行,循环打印i。正常来讲,主线程和“耗子尾汁”线程应该处于并行执行的状态,即二者会各自执行自己的for循环。但由于在主线程的for循环中调用了join方法,使得主线程交出了CPU的控制权,并返回到“耗子尾汁”线程,等待该线程执行完毕,主线程才继续执行。所以join方法就相当于在主线程中同步“耗子尾汁”线程,使“耗子尾汁”线程执行完,才会继续执行主线程。其最终效果就是可以使用该方法让线程之间的并行执行变为串行执行。
join方法是可以传参的。join(10)的意思就是,如果在A线程中调用了B线程.join(10),那么A线程就会同步等待B线程10毫秒,10毫秒后,A、B线程就会并行执行。
同时也要注意,只有线程启动了,调用join方法才有意义。在上述代码中,如果“耗子尾汁”线程没有调用start方法来启动,那么join并不会起作用。
(4)、getId方法、getName方法、setName方法
是否为static方法:均为否。
作用:
①getId方法:获取线程长整型的id、这个线程id是唯一的。
②getName方法:获取线程名
③setName(String):设置线程名
(5)、getPriority方法、setPriority(int)方法
是否为static方法:均为否。
作用:
①setPriority(int)方法:设置线程的优先级,优先级的范围为1-10。
②getPriority方法:获取线程的优先级。
现在的主流操作系统(windows、Linux等)基本都采用了时分的形式来调度运行线程,即将CPU的时间分为一个个时间片(这些时间片相等的),线程会得到若干时间片,时间片用完就会发生线程调度,并等待下一次的分配。线程优先级就是决定线程需要多或者少分配一些时间片。
Java线程的优先级范围为1-10,默认优先级为5。优先级高的线程分配的时间片的数量要都多于优先级低的线程。可通过setPriority(int)方法来设置。频繁阻塞的线程(比如I/O操作或休眠)的线程需要设置较高优先级,而计算任务较重(比如偏向运算操作或需要较多CPU时间)的线程则设置较低优先级,以避免CPU会被独占。
需要注意的是,Java线程的优先级设置只能给操作系统建议,并不能直接决定线程的调度,Java线程的调度只能由操作系统决定。操作系统完全可以忽略Java线程的优先级设置。在不同的操作系统上Java线程的优先级会存在差异,一些操作系统会直接无视优先级的设置。所以一些在逻辑上有先后顺序的操作,不能依靠设置Java线程的优先级来完成。
Java子线程的默认优先级与父线程的优先级一致,例如在main方法中创建线程,那么主线程(默认为5)就是这个新线程的父线程,该新线程的默认优先级为父线程的优先级。如果给主线程设置优先级为4,那么这个新线程的默认优先级就为4。
(6)、getState()方法、isAlive()方法
是否为static方法:均为否。
作用:
①getState()方法:获取线程的状态(NEW、RUNNABLE、WATING、BLOCKED、TIME_WATING、TERMINATED)
②isAlive()方法:判断线程是否存活,即是否线程已启动但尚未终止((还没有运行完
毕))。
(7)、interrupt()方法
是否为static方法:否。
作用:中断线程,当A线程运行时,B线程可以通过A线程的对象实例来调用A线程的interrput()方法设置线程A的中断标志位true,并立即返回。设置中断仅仅是设置标志,通过设置中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。如果打断的是正在运行中的线程,那么该线程就会被设置中断标志。但如果线程正在执行sleep方法或者上面所说的join方法时,被调用了interrupt方法,那么这个被打断的线程会抛出出 InterruptedException异常,并清除打断标志。
(8)、interrupted()方法、isInterrupted()方法
是否为static方法:interrupted为static方法、isInterrupted为非static方法
作用:均为判断线程是否被打断。区别在于interrupted()方***清除中断标记,isInterrupted()方法不会清除中断标志。
说明:在interrupted()方法内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
(9)、sleep(long n)方法
是否为static方法:是。
作用:让线程休眠,当一个执行中的线程调用sleep方法后,该线程就会挂起,并把剩下的CPU时间片交给其他线程,但并不会直接指定由哪个线程占用,需要操作系统来进行调度。线程在休眠期间不参与CPU调度,但也不会把线程占有的其他资源(比如锁)进行释放。
需要注意的是,休眠时间到后线程也并不会直接继续执行,而是进入等待CPU调度的状态。同时由于sleep方法是静态方法,使用t.sleep()并不会让t线程进入休眠,而是让当前线程进入休眠(比如在main方法中调用t.sleep(),实际上是让主线程进入休眠)。
(10)、yield() 方法
是否为static方法:是。
作用:使线程让出CPU控制权。实际上该方法只是向操作系统请求让出自己的CPU控制权,但操作系统也可以选择忽略。线程调用该方法让出CPU控制权后,会进入就绪状态,也有可能遇到刚让出CPU控制权后又被CPU调度执行的情况。