JUC 并发编程

407 阅读29分钟

简介

Java 5.0 提供了 java.util.concurrent (简称 JUC )等合计 3 个包,JavaSE 8 API文档下载地址JUC
在包中增加了并发编程中常用的实用工具类,包括线程池、异步 IO、锁和轻量级任务框架,提供可调的、灵活的线程池,还提供了线程安全的 Collection 实现等。

进程和线程

  • 进程:一个程序,至少包含一个线程。进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

Thread 用来在 Java 中开启线程,那么 Java 可以开启线程吗?

// java.lang.Thread
...
public class Thread implements Runnable {
...
public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();
...

可以从源码中发现最终调用的是 start0(),而这个是 native 修饰的本地方法,底层是 C++,Java 无法直接操作硬件,所以 Java 不能开启线程

Runnable 和 Callable 的区别

  1. Runnable 提供 run 方法,无法通过 throws 抛出异常,所有 CheckedException 必须在 run 方法内部处理;Callable 提供 call 方法,直接抛出 Exception 异常。
  2. Runnable 的 run 方法无返回值;Callable 的 call 方法提供返回值用来表示任务运行的结果。
  3. Runnable 可以作为 Thread 构造器的参数,通过开启新的线程来执行,也可以通过线程池来执行;而 Callable 不能直接作为 Thread 构造器的参数,需要通过 FutureTask 适配类转换为 Runnable,也可以通过线程池执行。

并发和并行

  • 并发:指多个线程操作同一个资源。
  • 并行:指同时执行多个不同的任务。

CPU 执行线程的任务,一个 CPU 内核一次只能执行一个线程任务,执行一段时间切换其他线程执行。现在需要做一件事,如果假设总共有 10 个线程并且 CPU 的时间片平均分配并且不考虑锁的问题,那么现在开启一个线程干活,那么分配到的时间片几率为十一分之一,如果开启两个线程干活几率就是十二分之二,很明显干活耗时变短了。

线程有几种状态

// java.lang.Thread.State
public enum State {
  // 新生
  NEW,
  // 运行
  RUNNABLE,
  // 阻塞
  BLOCKED,
  // 等待
  WAITING,
  // 限时等待
  TIMED_WAITING,
  // 终结
  TERMINATED;
}

6 种

wait 和 sleep 的区别

  1. wait 释放锁;sleep 不会释放锁。
  2. wait 是 Object 的方法;sleep 是 Thread 的方法。

都需要捕获 InterruptedException。


Lock

Lock

加锁方式

传统的 synchronized

// synchronized 可以锁对象,也可以锁类(实际就是类对应的 Class 对象)
public class ResourceSync {

  private int num = 50;

  // 实例方法锁的是对象,和 sale2 是一样的效果
  public synchronized void sale1() {
    if (num > 0) {
      num--;
      System.out.printf("%s 买到了,剩下 %d 个。\n", Thread.currentThread().getName(), num);
    }
  }

  public void sale2() {
    synchronized (this) {
      if (num > 0) {
        num--;
        System.out.printf("%s 买到了,剩下 %d 个。\n", Thread.currentThread().getName(), num);
      }
    }
  }
}

现在的 Lock

锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ResourceLock {
  private Lock lock = new ReentrantLock();//声明锁对象,ReentrantLock 默认非公平锁

  private int num = 50;

  public void sale() {
    lock.lock();// 加锁
    try {
      if (num > 0) {
        num--;
        System.out.printf("%s 买到了,剩下 %d 个。\n", Thread.currentThread().getName(), num);
      }
    } finally {
      lock.unlock();// 解锁
    }
  }
}

synchronized 和 Lock 的区别

  1. synchronized 是内置的 Java 关键字;Lock 是一个 Java 类。
  2. synchronized 无法判断获取锁的状态;Lock 可以判断是否获取到了锁。
boolean tryLock();
  1. synchronized 会自动释放锁(执行完毕或者发生异常);lock 必须要手动释放锁,如果不释放锁,则死锁。
  2. synchronized 线程 1 获得锁之后阻塞,线程 2 必须一直等待线程 1 释放锁;Lock 锁就不一定会等待下去。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  1. synchronized 可重入锁,不可以中断,非公平;Lock 可重入锁,可以判断锁,可公平(可以 自己设置)。
  2. synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码。

线程间通信

典型的例子就是生产者和消费者的问题。

synchronized 版本

public class Test01 {
  public static void main(String[] args) {
    Resource resource = new Resource();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.incr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "A").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.decr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "B").start();
  }
}

class Resource {
  private int num = 0;

  // 判断 等待 干活 唤醒
  public synchronized void incr() throws InterruptedException {
    if (num != 0) {// 判断
      this.wait();// 等待
    }
    num++;// 干活
    System.out.println(Thread.currentThread().getName() + " => " + num);
    this.notifyAll();// 唤醒
  }

  // 判断 等待 干活 唤醒
  public synchronized void decr() throws InterruptedException {
    if (num == 0) {// 判断
      this.wait();// 等待
    }
    num--;// 干活
    System.out.println(Thread.currentThread().getName() + " => " + num);
    this.notifyAll();// 唤醒
  }
}

一切都毫无问题,但是上面的存在虚假唤醒问题,如果是四个线程,就会形成下面的情况。
在官方文档中,java.lang.Object 的 wait() 说明里面有防止虚假唤醒问题产生的说明,就是使用 while,而不是使用 if。

public class Test01 {
  public static void main(String[] args) {
    Resource resource = new Resource();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.incr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "A").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.decr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "B").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.incr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "C").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.decr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "D").start();
  }
}

