异常处理
Java的异常
调用方获取调用失败的信息
约定返回错误码
处理一个文件,如果返回
0,表示成功,返回其他整数,表示约定的错误码:int code = processFile("C:\\test.txt"); if (code == 0) { // ok: } else { // error: switch (code) { case 1: // file not found: case 2: // no read permission: default: // unknown error: } }语言层面上提供一个异常处理机制、
try { String s = processFile(“C:\\test.txt”); // ok: } catch (FileNotFoundException e) { // file not found: } catch (SecurityException e) { // no read permission: } catch (IOException e) { // io error: } catch (Exception e) { // other error: }异常是
class,它的继承关系如下:┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Throwable │ └───────────┘ ▲ ┌─────────┴─────────┐ │ │ ┌───────────┐ ┌───────────┐ │ Error │ │ Exception │ └───────────┘ └───────────┘ ▲ ▲ ┌───────┘ ┌────┴──────────┐ │ │ │ ┌─────────────────┐ ┌─────────────────┐┌───────────┐ │OutOfMemoryError │... │RuntimeException ││IOException│... └─────────────────┘ └─────────────────┘└───────────┘ ▲ ┌───────────┴─────────────┐ │ │ ┌─────────────────────┐ ┌─────────────────────────┐ │NullPointerException │ │IllegalArgumentException │... └─────────────────────┘ └─────────────────────────┘Throwable有两个体系:Error表示严重的错误,程序对此一般无能为力,例如:OutOfMemoryError:内存耗尽NoClassDefFoundError:无法加载某个ClassStackOverflowError:栈溢出
Exception则是运行时的错误,它可以被捕获并处理某些异常是应用程序逻辑处理的一部分,应该捕获并处理:
NumberFormatException:数值类型的格式错误FileNotFoundException:未找到文件SocketException:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身:
NullPointerException:对某个null的对象调用方法或字段IndexOutOfBoundsException:数组索引越界
Java规定:
- 必须捕获的异常,包括
Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception - 不需要捕获的异常,包括
Error及其子类,RuntimeException及其子类
- 必须捕获的异常,包括
捕获异常
捕获异常使用
try...catch语句// try...catch import java.io.UnsupportedEncodingException; import java.util.Arrays; public class Main { public static void main(String[] args) { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } static byte[] toGBK(String s) { try { // 用指定编码转换String为byte[]: return s.getBytes("GBK"); } catch (UnsupportedEncodingException e) { // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException: System.out.println(e); // 打印异常信息 return s.getBytes(); // 尝试使用默认编码 } } }只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获
import java.io.UnsupportedEncodingException; import java.util.Arrays; public class Main { public static void main(String[] args) { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } static byte[] toGBK(String s) throws UnsupportedEncodingException { return s.getBytes("GBK"); } }上述代码仍然会得到编译错误,但这一次,编译器提示的不是调用
return s.getBytes("GBK");的问题,而是byte[] bs = toGBK("中文");// try...catch import java.io.UnsupportedEncodingException; import java.util.Arrays; public class Main { public static void main(String[] args) { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } static byte[] toGBK(String s) { return s.getBytes("GBK"); } }以上代码,编译器会报错
以下修复方法是在
main()方法中捕获异常并处理:// try...catch import java.io.UnsupportedEncodingException; import java.util.Arrays; public class Main { public static void main(String[] args) { try { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } catch (UnsupportedEncodingException e) { System.out.println(e); } } static byte[] toGBK(String s) throws UnsupportedEncodingException { // 用指定编码转换String为byte[]: return s.getBytes("GBK"); } }因为
String.getBytes(String)方法定义是:public byte[] getBytes(String charsetName) throws UnsupportedEncodingException { ... }在方法定义的时候,使用
throws Xxx表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错
上面的写法就略显麻烦。如果不想写任何
try代码,可以直接把main()方法定义为throws Exception:// try...catch import java.io.UnsupportedEncodingException; import java.util.Arrays; public class Main { public static void main(String[] args) throws Exception { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } static byte[] toGBK(String s) throws UnsupportedEncodingException { // 用指定编码转换String为byte[]: return s.getBytes("GBK"); }
捕获异常
Java中,凡是可能抛出异常的语句,都可以用
try ... catch捕获多
catch语句多个
catch语句只有一个能被执行。例如:public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException e) { System.out.println(e); } catch (NumberFormatException e) { System.out.println(e); } }存在多个
catch的时候,catch的顺序非常重要:子类必须写在前面public static void main(String[] args) { try { process1(); process2(); process3(); } catch (UnsupportedEncodingException e) { System.out.println("Bad encoding"); } catch (IOException e) { System.out.println("IO error"); } }
finally语句finally语句不是必须的,可写可不写;finally总是最后执行。
如果没有发生异常,就正常执行
try { ... }语句块,然后执行finally。如果发生了异常,就中断执行try { ... }语句块,然后跳转执行匹配的catch语句块,最后执行finally。可见,
finally是用来保证一些代码必须执行的捕获多种异常
处理
IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:public static void main(String[] args) { try { process1(); process2(); process3(); } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException System.out.println("Bad input"); } catch (Exception e) { System.out.println("Unknown error"); } }
抛出异常
异常的传播
// exception public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { process2(); } static void process2() { Integer.parseInt(null); // 会抛出NumberFormatException } }java.lang.NumberFormatException: null at java.base/java.lang.Integer.parseInt(Integer.java:614) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Main.process2(Main.java:16) at Main.process1(Main.java:12) at Main.main(Main.java:5)printStackTrace()对于调试错误非常有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:main()调用process1();process1()调用process2();process2()调用Integer.parseInt(String);Integer.parseInt(String)调用Integer.parseInt(String, int)。
查看
Integer.java源码可知,抛出异常的方法代码如下:public static int parseInt(String s, int radix) throws NumberFormatException { if (s == null) { throw new NumberFormatException("null"); } ... }抛出异常
抛出异常分两步:
- 创建某个
Exception的实例; - 用
throw语句抛出。
- 创建某个
举例子
void process2(String s) { if (s==null) { throw new NullPointerException(); } }如果一个方法捕获了某个异常后,又在
catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:// exception public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(); } } static void process2() { throw new NullPointerException(); } }java.lang.IllegalArgumentException at Main.process1(Main.java:15) at Main.main(Main.java:5)这说明新的异常丢失了原始异常信息,我们已经看不到原始异常
NullPointerException的信息了为了能追踪到完整的异常栈,在构造异常的时候,把原始的
Exception实例传进去// exception public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(e); } } static void process2() { throw new NullPointerException(); } }java.lang.IllegalArgumentException: java.lang.NullPointerException at Main.process1(Main.java:15) at Main.main(Main.java:5) Caused by: java.lang.NullPointerException at Main.process2(Main.java:20) at Main.process1(Main.java:13)
异常屏蔽
定义:说明
finally抛出异常后,原来在catch中准备抛出的异常就“消失”了,因为只能抛出一个异常// exception public class Main { public static void main(String[] args) { try { Integer.parseInt("abc"); } catch (Exception e) { System.out.println("catched"); throw new RuntimeException(e); } finally { System.out.println("finally"); throw new IllegalArgumentException(); } } }catched finally Exception in thread "main" java.lang.IllegalArgumentException at Main.main(Main.java:11)在极少数的情况下,我们需要获知所有的异常
方法是先用
origin变量保存原始异常,然后调用Throwable.addSuppressed(),把原始异常添加进来,最后在finally抛出:// exception public class Main { public static void main(String[] args) throws Exception { Exception origin = null; try { System.out.println(Integer.parseInt("abc")); } catch (Exception e) { origin = e; throw e; } finally { Exception e = new IllegalArgumentException(); if (origin != null) { e.addSuppressed(origin); } throw e; } } }当
catch和finally都抛出了异常时,虽然catch的异常被屏蔽了,但是,finally抛出的异常仍然包含了它:Exception in thread "main" java.lang.IllegalArgumentException at Main.main(Main.java:11) Suppressed: java.lang.NumberFormatException: For input string: "abc" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.base/java.lang.Integer.parseInt(Integer.java:652) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Main.main(Main.java:6)
绝大多数情况下,在
finally中不要抛出异常。因此,我们通常不需要关心Suppressed Exception
自定义异常
Java标准库定义的常用异常包括:
Exception ├─ RuntimeException │ ├─ NullPointerException │ ├─ IndexOutOfBoundsException │ ├─ SecurityException │ └─ IllegalArgumentException │ └─ NumberFormatException ├─ IOException │ ├─ UnsupportedCharsetException │ ├─ FileNotFoundException │ └─ SocketException ├─ ParseException ├─ GeneralSecurityException ├─ SQLException └─ TimeoutException自定义的
BaseException应该提供多个构造方法:public class BaseException extends RuntimeException { public BaseException() { super(); } public BaseException(String message, Throwable cause) { super(message, cause); } public BaseException(String message) { super(message); } public BaseException(Throwable cause) { super(cause); } }上述构造方法实际上都是原样照抄
RuntimeException。这样,抛出异常的时候,就可以选择合适的构造方法
NullPointerExceptionNullPointerException即空指针异常,如果一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM抛出的public class Main { public static void main(String[] args) { String s = null; System.out.println(s.toLowerCase()); } }处理
NullPointerExceptionNullPointerException是一种代码逻辑错误,遇到NullPointerException,遵循原则是早暴露,早修复,严禁使用catch来隐藏这种编码错误:// 错误示例: 捕获NullPointerException try { transferMoney(from, to, amount); } catch (NullPointerException e) { }使用空字符串
""而不是默认的null可避免很多NullPointerException,编写业务逻辑时,用空字符串""表示未填写比null安全得多。返回空字符串
""、空数组而不是null:public String[] readLinesFromFile(String file) { if (getFileSize(file) == 0) { // 返回空数组而不是null: return new String[0]; } ... }
使用断言(Assertion)
断言(Assertion)是一种调试程序的方式
看一个例子:
public static void main(String[] args) { double x = Math.abs(-123.45); assert x >= 0; System.out.println(x); }语句
assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError还可以添加一个可选的断言消息:
assert x >= 0 : "x must >= 0";这样,断言失败的时候,
AssertionError会带上消息x must >= 0对于可恢复的程序错误,不应该使用断言。例如:
void sort(int[] arr) { assert arr != null; }应该抛出异常并在上层捕获:
void sort(int[] arr) { if (arr == null) { throw new IllegalArgumentException("array cannot be null"); } }要执行
assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言。$ java -ea Main.java Exception in thread "main" java.lang.AssertionError at Main.main(Main.java:5)
使用JDK Logging
在编写程序的过程中,发现程序运行结果与预期不符,怎么办?
- 用
System.out.println()打印出执行过程中的某些变量,观察每一步的结果与代码逻辑是否符合,然后有针对性地修改代码 - 但太麻烦,最好解决方法是使用日志
- 用
日志的好处
- 日志就是Logging,它的目的是为了取代
System.out.println() - 输出日志,而不是用
System.out.println(),有以下几个好处:- 可以设置输出样式,避免自己每次都写
"ERROR: " + var; - 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
- 可以被重定向到文件,这样可以在程序运行结束后查看日志;
- 可以按包名控制日志级别,只输出某些包打的日志;
- 可以设置输出样式,避免自己每次都写
- 日志就是Logging,它的目的是为了取代
使用日志
Java标准库内置了日志包
java.util.logging,我们可以直接用// logging import java.util.logging.Level; import java.util.logging.Logger; public class Hello { public static void main(String[] args) { Logger logger = Logger.getGlobal(); logger.info("start process..."); logger.warning("memory is running out..."); logger.fine("ignored."); logger.severe("process will be terminated..."); } }Mar 02, 2019 6:32:13 PM Hello main INFO: start process... Mar 02, 2019 6:32:13 PM Hello main WARNING: memory is running out... Mar 02, 2019 6:32:13 PM Hello main SEVERE: process will be terminated...logger.fine()没有打印。这是因为,日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
因为默认级别是INFO,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
使用Commons Logging
Commons Logging是一个第三方日志库,它是由Apache创建的日志模块
使用Commons Logging
第一步,通过
LogFactory获取Log类的实例第二步,使用
Log实例的方法打日志import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class Main { public static void main(String[] args) { Log log = LogFactory.getLog(Main.class); log.info("start..."); log.warn("end."); } }运行上述代码,肯定会得到编译错误
Apache Commons Logging – Download Apache Commons Logging下载下来,
找到
commons-logging-1.2.jar这个文件,再把Java源码Main.java放到一个目录下,例如work目录:work ├─ commons-logging-1.2.jar └─ Main.java用
javac编译Main.java,编译的时候要指定classpathjavac -cp commons-logging-1.2.jar Main.java编译成功,当前目录下就会多出一个
Main.class文件work ├─ commons-logging-1.2.jar ├─ Main.java └─ Main.class现在可以执行这个
Main.class,使用java命令,也必须指定classpathjava -cp .;commons-logging-1.2.jar Main注意到传入的
classpath有两部分:一个是.,一个是commons-logging-1.2.jar,用;分割。.表示当前目录,如果没有这个.,JVM不会在当前目录搜索Main.class,就会报错
使用Log4j
Log4j是一种非常流行的日志框架,最新版本是2.x。
Log4j是一个组件化设计的日志系统,它的架构大致如下:
log.info("User signed in.");
│
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
├──▶│ Appender │───▶│ Filter │───▶│ Layout │───▶│ Console │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
├──▶│ Appender │───▶│ Filter │───▶│ Layout │───▶│ File │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
└──▶│ Appender │───▶│ Filter │───▶│ Layout │───▶│ Socket │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。
console:输出到屏幕;
file:输出到文件;
socket:通过网络输出到远程计算机;
以XML配置为例,使用Log4j的时候,我们把一个
log4j2.xml的文件放到classpath下就可以让Log4j读取配置文件并按照我们的配置来输出日志。下面是一个配置文件的例子:<?xml version="1.0" encoding="UTF-8"?> <Configuration> <Properties> <!-- 定义日志格式 --> <Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property> <!-- 定义文件名变量 --> <Property name="file.err.filename">log/err.log</Property> <Property name="file.err.pattern">log/err.%i.log.gz</Property> </Properties> <!-- 定义Appender,即目的地 --> <Appenders> <!-- 定义输出到屏幕 --> <Console name="console" target="SYSTEM_OUT"> <!-- 日志格式引用上面定义的log.pattern --> <PatternLayout pattern="${log.pattern}" /> </Console> <!-- 定义输出到文件,文件名引用上面定义的file.err.filename --> <RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}"> <PatternLayout pattern="${log.pattern}" /> <Policies> <!-- 根据文件大小自动切割日志 --> <SizeBasedTriggeringPolicy size="1 MB" /> </Policies> <!-- 保留最近10份 --> <DefaultRolloverStrategy max="10" /> </RollingFile> </Appenders> <Loggers> <Root level="info"> <!-- 对info级别的日志,输出到console --> <AppenderRef ref="console" level="info" /> <!-- 对error级别的日志,输出到err,即上面定义的RollingFile --> <AppenderRef ref="err" level="error" /> </Root> </Loggers> </Configuration>
反射Reflection
Java的反射是指程序在运行期可以拿到一个对象的所有信息
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:
// Main.java import com.itranswarp.learnjava.Person; public class Main { String getFullName(Person p) { return p.getFirstName() + " " + p.getLastName(); } }
Class类class(包括interface)的本质是数据类型(Type)- 无继承关系的数据类型无法赋值:
Number n = new Double(123.456); // OK String s = new Double(123.456); // compile error!而
class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存每加载一种
class,JVM就为其创建一个Class类型的实例,并关联起来。注意:**这里的Class类型是一个名叫Class的class**public final class Class { private Class() {} }以
String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:Class cls = new Class(String);这个
Class实例是JVM内部创建的,可以发现Class类的构造方法是private,只有JVM能创建Class实例,我们自己的Java程序是无法创建Class实例JVM持有的每个
Class实例都指向一个数据类型(class或interface):┌───────────────────────────┐ │ Class Instance │────▶ String ├───────────────────────────┤ │name = "java.lang.String" │ └───────────────────────────┘ ┌───────────────────────────┐ │ Class Instance │────▶ Random ├───────────────────────────┤ │name = "java.util.Random" │ └───────────────────────────┘ ┌───────────────────────────┐ │ Class Instance │────▶ Runnable ├───────────────────────────┤ │name = "java.lang.Runnable"│ └───────────────────────────┘一个
Class实例包含了该class的所有完整信息:┌───────────────────────────┐ │ Class Instance │────▶ String ├───────────────────────────┤ │name = "java.lang.String" │ ├───────────────────────────┤ │package = "java.lang" │ ├───────────────────────────┤ │super = "java.lang.Object" │ ├───────────────────────────┤ │interface = CharSequence...│ ├───────────────────────────┤ │field = value[],hash,... │ ├───────────────────────────┤ │method = indexOf()... │ └───────────────────────────┘通过
Class实例获取class信息的方法称为反射(Reflection)获取一个
class的Class实例?直接通过一个
class的静态变量class获取:Class cls = String.class;如果我们有一个实例变量,可以通过该实例变量提供的
getClass()方法获取:String s = "Hello"; Class cls = s.getClass();如果知道一个
class的完整类名,可以通过静态方法Class.forName()获取:Class cls = Class.forName("java.lang.String");
Class实例在JVM中是唯一的,上述方法获取的Class实例是同一个实例注意到数组(例如
String[])也是一种类,而且不同于String.class,它的类名是[Ljava.lang.String;获取到了一个
Class实例,我们就可以通过该Class实例来创建对应类型的实例:// 获取String的Class实例: Class cls = String.class; // 创建一个String实例: String s = (String) cls.newInstance();上述代码相当于
new String()局限是:只能调用
public的无参数构造方法动态加载
JVM在执行Java程序的时候,不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载
public class Main { public static void main(String[] args) { if (args.length > 0) { create(args[0]); } } static void create(String name) { Person p = new Person(name); } }当执行
Main.java时,由于用到了Main,因此,JVM首先会把Main.class加载到内存。然而,并不会加载Person.class,除非程序执行到create()方法,JVM发现需要加载Person类时,才会首次加载Person.class动态加载举例
动态加载
class的特性对于Java程序非常重要。利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:// Commons Logging优先使用Log4j: LogFactory factory = null; if (isClassPresent("org.apache.logging.log4j.Logger")) { factory = createLog4j(); } else { factory = createJdkLog(); } boolean isClassPresent(String name) { try { Class.forName(name); return true; } catch (Exception e) { return false; } }
访问字段
对任意的一个
Object实例,只要我们获取了它的Class,就可以获取它的一切信息Class类提供了以下几个方法来获取字段:- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
// reflection public class Main { public static void main(String[] args) throws Exception { Class stdClass = Student.class; // 获取public字段"score": System.out.println(stdClass.getField("score")); // 获取继承的public字段"name": System.out.println(stdClass.getField("name")); // 获取private字段"grade": System.out.println(stdClass.getDeclaredField("grade")); } } class Student extends Person { public int score; private int grade; } class Person { public String name; }public int Student.score public java.lang.String Person.name private int Student.gradepublic final class String { private final byte[] value; }Field f = String.class.getDeclaredField("value"); f.getName(); // "value" f.getType(); // class [B 表示byte[]类型 int m = f.getModifiers(); Modifier.isFinal(m); // true Modifier.isPublic(m); // false Modifier.isProtected(m); // false Modifier.isPrivate(m); // true Modifier.isStatic(m); // false获取字段值
对
Person实例,可以先拿name字段对应Field,再获取这实例的name字段值:// reflection import java.lang.reflect.Field; public class Main { public static void main(String[] args) throws Exception { Object p = new Person("Xiao Ming"); Class c = p.getClass(); Field f = c.getDeclaredField("name"); Object value = f.get(p); System.out.println(value); // "Xiao Ming" } } class Person { private String name; public Person(String name) { this.name = name; } }运行代码,如果不出意外,会得到一个
IllegalAccessException,这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get(p);前,先写一句:f.setAccessible(true);设置字段值
import java.lang.reflect.Field; public class Main { public static void main(String[] args) throws Exception { Person p = new Person("Xiao Ming"); System.out.println(p.getName()); // "Xiao Ming" Class c = p.getClass(); Field f = c.getDeclaredField("name"); f.setAccessible(true); f.set(p, "Xiao Hong"); System.out.println(p.getName()); // "Xiao Hong" } } class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return this.name; } }
调用方法
已通过
Class实例获取所有Field对象,可通过Class实例获取所有Method信息:Method getMethod(name, Class...):获取某个public的Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)Method[] getMethods():获取所有public的Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
// reflection public class Main { public static void main(String[] args) throws Exception { Class stdClass = Student.class; // 获取public方法getScore,参数为String: System.out.println(stdClass.getMethod("getScore", String.class)); // 获取继承的public方法getName,无参数: System.out.println(stdClass.getMethod("getName")); // 获取private方法getGrade,参数为int: System.out.println(stdClass.getDeclaredMethod("getGrade", int.class)); } } class Student extends Person { public int getScore(String type) { return 99; } private int getGrade(int year) { return 1; } } class Person { public String getName() { return "Person"; } }public int Student.getScore(java.lang.String) public java.lang.String Person.getName() private int Student.getGrade(int)调用方法
用反射来调用
substring方法import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { // String对象: String s = "Hello world"; // 获取String substring(int)方法,参数为int: Method m = String.class.getMethod("substring", int.class); // 在s对象上调用该方法并获取结果: String r = (String) m.invoke(s, 6); // 打印调用结果: System.out.println(r); // "world" } }注意到
substring()有两个重载方法,我们获取的是String substring(int)这个方法。
调用静态方法
如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以
invoke方法传入的第一个参数永远为null// reflection import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { // 获取Integer.parseInt(String)方法,参数为String: Method m = Integer.class.getMethod("parseInt", String.class); // 调用该静态方法并获取结果: Integer n = (Integer) m.invoke(null, "12345"); // 打印调用结果: System.out.println(n); } }调用非
public方法虽然可以通过
Class.getDeclaredMethod()获取该方法实例,但直接对其调用将得到一个IllegalAccessException。为了调用非public方法,我们通过Method.setAccessible(true)允许其调用:// reflection import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { Person p = new Person(); Method m = p.getClass().getDeclaredMethod("setName", String.class); m.setAccessible(true); m.invoke(p, "Bob"); System.out.println(p.name); } } class Person { String name; private void setName(String name) { this.name = name; } }多态
一个
Person类定义了hello()方法,并且它的子类Student也覆写了hello()方法,那么,从Person.class获取的Method,作用于Student实例时/ reflection import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { // 获取Person的hello方法: Method h = Person.class.getMethod("hello"); // 对Student实例调用hello方法: h.invoke(new Student()); } } class Person { public void hello() { System.out.println("Person:hello"); } } class Student extends Person { public void hello() { System.out.println("Student:hello"); } }运行上述代码,发现打印出的是
Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)Method m = Person.class.getMethod("hello"); m.invoke(new Student());Person p = new Student(); p.hello();
调用构造方法
Person p = new Person();通过反射来创建新的实例,可以调用Class提供的newInstance()方法:
Person p = Person.class.newInstance();调用
Class.newInstance()的局限是,它只能调用该类的public无参数构造方法为了调用任意的构造方法,Java的反射API提供了
Constructor对象import java.lang.reflect.Constructor; public class Main { public static void main(String[] args) throws Exception { // 获取构造方法Integer(int): Constructor cons1 = Integer.class.getConstructor(int.class); // 调用构造方法: Integer n1 = (Integer) cons1.newInstance(123); System.out.println(n1); // 获取构造方法Integer(String) Constructor cons2 = Integer.class.getConstructor(String.class); Integer n2 = (Integer) cons2.newInstance("456"); System.out.println(n2); } }getConstructor(Class...):获取某个public的Constructor;getDeclaredConstructor(Class...):获取某个Constructor;getConstructors():获取所有public的Constructor;getDeclaredConstructors():获取所有Constructor。
调用非
public的Constructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败
获取继承关系
获取父类的
Class// reflection public class Main { public static void main(String[] args) throws Exception { Class i = Integer.class; Class n = i.getSuperclass(); System.out.println(n); Class o = n.getSuperclass(); System.out.println(o); System.out.println(o.getSuperclass()); } }运行代码,可以看到,
Integer的父类类型是Number,Number的父类是Object,Object的父类是null
获取
interface由于类可能实现一个或多个接口,通过
Class我们就可以查询到实现的接口类型// reflection import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { Class s = Integer.class; Class[] is = s.getInterfaces(); for (Class i : is) { System.out.println(i); } } }运行上述代码可知,
Integer实现的接口有:- java.lang.Comparable
- java.lang.constant.Constable
- java.lang.constant.ConstantDesc
特别注意:
getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型
继承关系
当我们判断一个实例是否是某个类型时,正常情况下,使用
instanceof操作符:Object n = Integer.valueOf(123); boolean isDouble = n instanceof Double; // false boolean isInteger = n instanceof Integer; // true boolean isNumber = n instanceof Number; // true boolean isSerializable = n instanceof java.io.Serializable; // true如果是两个
Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom():// Integer i = ? Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer // Number n = ? Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number // Object o = ? Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object // Integer i = ? Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
动态代理(Dynamic Proxy)
我们来比较Java的
class和interface的区别:- 可以实例化
class(非abstract) - 不能实例化
interface
所有
interface类型的变量总是通过某个实例向上转型并赋值给接口类型变量的:CharSequence cs = new StringBuilder();- 可以实例化
动态代理(Dynamic Proxy)机制:可以在运行期动态创建某个
interface的实例静态代码写法:
定义接口:
public interface Hello { void morning(String name); }编写实现类:
public class HelloWorld implements Hello { public void morning(String name) { System.out.println("Good morning, " + name); } }创建实例,转型为接口并调用:
Hello hello = new HelloWorld(); hello.morning("Bob");动态代码写法
我们仍然先定义了接口
Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class Main { public static void main(String[] args) { InvocationHandler handler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method); if (method.getName().equals("morning")) { System.out.println("Good morning, " + args[0]); } return null; } }; Hello hello = (Hello) Proxy.newProxyInstance( Hello.class.getClassLoader(), // 传入ClassLoader new Class[] { Hello.class }, // 传入要实现的接口 handler); // 传入处理调用方法的InvocationHandler hello.morning("Bob"); } } interface Hello { void morning(String name); }在运行期动态创建一个
interface实例的方法如下:- 定义一个
InvocationHandler实例,它负责实现接口的方法调用; - 通过
Proxy.newProxyInstance()创建interface实例,它需要3个参数:- 使用的
ClassLoader,通常就是接口类的ClassLoader; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler实例。
- 使用的
- 将返回的
Object强制转型为接口
- 定义一个
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,把上面的动态代理改写为静态实现类大概长这样:
public class HelloDynamicProxy implements Hello { InvocationHandler handler; public HelloDynamicProxy(InvocationHandler handler) { this.handler = handler; } public void morning(String name) { handler.invoke( this, Hello.class.getMethod("morning", String.class), new Object[] { name } ); } }
多线程
基础
- Java程序入口就是由JVM启动
main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束 - 某些进程内部需要同时执行多个子任务。
- 在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程
- 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃
- 一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行
main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等 - 多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步
- Java程序入口就是由JVM启动
创建新线程
需要实例化一个
Thread实例,然后调用它的start()方法:// 多线程 public class Main { public static void main(String[] args) { Thread t = new Thread(); t.start(); // 启动新线程 } }从
Thread派生一个自定义类,然后覆写run()方法:// 多线程 public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); // 启动新线程 } } class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); } }执行上述代码,注意到
start()方法会在内部自动调用实例的run()方法创建
Thread实例时,传入一个Runnable实例:// 多线程 public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动新线程 } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("start new thread!"); } }
使用线程执行的打印语句,和直接在
main()方法执行的区别public class Main { public static void main(String[] args) { System.out.println("main start...");//蓝色 Thread t = new Thread() {//蓝色 public void run() { System.out.println("thread run...");//红色 System.out.println("thread end.");//红色 } }; t.start();//蓝色 System.out.println("main end...");//蓝色 } }蓝色表示主线程,就是
main线程,main线程执行的代码4行,打印main start然后创建
Thread对象,紧接着调用start()启动新线程当
start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread run和thread end语句当
run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了我们再来看线程的执行顺序:
main线程肯定是先打印main start,再打印main end;t线程肯定是先打印thread run,再打印thread end。
除了可以肯定,
main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序
线程的优先级
可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(int n) // 1~10, 默认值5
线程的状态
在Java程序中,一个线程对象只能调用一次
start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行
run()方法的Java代码; - Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行
sleep()方法正在计时等待; - Terminated:线程已终止,因为
run()方法执行完毕
线程启动后,它可以在
Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止线程终止的原因有:
- 线程正常终止:
run()方法执行到return语句返回; - 线程意外终止:
run()方法因为未捕获的异常导致线程终止; - 对某个线程的
Thread实例调用stop()方法强制终止(强烈不推荐使用)
- 线程正常终止:
一个线程还可以等待另一个线程直到其运行结束。例如,
main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:// 多线程 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("hello"); }); System.out.println("start"); t.start(); // 启动t线程 t.join(); // 此处main线程会等待t结束 System.out.println("end"); } }start hello end
中断线程
定义:其他线程给该线程发一个信号,该线程收到信号后结束执行
run()方法,使得自身线程能立刻结束运行中断一个线程非常简单,只需要在其他线程中对目标线程调用
interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行举例:从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行
// 中断线程 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1); // 暂停1毫秒 t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); } } class MyThread extends Thread { public void run() { int n = 0; while (! isInterrupted()) { n ++; System.out.println(n + " hello!"); } } }main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法
// 中断线程 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); } } class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 启动hello线程 try { hello.join(); // 等待hello线程结束 } catch (InterruptedException e) { System.out.println("interrupted!"); } hello.interrupt(); } } class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } }main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出另一个中断线程的方法:设置标志位
通常会用一个
running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束// 中断线程 public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 标志位置为false } } class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n ++; System.out.println(n + " hello!"); } System.out.println("end!"); } }HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值volatile关键字的目的是告诉虚拟机:- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值
守护线程
但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
class TimerThread extends Thread { @Override public void run() { while (true) { System.out.println(LocalTime.now()); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } } }守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
创建守护线程:
方法和普通线程一样,只是在调用
start()方法前,调用setDaemon(true)把该线程标记为守护线程:Thread t = new MyThread(); t.setDaemon(true); t.start();
同步-理解
多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。任何一个线程都可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行
// 多线程 public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count);//每次结果,都是不一样的 } } class Counter { public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count += 1; } } } class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { Counter.count -= 1; } } }原子操作
对于语句:
n = n + 1;看上去是一行语句,实际上对应了3条指令:
ILOAD IADD ISTORE多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待
临界区:加锁和解锁之间的代码块
在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行
任何时候临界区最多只有一个线程能执行
Java程序使用
synchronized关键字对一个对象进行加锁:synchronized(lock) { n = n + 1; }synchronized保证了代码块在任意时刻最多只有一个线程能执行
// 多线程 public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count);//结果都是0 } } class Counter { public static final Object lock = new Object(); public static int count = 0; } class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count += 1; } } } } class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count -= 1; } } } }synchronized(Counter.lock) { // 获取锁 ... } // 释放锁用
Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁来概括一下如何使用
synchronized:- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用
synchronized(lockObject) { ... }
使用
synchronized,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:public void add(int m) { synchronized (obj) { if (m < 0) { throw new RuntimeException(); } this.value += m; } // 无论有无异常,都会在此释放锁 }
不需要
synchronized操作JVM规范定义了几种原子操作:
- 基本类型(
long和double除外)赋值,例如:int n = m; - 引用类型赋值,例如:
List<String> list = anotherList long和double是64位数据,在x64平台的JVM是把long和double的赋值作为原子操作实现的
- 基本类型(
单条原子操作的语句不需要同步
public void set(int m) { synchronized(lock) { this.value = m; } }可改为:
public void set(String s) { this.value = s; }
如果是多行赋值语句,就必须保证是同步操作,例如:
class Point { int x; int y; public void set(int x, int y) { synchronized(this) { this.x = x; this.y = y; } } }多线程连续读写多个变量时,同步的目的是为了保证程序逻辑正确!
class Point { int x; int y; public void set(int x, int y) { synchronized(this) { this.x = x; this.y = y; } } public int[] get() { int[] copy = new int[2]; copy[0] = x; copy[1] = y; } }通过一些巧妙的转换,可以把非原子操作变为原子操作,改为:
class Point { int[] ps; public void set(int x, int y) { int[] ps = new int[] { x, y }; this.ps = ps; } }this.ps = ps是引用赋值的原子操作ps是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步不可变对象无需同步
如果多线程读写的是一个不可变对象,那么无需同步,因为不会修改对象的状态:
class Data { List<String> names; void set(String[] names) { this.names = List.of(names); } List<String> get() { return this.names; } }分析变量是否能被多线程访问
首先要理清概念,多线程同时执行的是方法
class Status { List<String> names; int x; int y; void set(String[] names, int n) { List<String> ns = List.of(names); this.names = ns; int step = n * 10; this.x += step; this.y += step; } StatusRecord get() { return new StatusRecord(this.names, this.x, this.y); } }如果有A、B两个线程,同时执行是指:
- 可能同时执行set();
- 可能同时执行get();
- 可能A执行set(),同时B执行get()
类的成员变量
names、x、y显然能被多线程同时读写,但局部变量(包括方法参数)如果没有“逃逸”,那么只有当前线程可见;局部变量
step仅在set()方法内部使用,因此每个线程同时执行set时都有一份独立的step存储在线程的栈上,互不影响,但是局部变量ns虽然每个线程也各有一份,但后续赋值后对其他线程就变成可见了对
set()方法同步时,如果要最小化synchronized代码块,可以改写如下:void set(String[] names, int n) { // 局部变量其他线程不可见: List<String> ns = List.of(names); int step = n * 10; synchronized(this) { this.names = ns; this.x += step; this.y += step; } }
同步-同步方法
public class Counter { private int count = 0; public void add(int n) { synchronized(this) { count += n; } } public void dec(int n) { synchronized(this) { count -= n; } } public int get() { return count; } }线程调用
add()、dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()、dec()方法内部我们注意到,
synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行var c1 = Counter(); var c2 = Counter(); // 对c1进行操作的线程: new Thread(() -> { c1.add(); }).start(); new Thread(() -> { c1.dec(); }).start(); // 对c2进行操作的线程: new Thread(() -> { c2.add(); }).start(); new Thread(() -> { c2.dec(); }).start();线程安全
- 如果一个类被设计为允许多线程正确访问,Java标准库的
java.lang.StringBuffer也是线程安全的 - 不变类,例如
String,Integer,LocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的 - 类似
Math这些只提供静态方法,没有成员变量的类,也是线程安全的
- 如果一个类被设计为允许多线程正确访问,Java标准库的
同步-死锁
Java的线程锁是可重入的锁
public class Counter { private int count = 0; public synchronized void add(int n) { if (n < 0) { dec(-n); } else { count += n; } } public synchronized void dec(int n) { count += n; } }synchronized修饰的add()方法,一旦线程执行到add()方法内部,已经获取当前实例的this锁,如果传入的n < 0,将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁对同一个线程,能否在获取到锁以后继续获取同一个锁?
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁
因为Java线程锁是可重入锁,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出
synchronized块,记录-1,减到0的时候,才会真正释放锁
死锁
一个线程可以获取一个锁后,再继续获取另一个锁
public void add(int m) { synchronized(lockA) { // 获得lockA的锁 this.value += m; synchronized(lockB) { // 获得lockB的锁 this.another += m; } // 释放lockB的锁 } // 释放lockA的锁 } public void dec(int m) { synchronized(lockB) { // 获得lockB的锁 this.another -= m; synchronized(lockA) { // 获得lockA的锁 this.value -= m; } // 释放lockA的锁 } // 释放lockB的锁 }两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程
防止死锁
线程获取锁的顺序要一致
即严格按照先获取
lockA,再获取lockB的顺序,改写dec()方法如下:public void dec(int m) { synchronized(lockA) { // 获得lockA的锁 this.value -= m; synchronized(lockB) { // 获得lockB的锁 this.another -= m; } // 释放lockB的锁 } // 释放lockA的锁 }
同步-
wait和notifysynchronized解决了多线程竞争的问题对于一个任务管理器,多个线程同时往队列中添加任务,可以用
synchronized加锁:class TaskQueue { Queue<String> queue = new LinkedList<>(); public synchronized void addTask(String s) { this.queue.add(s); } }编写一个
getTask()方法取出队列的任务class TaskQueue { Queue<String> queue = new LinkedList<>(); public synchronized void addTask(String s) { this.queue.add(s); } public synchronized String getTask() { while (queue.isEmpty()) { } return queue.remove(); } }问题:实际上
while()循环永远不会退出。因为线程在执行while()循环时,已经在getTask()入口获取了this锁,其他线程根本无法调用addTask(),因为addTask()执行条件也是获取this锁执行上述代码,线程会在
getTask()中因为死循环而100%占用CPU资源只能在锁对象上调用
wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法:public synchronized String getTask() { while (queue.isEmpty()) { // 释放this锁: this.wait(); // 重新获取this锁 } return queue.remove(); }wait()方法的执行机制非常复杂。是定义在Object类的一个native方法,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回时,线程又会重新试图获得锁在相同的锁对象上调用
notify()方法public synchronized void addTask(String s) { this.queue.add(s); this.notify(); // 唤醒在this锁等待的线程 }往队列中添加了任务后,线程立刻对
this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回
import java.util.*; public class Main { public static void main(String[] args) throws InterruptedException { var q = new TaskQueue(); var ts = new ArrayList<Thread>(); for (int i=0; i<5; i++) { var t = new Thread() { public void run() { // 执行task: while (true) { try { String s = q.getTask(); System.out.println("execute task: " + s); } catch (InterruptedException e) { return; } } } }; t.start(); ts.add(t); } var add = new Thread(() -> { for (int i=0; i<10; i++) { // 放入task: String s = "t-" + Math.random(); System.out.println("add task: " + s); q.addTask(s); try { Thread.sleep(100); } catch(InterruptedException e) {} } }); add.start(); add.join(); Thread.sleep(100); for (var t : ts) { t.interrupt(); } } } class TaskQueue { Queue<String> queue = new LinkedList<>(); public synchronized void addTask(String s) { this.queue.add(s); this.notifyAll(); } public synchronized String getTask() throws InterruptedException { while (queue.isEmpty()) { this.wait(); } return queue.remove(); } }重点关注
addTask()方法,内部调用了this.notifyAll()而不是this.notify(),使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)
同步-ReentrantLock
传统:
public class Counter { private int count; public void add(int n) { synchronized(this) { count += n; } } }改造后:
public class Counter { private final Lock lock = new ReentrantLock(); private int count; public void add(int n) { lock.lock(); try { count += n; } finally { lock.unlock(); } } }ReentrantLock是可重入锁,和synchronized一样,一个线程可以多次获取同一个锁和
synchronized不同的是,ReentrantLock可以尝试获取锁:if (lock.tryLock(1, TimeUnit.SECONDS)) { try { ... } finally { lock.unlock(); } }上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,
tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去使用
ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁
同步-Condition
synchronized可以配合wait和notify实现线程在条件不满足时等待用
ReentrantLock我们使用Condition对象来实现wait和notify的功能class TaskQueue { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private Queue<String> queue = new LinkedList<>(); public void addTask(String s) { lock.lock(); try { queue.add(s); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { condition.await(); } return queue.remove(); } finally { lock.unlock(); } } }Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:await()会释放当前锁,进入等待状态;signal()会唤醒某个等待线程;signalAll()会唤醒所有等待线程;- 唤醒线程从
await()返回后需要重新获得锁
和
tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来:if (condition.await(1, TimeUnit.SECOND)) { // 被其他线程唤醒 } else { // 指定时间内没有被其他线程唤醒 }使用
Condition配合Lock,可以实现更灵活的线程同步
同步-ReadWriteLock
public class Counter { private final Lock lock = new ReentrantLock(); private int[] counts = new int[10]; public void inc(int index) { lock.lock(); try { counts[index] += 1; } finally { lock.unlock(); } } public int[] get() { lock.lock(); try { return Arrays.copyOf(counts, counts.length); } finally { lock.unlock(); } } }任何时刻,只允许一个线程修改,也就是调用
inc()方法是必须获取锁,但是,get()方法只读取数据,不修改数据,它实际上允许多个线程同时调用想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待
ReadWriteLock可解决这个问题,它保证:- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)
创建一个
ReadWriteLock实例,然后分别获取读锁和写锁:public class Counter { private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); // 注意: 一对读锁和写锁必须从同一个rwlock获取: private final Lock rlock = rwlock.readLock(); private final Lock wlock = rwlock.writeLock(); private int[] counts = new int[10]; public void inc(int index) { wlock.lock(); // 加写锁 try { counts[index] += 1; } finally { wlock.unlock(); // 释放写锁 } } public int[] get() { rlock.lock(); // 加读锁 try { return Arrays.copyOf(counts, counts.length); } finally { rlock.unlock(); // 释放读锁 } } }例如实际使用:
一个论坛的帖子,回复可以看做写入操作,它是不频繁的,但是,浏览可以看做读取操作,是非常频繁的,这种情况就可以使用
ReadWriteLock
同步-StampedLock
ReadWriteLock会有潜在问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁StampedLock改进之处:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁- 乐观锁:乐观地估计读的过程中大概率不会有写入
- 悲观锁:读的过程中拒绝有写入,也就是写入必须等待
- 乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需能检测出来,再读一遍
public class Point { private final StampedLock stampedLock = new StampedLock(); private double x; private double y; public void move(double deltaX, double deltaY) { long stamp = stampedLock.writeLock(); // 获取写锁 try { x += deltaX; y += deltaY; } finally { stampedLock.unlockWrite(stamp); // 释放写锁 } } public double distanceFromOrigin() { long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁 // 注意下面两行代码不是原子操作 // 假设x,y = (100,200) double currentX = x; // 此处已读取到x=100,但x,y可能被写线程修改为(300,400) double currentY = y; // 此处已读取到y,如果没有写入,读取是正确的(100,200) // 如果有写入,读取是错误的(100,400) if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生 stamp = stampedLock.readLock(); // 获取一个悲观读锁 try { currentX = x; currentY = y; } finally { stampedLock.unlockRead(stamp); // 释放悲观读锁 } } return Math.sqrt(currentX * currentX + currentY * currentY); } }StampedLock把读锁细分为乐观读和悲观读:- 代码更加复杂,
StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁
同步-Semaphore
锁是保护一种受限资源,保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock)
还有一种受限资源,它需要保证同一时刻最多有N个线程能访问
- 比如:同一时刻最多创建100个数据库连接,最多允许10个用户下载等
使用
Semaphore,例如,最多允许3个线程同时访问:public class AccessLimitControl { // 任意时刻仅允许最多3个线程获取许可: final Semaphore semaphore = new Semaphore(3); public String access() throws Exception { // 如果超过了许可数量,其他线程将在此等待: semaphore.acquire(); try { // TODO: return UUID.randomUUID().toString(); } finally { semaphore.release(); } } }调用
acquire()可能会进入等待,直到满足条件为止。也可以使用tryAcquire()指定等待时间:if (semaphore.tryAcquire(3, TimeUnit.SECONDS)) { // 指定等待时间3秒内获取到许可: try { // TODO: } finally { semaphore.release(); } }Semaphore本质上就是一个信号计数器,用于限制同一时间的最大访问数量
同步-Concurrent集合
ReentrantLock和Condition实现了一个BlockingQueue:public class TaskQueue { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private Queue<String> queue = new LinkedList<>(); public void addTask(String s) { lock.lock(); try { queue.add(s); condition.signalAll(); } finally { lock.unlock(); } } public String getTask() { lock.lock(); try { while (queue.isEmpty()) { condition.await(); } return queue.remove(); } finally { lock.unlock(); } } }BlockingQueue:当一个线程调用这个TaskQueue的getTask()方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。java.util.concurrent包也提供了对应的并发集合类。我们归纳一下:interface non-thread-safe thread-safe List ArrayList CopyOnWriteArrayList Map HashMap ConcurrentHashMap Set HashSet / TreeSet CopyOnWriteArraySet Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue Deque ArrayDeque / LinkedList LinkedBlockingDeque java.util.Collections工具类还提供了一个旧的线程安全集合转换器Map unsafeMap = new HashMap(); Map threadSafeMap = Collections.synchronizedMap(unsafeMap);实际上是一个包装类包装了非线程安全的
Map,然后对所有读写方法都用synchronized加锁,这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用
同步-Atomic
一组原子操作的封装类,它们位于
java.util.concurrent.atomicAtomicInteger为例,它提供的主要操作有:- 增加值并返回新值:
int addAndGet(int delta) - 加1后返回新值:
int incrementAndGet() - 获取当前值:
int get() - 用CAS方式设置:
int compareAndSet(int expect, int update)
- 增加值并返回新值:
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set
我们自己通过CAS编写
incrementAndGet(),可能这样public int incrementAndGet(AtomicInteger var) { int prev, next; do { prev = var.get(); next = prev + 1; } while ( ! var.compareAndSet(prev, next)); return next; }CAS:这操作中,如果
AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的
利用
AtomicLong编写一个多线程安全的全局唯一ID生成器:class IdGenerator { AtomicLong var = new AtomicLong(0); public long getNextId() { return var.incrementAndGet(); } }使用
java.util.concurrent.atomic提供的原子操作可以简化多线程编程:- 原子操作实现了无锁的线程安全
- 适用于计数器,累加器等
线程池
创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间
线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理
ExecutorService接口表示线程池,它的典型用法如下:// 创建固定大小的线程池: ExecutorService executor = Executors.newFixedThreadPool(3); // 提交任务: executor.submit(task1); executor.submit(task2); executor.submit(task3); executor.submit(task4); executor.submit(task5);- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池
以
FixedThreadPool为例,看看线程池的执行逻辑:// thread-pool import java.util.concurrent.*; public class Main { public static void main(String[] args) { // 创建一个固定大小的线程池: ExecutorService es = Executors.newFixedThreadPool(4); for (int i = 0; i < 6; i++) { es.submit(new Task("" + i)); } // 关闭线程池: es.shutdown(); } } class Task implements Runnable { private final String name; public Task(String name) { this.name = name; } @Override public void run() { System.out.println("start task " + name); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println("end task " + name); } }一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务
如果我们把线程池改为
CachedThreadPool,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行想把线程池的大小限制在4~10个之间动态调整
- 查看
Executors.newCachedThreadPool()源码:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor( 0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }- 想创建指定动态范围的线程池
int min = 4; int max = 10; ExecutorService es = new ThreadPoolExecutor( min, max, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());- 查看
ScheduledThreadPool还有一种任务,需要定期反复执行:
- 每秒刷新证券价格。这种任务本身固定,需要反复执行的
创建一个
ScheduledThreadPool仍然是通过Executors类:ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);我们可以提交一次性任务,它会在指定延迟后只执行一次:
// 1秒后执行一次性任务: ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);如果任务以固定的每3秒执行,我们可以这样写:
// 2秒后开始执行定时任务,每3秒执行: ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);如果任务以固定的3秒为间隔执行,我们可以这样写:
// 2秒后开始执行定时任务,以3秒为间隔执行: ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);注意FixedRate和FixedDelay的区别
FixedRate:任务总是以固定时间间隔触发,不管任务执行多长时间:
│░░░░ │░░░░░░ │░░░ │░░░░░ │░░░ ├───────┼───────┼───────┼───────┼────▶ │◀─────▶│◀─────▶│◀─────▶│◀─────▶│FixedDelay:上一次任务执行完毕后,等待固定时间间隔,再执行下一次任务
│░░░│ │░░░░░│ │░░│ │░ └───┼───────┼─────┼───────┼──┼───────┼──▶ │◀─────▶│ │◀─────▶│ │◀─────▶│
使用Future
Java标准库还提供了一个
Callable接口,和Runnable接口比,它多了一个返回值:class Task implements Callable<String> { public String call() throws Exception { return longTimeCalculation(); } }ExecutorService executor = Executors.newFixedThreadPool(4); // 定义任务: Callable<String> task = new Task(); // 提交任务并获得Future: Future<String> future = executor.submit(task); // 从Future获取异步执行返回的结果: String result = future.get(); // 可能阻塞
使用CompletableFuture
获取股票价格为例,看看如何使用
CompletableFuture// CompletableFuture import java.util.concurrent.CompletableFuture; public class Main { public static void main(String[] args) throws Exception { // 创建异步执行任务: CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice); // 如果执行成功: cf.thenAccept((result) -> { System.out.println("price: " + result); }); // 如果执行异常: cf.exceptionally((e) -> { e.printStackTrace(); return null; }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(200); } static Double fetchPrice() { try { Thread.sleep(100); } catch (InterruptedException e) { } if (Math.random() < 0.3) { throw new RuntimeException("fetch price failed!"); } return 5 + Math.random() * 20; } }这里我们都用lambda语法简化了代码。
可见
CompletableFuture的优点是:- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行
多个
CompletableFuture可以串行执行例如:定义两个
CompletableFuture,第一个CompletableFuture根据证券名称查询证券代码,第二个CompletableFuture根据证券代码查询证券价格// CompletableFuture import java.util.concurrent.CompletableFuture; public class Main { public static void main(String[] args) throws Exception { // 第一个任务: CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> { return queryCode("中国石油"); }); // cfQuery成功后继续执行下一个任务: CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> { return fetchPrice(code); }); // cfFetch成功后打印结果: cfFetch.thenAccept((result) -> { System.out.println("price: " + result); }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(2000); } static String queryCode(String name) { try { Thread.sleep(100); } catch (InterruptedException e) { } return "601857"; } static Double fetchPrice(String code) { try { Thread.sleep(100); } catch (InterruptedException e) { } return 5 + Math.random() * 20; } }并行执行
例如:同时从新浪和网易查询证券代码,只要任意一个返回结果,就进行下一步查询价格,查询价格也同时从新浪和网易查询,只要任意一个返回结果,就完成操作
// CompletableFuture import java.util.concurrent.CompletableFuture; public class Main { public static void main(String[] args) throws Exception { // 两个CompletableFuture执行异步查询: CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> { return queryCode("中国石油", "https://finance.sina.com.cn/code/"); }); CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> { return queryCode("中国石油", "https://money.163.com/code/"); }); // 用anyOf合并为一个新的CompletableFuture: CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163); // 两个CompletableFuture执行异步查询: CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> { return fetchPrice((String) code, "https://finance.sina.com.cn/price/"); }); CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> { return fetchPrice((String) code, "https://money.163.com/price/"); }); // 用anyOf合并为一个新的CompletableFuture: CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163); // 最终结果: cfFetch.thenAccept((result) -> { System.out.println("price: " + result); }); // 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭: Thread.sleep(200); } static String queryCode(String name, String url) { System.out.println("query code from " + url + "..."); try { Thread.sleep((long) (Math.random() * 100)); } catch (InterruptedException e) { } return "601857"; } static Double fetchPrice(String code, String url) { System.out.println("query price from " + url + "..."); try { Thread.sleep((long) (Math.random() * 100)); } catch (InterruptedException e) { } return 5 + Math.random() * 20; } }上述逻辑实现的异步查询规则实际上是:
┌─────────────┐ ┌─────────────┐ │ Query Code │ │ Query Code │ │ from sina │ │ from 163 │ └─────────────┘ └─────────────┘ │ │ └───────┬───────┘ ▼ ┌─────────────┐ │ anyOf │ └─────────────┘ │ ┌───────┴────────┐ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Query Price │ │ Query Price │ │ from sina │ │ from 163 │ └─────────────┘ └─────────────┘ │ │ └────────┬───────┘ ▼ ┌─────────────┐ │ anyOf │ └─────────────┘ │ ▼ ┌─────────────┐ │Display Price│ └─────────────┘
使用Fork/Join
目标:把一个大任务拆成多个小任务并行执行
如果要计算一个超大数组的和,最简单的做法是用一个循环在一个线程内完成:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘还有一种方法,可以把数组拆成两部分甚至更多,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务
使用Fork/Join对大数据进行并行求和
import java.util.Random; import java.util.concurrent.*; public class Main { public static void main(String[] args) throws Exception { // 创建2000个随机数组成的数组: long[] array = new long[2000]; long expectedSum = 0; for (int i = 0; i < array.length; i++) { array[i] = random(); expectedSum += array[i]; } System.out.println("Expected sum: " + expectedSum); // fork/join: ForkJoinTask<Long> task = new SumTask(array, 0, array.length); long startTime = System.currentTimeMillis(); Long result = ForkJoinPool.commonPool().invoke(task); long endTime = System.currentTimeMillis(); System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms."); } static Random random = new Random(0); static long random() { return random.nextInt(10000); } } class SumTask extends RecursiveTask<Long> { static final int THRESHOLD = 500; long[] array; int start; int end; SumTask(long[] array, int start, int end) { this.array = array; this.start = start; this.end = end; } @Override protected Long compute() { if (end - start <= THRESHOLD) { // 如果任务足够小,直接计算: long sum = 0; for (int i = start; i < end; i++) { sum += this.array[i]; // 故意放慢计算速度: try { Thread.sleep(1); } catch (InterruptedException e) { } } return sum; } // 任务太大,一分为二: int middle = (end + start) / 2; System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end)); SumTask subtask1 = new SumTask(this.array, start, middle); SumTask subtask2 = new SumTask(this.array, middle, end); invokeAll(subtask1, subtask2); Long subresult1 = subtask1.join(); Long subresult2 = subtask2.join(); Long result = subresult1 + subresult2; System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result); return result; } }观察上述代码的执行过程,一个大的计算任务0~2000首先分裂为两个小任务0~1000和1000~2000,这两个小任务仍然太大,继续分裂为更小的0~500,500~1000,1000~1500,1500~2000,最后,计算结果被依次合并
核心代码
SumTask继承自RecursiveTask,在compute()方法中,关键是如何“分裂”出子任务并且提交子任务class SumTask extends RecursiveTask<Long> { protected Long compute() { // “分裂”子任务: SumTask subtask1 = new SumTask(...); SumTask subtask2 = new SumTask(...); // invokeAll会并行运行两个子任务: invokeAll(subtask1, subtask2); // 获得子任务的结果: Long subresult1 = subtask1.join(); Long subresult2 = subtask2.join(); // 汇总结果: return subresult1 + subresult2; } }
使用ThreadLocal
Thread对象代表一个线程,我们可以在代码中调用Thread.currentThread()获取当前线程。- 例如:打印日志时,可以同时打印出当前线程的名字:
// Thread public class Main { public static void main(String[] args) throws Exception { log("start main..."); new Thread(() -> { log("run task..."); }).start(); new Thread(() -> { log("print..."); }).start(); log("end main."); } static void log(String s) { System.out.println(Thread.currentThread().getName() + ": " + s); } }对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程
Web应用程序就多任务应用,每个用户请求页面时,我们都会创建一个任务
public void process(User user) { checkPermission(); doWork(); saveStatus(); sendResponse(); }观察
process()方法,它内部需要调用若干其他方法,如何在一个线程内传递状态?public void process(User user) { checkPermission(user); doWork(user); saveStatus(user); sendResponse(user); }void doWork(User user) { queryStatus(user); checkStatus(); setNewStatus(user); log(); }在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等
给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,
User对象就传不进去了
ThreadLocal:可以在一个线程中传递同一个对象ThreadLocal实例通常总是以静态字段初始化如下:static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();典型使用方式如下:
void processUser(user) { try { threadLocalUser.set(user); step1(); step2(); log(); } finally { threadLocalUser.remove(); } }通过设置一个
User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:void step1() { User u = threadLocalUser.get(); log(); printUser(); } void step2() { User u = threadLocalUser.get(); checkUser(u.id); } void log() { User u = threadLocalUser.get(); println(u.name); }普通方法调用一定是同一个线程执行的,所以,
step1()、step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例
深入理解:
可以把
ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:Object threadLocalValue = threadLocalMap.get(Thread.currentThread());ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰特别注意
ThreadLocal一定要在finally中清除:try { threadLocalUser.set(user); ... } finally { threadLocalUser.remove(); }
保证能释放
ThreadLocal关联的实例- 可通过
AutoCloseable接口配合try (resource) {...}结构,让编译器关闭 - 例:一个保存了当前用户名的
ThreadLocal可以封装为一个UserContext对象
public class UserContext implements AutoCloseable { static final ThreadLocal<String> ctx = new ThreadLocal<>(); public UserContext(String user) { ctx.set(user); } public static String currentUser() { return ctx.get(); } @Override public void close() { ctx.remove(); } }使用的时候,我们借助
try (resource) {...}结构,可以这么写:try (var ctx = new UserContext("Bob")) { // 可任意调用UserContext.currentUser(): String currentUser = UserContext.currentUser(); } // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象就在
UserContext中完全封装了ThreadLocal,外部代码在try (resource) {...}内部可以随时调用UserContext.currentUser()获取当前线程绑定的用户名- 可通过
虚拟线程
虚拟线程(Virtual Thread)是Java 19引入的一种轻量级线程,它在很多其他语言中被称为协程、纤程、绿色线程、用户态线程等
线程的特点:
- 线程是由操作系统创建并调度的资源;
- 线程切换会耗费大量CPU时间;
- 一个系统能同时调度的线程数量是有限的,通常在几百至几千级别
在服务器端,对用户请求,通常都实现为一个线程处理一个请求。由于用户的请求数往往远超操作系统能同时调度的线程数量,所以通常使用线程池来尽量减少频繁创建和销毁线程的成本
对于需要处理大量IO请求的任务来说,使用线程是低效的,因为一旦读写IO,线程就必须进入等待状态,直到IO数据返回。
常见的IO操作包括:
- 读写文件
- 读写网络,例如HTTP请求
- 读写数据库,本质上是通过JDBC实现网络调用
举例:一个处理HTTP请求的线程,读写网络、文件的时候会进入等待状态
Begin ──────── Blocking ──▶ Read HTTP Request Wait... Wait... Wait... ──────── Running ──────── Blocking ──▶ Read Config File Wait... ──────── Running ──────── Blocking ──▶ Read Database Wait... Wait... Wait... ──────── Running ──────── Blocking ──▶ Send HTTP Response Wait... Wait... ──────── End
真正由CPU执行的代码消耗的时间非常少,线程的大部分时间都在等待IO。这类任务称为IO密集型任务
为高效执行IO密集型任务,引入了虚拟线程
- 虚拟线程不是由操作系统调度,而是由普通线程调度,即成百上千个虚拟线程可以由一个普通线程调度
- 任何时刻,只能执行一个虚拟线程,但是,一旦该虚拟线程执行一个IO操作进入等待时,它会被立刻“挂起”,然后执行下一个虚拟线程
- 什么时候IO数据返回了,这个挂起的虚拟线程才会被再次调度
Begin ─────────── V1 Runing V1 Blocking ──▶ Read HTTP Request ─────────── V2 Runing V2 Blocking ──▶ Read HTTP Request ─────────── V3 Runing V3 Blocking ──▶ Read HTTP Request ─────────── V1 Runing V1 Blocking ──▶ Read Config File ─────────── V2 Runing V2 Blocking ──▶ Read Database ─────────── V1 Runing V1 Blocking ──▶ Read Database ─────────── V3 Runing V3 Blocking ──▶ Read Database ─────────── V2 Runing V2 Blocking ──▶ Send HTTP Response ─────────── V1 Runing V1 Blocking ──▶ Send HTTP Response ─────────── V3 Runing V3 Blocking ──▶ Send HTTP Response ─────────── End看一个虚拟线程的代码,在一个方法中:
void register() { config = readConfigFile("./config.json"); // #1 if (config.useFullName) { name = req.firstName + " " + req.lastName; } insertInto(db, name); // #2 if (config.cache) { redis.set(key, name); // #3 } }涉及IO读写的#1、#2、#3处,执行到这些地方的时候(进入相关的JNI方法内部时)会自动挂起,并切换到其他虚拟线程执行。等到数据返回后,当前虚拟线程会再次调度并执行,因此,代码看起来是同步执行,但实际上是异步执行的
使用虚拟线程
虚拟线程的接口和普通线程一样,区别在于创建虚拟线程只能通过特定方法
方法一:直接创建虚拟线程并运行:
// 传入Runnable实例并立刻运行: Thread vt = Thread.startVirtualThread(() -> { System.out.println("Start virtual thread..."); Thread.sleep(10); System.out.println("End virtual thread."); });方法二:创建虚拟线程但不自动运行,而是手动调用
start()开始运行:// 创建VirtualThread: Thread.ofVirtual().unstarted(() -> { System.out.println("Start virtual thread..."); Thread.sleep(1000); System.out.println("End virtual thread."); }); // 运行: vt.start();方法三:通过虚拟线程的ThreadFactory创建虚拟线程,然后手动调用
start()开始运行:// 创建ThreadFactory: ThreadFactory tf = Thread.ofVirtual().factory(); // 创建VirtualThread: Thread vt = tf.newThread(() -> { System.out.println("Start virtual thread..."); Thread.sleep(1000); System.out.println("End virtual thread."); }); // 运行: vt.start();直接调用
start()实际上是由ForkJoinPool的线程来调度的。我们也可以自己创建调度线程,然后运行虚拟线程:// 创建调度器: ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); // 创建大量虚拟线程并调度: ThreadFactory tf = Thread.ofVirtual().factory(); for (int i=0; i<100000; i++) { Thread vt = tf.newThread(() -> { ... }); executor.submit(vt); // 也可以直接传入Runnable或Callable: executor.submit(() -> { System.out.println("Start virtual thread..."); Thread.sleep(1000); System.out.println("End virtual thread."); return true; }); }由于虚拟线程属于非常轻量级的资源,因此,用时创建,用完就扔,不要池化虚拟线程
使用限制
只有以虚拟线程方式运行的代码,才会在执行IO操作时自动被挂起并切换到其他虚拟线程。普通线程的IO操作仍然会等待
例如,我们在
main()方法中读写文件,是不会有调度和自动挂起的。可以自动引发调度切换的操作包括:
- 文件IO;
- 网络IO;
- 使用Concurrent库引发等待;
- Thread.sleep()操作。
计算密集型任务不应使用虚拟线程,只能通过增加CPU核心解决,或者利用分布式计算资源