1、Java线程的创建方式
常见的Java线程的4种创建方式:
- 继承Thread类
- 实现Runnable
- 通过ExecutorService和Callable<Class>实现由返回值的线程
- 基于线程池
1.1 继承Thread类
Thread类实现Runnable接口并定义了操作线程的一些方法,可以通过继承thread类的方式创建一个线程。
具体代码如下:
(1)通过继承thread类创建NewThread线程:
public class NewThread extends Thread { public void run(){ //具体业务逻辑实现 System.out.println("创建一个新线程"); } }
(2)实例化Newthread并启动:
public class Main { public static void main(String[] args) { NewThread newThread = new NewThread(); newThread.start(); } }
1.2 实现Runnable接口
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。为了实现 Runnable,一个类只需要执行一个方法调用 run()。
具体代码如下:
(1)通过实现Runnable接口的方式创建MyRunnable线程:
public class MyRunnable implements Runnable{ public void run() { System.out.println("通过Runnable创建一个线程"); } }
(2)实例化MyRunnable对象,并创建一个线程对象传入已经实例化MyRunnable实例:
public class Main { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); } }
1.3 通过ExecutorService和Callable<Class>实现有返回值的线程
在主线程中开启多个线程并发执行一个任务,收集各个线程执行返回的结果并将最终结果汇总起来,就应该使用Callable接口。
具体代码如下:
(1)通过Callable接口创建MyCallable线程:
import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { private String name; public MyCallable(String name){ this.name = name; }; public String call() throws Exception { //call方法内实现逻辑 return name; } }
(2)创建一个线程池接收MyCallable实例:
public class Main { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建一个固定大小为5的线程池 ExecutorService pool = Executors.newFixedThreadPool(5); //创建多个有返回值的任务列表 List<Future> list = new ArrayList<Future>(); for (int i = 0; i < 5; i++) { //创建一个有返回值的线城实例 Callable callable = new MyCallable(i + ""); //提交线程,获取Future对象 Future future = pool.submit(callable); System.out.println("提交Callable线程" + i); list.add(future); } pool.shutdown(); for (Future future:list) { System.out.println("获得Callable线程结果"+future.get().toString()); } } }
结果:
提交Callable线程0
提交Callable线程1
提交Callable线程2
提交Callable线程3
提交Callable线程4
获得Callable线程结果0
获得Callable线程结果1
获得Callable线程结果2
获得Callable线程结果3
获得Callable线程结果4
1.4 基于线程池创建线程
为了避免浪费线程资源,可以使用线程池来创建线程。
具体代码如下:
package com.jzq.concurrent.Thread.ThreadPool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPool { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(7); //提交多个线程任务并执行 for (int i = 0; i < 7; i++) { threadPool.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"在运行"); } } ); } } }
结果:
pool-1-thread-1在运行 pool-1-thread-4在运行 pool-1-thread-3在运行 pool-1-thread-2在运行 pool-1-thread-5在运行 pool-1-thread-6在运行 pool-1-thread-7在运行
2、线程的生命周期
线程的生命周期分为新建、就绪、运行、阻塞和死亡这5种状态。
具体状态转换如图:
其流程如下:
- 调用new方法新建一个线程,这时线程处于新建状态。
- 调用一个start方法启动一个线程,这时线程处于就绪状态。
- 处于就绪状态的线程等待线程获取CPU资源,在等待其获取CPU资源后线程会执行run方法进入运行状态。
- 正在运行的线程在调用了yield方法或失去处理器资源时,会再次进入就绪状态。
- 正在执行的线程在执行了sleep方法、I/O阻塞、等待同步锁、等待通知、调用suspend方法等操作后,会挂起并进入阻塞状态,进入Blocked池。
- 阻塞状态的线程由于出现sleep时间已到、I/O方法返回、获得同步锁、收到通知、调用resume方法等情况,会再次进入就绪状态,等待CPU时间片的轮询。该线程在获取CPU资源后,会再次进入运行状态。
- 处于运行状态的线程,在调用run方法或call方法正常执行完成、调用stop方法停止线程或者程序执行错误导致异常退出时,会进入死亡状态。
2.1 新建状态:New
在Java中使用new关键字创建一个线程,新创建的线程将处于新建状态。在创建线程时主要是为线程分配内存并初始化其成员变量的值。
2.2 就绪状态:Runnable
新建的线程对象在调用start方法之后将转为就绪状态。此时JVM完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。
2.3 运行状态:Running
就绪状态的线程在竞争到CPU的使用权并开始执行run方法的线程执行体时,会转为运行状态,处于运行状态的线程的主要任务就是执行run方法中的逻辑代码。
2.4 阻塞状态:Blocked
运行中的线程会主动或被动地放弃CPU的使用权并暂停运行,此时该线程将转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到CPU使用权并转为运行状态。
阻塞状态分为三种:
- 等待阻塞:在运行状态的线程调用object.wait方法时,JVM会把该线程放入等待队列中,线程转为阻塞状态。
- 同步阻塞:在运行状态的线程尝试获取正在被其他线程占用的对象同步锁时,JVM会把该线程放入锁池中,此时线程转为阻塞状态。
- 其他阻塞:运行状态的线程在执行Thread.sleep(long ms)、Thread.join()或者发出I/O请求时,JVM会把该线程转为阻塞状态。直到sleep()状态超时、Thread.join()等待线程终止或超时,或者I/O处理完毕,线程才重新转为可运行状态。
2.5 死亡状态:Dead
线程在以下三种方式结束后转为死亡状态:
- 线程正常结束:run方法或call方法执行完成。
- 线程异常结束:运行中的线程抛出一个Error或未捕获的Exception,线程异常退出。
- 手动结束:调用线程对象stop方法手动结束运行中的线程(会导致锁混乱和死锁,不推荐)。
3、线程的基本方法
线程的基本方法有wait、notify、notifyAll、sleep、join、yield等,这些方法控制线程的运行,影响线程变化。
3.1 线程等待:wait方法
调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。在调用wait方法后会释放对象锁,会导致当前线程进入Waiting状态。
3.2 线程睡眠:sleep方法
调用sleep方法会导致当前线程休眠。sleep方法不会释放当前占用的锁,会导致线程进入Time-Waiting状态。
3.3 线程让步:yield方法
调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起竞争CPU时间片。
3.4 线程中断:interrupt方法
interrupt方法用于向线程发送一个终止通知信号,会影响该线程内部的一个中断标识位,线程并不会因为调用了interrupt方法而改变状态。状态的具体变化需要等待接收到中断标识的程序的最终处理结果来判定。
3.5 线程加入:join方法
join方法用于等待其他线程终止,如果在当前线程中调用一个线程join方法,则当前线程转为阻塞状态,等待另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取CPU的使用权。
3.6 线程唤醒:notify方法
notify方法用于唤醒在此对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。
3.7 后台守护线程:setDaemon方法
setDaemon方法用于定义一个守护线程,该线程是后台线程,其特征为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
3.8 终止线程的4种方式:
1、正常运行结束
指线程体执行完成,线程自动结束。
2、使用退出标志退出线程
一般情况下,在run方法执行完毕时,线程会正常结束。但有些线程需要长时间运行,只有在系统满足默写特殊条件后,才能触发关闭这些线程。
比如设置一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,代码如下:
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run(){ while (!exit){ //TODO 执行业务逻辑代码 } } }
在定义exit时使用了关键字volatile,volatile用于时exit线程同步安全。
3、使用interrupt方法终止线程
使用interrupt方法终止线程有以下两种情况:
(1)线程处于阻塞状态。在线程处于阻塞状态时,在调用线程的interrupt方法时,会抛出InterruptException异常,我们通过代码捕获异常,然后通过break跳出状态循环结束run方法。
(2)线程处于非阻塞状态。使用isInterrupted方法判断线程的中断标志来退出循环。调用interrupt方法时,中断标志会被设置为true,不能立刻退出线程,而是执行线程终止前的资源释放操作,等待资源释放完毕后退出该线程。
具体代码如下:
public class ThreadSafeDead extends Thread { public void run(){ while (!isInterrupted()){//在非阻塞过程中通过判断中断标志来退出 try { Thread.sleep(5*1000);// }catch(InterruptedException e){ e.printStackTrace(); break;//在捕获到异常后执行break跳出循环 } } } }
4、使用stop方法终止线程:不安全
在程序使用Thread.stop方法终止线程时,该线程的子线程会抛出ThreadDeatherror错误,并释放线程持有的所有锁。