class Resource {
  private int num = 0;

  public synchronized void incr() throws InterruptedException {
    while (num != 0) {
      this.wait();
    }
    num++;
    System.out.println(Thread.currentThread().getName() + " => " + num);
    this.notifyAll();
  }

  public synchronized void decr() throws InterruptedException {
    while (num == 0) {
      this.wait();
    }
    num--;
    System.out.println(Thread.currentThread().getName() + " => " + num);
    this.notifyAll();
  }
}

Lock 版本

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test02 {
  public static void main(String[] args) {
    Resource2 resource = new Resource2();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.incr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "A").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.decr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "B").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.incr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "C").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.decr();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "D").start();
  }
}

class Resource2 {
  private Lock lock = new ReentrantLock();
  private Condition condition = lock.newCondition();
  private int num = 0;

  public void incr() throws InterruptedException {
    lock.lock();
    try {
      while (num != 0) {
        condition.await();
      }
      num++;
      System.out.println(Thread.currentThread().getName() + " => " + num);
      condition.signalAll();
    } finally {
      lock.unlock();
    }
  }

  public void decr() throws InterruptedException {
    lock.lock();
    try {
      while (num == 0) {
        condition.await();
      }
      num--;
      System.out.println(Thread.currentThread().getName() + " => " + num);
      condition.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

可以使用 Condition 的 await 和 signal 方法替代 wait 和 notify。 新技术的诞生肯定比原来的有优势! Condition 可以精准的唤醒线程。比如:现在需求三个线程轮流输出 A,B,C。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test03 {
  public static void main(String[] args) {
    Resource3 resource = new Resource3();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.printA();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "A").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.printB();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "B").start();
    new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          resource.printC();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }, "C").start();
  }
}

class Resource3 {
  private Lock lock = new ReentrantLock();
  private Condition a = lock.newCondition();
  private Condition b = lock.newCondition();
  private Condition c = lock.newCondition();
  private int flag = 1;// 1: A; 2: B; 3: C

  public void printA() throws InterruptedException {
    lock.lock();
    try {
      while (flag != 1) {// 判断
        a.await();// 等待
      }
      System.out.println(Thread.currentThread().getName() + " => A");
      flag = 2;
      b.signal();// 唤醒 B
    } finally {
      lock.unlock();
    }
  }

  public void printB() throws InterruptedException {
    lock.lock();
    try {
      while (flag != 2) {// 判断
        b.await();// 等待
      }
      System.out.println(Thread.currentThread().getName() + " => B");
      flag = 3;
      c.signal();// 唤醒 C
    } finally {
      lock.unlock();
    }
  }

  public void printC() throws InterruptedException {
    lock.lock();
    try {
      while (flag != 3) {// 判断
        c.await();// 等待
      }
      System.out.println(Thread.currentThread().getName() + " => C");
      flag = 1;
      a.signal();// 唤醒 A
    } finally {
      lock.unlock();
    }
  }
}

8 锁问题

1. 标准访问,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test1 {

  public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(phone::sendSMS).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(phone::call).start();
  }

  private static class Phone {
    synchronized void sendSMS() {
      System.out.println("发短信");
    }

    synchronized void call() {
      System.out.println("打电话");
    }
  }
}

2. 在短信方法内停 4 秒,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test2 {

  public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(phone::sendSMS).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(phone::call).start();
  }

  private static class Phone {
    synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
    }

    synchronized void call() {
      System.out.println("打电话");
    }
  }
}

3. 普通的 hello 方法,是先发短信还是 hello?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test3 {

  public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(phone::sendSMS).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(phone::hello).start();
  }

  private static class Phone {
    synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
    }

    synchronized void call() {
      System.out.println("打电话");
    }

    void hello() {
      System.out.println("hello");
    }
  }
}

4. 现在有两部手机,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test4 {
  public static void main(String[] args) {
    Phone phone1 = new Phone();
    Phone phone2 = new Phone();
    new Thread(phone1::sendSMS).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(phone2::call).start();
  }

  private static class Phone {
    synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
    }

    synchronized void call() {
      System.out.println("打电话");
    }
  }
}

5. 两个静态同步方法,1 部手机,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test5 {
  public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(() -> phone.sendSMS()).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(() -> phone.call()).start();
  }

  private static class Phone {
    static synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
    }

    static synchronized void call() {
      System.out.println("打电话");
    }
  }
}

6. 两个静态同步方法,2 部手机,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test6 {
  public static void main(String[] args) {
    Phone phone1 = new Phone();
    Phone phone2 = new Phone();
    new Thread(() -> phone1.sendSMS()).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(() -> phone2.call()).start();
  }

  private static class Phone {
    static synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
      Class<Test5> test5Class = Test5.class;
    }

    static synchronized void call() {
      System.out.println("打电话");
    }
  }
}

7. 1 个静态同步方法,1 个普通同步方法,1 部手机,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test7 {
  public static void main(String[] args) {
    Phone phone = new Phone();
    new Thread(() -> phone.sendSMS()).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(() -> phone.call()).start();
  }

  private static class Phone {
    static synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
      Class<Test5> test5Class = Test5.class;
    }

    synchronized void call() {
      System.out.println("打电话");
    }
  }
}

8. 1 个静态同步方法,1 个普通同步方法,2 部手机,先发短信还是打电话?

package com.example.lock8;

import java.util.concurrent.TimeUnit;

public class Test8 {
  public static void main(String[] args) {
    Phone phone1 = new Phone();
    Phone phone2 = new Phone();
    new Thread(() -> phone1.sendSMS()).start();
    // 延迟 1 秒
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    new Thread(() -> phone2.call()).start();
  }

