Java 知识
kecho

本文包含 Java 继承、抽象、多态、异常与日志、注解、泛型、输入输出、网络编程、反射、多线程等知识

继承、抽象、多态

  • 继承是 is 关系,组合是 has 关系
  • 如果父类使用带参数列表的构造方法,子类的构造方法就必须显式调用 super(参数列表)
  • publicprotectedprivatefriendly(也称 default
    均可修饰类、方法、变量,它们的作用范围:
当前类 当前包 子孙类 其它包
public
protected ×
friendly × ×
private × × ×
  • final 可修饰类、方法、变量

    1. 标记的类不能被继承。
    2. 标记的方法不能被子类重写。
    3. 标记的变量(成员变量或局部变量)即为常量,只能赋值一次
  • static 可修饰成员方法、成员变量、代码块

    1. 静态方法或变量不需要依赖于对象进行访问,可通过类名访问
    2. 静态变量只分配一次内存
    3. 静态方法中不能使用 thissuper 关键字
    4. 非静态方法可以访问类的静态成员方法和静态成员变量
    5. 静态方法不能直接访问类的非静态成员方法和非静态成员变量,但可通过实例对象访问
  • 接口(interface

    1. 一个接口可以继承多个接口(类不能多继承),一个类可以实现多个接口
    2. 接口的变量会隐式地指定为 public static final 变量(一般接口中不定义变量),方法会被隐式地指定为 public abstract ,即接口中只能有抽象方法
    3. 子类实现一个接口则必须实现所有的抽象方法,否则子类必须定义为抽象类
    4. 接口不能被实例化,没有构造函数
  • 抽象类(abstract

    1. 一个类只能继承一个抽象类
    2. 含有抽象方法的类一定是抽象类,但是抽象类不一定含有抽象方法,抽象类中可以有具体方法。抽象方法的修饰符只能为 public 或者 protected ,一般为 public
    3. 子类继承一个抽象类则必须实现所有的抽象方法,否则子类必须定义为抽象类
    4. 抽象类不能被实例化,但可以有构造函数供子类调用
  • 重载(方法重载是指同一类中的)
    1. 方法名称必须相同
    2. 参数列表必须不同(个数不同、或类型不同、或排列顺序不同)
    3. 仅仅返回类型不同不足以成为方法的重载
    4. 重载是发生在编译时的

  • 重写(重写是子类和父类之间的)

    1. 重写的方法必须和父类保持一致,包括返回类型、方法名、参数列表
    2. 重写的方法可以使用 @Override 注解来标识
    3. 子类中重写方法的访问权限不能低于父类中方法的访问权限
  • 多态性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) {
    //假设 Student 类继承于 Person 类,并重写其 run() 方法

    Person p = new Student();
    p.run(); // 运行的是Student.run

    // Student s = new Person(); 不能这么写,需要强制类型转换
    // Student s = (Student) new Person();
    }

异常处理与日志

  • 捕获多种异常

    1. 使用多个 catch 语句
      此时子类必须写在前面。因为JVM 在捕获到异常后会从上到下匹配 catch 语句,匹配到某个 catch 后就执行 catch 中代码块,然后就不再继续匹配
    2. 在一个 catch 语句中使用 | 连接多个异常类型
      此时要求多个异常类型之间不存在继承关系,可写成 catch ( ExA | ExB e) { ... }
  • try…catch…finally 执行逻辑
    如果没有发生异常,就正常执行 try { … } 语句块,然后执行 finally { … }
    如果发生了异常,就中断执行 try { … } 语句块,然后跳转执行匹配的 catch 代码块,最后执行 finally { … }

  • 抛出异常 throw
    catch 捕获异常后再抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常
    当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个 try … catch 被捕获为止

  • 自定义异常
    推荐从 RuntimeException 派生一个 BaseException 作为“根异常”,然后在“根异常”基础上派生出各种业务类型的异常

  • 断言 assert
    断言是一种调试方式,断言失败会抛出 AssertionError,只能在开发和测试阶段启用断言
    JVM默认关闭断言指令,即遇到 assert 语句就自动忽略

  • 日志系统的使用
    在开发阶段,始终使用 Commons Logging 接口来写入日志,并且开发阶段无需引入 Log4j 。如果需要把日志写入文件, 只需要把正确的配置文件和 Log4j 相关的 jar 包放入 classpath ,就可以自动把日志切换成使用 Log4j 写入,无需修改任何代码。

注解

泛型

输入输出

网络编程

反射

JVM 动态加载类,当需要使用某类时才将其加载进内存,而不是一次性把所有用到的类全部加载到内存
JVM加载类时,就为每个类创建一个与之关联的 Class 类型的实例,这个实例中保存了该类的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。

通过 Class 实例获取类的相关信息的方式称为反射

Class 实例的获取

获取本类 Class 实例

1
2
3
4
5
6
7
8
9
//法一
Class cls = String.class

//法二
String s = "hello";
Class cls = s.getClass();

//法三
Class cls = Class.forName("java.lang.String");

获取父类 Class 实例

可以通过 Class 实例的 getSuperclass() 方法获取父类的 Class 实例
对所有 interface 的 Class 调用 getSuperclass() 返回的是 null,任何非 interface 的 Class 都必定存在一个父类类型

1
2
3
4
5
6
Class a = Integer.class;
Class b = a.getSuperclass();
System.out.println(b); // 输出 class java.lang.Number

//可简写为
System.out.println(Integer.class.getSuperclass());

获取本类实现接口的 Class 实例

可以通过类的 Class 实例的 getInterfaces() 方法返回当前类直接实现的接口类型的 Class 实例数组,其中并不包括其父类实现的接口类型
getInterfaces() 也可获取接口的父接口的 Class 实例数组
如果一个类没有实现任何接口,那么 getInterfaces() 返回空数组

Class 实例的应用

获取类的相关信息

1
2
3
4
5
6
7
8
9
10
11
static void printClassInfo(Class cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
}

创建实例

可以使用 Constructor 实例调用任意的构造方法来创建原实例
通过 Class 实例获取 Constructor 的方法如下:

  • getConstructor(参数的Class…):获取某个 public 的 Constructor
  • getDeclaredConstructor(参数的Class…):获取某个 Constructor
  • getConstructors():获取所有 public 的 Constructor
  • getDeclaredConstructors():获取所有 Constructor

注意: Constructor 总是获取当前类定义的构造方法,和父类无关

1
2
3
4
5
6
7
8
9
10
11
Class cls = String.class;
Constructor con;
String s;

con = cls.getDeclaredConstructor();
s = (String) con.newInstance(); //相当于 String s = new String();
con = cls.getDeclaredConstructor(String.class);
s = (String) con.newInstance("123"); //相当于 String s = new String("123");

//简写为 String s = String.class.getDeclaredConstructor().newInstance();
//简写为 String s = String.class.getDeclaredConstructor(String.class).newInstance("123");

获取字段信息

  • Field getField(name):根据字段名获取某个 public 字段(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个字段(不包括父类)
  • Field[ ] getFields():获取所有 public 字段(包括父类)
  • Field[ ] getDeclaredFields():获取当前类的所有字段(不包括父类)
    一个 Field 对象包含了一个字段的所有信息:
    • getName():返回字段名称,例如,”name”
    • getType():返回字段类型,也是一个 Class 实例,例如,String.class
    • getModifiers():返回字段的修饰符,它是一个 int 类型的值,不同的值表示不同的含义
1
2
3
4
5
6
7
8
9
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

获取和设置字段值

可通过 Field 对象的 get ( Object ) 函数获取指定 Object 的指定字段的值,返回类型为 Object 。
通过反射能够突破类的封装,使得字段不受 private 修饰的限制

可通过Field 对象的 set(Object, Object) 函数实现,其中第一个参数是指定的实例,第二个参数是待修改的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming"); // 可换成 Person p = new Person("Xiao Ming");
Class c = p.getClass(); // 可换成 Class c = Person.class;
Field f = c.getDeclaredField("name");
f.setAccessible(true); //不写这句就会因为字段是 private 类型无法访问而抛出异常
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
f.set(p,"Zhang San");
System.out.println(p.getName()); //"Zhang San"
}
}

class Person {
private String name;
public Person(String name) {
this.name = name;
}
}

获取方法信息

  • Method getMethod(name, 参数的Class…):获取某个 public 方法(包括父类)
  • Method getDeclaredMethod(name, 参数的Class…):获取当前类的某个方法(不包括父类)
  • Method[ ] getMethods():获取所有 public 方法(包括父类)
  • Method[ ] getDeclaredMethods():获取当前类的所有方法(不包括父类)
    一个Method对象包含一个方法的所有信息:
    • getName():返回方法名称,例如:”getScore”
    • getReturnType():返回方法返回值类型,也是一个 Class 实例,例如:String.class
    • getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class}
    • getModifiers():返回方法的修饰符,它是一个 int 类型的值,不同的值表示不同的含义

