线程是进程中的一个单一的顺序控制流,因此单个进程可以拥有多个并发执行的任务。
线程模式为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。在使用线程时,CPU将轮流给每个任务分配其占用时间。
线程可以驱动任务,因此你需要一种描述任务的方式。这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。
package com.fallsown.fallsownthread; /** * @author: 红烧鲈鱼 * @date: 2021/4/13 */ public class LiftOff implements Runnable{ protected int countDown = 10; private static int taskCount = 0; private final int id = taskCount++; public LiftOff(){} public LiftOff(int countDown){ this.countDown = countDown; } public String status(){ return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + ")."; } public void run(){ while(countDown-- > 0){ System.out.println(status()); Thread.yield(); } } }
标识符id可以用来区分任务的多个实例,它是final的,因为它一旦被初始化之后就不希望被修改。
任务的run()方法通常总会有某种形式的循环,使得任务可以一直运行下去直到不需要,所以要设定跳出循环的条件(由一种选择是直接从run()返回)
通常run()被写成无线循环的形式,这意味着除非由某个条件使得run()终止,否则它将永远运行下去。
在run()中对静态方法Thread.yield()的调用是对线程调度器(Java线程机制的一部分,可以将cpu从一个线程转移给另一个线程)的一种建议。它在声明“我已经执行完声明周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机。”
这完全是选择性的,但是这里使用它会在这些实例中产生更加有趣的输出。你更有可能会看到任务换进换出的证据。
public class MainThread{ public static void main(String[] args){ LiftOff launch = new LiftOff(); launch.run(); } }
将Runnable导出一个类,它必须具有run()方法,但是这个方法没有特殊之处--它不会产生任何内在的线程能力。要实现线程行为,就必须显式将一个任务附在线程上。
将Runnable对象转变为工作任务的传统方式,是把它提交给一个Thread构造器,下面的示例展示了如何使用Thread来驱动LiftOff对象
package com.fallsown.fallsownthread; /** * @author: 红烧鲈鱼 * @date: 2021/4/13 */ public class BasicThreads { public static void main(String[] args){ Thread t = new Thread(new LiftOff()); t.start(); System.out.println("Waiting for LiftOff"); } }
Thread构造器只需要一个Runnable对象,调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。
尽管start()看起来是产生了一个对长期运行方法的调用。但是从输出中可以看到,start()迅速返回了。因为Waiting for LiftOff消息在倒计时完成之前就出现了。
实际上,你产生的是对LiftOff.run()的方法调用,并且这个方法还没有完成,但是因为LiftOff.run()是由不同的线程执行的,因此你任然可以执行main()线程中的其他操作。(这种能力并不局限于main()线程,任何线程都可以启动另一个线程)。main()和LiftOff.run()是程序中与其他线程“同时”执行的代码。
Java SE5的java.util.concurrent包中的执行器(Executor)将为你管理Thread对象。LiftOff对象知道如何运行具体的任务,与命令设计模式一样,它暴露了要执行的单一方法。
ExecutorService(具有服务生命周期的Executor,例如关闭)只得到如何构建恰当的上下文来执行Runnable对象。
在下面的示例中,CachedThreadPool将为每个任务都创建一个线程,注意ExecutorService对象是使用静态的Executor方法创建的,这个方法可以确定其Executor类型。
package com.fallsown.fallsownthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author: 红烧鲈鱼 * @date: 2021/4/13 */ public class CachedThreadPool { public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++){ exec.execute(new LiftOff()); } exec.shutdown(); } }
非常常见的是单个Executor被用来创建和管理系统中的所有任务。
对shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程(在本例中,即驱动main()的线程)将继续运行在shutdown()被调用之前提交的任务。这个程序将在Executor中所有任务完成之后尽快退出。
可以很容易地将前面示例中地CachedThreadPool替换为不同类型地Executor,FixedThreadPool使用了有限地线程集来执行所提交的任务。
package com.fallsown.fallsownthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author: 红烧鲈鱼 * @date: 2021/4/13 */ public class CachedThreadPool { public static void main(String[] args){ ExecutorService exec = Executors.newFixedThreadPool(5); for(int i = 0; i < 5; i++){ exec.execute(new LiftOff()); } exec.shutdown(); } }
有了FixedThreadPool,就可以一次性预先支付代价高昂的线程分配。因而可以限制线程数量。
在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程,也可以如你所愿地尽快得到服务。你不会滥用可获得地资源,因为FixedThreadPool使用Thread对象地数量是有界地。
CachedThreadPool在程序执行过程中通常会创建与所需数量相同地线程,然后再它回收旧线程时停止创建新线程。因此它是合理地Executor的首选。只有当这种方式引发问题的时候,你才需要切换到FixedThreadPool
SingleThreadExecutor就像是线程数量为1的FixedThreadPool,这对于你希望在另一个线程中连续运行的任何事务(长期存活的任务)来说,都是很有用的。例如监听进入的套接字连接的任务,它对于希望在线程中运行的短任务也同样很方便。例如,更新本地或远程日志的小任务,或者是事件分发线程。
如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束。所有的任务将使用相同的线程。
SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己(隐藏)的悬挂任务队列
它还提供了一种重要并发机制,其他线程不会(即没有两个线程会)被并发调用。这会改变任务的加锁需求。
Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成时能返回一种值,那么可以实现Callable接口而不是Runnable接口
在Java SE5中引入的Callabel是一种具有类型参数的泛型。它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它,下面是一种简单示例:
package com.fallsown.fallsownthread; import java.util.ArrayList; import java.util.concurrent.*; /** * @author: 红烧鲈鱼 * @date: 2021/4/13 */ class TaskWithResult implements Callable<String>{ private int id; public TaskWithResult(int id){ this.id = id; } public String call(){ return "result of TaskWithResult " + id; } } public class CallableDemo { public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future<String>> results = new ArrayList<>(); for(int i = 0; i < 10; i++){ results.add(exec.submit(new TaskWithResult(i))); } for(Future<String> fs : results) try { System.out.println(fs.get()); }catch (InterruptedException e){ System.out.println(e); return; }catch (ExecutionException e){ System.out.println(e); }finally { exec.shutdown(); } } }
submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。你可以用isDone()方法来查询Future是否已经完成。
当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果,你也可以不用isDone()进行检查就直接调用get(),在这种情况下get()将会阻塞,直至结果准备就绪。
你还可以在试图调用get()来获取结果之前,先调用具有超时的get(),或者调用isDone()来查看任务是否完成
影响任务行为的一种简单方法就是调用sleep(),这将使任务终止执行给定的事件,在LiftOff类中,要是把对yield()的调用换成是调用sleep(),将得到如下结果。
package com.fallsown.fallsownthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @author: 红烧鲈鱼 * @date: 2021/4/13 */ public class SleepingTask extends LiftOff { @Override public void run(){ try{ while (countDown-- > 0){ System.out.println(status()); TimeUnit.MILLISECONDS.sleep(100); } }catch (InterruptedException e){ System.out.println("Interrupted"); } } public static void main(String[] args){ ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++){ exec.execute(new SleepingTask()); } exec.shutdown(); } }
对sleep()的调用可以抛出InterruptedException异常,并且你可以看到,它在run()中被捕获。因为异常不能跨线程传播回main(),所以你必须在本地处理所有在任务内部出现的异常
Java SE5引入了更加显式的sleep()版本,作为TimeUnit类的一部分,就像上面示例所示的那样,这个方法允许你指定sleep()延迟的事件单元。因此可以提供更好的可阅读性。
TimeUnit还可以被用来执行转换。
线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理器现有线程集的顺序是不确定的,但是调度器将倾向于让优先权最高的线程先执行。然而这并不是意味着优先权较低的线程将得不到执行(也就是说优先级不会造成死锁)。优先级较低的线程仅仅是执行的频率较低。
在绝大部分事件中,所有线程都应该以默认的优先级运行,试图操作优先级通常是一种错误。
下面是一个演示优先级等级的示例,你可以用getPriority()来读取现有线程的优先级,并且在任何时刻都可以通过setPriority()来修改它
package com.fallsown.fallsownthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ public class SimplePriorities implements Runnable{ private int countDown = 5; private volatile double d; private int priority; public SimplePriorities(int priority){ this.priority = priority; } public String toString(){ return Thread.currentThread() + ": " + countDown; } public void run(){ // 在run()中设置优先级 Thread.currentThread().setPriority(priority); while (true){ for(int i = 1; i < 100000; i++){ d += (Math.PI + Math.E) / (double)i; if(i % 1000 == 0) Thread.yield(); System.out.println(this); if(--countDown == 0) return; } } } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { exec.execute(new SimplePriorities(Thread.MIN_PRIORITY)); } exec.execute(new SimplePriorities(Thread.MAX_PRIORITY)); exec.shutdown(); } }
toString()方法被覆盖,以便使用Thread.toString()方法来打印自己的线程名称。
通过调用Thread.currentThread()来获得对驱动该任务的Thread对象的引用
在run()中执行了100000次开销相当大的浮点运算。包括double类型的加法与除法。变量d是volatile的,以努力确保不进行任何编译器优化。如果没有加如这些运算的话。就看不到设置优先级的效果。
不同系统的优先级不同,所以建议只使用
MAX_PRIORITY
NORM_PRIORITY
MIN_PRIORITY
如果知道已经完成了在run()方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:你的工作已经做的差不多了,可以让别的线程使用cpu了。
这个暗示通过yield()方法来作出(不过这是一个暗示,没有任何机制保证它会被采纳)。当调用yield()时,你也是在建议具有相同优先级的其他线程可以运行。
LiftOff.java使用yield()在各种不同的LiftOff任务之间产生分布良好的处理机制。尝试着注释掉LiftOff.run()中的Thread.yield()其实没啥区别。
是指在程序运行的时候在后台提供一种通用服务的线程。并且这种线程并不属于程序中不可或缺的部分。
因此当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中所有后台线程。
反过来说,只要有非后台线程还在运行,程序就不会dead。执行main()就是一个非后台线程
package com.fallsown.fallsownthread; import java.util.concurrent.TimeUnit; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ public class SimpleDaemons implements Runnable{ public void run(){ try{ while(true){ TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread() + " " + this); } }catch (InterruptedException e){ System.out.println("sleep() Interrupted"); } } public static void main(String[] args) throws Exception{ for(int i = 0; i < 10; i++){ Thread daemon = new Thread(new SimpleDaemons()); // 设置为后台进程 daemon.setDaemon(true); daemon.start(); } System.out.println("All daemons started"); TimeUnit.MILLISECONDS.sleep(175); } }
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
一旦main()完成其工作,就没什么能阻止程序终止了,因为除了后台线程之外,没有线程在运行了。main()线程被设定为短暂睡眠。
SimpleDaemons.java创建了显式的线程,以便可以设置它们的后台标志。通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(后台、优先级、名称);
package com.fallsown.fallsownthread; import java.util.concurrent.ThreadFactory; /** * 交给线程工厂去创建线程 * @author: 红烧鲈鱼 * @date: 2021/4/14 */ public class DaemonThreadFactory implements ThreadFactory { public Thread newThread(Runnable r){ Thread t = new Thread(r); t.setDaemon(true); return t; } }
之后就可以使用
package com.fallsown.fallsownthread; import java.util.concurrent.TimeUnit; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ public class SimpleDaemons implements Runnable{ public void run(){ try{ while(true){ TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread() + " " + this); } }catch (InterruptedException e){ System.out.println("sleep() Interrupted"); } } public static void main(String[] args) throws Exception{ ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory); for(int i = 0; i < 10; i++){ exec.execute(new DaemonFromFactory()) } System.out.println("All daemons started"); TimeUnit.MILLISECONDS.sleep(175); } }
但是后台进程由于非后台线程的限制,会导致突然死亡,导致finally语句不会被执行。
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。
如果某个线程在另一个线程t上调用t.join(),此线程将会被挂起,直到目标线程结束才恢复。
也可以在调用join()时带上一个超时参数(单位可以是毫秒或纳秒),这样如果目标线程在这段时间到期还没有结束的话join()方法总能返回
对join方法的调用可以被终端,做法就是在调用线程上调用interrupt()方法,这时需要用到try-catch
package com.fallsown.fallsownthread; import java.util.StringJoiner; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ class Sleeper extends Thread{ private int duration; public Sleeper(String name,int sleepTime){ // 创建线程名称 super(name); duration = sleepTime; // 运行本线程 start(); } public void run(){ try{ sleep(duration); }catch (InterruptedException e){ System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted()); return; } System.out.println(getName() + " has awakened"); } } class Joiner extends Thread{ private Sleeper sleeper; public Joiner(String name,Sleeper sleeper){ super(name); this.sleeper = sleeper; // 开始任务 start(); } public void run(){ try { sleeper.join(); }catch (InterruptedException e){ System.out.println("Interrupted"); } System.out.println(getName() + " join completed"); } } public class Joining { public static void main(String[] args){ Sleeper sleepy = new Sleeper("Sleepy",1500), grumpy = new Sleeper("Grumpy",1500); Joiner doper = new Joiner("Dopey",sleepy), doc = new Joiner("Doc",grumpy); // grumpy被中断 grumpy.interrupt(); } }
Sleeper是一个Thread类型,它要休眠一段时间,这段时间由构造函数来指定。
在run()中,sleep()方法由可能在指定时间期满时返回,但也可能被中断。
在catch()子句中,在异常被捕获的时候,这个标志总为假。除异常之外,这个标志还可用于其他情况,比如线程可能会检查其中断状态。
但是异常捕获的时候会清理这个标志,所以总是false
注意,Java SE5的java.util.concurrent类库包含诸如CyclicBarrier这样的工具,比join()更加适合。
使用线程的动机之一就是建立有响应的用户界面。
package com.fallsown.fallsownthread; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ class UnresponsiveUI{ private volatile double d = 1; public UnresponsiveUI() throws Exception{ while(d > 0){ d = d + (Math.PI + Math.E) / d; } System.in.read(); //never gets here } } public class ResonsiveUI extends Thread{ private static volatile double d = 1; public ResonsiveUI(){ setDaemon(true); start(); } public void run(){ while(true){ d = d + (Math.PI + Math.E) / d; } } public static void main(String[] args) throws Exception{ new ResonsiveUI(); System.in.read(); System.out.println(d); } }
UnresponsiveUI在一个无线的while循环里执行运算,显然程序不可能到达读取控制台输入的哪一行(编译器被欺骗了,相信while的条件使得程序能到达读取控制台那一行)
如果把建立UnresponsiveUI的那一行的注释解除掉再运行程序,那么要终止它,就只能杀死这个进程。
要想让程序有相应,就得把计算程序放在run()方法中,这样就能让出处理器给别的程序。
没啥屌用 java编程思想 1146页
由于线程的本质特征,导致无法从线程中捕获逃逸的异常。一旦异常掏出任务的run()方法,这样就会向外传播到控制台
再Java SE5之前,通过使用线程组来捕获这些异常。
但是在Java SE5之后则可以用Executor来解决这个问题。
package com.fallsown.fallsownthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ public class ExceptionThread implements Runnable { public void run(){ throw new RuntimeException(); } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new ExceptionThread()); } }
"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" "-javaagent:D:\softwareHome\IDEA\IntelliJ IDEA 2018.3.5\lib\idea_rt.jar=50808:D:\softwareHome\IDEA\IntelliJ IDEA 2018.3.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\ideaSave\fallsownTest01\out\production\fallsown;E:\chromeDownload\xom-1.3.7.jar" com.fallsown.fallsownthread.ExceptionThread Exception in thread "pool-1-thread-1" java.lang.RuntimeException at com.fallsown.fallsownthread.ExceptionThread.run(ExceptionThread.java:12) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
为了解决这个问题,我们需要修改Executor产生线程的方式。Thread.UncauthtExceptionHandler是Java SE5中的新接口
它允许你在每个Thread对象上都附着一个异常处理器。
Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而面临死亡的时候被调用
为了使用它,我们创建一个新类型ThreadFactory,它将每个创建的Thread对象上附着一个Thread.UncaughtExceptionHandler,我们将这个工厂传递给Executors创建新的ExecutorService的方法
package com.fallsown.fallsownthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; /** * @author: 红烧鲈鱼 * @date: 2021/4/14 */ class ExceptionThread2 implements Runnable{ public void run(){ Thread t = Thread.currentThread(); System.out.println("run() by " + t); System.out.println("eh = " + t.getUncaughtExceptionHandler()); throw new RuntimeException(); } } class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{ public void uncaughtException(Thread t, Throwable e){ System.out.println("caught " + e); } } // 继承于线程工厂 class HandlerThreadFactory implements ThreadFactory{ public Thread newThread(Runnable r){ System.out.println(this + " creating new Thread"); Thread t = new Thread(r); System.out.println("created " + t); t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); System.out.println("eh = " + t.getUncaughtExceptionHandler()); return t; } } public class CaptureUncaughtException { public static void main(String[] args) { // CachedThreadPool中的参数是使用这个线程工厂构造器 ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory()); exec.execute(new ExceptionThread2()); } }
在程序中设置了额外的跟踪机制,用来验证工厂创建的线程会传递给UncaughtExceptionHandler。
现在可以看到,未捕获异常是通过uncaughtException来捕获的。
public class SettingDefaultHandler{ Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new ExceptionThread()); }