  private static class Phone {
    static synchronized void sendSMS() {
      // 延迟 4 秒
      try {
        TimeUnit.SECONDS.sleep(4);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("发短信");
      Class<Test5> test5Class = Test5.class;
    }

    synchronized void call() {
      System.out.println("打电话");
    }
  }
}

解答

  1. 标准访问,先发短信还是打电话?发短信
    • 因为第一个线程先获取了对象锁,所以先发短信
  2. 在短信方法内停 4 秒,先发短信还是打电话?发短信
    • 同上
  3. 普通的 hello 方法,是先发短信还是 hello?hello
    • 第一个线程获取了对象锁,但是第二个线程执行的 hello 方法不需要锁,所以先 hello
  4. 现在有两部手机,先发短信还是打电话?打电话
    • 第一个线程获取了 phone1 的对象锁,第二个线程获取了 phone2 的对象锁,是两把锁,所以先打电话
  5. 两个静态同步方法,1 部手机,先发短信还是打电话?发短信
    • 第一个线程获取了类锁(因为方法用了 static 修饰变为静态方法,而 Class<Test5> 对象全局唯一),所以先发短信
  6. 两个静态同步方法,2 部手机,先发短信还是打电话?发短信
    • 同上
  7. 1 个静态同步方法,1 个普通同步方法,1 部手机,先发短信还是打电话?打电话
    • 第一个线程获取了类锁,第二个线程获取了对象锁,所以先打电话
  8. 1 个静态同步方法,1 个普通同步方法,2 部手机,先发短信还是打电话?打电话 总结:
  • 本质上只有对象锁;
  • 普通同步方法锁的是方法调用者,即 this。如果有一个线程获取到该对象的锁,那么其他线程将无法访问该对象的其他普通同步方法;
  • 静态同步方法锁的是类对象,即 类.class,该对象是类加载器加载该 class 文件时生成的对象,全局唯一。如果有一个线程获取到该类对象的锁,那么其他线程将无法访问该类对象的其他静态同步方法,非同步方法不受限制;
  • 非同步(静态)方法不受锁的限制。

集合不安全

ArrayList、HashSet、HashMap 多线程修改数据时会出现并发问题,抛出 ConcurrentModificationException 并发修改异常。

List

package com.example.collections;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListTest {
  public static void main(String[] args) {
//    List<String> list = new ArrayList<>();// 多线程修改会出现 ConcurrentModificationException
//    List<String> list = new Vector<>();// java.util 包下
//    List<String> list = Collections.synchronizedList(new ArrayList<>());// java.util 包下
    List<String> list = new CopyOnWriteArrayList<>();// java.util.concurrent 包下
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        list.add(UUID.randomUUID().toString().substring(0, 4));
        System.out.println(list);
      }).start();
    }
  }
}

ArrayList 源码

// 没有加上同步,线程不安全
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

Vector 源码

// 加上了 synchronized,线程安全
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

Collections.synchronizedList 源码

// SynchronizedRandomAccessList 和 SynchronizedList 都是 Collections 的静态内部类
// SynchronizedRandomAccessList 继承了 SynchronizedList,且没有覆盖 add 方法
// SynchronizedList 继承了 SynchronizedCollection,也没有覆盖 add 方法
public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    private static final long serialVersionUID = 3053995032091335093L;

    final Collection<E> c;  // Backing Collection
    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    SynchronizedCollection(Collection<E> c, Object mutex) {
        this.c = Objects.requireNonNull(c);
        this.mutex = Objects.requireNonNull(mutex);
    }
    ...
    // 锁了 mutex 对象,线程安全的,如果不指定 mutex,那么默认是 this
    public boolean add(E e) {
        synchronized (mutex) {return c.add(e);}
    }
    ...
}

CopyOnWriteArrayList 源码

CopyOnWrite 写时复制,简写 COW,是计算机程序设计领域的一种优化策略,读写分离。读取时不会生成数据的副本,多线程读取的都是同一个副本;修改、删除、增加时会生成数据的副本,操作都在副本上进行,不会对其他线程的读取造成任何影响,这个过程对其他线程是透明的,操作完成后将副本赋值给数据引用。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();// 使用了 Lock 锁
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 先将数据复制一份出来
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在复制出来的数据上做修改
        newElements[len] = e;
        // 再复制回去
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final Object[] getArray() {
    return array;
}

final void setArray(Object[] a) {
    array = a;
}

Set

package com.example.collections;

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

public class SetTest {
  public static void main(String[] args) {
//    Set<String> set = new HashSet<>();// 多线程修改会出现 ConcurrentModificationException
//    Set<String> set = Collections.synchronizedSet(new HashSet<>());// java.util 包下
    Set<String> set = new CopyOnWriteArraySet<>();// java.util.concurrent 包下
    for (int i = 0; i < 20; i++) {
      new Thread(() -> {
        set.add(UUID.randomUUID().toString().substring(0, 4));
        System.out.println(set);
      }).start();
    }
  }
}

HashSet 源码

// HashSet 底层实际就是 HashMap,key 是元素,value 全是 Object 对象。
private transient HashMap<E,Object> map;

private static final Object PRESENT = new Object();

public boolean add(E e) {
    // 那么 HashSet 是不是线程安全的只需要知道 HashMap 是不是线程安全的就行。
    return map.put(e, PRESENT)==null;
}

HashMap 源码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 并没有发现任何有关锁的内容,所以线程不安全
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

Collections.synchronizedSet 源码