调用函数执行

可通过 Method 对象的 invoke(参1,参2,…) 函数实现原函数的执行,参1是指定的实例,参2及之后的参数是原函数参数列表中一一对应的参数
对于获取到的静态函数,invoke 方法传入的第一个参数永远为 null

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) throws Exception {
String s = "Hello world";
Method method = String.class.getMethod("substring", int.class); // 获取 String substring(int) 方法,参数为 int
Method.setAccessible(true); //解除 private 的限制

// invoke( 被作用实例,原方法参数列表)
String r = (String) method.invoke(s, 6); //等价于 s.substring(6);
System.out.println(r);
}
}

多态性:

1
2
3
4
5
6
Method m = Person.class.getMethod("hello");
m.invoke(new Student());

//等价于
Person p = new Student();
p.hello(); // 执行的是 Student 中重写的 hello 方法

继承关系的判断

  • instanceof
    可用 instanceof 判断某个实例

    • 是否是某类型
    • 是否是某类的子类类型
    • 是否实现某接口
    • 是否是实现某接口的类型的子类
      1
      2
      3
      4
      5
      6
      Object n = Integer.valueOf(123);
      boolean isDouble = n instanceof Double; // false
      boolean isInteger = n instanceof Integer; // true
      boolean isNumber = n instanceof Number; // true
      boolean isComparable = n instanceof java.lang.Comparable; // true , Integer 实现了接口 java.lang.Comparable
      boolean isSerializable = n instanceof java.io.Serializable; // true ,Number 实现了接口 java.io.Serializable
  • Class 实例比较 和 instanceof 的差别

    1
    2
    3
    4
    5
    6
    Integer n = new Integer(123);
    boolean b1 = n instanceof Integer; // true,因为n是Integer类型
    boolean b2 = n instanceof Number; // true,因为n是Number类型的子类

    boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
    boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
  • isAssignableFrom
    可使用 isAssignableFrom() 判断一个 Class 实例能否向上转型为另一个 Class 实例

    1
    2
    Integer.class.isAssignableFrom(Integer.class); // true,可以写 Integer i = new Interger();
    Number.class.isAssignableFrom(Integer.class); // true,可以写 Number i = new Interger();

