异常处理

  • 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:无法加载某个Class

          • StackOverflowError:栈溢出

        • 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语句

      1. finally语句不是必须的,可写可不写;
      2. finally总是最后执行。

      如果没有发生异常,就正常执行try { ... }语句块,然后执行finally。如果发生了异常,就中断执行try { ... }语句块,然后跳转执行匹配的catch语句块,最后执行finally

      可见,finally是用来保证一些代码必须执行的

    • 捕获多种异常

      处理IOExceptionNumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:

      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方法中被抛出的,从下往上看,调用层次依次是:

      1. main()调用process1()
      2. process1()调用process2()
      3. process2()调用Integer.parseInt(String)
      4. 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");
          }
          ...
      }
      
    • 抛出异常

      • 抛出异常分两步:

        1. 创建某个Exception的实例;
        2. 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;
                }
            }
        }
        

        catchfinally都抛出了异常时,虽然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。这样,抛出异常的时候,就可以选择合适的构造方法

  • NullPointerException

    • NullPointerException即空指针异常,如果一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM抛出的

      public class Main {
          public static void main(String[] args) {
              String s = null;
              System.out.println(s.toLowerCase());
          }
      }
      
    • 处理NullPointerException

      NullPointerException是一种代码逻辑错误,遇到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(),有以下几个好处:
        1. 可以设置输出样式,避免自己每次都写"ERROR: " + var
        2. 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
        3. 可以被重定向到文件,这样可以在程序运行结束后查看日志;
        4. 可以按包名控制日志级别,只输出某些包打的日志;
    • 使用日志

      • 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,编译的时候要指定classpath

        javac -cp commons-logging-1.2.jar Main.java
        

        编译成功,当前目录下就会多出一个Main.class文件

        work
        ├─ commons-logging-1.2.jar
        ├─ Main.java
        └─ Main.class
        
      • 现在可以执行这个Main.class,使用java命令,也必须指定classpath

        java -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类型是一个名叫Classclass**

      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实例都指向一个数据类型(classinterface):

      ┌───────────────────────────┐
      │      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)

    • 获取一个classClass实例?

      • 直接通过一个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.grade
      
    • public 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...):获取某个publicMethod(包括父类)
      • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
      • Method[] getMethods():获取所有publicMethod(包括父类)
      • 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...):获取某个publicConstructor
      • getDeclaredConstructor(Class...):获取某个Constructor
      • getConstructors():获取所有publicConstructor
      • getDeclaredConstructors():获取所有Constructor

      调用非publicConstructor时,必须首先通过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的父类类型是NumberNumber的父类是ObjectObject的父类是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的classinterface的区别:

      • 可以实例化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实例的方法如下:

        1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
        2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
          1. 使用的ClassLoader,通常就是接口类的ClassLoader
          2. 需要实现的接口数组,至少需要传入一个接口进去;
          3. 用来处理接口方法调用的InvocationHandler实例。
        3. 将返回的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还有负责垃圾回收的其他工作线程等
    • 多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步
  • 创建新线程

    • 需要实例化一个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 runthread end语句

        run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了

      • 我们再来看线程的执行顺序:

        1. main线程肯定是先打印main start,再打印main end
        2. 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()方法执行完毕
    • 线程启动后,它可以在RunnableBlockedWaitingTimed 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

        1. 找出修改共享变量的线程代码块;
        2. 选择一个共享实例作为锁;
        3. 使用synchronized(lockObject) { ... }
      • 使用synchronized,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

        public void add(int m) {
            synchronized (obj) {
                if (m < 0) {
                    throw new RuntimeException();
                }
                this.value += m;
            } // 无论有无异常,都会在此释放锁
        }
        
    • 不需要synchronized操作

      • JVM规范定义了几种原子操作:

        • 基本类型(longdouble除外)赋值,例如:int n = m
        • 引用类型赋值,例如:List<String> list = anotherList
        • longdouble是64位数据,在x64平台的JVM是把longdouble的赋值作为原子操作实现的
      • 单条原子操作的语句不需要同步

        • 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()

        类的成员变量namesxy显然能被多线程同时读写,但局部变量(包括方法参数)如果没有“逃逸”,那么只有当前线程可见;

        局部变量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也是线程安全的
      • 不变类,例如StringIntegerLocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的
      • 类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的
  • 同步-死锁

    • 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的锁
        }
        
  • 同步-waitnotify

    • synchronized解决了多线程竞争的问题

      对于一个任务管理器,多个线程同时往队列中添加任务,可以用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可以配合waitnotify实现线程在条件不满足时等待

    • ReentrantLock我们使用Condition对象来实现waitnotify的功能

      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把读锁细分为乐观读和悲观读:

      1. 代码更加复杂,
      2. 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集合

    • ReentrantLockCondition实现了一个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:当一个线程调用这个TaskQueuegetTask()方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。

    • java.util.concurrent包也提供了对应的并发集合类。我们归纳一下:

      interfacenon-thread-safethread-safe
      ListArrayListCopyOnWriteArrayList
      MapHashMapConcurrentHashMap
      SetHashSet / TreeSetCopyOnWriteArraySet
      QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
      DequeArrayDeque / LinkedListLinkedBlockingDeque
    • java.util.Collections工具类还提供了一个旧的线程安全集合转换器

      Map unsafeMap = new HashMap();
      Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
      

      实际上是一个包装类包装了非线程安全的Map,然后对所有读写方法都用synchronized加锁,这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用

  • 同步-Atomic

    • 一组原子操作的封装类,它们位于java.util.concurrent.atomic

      • AtomicInteger为例,它提供的主要操作有:
        • 增加值并返回新值: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核心解决,或者利用分布式计算资源