// SynchronizedSet 同样是 Collections 中的静态内部类,
// 继承了 SynchronizedCollection,同样没有覆盖 add 方法,
// 所以和 List 中的是一样的。
public static <T> Set<T> synchronizedSet(Set<T> s) {
    return new SynchronizedSet<>(s);
}

CopyOnWriteArraySet 源码

// 存储数据的部分使用了 CopyOnWriteArrayList,所以是线程安全的
private final CopyOnWriteArrayList<E> al;

public boolean add(E e) {
    return al.addIfAbsent(e);
}

Map

package com.example.collections;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class MapTest {
  public static void main(String[] args) {
//    Map<String, Object> map = new HashMap<>();// 多线程修改会出现 ConcurrentModificationException
//    Map<String, Object> map = Collections.synchronizedMap(new HashMap<>());// java.util 包下
    Map<String, Object> map = new ConcurrentHashMap<>();// java.util.concurrent 包下
    for (int i = 0; i < 20; i++) {
      new Thread(() -> {
        String kv = UUID.randomUUID().toString().substring(0, 4);
        map.put(kv, kv);
        System.out.println(map);
      }).start();
    }
  }
}

HashMap 源码上方已经分析过了。
Collections.synchronizedMap 源码

// SynchronizedMap 是 Collections 的静态内部类
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}

SynchronizedMap 源码

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;

    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize

    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }

    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        this.mutex = mutex;
    }
    ...
    // 锁了 mutex 对象,线程安全的,如果不指定 mutex,那么默认是 this
    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
    ...
}

ConcurrentHashMap 源码

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果数据还没有初始化,那么初始化数据
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 如果 key 在结点中不存在,那么新增结点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 需要扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 如果都不满足,则利用 synchronized 锁写入数据
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

Callable

获得多线程的方法有哪几种?

  1. 继承 Thread
    • Java 是单继承的语言,不要把继承用在这个上面
  2. 实现 Runnable
    • Runnale 没有返回值,不能抛出异常
  3. 实现 Callable
    • Callable 有返回值,抛出异常
  4. 通过线程池获得

可以直接替换 Runnable 吗?

Thread 构造器

很明显,Thread 构造器中不直接支持 Callable 接口。
既然 Thread 不直接支持,那么是否可以通过某种方式转换为 Runnable 呢,很显然是有的 ---- FutureTask(适配器模式)。 FutureTask

package com.example.callable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;

public class CallableTest {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
    RunnableFuture<Integer> future = new FutureTask<>(() -> {
      System.out.println(Thread.currentThread().getName() + " call~~~");
      return 1;
    });
    new Thread(future, "A").start();
    new Thread(future, "B").start();
    System.out.println(future.get());
  }
}

执行结果如下:

说明:

  1. future.get() 方法用来获取返回值,会阻塞线程,等待线程执行完毕拿到返回值,可以使用 get(long timeout, TimeUnit unit) 超时获取方法防止阻塞时间过长,也可以使用 cancel(boolean mayInterruptIfRunning) 方法尝试中断执行。
  2. future 放入线程运行时会将运行结果缓存起来,
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        // awaitDone 内部有一个 for(;;) 死循环,等待执行完毕、被中断或者抛出异常,
        // 所以会阻塞线程
        s = awaitDone(false, 0L);
    // 调用了 report 方法
    return report(s);
}

/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes

private V report(int s) throws ExecutionException {
    // 将结果返回,发现 outcome 是属性,说明结果缓存到属性 outcome 中了
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

public void run() {
	// 先判断执行状态,非 NEW 状态直接返回,不执行
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        // 多加一重判断,所以任务只会执行一次
        if (c != null && state == NEW) {
            ...
        }
    } finally {
        ...
    }
}

Runnable 和 Callable 的区别

  1. Runnable 提供 run 方法,无法通过 throws 抛出异常,所有 CheckedException 必须在 run 方法内部处理;Callable 提供 call 方法,直接抛出 Exception 异常。
  2. Runnable 的 run 方法无返回值;Callable 的 call 方法提供返回值用来表示任务运行的结果。
  3. Runnable 可以作为 Thread 构造器的参数,通过开启新的线程来执行,也可以通过线程池来执行;而 Callable 不能直接作为 Thread 构造器的参数,需要通过 FutureTask 适配类转换为 Runnable,也可以通过线程池执行。

3 个常用工具类

CountDownLatch

  • 发令枪功能:要求所有田径赛参赛人员进入准备状态,当发令枪枪响时开始跑。

当有多个线程需要处于同一状态的需求时,可以使用这个功能。比如手写压力测试工具,要求 N 个线程在同一时刻同时开始发请求(程序无法决定线程调度,由操作系统决定,无法做到真正意义上的同时)。

  • 统计排名功能:当所有参赛人员跑完后生成排名表。

一个大任务单线程运行需要时间很长,可以拆分成多个小任务,交给多个线程同时运行。比如有一个 100万数据的 List 需要处理,可以拆分成 10 份,开启 10 个线程运行,等所有数据全部处理完毕时继续下一步。

