一、JAVA线程实现/创建方式
在Java中,线程的创建与实现主要有三种方式:继承Thread类、实现Runnable接口以及使用Callable接口配合FutureTask和ExecutorService。下面将分别介绍这三种方式,并通过实际案例代码进行演示。
1. 继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
public class MyThread extends Thread {public void run() {System.out.println("MyThread.run()");}
}
MyThread myThread1 = new MyThread();
myThread1.start();
2. 实现Runnable接口
实现Runnable接口并重写其run()方法,然后将该对象作为参数传递给Thread类的构造方法,也可以创建并启动一个线程。这种方式更加灵活,因为Java不支持多继承,但可以实现多个接口。
public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("MyRunnable is running: " + i); try { Thread.sleep(1000); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 // 主线程代码与上面案例相同 }
}
3. 使用Callable接口配合FutureTask和ExecutorService
这种方式可以实现有返回值的线程,并且可以处理异常。
- Callable接口类似于Runnable接口,但call()方法可以有返回值并且可以抛出异常;
- FutureTask是Callable接口的实现类,ExecutorService可以管理线程的启动、执行和关闭。
import java.util.concurrent.*; public class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; Thread.sleep(10); // 模拟耗时操作 } return sum; } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newSingleThreadExecutor(); Future<Integer> future = executorService.submit(new MyCallable()); // 主线程可以继续执行其他任务 // 当需要获取子线程的计算结果时 Integer result = future.get(); // get()方法会阻塞,直到子线程执行完毕并返回结果 System.out.println("Result from MyCallable: " + result); executorService.shutdown(); // 关闭线程池 }
}
以上三种方式各有优缺点,可以根据具体需求选择合适的方式来创建和管理线程。在实际应用中,通常会选择使用线程池(如ExecutorService)来管理线程,以提高系统的性能和稳定性。
二、JAVA 线程池 ExecutorService
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
三、4种线程池
1. newFixedThreadPool:固定大小的线程池。
- 特点:可以控制最大并发数,即线程池中的线程数量是固定的。
- 使用场景:适用于需要控制并发数,且任务数量相对稳定的场景。
- 示例:
ExecutorService executorService = Executors.newFixedThreadPool(60);
2. newCachedThreadPool:可缓存的线程池。
- 特点:线程池大小不受限,可以根据需要动态地创建和销毁线程。如果线程池中的线程空闲时间超过指定时间,则会被销毁;如果线程池中的线程不足,则会创建新的线程。
- 使用场景:适用于执行大量短时间异步任务的情况,可以减少因创建和销毁线程所带来的开销。
- 示例:
ExecutorService executorService = Executors.newCachedThreadPool();
3. newScheduledThreadPool:定时和周期执行任务的线程池。
- 特点:支持定时及周期性任务执行。
- 使用场景:适用于需要定时或周期性执行任务的场景,如定时发送邮件、定时检查数据库等。
- 示例:
// 创建一个固定大小的ScheduledExecutorService线程池,大小为3 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); // 延迟3秒后执行一次任务 scheduledThreadPool.schedule(new Runnable() { @Override public void run() { System.out.println("延迟三秒"); } }, 3, TimeUnit.SECONDS); // 初始延迟1秒后,每三秒执行一次任务 scheduledThreadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("延迟1秒后每三秒执行一次"); } }, 1, 3, TimeUnit.SECONDS); // 注意:通常在实际应用中,您可能需要保持对ScheduledExecutorService的引用, // 并在不再需要时调用shutdown()或shutdownNow()来关闭线程池。 // 例如: // scheduledThreadPool.shutdown(); // 当不再需要线程池时调用
4. newSingleThreadExecutor:单线程化的线程池。
- 特点:只有一个线程来执行任务,保证所有任务按照指定顺序(FIFO,先进先出)执行。
这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
- 使用场景:适用于需要保证任务顺序执行的场景,如数据导入、日志处理等。
- 示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
四、JAVA 线程生命周期(状态)
在Java中,线程的生命周期可以描述为一系列的状态转换。线程的状态是由线程类(java.lang.Thread)中的枚举类型Thread.State来定义的。以下是线程可能经历的主要状态:
1. NEW(新建):
- 当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值;
- 此时,线程还没有开始执行。可以通过调用线程的start()方法使其进入就绪状态。
2. RUNNABLE(就绪状态):
- 当线程调用start()方法后,它进入RUNNABLE状态。
- 线程调度器负责将处于RUNNABLE状态的线程转换为运行状态,并分配CPU时间片给它执行。
- 如果线程在run()方法中调用了一个阻塞的I/O操作,或者调用了sleep(), wait(), join()等使线程暂停的方法,线程将不会立即执行。但是,从线程状态的角度看,它仍然处于RUNNABLE状态。
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 线程方法
线程在Java中的常用方法主要包括以下几种:
- start():
启动线程,使线程进入就绪状态(Runnable),等待获取CPU的执行权。当线程对象调用此方法时,Java虚拟机调用该线程的run()方法。 - run():
线程在被调度时执行的操作。通常我们会在自己的线程类中重写此方法,包含线程需要执行的任务。 - sleep(long millis):
使当前线程(即调用该方法的线程)暂停执行一段时间(以毫秒为单位),让其他线程有机会继续执行。但此方法不会释放对象锁。调用此方法时需要处理InterruptedException。 - yield():
暂停当前正在执行的线程对象,并执行其他线程。但具体哪个线程会执行是取决于操作系统的线程调度器的。此方法只能让同优先级的线程有执行的机会。 - join():
在线程A中调用线程B的join()方法,则线程A会等待线程B执行完毕后,再继续执行。这常常用于确保某些线程(如后台工作线程)在其他线程(如主线程)之前结束。 - interrupt():
中断线程。线程的中断并不是强制停止线程,而是设置一个中断状态。线程需要自行检查该状态,并据此决定是否停止执行。 - isInterrupted():
检测当前线程是否已经中断。 - isAlive():
测试线程是否处于活动状态。当线程启动后、尚未终止之前,即处于活动状态。 - currentThread():
返回对当前正在执行的线程对象的引用。 - wait()、notify()、notifyAll():
这三个方法都必须在同步方法或同步代码块中使用,因为它们是Java对象锁的一部分。
wait()会使当前线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法;
notify()会唤醒在此对象监视器上等待的单个线程;
notifyAll()会唤醒在此对象监视器上等待的所有线程。
需要注意的是,虽然suspend()和resume()这两个方法也曾用于控制线程的执行,但它们因为容易导致死锁,已经被废弃,不再推荐使用。