多线程

线程状态图

创建线程

可通过 t.setPriority(int) 设置线程 t 优先级,线程创建的方法如下:

  1. 继承 Thread 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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!");
    }
    }
  2. 实现 Runable 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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!");
    }
    }

    //可简写为
    public class Main {
    public static void main(String[] args) {
    Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("start new thread!");
    }
    });
    t.start();
    }
    }
  3. 通过 Callable 和 FutureTasker 创建线程
    Callable 接口与 Runnable 类似,但是有返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class Main {
    public static void main(String[] args) {
    Callable<Integer> callable = new Callable<Integer>() {
    public Integer call() throws Exception {
    Integer i = new Random().nextInt(100);
    System.out.println("在子线程打印:"+i);
    return i;
    }
    };
    FutureTask<Integer> f = new FutureTask<Integer>(callable);
    Thread thread = new Thread(f);
    thread.start();
    try {
    Thread.sleep(2000);
    System.out.println("在主线程打印:"+f.get());
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExecutionException e) {
    e.printStackTrace();
    }
    }
    }

中断线程

  1. 通过 interrupt()
    可使用 interrupt() 来中断线程,线程可以通过 isInterrupted() 方法来做出反应(只适用于第一种方法创建的线程),也可以在检测到 InterruptedException 时做出反应(均适用)
    如果执行 interrupt() 后线程没有对应的处理方法,线程并不会终止执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Main {
    public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("start new thread!");
    int i = 1;
    while (true){
    try {
    System.out.println(i++);
    Thread.sleep(100); //该线程暂停 0.1 秒
    }catch (InterruptedException e) {
    break; //当被中断时退出循环
    }
    }
    }
    });
    t.start();
    Thread.sleep(500); //主线程暂停 0.5 秒
    t.interrupt(); //申请中断 t 线程
    t.join(); //等待 t 线程执行完毕再执行主程序
    System.out.println("退出程序");
    }
    }
  2. 通过共享标志位
    可以用一个线程间共享变量 running 来标识线程是否应该继续运行,该变量需要用 volatile 关键字标记。

    因为如果线程修改了变量的值,虚拟机会在某个不确定的时间把修改后的值从工作内存回写到主内存,导致其它线程从主存读取到的值不是最新的。 volatile 只保证变量改动之后立刻回写,之后新线程读取到的肯定是更新后的值,但之前已经读取过此变量到工作内存的线程无法得知主内存中这个变量有了修改。

    即使有 volatile 修饰,当线程从主存读入数据到工作内存后被阻塞,恢复执行时不会重新从主存读入数据,而是使用已经读取到工作内存的数据,这就涉及到线程同步了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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!");
    }
    }