package com.example.util;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class CountDownLatchTest {
  public static void main(String[] args) throws InterruptedException {
    int n = 10;
    System.out.println("现有 " + n + " 名参赛选手,进入准备!");
    CountDownLatch startingGun = new CountDownLatch(1);// 设定 1 次计数,只要调用一次 countDown 方法就开始运行。
    CountDownLatch countDownLatch = new CountDownLatch(n);
    List<String> rankList = new CopyOnWriteArrayList<>();
    for (int i = 0; i < n; i++) {
      String name = i + 1 + " 号选手";
      new Thread(() -> {
        System.out.println(name + "准备就绪");
        try {
          // 所有线程阻塞,处于同一状态
          startingGun.await();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        rankList.add(name);
        countDownLatch.countDown();// 计数 -1
      }).start();
    }
    TimeUnit.SECONDS.sleep(1);
    System.out.println("开始跑!");
    startingGun.countDown();
    countDownLatch.await();// 阻塞等待全部跑完
    for (int i = 0; i < rankList.size(); i++) {
      System.out.printf("第 %d 名是:%s\n", i + 1, rankList.get(i));
    }
  }
}

显然,如果设定的 count 数大于调用 countDown 方法次数,那么 await 永远阻塞。

CyclicBarrier

  • 可以使线程处于同一状态,当有足够的线程时,阻塞解除并调用回调函数(如果有的话),可循环使用。也能起到发令枪和统计排名的效果
package com.example.util;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
  public static void main(String[] args) {
    int number = 7;
    CyclicBarrier cyclicBarrier = new CyclicBarrier(number, () -> {
      // 回调函数
      System.out.println("集齐 7 颗龙珠,召唤神龙!");
    });
    for (int i = 0; i < number; i++) {
      new Thread(() -> {
        System.out.println("第 " + Thread.currentThread().getName() + " 颗星龙珠被收集。");
        try {
       	  // 等待 7 个线程全部运行到这一步
          cyclicBarrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
          e.printStackTrace();
        }
      }, String.valueOf(i + 1)).start();
    }
  }
}

Semaphore

Semaphore(信号量)可以多次使用。 需求:现在有一个停车场,内部有 5 个停车位,有 10 辆车需要停泊。

package com.example.util;

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreTest {
  public static void main(String[] args) {
    Semaphore semaphore = new Semaphore(5);// 5 个停车位
    Random random = new Random();
    for (int i = 0; i < 10; i++) {// 10 辆汽车
      new Thread(() -> {
        try {
          semaphore.acquire();// 请求停车位
          System.out.println(Thread.currentThread().getName() + " 抢到了车位");
          TimeUnit.SECONDS.sleep(random.nextInt(5));
          System.out.println(Thread.currentThread().getName() + " 离开");
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {// 资源的释放一定要放在 finally 块中
          semaphore.release();
        }
      }, String.valueOf(i + 1)).start();
    }
  }
}

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制(限流)。


ReadWriteLock

读写锁。读锁:多线程可以一起读,又称共享锁;写锁:一次只能有一个线程可以写入,又称排他锁。

特点

  1. 多线程可以一起读;
  2. 只能一个线程写;
  3. 当有线程获取读锁(正在读)时,无法被其他线程写(阻塞);
  4. 当有线程获取写锁(正在写)时,无法被其他线程读(阻塞)。

为什么需要有读写锁?

  • 如果读写都加上排他锁,效率明显低下。
  • 如果写加排他锁,读不加锁,会出现一些问题。
  • 都不加锁,在多线程场景下会出现 ConcurrentModificationException。
package com.example.rwl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MapCache<K, V> {
  private Map<K, V> dataMap = new HashMap<>();
  private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

  public void put(K key, V value) {
    readWriteLock.writeLock().lock();
    try {
      dataMap.put(key, value);
    } finally {
      readWriteLock.writeLock().unlock();
    }
  }

  public V get(K key) {
    readWriteLock.readLock().lock();
    try {
      return dataMap.get(key);
    } finally {
      readWriteLock.readLock().unlock();
    }
  }
}

阻塞队列

阻塞队列首先是一个队列(FIFO 先进先出),然后在两种情况下必须要阻塞。

  1. 当队列是空的,从队列中获取元素的操作将会被阻塞,直到其他线程往空的队列插入新的元素;
  2. 当队列是满的,从队列中添加元素的操作将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空。 阻塞队列家族图谱
  3. SynchronousQueue:同步队列,没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素。
  4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  5. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  6. ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  7. LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为 Integer.MAX_VALUE)阻塞队列。
  8. LinkedTransferQueue:由链表组成的无界阻塞队列。
  9. LinkedBlockingDeque:由链表组成的双向阻塞队列。

阻塞队列的用处

  • 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
  • 线程池中会使用阻塞队列存放任务

这样代码中就不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了。

4 组 API

方式抛异常有返回值,不抛异常阻塞等待超时等待
添加add(E e)offer(E e)put(E e)offer(E e, long timeout, TimeUnit unit)
移除remove()poll()take()poll(long timeout, TimeUnit unit)
查看队首元素element()peek()--
  /**
   * 抛异常
   */
  private static void throwException() {
    ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.add("A"));
    System.out.println(blockingQueue.add("B"));
    System.out.println(blockingQueue.add("C"));
    System.out.println(blockingQueue.element());
    // java.lang.IllegalStateException: Queue full
//    System.out.println(blockingQueue.add("D"));
    System.out.println("----------------------");
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.element());
    System.out.println(blockingQueue.remove());
    System.out.println(blockingQueue.remove());
    // java.util.NoSuchElementException
//    System.out.println(blockingQueue.remove());
    // java.util.NoSuchElementException
