Java 并发编程
Java 创建多线程的方式
-
继承 Thread 类
// 自定义线程类,继承 Thread 类 public class MyThread extends Thread{ // 线程的运行逻辑 public void run(){ System.out.println("My Thread run ... "); } } // 定义测试类 public class MyThreadTest{ public static void main(String[] args){ // 实例化自定义的线程对象 MyThread myThread = new MyThread(); // 调用 start 方法启动线程 myThread.start(); } }
-
实现 Runnable 接口
// 自定义线程类,实现 Runnable 接口,覆写 run 方法 public class MyThread implements Runnable{ @Override public void run(){ System.out.println("My Thread run ..."); } } // 定义测试类 public class MyThreadTest(){ public static void main(String[] args){ // 实例化自定义的线程类对象 MyThread myThread = new MyThread(); // 创建线程对象,并传入线程实例 Thread thread = new Thread(myThread) // 调用 start 方法启动线程 thread.start(); } }
当然,也可以用匿名内部类或者 Lambda 表达式来写,更加方便
new Thread(() -> { System.out.println("myThread"); }).start(); new Thread(new Runnable(){ @Override public void run(){ System.out.println("myThread"); } }).start();
-
基于线程池
使用线程池来管理线程,可以最大限度的利用线程资源。
JVM 会先根据用户的参数创建指定数量的可运行线程任务,并将其放入队列中,
在线程创建后启动这些任务,如果正在运行的线程数量超过用户设置的最大线程数量,则超出的线程排队等候,
在有线程执行完成后,线程池调度器会从队列中取出一个等待的任务并执行public class MyThreadByThreadPool { @Test public void myThreadByThreadPool(){ // 创建线程池,初始化大小 ExecutorService pool = Executors.newFixedThreadPool(10); // 循环提交多个线程任务 for (int i = 0; i < 10; i ++){ pool.execute(() -> System.out.println(Thread.currentThread().getName() + "running")); } } }
-
通过
ExecutorService
和Callable<Class>
实现适用于有返回值得线程。比如有时候需要并发执行一个任务,开启多个线程,然后收集各个线程执行返回的结果并将最终的结果汇总起来。
/** * 创建线程,实现 Callable 接口 */ class MyCallable implements Callable<String> { private String name; // 构造函数为线程传参 public MyCallable(String name){ this.name = name; } /** * 覆写 call 方法,编写线程实现逻辑 * @return * @throws Exception */ @Override public String call() throws Exception { return name; } }
public class MyThreadByCallable{ @Test public void myThreadByCallableTest() throws ExecutionException, InterruptedException { // 创建一个线程池,指定大小 5 ExecutorService pool = Executors.newFixedThreadPool(5); // 创建多个有返回值的任务 List<Future> list = new ArrayList<>(); for (Future item : list){ Callable callable = new MyCallable(item.toString() ); // 提交线程,获取 Future 对象并将其保存到 list 中 Future future = pool.submit(callable); System.out.println("Submit a callable thread ..."); list.add(future); } // 关闭线程池,等待线程执行结束 pool.shutdown(); // 获取所有线程的运行结果 for (Future future: list){ String result = future.get().toString(); System.out.println(result); } } }
常用的线程池
-
newCachedThreadPool
缓存线程池,线程会在任务完成后,继续在缓存区中保留一定时间(默认 60s),创建新线程时如果缓存区中有可重用线程,则重用它们。对于任务量大,但执行时间较短的场景非常适用。
-
newFixedThreadPool
最普遍的线程池。创建固定线程数量的线程池,并将线程资源存放在队列中循环使用。
-
newScheduledThreadPool
可定时调度的线程池。可以设置在给定的延迟时间后执行或者定期指定某线程任务。
-
newSingleThreadExecutor
该线程池永远保证有且只有一个可用线程,如该线程终止,则线程池会立即补充一个新线程替代之。
-
newWorkStealingPool
JDK 1.8 新增的线程池。创建足够大小的线程池,通过创建多个队列来减少各线程调度产生的竞争,这里的足够大指的是最大程度的利用系统资源,申请足够的线程数。
线程生命周期
- 线程的生命周期分为新建(new)、就绪(Runnable)、运行(Running)、死亡(Dead)、阻塞(Blocked) 5 种状态
- 线程调用
new
方法后,处于新建状态 - 线程调用
start
方法后,处于就绪状态(此时,并非立即运行,只能说明该线程可运行) - 线程调用
run
方法后,处于运行状态(就绪状态的线程竞争到 CPU 的使用权后,会自动执行 run 方法进入运行状态,执行线程中的逻辑代码) - 线程运行状态中,被动(I/O阻塞、等待同步锁、等待通知) 或者 主动(调用sleep方法、调用suspend方法) 的放弃 CPU 使用权,会进入阻塞状态,进入阻塞状态后,需要 重新竞争(I/O成功返回结果、获得同步锁、收到通知、sleep时间到、调用resume方法) CPU 资源成功后才可重新进入运行状态。
- 线程运行状态中,也可以直接让线程 重回就绪状态(调用yield方法)。
- 线程运行状态中,被动(Error、Exception、run/call方法执行完成) 或者 主动(调用stop方法) 的结束运行状态,会进入死亡状态,进入死亡状态后,线程结束,无法再次唤醒。
线程的状态与方法
上面说到线程生命周期的 5 种状态,而线程的状态,除了这 5 个生命周期之外,还有 3 个状态:等待(Waiting)、超时等待(Time Waiting)、就绪(Ready)
线程常用的方法并不多:
- start
开启线程,使线程进入就绪状态 - run
运行线程,使线程进入运行状态 - wait
等待,使线程进入等待状态 - sleep
休眠,使线程进入超时等待状态。与 wait 方法不同的是,sleep 方法不会释放当前占有的锁,wait 方法会释放对象的锁;sleep 方法会在休眠指定时间后自动恢复运行状态,wait 方法会持续等待直到其他线程将其唤醒或者中断
-yield
释放CPU资源,使线程重回就绪状态,重新与其他就绪状态的线程一起竞争CPU的使用权 - interrupt
中断,向线程发送中断信号。只会影响一个中断标识,并不会真正改变线程的状态,最终线程的状态需要等待接收到的中断标识的程序处理。 - join
加入,通俗的来将,就像插队。如果线程1调用了 join 线程2 的方法,那么线程1会进入阻塞状态,等待线程2执行完成后,线程1才会转为就绪状态继续等待获取CPU的使用权。通常应用于某一线程的逻辑需要依赖于其他子线程的场景下。 - notify
唤醒,使线程从等待状态进入就绪状态 - setDaemon
设置为守护线程。守护线程是一种特殊的线程,其依赖于 JVM,如果 JVM 中只剩下守护线程,那么 JVM 就会退出 ,JVM 的垃圾回收期(GC)就是典型的守护线程。
终止线程
-
自动终止(线程运行完成后自动结束)
-
stop 方法( 极不推荐 )
Thread.stop
方法强行终止线程,会抛出ThreadDeatherror
错误,并释放所有锁,从而导致被锁保护的资源因突然释放而出现不一致或者数据丢失 -
boolean 型变量控制退出
使用一个 volatile 修饰的 boolean 型变量,控制退出public class MyThread extends Thread { public volatile boolean exit = false; while(!exit){ // TODO if(退出条件) exit = true; } }
代码看上去逻辑非常简单,至于为什么强调使用 volatile 关键字修饰,是因为这里需要利用 volatile 关键字的特性来保证同一时间只有一个线程可以修改 exit 的值。(volatile 的特性之前在)单例设计模式的双重锁校验部分已经做过一次介绍,点此可以直接跳转查看
-
interrupt 方法
调用interrupt
方法会使线程的中断标识设置为 true,切记,并不是立即终止线程,仅仅是设置其中断标识,然后使用isInterrupted
方法循环判断线程中断标志,等到为 true 时会等待执行线程资源释放完成后安全退出线程。需要注意的是,如果线程处于阻塞状态,调用
interrupt
会抛出InterruptedException
异常,需要在 catch 语句中捕获该异常并break
跳出循环,才可以安全退出线程。public class MyThread extends Thread { public void run (){ // 正常情况下,通过判断 isInterrupted 的值来决定是否退出 while(!isInterrupted()) // 如果 isInterrupted 的值为 false,再判断是否有阻塞情况下异常 try{ Thread.sleep(1000); } catch(InterruptedException e) { break; // 跳出循环 } } }