守护线程

对于普通线程,JVM 进程只有在其它普通线程全部结束后才会结束,而对于守护线程,JVM 进程不管它是否结束,只要其它普通线程全部结束就结束
守护线程不能持有任何需要关闭的资源,例如打开文件等
通过 t.setDaemon(true) 可将其设置为守护线程

线程同步

线程选择锁对象

多线程同时读写共享变量时,需要通过 synchronized 同步,注意具有同步关系的一组线程的加锁对象必须是同一个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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 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;
}
}
}
}

某些单个赋值语句不需要同步,如:

  • 基本类型( long 和 double 除外)赋值,例如:int n = m
  • 引用类型赋值,例如:List list = anotherList

如果是多行赋值语句,就必须保证是同步操作
有时可以将非原子转化为原子操作,就不用同步了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Pair {
int first;
int last;
public void set(int first, int last) {
synchronized(this) { //需要同步,锁对象为 Pair 实例
this.first = first;
this.last = last;
}
}
}

class Pair {
int[] pair;
public void set(int first, int last) { //不需要同步
this.pair = new int[] { first, last }; // 局部变量仅存在于自己的工作内存
}
}
//如果一个线程想把一个人从坐标(10,10)移动到(20,20),另一个线程把坐标准备移动到(30,30)
//对 set 方法加锁使最终结果要么是(20,20)要么是(30,30),如果不同步就有可能出现(20, 30)或者(30,20)这种逻辑错误
//如果要保证从(10,10)是先移到(20,20)还是先移到(30,30),还需要加锁限制两个线程的执行顺序

同步逻辑的封装