//    System.out.println(blockingQueue.element());
  }
  /**
   * 有返回值,不抛异常
   */
  private static void returnValue() {
    ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("A"));
    System.out.println(blockingQueue.offer("B"));
    System.out.println(blockingQueue.offer("C"));
    System.out.println(blockingQueue.peek());
    // false
    System.out.println(blockingQueue.offer("D"));
    System.out.println("----------------------");
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.peek());
    System.out.println(blockingQueue.poll());
    // null
    System.out.println(blockingQueue.poll());
    // null
    System.out.println(blockingQueue.peek());
  }
  /**
   * 阻塞等待
   */
  private static void blocking() throws InterruptedException {
    ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
    blockingQueue.put("A");
    blockingQueue.put("B");
    blockingQueue.put("C");
    // 队列已满,线程会一直阻塞
//    blockingQueue.put("D");
    System.out.println("----------------------");
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    // 队列已空,线程会一直阻塞
//    System.out.println(blockingQueue.take());
  }
  /**
   * 超时等待,不会抛异常
   */
  private static void blockingTimeout() throws InterruptedException {
    ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("A", 1, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("B", 1, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("C", 1, TimeUnit.SECONDS));
    // 阻塞 1 秒后返回 false
    System.out.println(blockingQueue.offer("D", 1, TimeUnit.SECONDS));
    System.out.println("----------------------");
    System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
    // 阻塞 1 秒后返回 null
    System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS));
  }

线程池

池化技术

程序的运行都会占用系统资源,可以使用池化技术优化资源的使用。

池化技术就是事先准备好一些资源,使用的时候,就从池里面取,用完之后返回池中。

那么为什么需要池化技术呢? 因为很多资源的创建和销毁都十分耗时,如果频繁的创建、销毁会带来很大的性能问题。

常见的池化技术:线程池、连接池、常量池...

线程池之三大方法

package com.example.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsTest {
  public static void main(String[] args) {
    // 单个线程的线程池
    ExecutorService threadPool = Executors.newSingleThreadExecutor();
    // 固定数量的线程池
//    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    // 可伸缩的线程池
//    ExecutorService threadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 100; i++) {
      threadPool.execute(() -> System.out.println(Thread.currentThread().getName()));
    }
  }
}

源码剖析

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  1. 发现三方法内部创建的都是 ThreadPoolExecutor;
  2. newSingleThreadExecutornewFixedThreadPool 线程数量有了很好的控制,但是任务队列使用的默认构造器,而源码是下面这样的,可以发现队列能存放 Integer.MAX_VALUE 约 21 亿个元素!可能发生 OOM;
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
  1. newCachedThreadPool 线程的任务队列只能存放一个元素,但是最大线程数是 Integer.MAX_VALUE!也可能发生 OOM。 阿里巴巴 Java 开发手册中也说了不允许使用 Executors 创建线程池不要使用 Executors

线程池之七大参数

从上面的源码就可以看出关键在于 ThreadPoolExecutor 类,查看参数最多的构造器。

核心构造器源码

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  1. int corePoolSize:常驻核心线程数,线程池中线程的最小数量。
  2. int maximumPoolSize:最大线程数,线程池能够容纳的最大数量。
  3. long keepAliveTime:线程数量超过 corePoolSize 时,线程的空闲时间达到 keepAliveTime 时,多余线程会被销毁直到只剩下 corePoolSize 个线程为止。
  4. TimeUnit unit:keepAliveTime 的时间单位。
  5. BlockingQueue<Runnable> workQueue:任务队列,存放已提交但是未执行的任务。
  6. ThreadFactory threadFactory:创建线程的线程工厂,一般使用默认的 Executors.defaultThreadFactory()
  7. RejectedExecutionHandler handler:拒绝策略,就是当任务队列满并且工作线程达到 maximumPoolSize 时,如何拒绝新的任务,有四种。

ThreadPoolExecutor 原理说明

  1. 在创建了线程池后,开始等待任务。
  2. 当调用 execute() 方法添加一个任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize ,开启新的线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize:
    2.2.1 队列未满,那么将这个任务放入队列;
    2.2.2 队列已满且正在运行的线程数量还小于 maximumPoolSize,开启新的非核心线程运行这个任务;
    2.2.3 队列已满且正在运行的线程数量大于或等于 maximumPoolSize,那么执行拒绝策略。
  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  2. 当一个线程无事可做超过一定的时间(keepAliveTime + unit)时,线程会判断:如果当前线程数大于 corePoolSize,那么销毁该线程。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

execute() 源码解析

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 1. 如果工作的线程数小于 corePoolSize,开启新的线程执行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2. 如果核心线程都在工作,并且新任务可以存入任务队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 2.1 双重检查,防止上次检查之后有线程被销毁或者线程池关闭,
        // 如果发生了这种情况,那么回滚任务并且执行拒绝策略。
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 2.2 如果没有线程,那么启动一个新的线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3. 如果任务队列已满,那么尝试开启新的线程去执行,
    // 只要线程数小于 maximumPoolSize 就可以开启成功,否则执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

原理验证

package com.example.pool;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorTest {
  public static void main(String[] args) {
    int number = 2;
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,// 2 个常驻核心线程
        5,// 最大 5 个线程
        3,// 超过核心线程数时,线程空闲时间达到 3 秒,则销毁空闲线程直到剩余线程数为 2
        TimeUnit.SECONDS,// 单位:秒
        new LinkedBlockingQueue<>(3),// 任务队列,可以存放 3 个任务
        Executors.defaultThreadFactory(),// 默认的线程工厂
        new ThreadPoolExecutor.AbortPolicy());// 拒绝策略
    for (int i = 0; i < number; i++) {
      threadPool.execute(() -> System.out.println(Thread.currentThread().getName()));
    }
    System.out.println(threadPool.getPoolSize());
    TimeUnit.SECONDS.sleep(4);
    System.out.println(threadPool.getPoolSize());
    threadPool.shutdown();
  }
}

依次将 number 变量值改为 2、5、6、7、8、9,会发现上面源码的规律:

  1. number = 2 时,因为核心线程数都空闲,所以由两个线程分别执行;
  2. number = 5 时,因为 2 个核心线程处理了 2 个任务,还有 3 个任务存放在任务队列中,5 个任务由两个线程执行;
  3. numebr = 6 时,发现 2 个核心线程都在工作,并且任务队列存放了 3 个任务(已满),所以尝试开启新的线程执行任务,此时有 3 个线程,并且等待 4 秒后恢复为 2 个线程;
  4. numebr = 7 时,发现 2 个核心线程和 1 个临时线程都在工作,并且任务队列存放了 3 个任务(已满),所以尝试开启新的线程执行任务,此时有 4 个线程,并且等待 4 秒后恢复为 2 个线程;
  5. numebr = 8 时,发现 2 个核心线程和 2 个临时线程都在工作,并且任务队列存放了 3 个任务(已满),所以尝试开启新的线程执行任务,此时有 5 个线程,并且等待 4 秒后恢复为 2 个线程;
  6. numebr = 9 时,发现 2 个核心线程和 3 个临时线程都在工作(已经达到最大线程数),并且任务队列存放了 3 个任务(已满),所以执行拒绝策略(此处的拒绝策略是抛异常 java.util.concurrent.RejectedExecutionException)。

线程池之四大拒绝策略

线程池之四大拒绝策略

  1. AbortPolicy(默认):直接抛出 RejectedExecutionException 异常阻止系统正常运行;
  2. CallerRunsPolicy:“调用者运行”,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退给调用 execute() 方法的线程,由该线程执行任务;
  3. DiscardPolicy:丢弃任务,不抛出异常,如果允许丢弃任务,这是最好的一种策略;
  4. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中,再次尝试提交当前任务,不抛出异常。

如何设置 maximumPoolSize?

需要分两种情况

  1. CPU 密集型:设置为 CPU 核心数,一个 CPU 内核一次只能执行一个线程,可以使用 Runtime.getRuntime().availableProcessors() 获取 CPU 内核数量。

CPU 密集型也叫计算密集型,就是大部分时间用来计算,比如挖矿、视频解码。

  1. IO 密集型:设置为 corePoolSize 的两倍。

IO 密集型就是频繁操作数据流,比如将日志追加到日志文件中、网络下载。


流式计算

Lambda 表达式

形式为:(参数列表) -> {业务代码}。
使用 Lambda 表达式代替匿名内部类,编译出来的 class 字节码文件不会有匿名内部类的字节码文件(注意:idea 中不会体现出匿名内部类的字节码文件,需要到文件夹中查看),而且代码非常简洁! Lambda 表达式

四大函数式接口

函数式接口就是有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。一般会在类上加 @FunctionalInterface 注解。Java 中函数式接口有很多,并不仅仅只有四个。Java 8 中可以 四大函数式接口 其他很多 java.util.function 包下的接口是这四个的扩展。

  1. Consumer 消费型接口:一个输入,没有输出。
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    ...
}
  1. Function 函数型接口:一个输入,有输出。
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
    ...
}
  1. Predicate 断定型接口:一个输入,boolean 输出。
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
    ...
}
  1. Supplier 供给型接口:无输入,有输出。
@FunctionalInterface
public interface Supplier<T> {

    T get();
}

测试:

package com.example.fi;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class FunctionalInterfaceTest {
  public static void main(String[] args) {
    Consumer<String> consumer = message -> System.out.println(message);
    Function<String, Integer> function = key -> key.hashCode();
    Predicate<String> predicate = target -> target == null || target.isEmpty();
    Supplier<String> supplier = () -> "Scala new bee!";

    String str = supplier.get();
    System.out.println(str);
    consumer.accept(str);
    System.out.println(function.apply(str));
    System.out.println(predicate.test(str));
  }
}

Scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程函数式编程的各种特性。Scala 运行在 Java 虚拟机上,并兼容现有的 Java 程序。Java 8 中的函数相关代码大部分由 Scala 作者 Martin Odersky 贡献。Spark 由 Scala 编写。

Stream 流式计算

Stream 流用于操作数据源生成的元素序列。存储交给集合、数据库;计算交给 Stream 流。

特点

  1. Stream 不存储数据;
  2. Stream 不改变源对象;
  3. Stream 是延迟执行,只有调用行动算子(即需要返回结果)时才会执行。
package com.example.stream;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * 题目:请按照给出数据,找出同时满足
 * 1. 偶数 ID
 * 2. 年龄大于 24
 * 3. 用户名转为大写
 * 4. 用户名字母倒排序
 * 5. 最后只输出一个用户名字
 */
public class StreamTest {
  public static void main(String[] args) {
    User u1 = new User(1L, "a", 23);
    User u2 = new User(2L, "b", 24);
    User u3 = new User(3L, "c", 22);
    User u4 = new User(4L, "d", 28);
    User u5 = new User(6L, "e", 26);
    List<User> userList = Arrays.asList(u1, u2, u3, u4, u5);
    userList.stream()
        .filter(user -> user.getId() % 2 == 0)
        .filter(user -> user.getAge() > 24)
        .map(user -> user.getName().toUpperCase())
        .sorted(Comparator.reverseOrder())
        .limit(1)
        .forEach(System.out::println);
  }
}

@Data
@AllArgsConstructor
class User {
  private Long id;
  private String name;
  private Integer age;
}

Stream
BaseStream

分支合并 ForkJoin

  • Fork:将复杂的事情进行拆分,变成一个个小任务;
  • Join:将拆分任务的执行结果合并。