让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把 synchronized 逻辑封装起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
Counter cc = new Counter();
Thread t1 = new Thread(() -> c.add(1));
Thread t2 = new Thread(() -> c.dec(1));
Thread tt1 = new Thread(() -> cc.add(11));
Thread tt2 = new Thread(() -> cc.dec(11));
t1.start();
t2.start();
tt1.start();
tt2.start();
}
}

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;
}
}
  • 如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的 Counter 类就是线程安全的。Java 标准库的 java.lang.StringBuffer 也是线程安全的。
  • 还有一些不变类,例如 String,Integer,LocalDate,它们的所有成员变量都是 final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
  • 最后,类似 Math 这些只提供静态方法,没有成员变量的类,也是线程安全的。
  • 除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么 ArrayList 是可以安全地在线程间共享的。
  • 没有特殊说明时,一个类默认是非线程安全的。
  • 同步普通方法
    锁对象为该类实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void test(int n) {
    synchronized(this) {
    ...
    }
    }
    //等价于
    public synchronized void test(int n) {
    ...
    }
  • 同步静态方法
    静态方法没有 this 实例
    锁对象为该类的 Class 实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void test(int n) {
    synchronized(Counter.class) {
    ...
    }
    }
    //等价于
    public synchronized static void test(int n) {
    ...
    }

死锁

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的锁
}
  • 首先
    • 线程1:进入 add(),获得 lockA
    • 线程2:进入 dec(),获得 lockB
  • 然后
    • 线程1:准备获得 lockB,失败,等待中
    • 线程2:准备获得 lockA,失败,等待中

于是就产生了死锁,那么如何避免死锁呢?答案是线程获取锁的顺序要一致
修改上面代码,使线程2获取锁的顺序同线程1保持一致,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(lockA) { // 获得 lockA 的锁
this.value -= m;
synchronized(lockB) { // 获得 lockB 的锁
this.annother -= m;
} // 释放 lockB 的锁
} // 释放 lockA 的锁
}

可重入锁

某个线程已经获得某个锁,如果该锁可以再次被获取而不会出现死锁情况的锁,就叫做可重入锁
上文提到的 synchronized 以及下文提到的 ReentrantLock都是可重入锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 和 wait()、notify()

同步逻辑的封装中已经介绍了 synchronized 的使用,虽然 synchronized 解决了多线程竞争的问题,但是未解决多线程协调的问题,在如下所示的代码中,while() 循环永远不会退出,因为 getTask() 获得锁后在内部先判断队列是否为空,如果为空就循环等待,直到另一个线程往队列中放入了一个任务,while() 循环才退出,然而其它线程要放入任务也得先获得锁,而此时锁已经被占用,就会导致 while() 循环一直执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
public 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();
}
}

此时,为实现多线程之间的协调,需要搭配 wait() 和 notify() 来使用。改进后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TaskQueue {
Queue<String> queue = new LinkedList<>();

public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll(); // 唤醒在this锁等待的线程
// notify() 只会唤醒其中一个等待线程(由操作系统决定是哪个),notifyAll() 将一次性全部唤醒等待线程
// 如果多个线程都被唤醒,则它们中只有一个会重新获取到 this 锁
}
public synchronized String getTask() { //必须在 synchronized 块中才能调用 wait() 方法
while (queue.isEmpty()) { // while 不能换成 if
...
// 释放 this 锁
this.wait();
// 重新获取 this 锁
...
}
return queue.remove();
}
}

假设有 A、B、C 3个线程在 isEmpty()=true 后进入了wait(),此时队列为空
线程D 执行 addTask() 放入了一个 task,此时 queue=[‘task1’],然后唤醒所有等待线程。
此刻 A、B、C 都要从 wait() 返回,但只有其中一个能先获得锁先执行,假设是A,它则继续执行 wait() 后面的代码,花括号内代码执行完后就再进行 while 循环判断,发现队列不为空就跳出循环返回队头元素并释放锁,这个时候队列又空了
随后 B 或 C 如果获得锁也从 wait() 后面开始执行代码,再次经过 while 判断发现队列还是空的,于是再次 wait()

上述代码中的 while 不能换成 if,如果换成了 if ,上述线程A执行正常,但当线程B 或 C 获得锁后,它从 wait() 后开始执行代码,花括号内执行完就会直接返回队头元素,而不是像 while 时需要再次进入循环,因为此时队列为空,返回队头元素必然报错

ReentrantLock 和 Condition

从 Java 5 开始,可用 java.util.concurrent.locks 包中的 ReentrantLock 替代 synchronized 加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Counter {
private int count;

public synchronized void add(int n) {
count += n;
}
}

//替换为
public class Counter {
private int count;
private final Lock lock = new ReentrantLock();

public void add(int n) {
lock.lock();
try {
count += n; // 需要考虑异常
} finally {
lock.unlock();
}
}
}

同 synchronized 一样 ReentrantLock 是可重入锁,一个线程可以多次获取同一个锁。
和 synchronized 不同的是,ReentrantLock 需要手动释放锁,所以使用 ReentrantLock 的时候加锁次数和释放次数要一样,否则会产生死锁
此外 ReentrantLock 还可以尝试获取锁

1
2
3
4
5
6
7
if (lock.tryLock(1, TimeUnit.SECONDS)) {  //若1秒后仍未获取到锁,tryLock() 返回false
try {
...
} finally {
lock.unlock();
}
}

Condition 对象提供的 await()、signal()、signalAll() 和 synchronized 锁对象的 wait()、notify()、notifyAll() 是一致的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
}
}
}

await() 可以在等待指定时间后自动醒来

1
2
3
4
5
if (condition.await(1, TimeUnit.SECOND)) {
// 指定时间内被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
ReadWriteLock 读写锁

ReentrantLock 保护有点过头,每次只有一个线程可以执行临界区代码,效率低下
而 ReadWriteLock 允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待

ReentrantReadWriteLock 是 ReadWriteLock 接口的实现,是可重入锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
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(); // 释放读锁
}
}
}
StampedLock 乐观读

StampedLock(Java 8 引入)相比 ReadWriteLock 引入了乐观读,它乐观估计读的过程中大概率不会有写入,使得读的过程中也允许获取写锁写入,但为了保证读取的数据一致,需要额外的代码判断读的过程中是否有写入

注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。

StampedLock 只有写锁是不可重入的,而读锁(readLock)是可重入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Point {
private final StampedLock stampedLock = new StampedLock();

private double x;
private double y;

public void move(double deltaX, double deltaY) {
// 相比较 ReadWriteLock
// 之前的加锁操作为 rwLock.writeLock().lock();
// 这里的加锁操作为 rwLock.writeLock();
// 一个线程获得了写锁,其它线程就会在这里阻塞
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}

public double distanceFromOrigin() {
// 实际上 tryOptimisticRead() 返回的是版本号,不是锁,所以 return 之前没有释放锁的代码
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);
}
}
Semaphore

Semaphore 保证某种受限资源同一时刻最多有N个线程能访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AccessLimitControl {
// 任意时刻仅允许最多3个线程获取许可:
final Semaphore semaphore = new Semaphore(3);

public void access() throws Exception {
// 如果超过了许可数量,其他线程将在此等待:
// 或者通过 semaphore.acquire() 进行不限时请求许可
if (semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
// 指定等待时间3秒内获取到许可:
try {
...
} finally {
semaphore.release();
}
}
}
}

线程池

  • Executors:线程池创建工厂类
  • ExecutorService:线程池类

CachedThreadPool 线程池的大小会根据任务数量动态调整
放入 ScheduledThreadPool 线程池的任务可以定期反复执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
// 4 < 6 ,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i)); //加入到线程池,Task 实现了 Runable 接口
}
// 关闭线程池:
es.shutdown();

// shutdown() 会等待正在执行的任务先完成,然后再关闭
// shutdownNow() 会立刻停止正在执行的任务
// awaitTermination() 会等待指定的时间让线程池关闭。
}
}

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);
}
}

参考

廖雪峰的官方网站

  • 本文标题:Java 知识
  • 本文作者:kecho
  • 创建时间:2022-09-30 14:28:03
  • 本文链接:https://blog.kecho.top/2022/Java 知识.html
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论