现在是大数据时代,数据量动则几十亿,普通的处理方式无法快速得到结果。Google 是第一家需要处理大数据的公司,于 2004 年发布了分布式并行计算论文 MapReduce。随后大数据框架 Hadoop 开发者 Doug Cutting 采用了 MapReduce 论文的思想,实现了分布式计算引擎 MapReduce。MapReduce 的核心思想是将一个庞大的计算任务分配给多台廉价服务器,并行计算(Map 阶段),将计算后的结果汇总到一起(Reduce 阶段)。Spark 的弹性分布式数据集(Resilient Distributed Dataset)也是类似的思想。

ForkJoin 可以并行执行任务,在数据量庞大的情况下能明显提高效率。而且 ForkJoin 中线程 A 执行完毕后发现线程 B 还没有执行完毕,线程 A 会帮助线程 B 执行任务(其任务采用的是双端队列,所以假如线程 B 从上往下执行,那么线程 A 会从下往上帮助执行任务,即工作窃取)。

package com.example.forkjoin;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

/**
 * 求和计算的任务!
 * 如何使用 ForkJoin
 * 1、ForkJoinPool 通过它来执行
 * 2、计算任务 ForkJoinPool.execute(ForkJoinTask task)
 * 3. 计算类要继承 ForkJoinTask
 */
public class ForkJoinTest {
  public static void main(String[] args) throws ExecutionException, InterruptedException {
    normal();
    forkJoin();
    stream();
  }

  // 普通处理方法
  private static void normal() {
    long sum = 0L;
    long start = System.currentTimeMillis();
    for (long i = 1L; i <= 10_0000_0000; i++) {
      sum += i;
    }
    long end = System.currentTimeMillis();
    System.out.println("normal, sum: " + sum + ", time: " + (end - start) + " ms.");
  }

  // ForkJoin
  private static void forkJoin() throws ExecutionException, InterruptedException {
    long start = System.currentTimeMillis();
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    ForkJoinTask<Long> task = new ForkJoinTaskDemo(0L, 10_0000_0000L);
    ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
    Long sum = submit.get();
    long end = System.currentTimeMillis();
    System.out.println("forkJoin, sum: " + sum + ", time: " + (end - start) + " ms.");
  }

  // 流
  private static void stream() {
    long start = System.currentTimeMillis(); // Stream并行流 () (]
    long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
    long end = System.currentTimeMillis();
    System.out.println("stream, sum: " + sum + ", time: " + (end - start) + " ms.");
  }
}

class ForkJoinTaskDemo extends RecursiveTask<Long> {
  private long start;
  private long end;

  private long temp = 100_0000L;

  public ForkJoinTaskDemo(long start, long end) {
    this.start = start;
    this.end = end;
  }

  @Override
  protected Long compute() {
    if ((end - start) < temp) {
      long sum = 0L;
      for (long i = start; i <= end; i++) {
        sum += i;
      }
      return sum;
    } else { // ForkJoin 递归
      long middle = (start + end) / 2; // 中间值
      ForkJoinTaskDemo task1 = new ForkJoinTaskDemo(start, middle);
      task1.fork(); // 拆分任务,把任务压入线程队列
      ForkJoinTaskDemo task2 = new ForkJoinTaskDemo(middle + 1, end);
      task2.fork(); // 拆分任务,把任务压入线程队列
      return task1.join() + task2.join();
    }
  }
}

执行结果
执行结果
ForkJoinTask 家族
ForkJoinTask 家族
ForkJoinPool 源码
ForkJoinPool 源码

异步回调

异步回调

常见的异步回调是 Ajax。

测试代码

package com.example.callback;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class CompletableFutureTest {
  public static void main(String[] args) throws InterruptedException, ExecutionException {
    // 没有返回值的异步回调
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("没有返回值的异步回调!");
    });
    System.out.println("处理业务!");
    // 防止因为没有守护线程而关闭应用,导致异步回调不打印
    TimeUnit.SECONDS.sleep(2);
    System.out.println(future.get());// 输出 null

    System.out.println("------------------------------");

    // 有返回值的异步回调
    CompletableFuture<String> resultFuture = CompletableFuture.supplyAsync(() -> {
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("有返回值的异步回调!");
      int i = 1 / 0;
      return "success";
    }).thenApply(t -> {// t: 上面正常运行返回的结果
      // 程序正常时走本方法,像 Ajax 中的 success 函数
      System.out.printf("t: %s\n", t);
      return "ok";// 返回结果会反映到 get 中
    }).exceptionally(e -> {// e: 上面程序报错的异常
      // 程序报错时走本方法返回一个值,像 Ajax 中的 error 函数
      System.out.println(e.getMessage());
      return "error";// 返回结果会反映到 get 中
    });
    System.out.println("处理业务!");
    // 防止因为没有守护线程而关闭应用,导致异步回调不打印
    TimeUnit.SECONDS.sleep(2);
    System.out.println(resultFuture.get());// 打印 error(如果把 int i = 1 / 0; 注释,则打印 ok)
  }
}

底层代码

// 线程池
private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

// 有返回值的方法
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    return asyncSupplyStage(asyncPool, supplier);
}

static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
                                                 Supplier<U> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<U> d = new CompletableFuture<U>();
    // 发现其实就是用线程池执行任务
    e.execute(new AsyncSupply<U>(d, f));
    return d;
}

分析

异步:就是使用另一个线程执行任务,达到异步效果;
回调:就是通过一系列方法将回调函数注册到任务中,当任务执行完毕后调用回调函数。

回调函数还有很多。


未完待续...