Java笔记13 - 多线程

多线程基础

  • 现代操作系统都可以同时执行多任务. 多任务就是同时运行多个任务
  • CPU执行代码都是一条条执行. 操作系统可以让CPU对多个任务轮流执行

进程

  • 一个任务是一个进程, 例如: 浏览器, 视频播放器
  • 进程内部可以同时执行多个子任务: 线程
  • 一个进程可以包含一个或多个线程, 但至少会有一个线程
  • 操作系统调用的最小任务单位是线程
  • 同一个应用程序可以有多个进程, 也可以有多个线程
  • 各种模式:
    • 多进程模式: 每个进程只有一个线程
    • 多线程模式: 一个进程有多个线程
    • 多进程 + 多线程模式

进程VS线程

  • 多任务可以由多线程实现, 也可以单进程内的多线程, 也可以混合模式
  • 多线程相比, 多进程缺点:
    • 进程比线程开销大,
    • 进程间的通信比线程慢, 因为线程间的通信就是读写同一个变量
  • 多进程优点:
    • 多进程的稳定性高, 一个进程的崩溃不会影响其他进程
    • 多线程中, 有一个线程崩溃导致整个进程崩溃

多线程

  • Java语言内置多线程支持

    • 一个Java程序就是一个JVM进程
    • JVM进程用一个主线程启用main()方法
    • main()方法内部, 可以启用多个线程
    • JVM内部负责垃圾回收其他线程
  • Java多线程编程特点:

    • 多线程模型是JAVA程序最基本的并发模型
    • 后续读写网络, 数据库, Web开发等都需要依赖Java多线程模型

创建新线程

  • 一个线程启动后, 如果什么也不做, 就结束了
  • 方法一: Thread派生一个自定义类, 复写run方法
  • 方法二: 创建Thread实例时, 传入一个Runnable实例
  • 多个线程同时运行, 由操作系统调度, 程序本身无法确定线程的调度顺序
  • 直接调用Thread.run()是无效的, 相当于直接执行java代码主线程
  • 可以使用Thread.setPriority(int n): 可以让操作系统调用优先级便高, 但不能保证优先级高的线程一定会先执行
  • Thread.sleep() 可以让当前线程暂停一段时间

线程的状态

  • 线程的状态

    • New: 新创建的线程, 尚未执行
    • Runable: 运行中的线程, 正在执行run()方法的Java代码
    • Blocked: 运行中的线程, 因为某些操作被阻塞而挂起
    • Waiting: 运行中的线程, 因为某些操作再等待中
    • Timed Waiting: 运行中的线程, 因为执行sleep()方法正在计时等待
    • Terminated: 线程已终止, 因为run()方法执行完毕
  • 线程再执行过程中, 可以在四种状态之间相互相互, 直到线程终止

  • 线程终止的原因:

    • 线程正常终止: run()方法执行到return语句返回
    • 线程意外终止: run()方法因为未捕获异常导致线程终止
    • 对某个线程Thread实例调用stop()方法终止
  • t.join()等待线程结束再继续执行

中断线程

  • 中断线程就是其他线程给该线程发一个信号, 该线程收到信号后结束执行run()方法
  • 在其他线程中对目标线程调用interrupt()方法
  • 目标线程反复检测自身状态是否未interrupted状态, 如果是, 就立刻结束运行
  • interrupt()仅仅是发出中断请求, 能不能立即中断, 要看具体代码
  • 再具体代码中使用isInterrupted进行判断
  • t.join()会让main线程进入等待状态
  • 如果对main线程调用interrupt(), join()方法, 会立刻抛出InterruptedException
  • 目标线程只要捕获到join()方法抛出的InterruptedException, 说明有其他线程对其调用了interrupt方法, 通常情况下线程应该立刻结束返回
public class Main {
  public static void main(String[] args) throws InterruptedException {
    Thread t = new MyThread();
    t.start();
    Thread.sleep(100);
    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线程结束, 一旦t被终止, 就会停止等待, 立刻抛出异常
    } catch (InterruptedException e) {
      System.out.println("interrupted");
    }
    hello.interrupt(); // 中止hello线程, t线程结束前会对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;
      }
    }
  }
}
  • 另一个终止的方法: 设置标志位
  • 通过running标志位停止线程运行
public class Main {
  public static void main(String[] args) throws InterruptedException {
    HelloThread t = new HelloThread();
    t.start();
    Thread.sleep(1);
    t.running = 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!");
  }
}
  • 存在与线程之间的变量用volatile修饰, 确保每个变量都能读到更新后的值

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
Main Memory
│ │
┌───────┐┌───────┐┌───────┐
│ │ var A ││ var B ││ var C │ │
└───────┘└───────┘└───────┘
│ │ ▲ │ ▲ │
─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─
│ │ │ │
┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐
▼ │ ▼ │
│ ┌───────┐ │ │ ┌───────┐ │
│ var A │ │ var C │
│ └───────┘ │ │ └───────┘ │
Thread 1 Thread 2
└ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘

  • 线程间变量关系
    • 在java虚拟机中变量的值保存再主内存中
    • 线程访问的时候先获取一个副本, 保存在自己的工作内存中
    • 如果线程改变了值, 虚拟机会在某个时刻把修改后的值写回到主内存中
    • 但是时间不确定
  • volatile关键字作用:
    • 每次访问变量时, 总是获取内存的最新值
    • 每次修改变量, 立刻回写到主内存
    • 解决了可见性的问题, 当一个线程修改了某个共享变量的值, 其他线程能够立刻看到修改后的值

守护线程

  • 守护线程是为其他线程服务的线程.
  • 在JVM中, 所有非守护线程都执行完毕后, 无论有没有守护线程, 虚拟机都会自动退出.
  • start()之前调用setDaemon(true)将线程变成守护线程
  • 在守护线程不能持有任何需要关闭的资源.
  • 例如: 打开文件等, 因为虚拟机退出时, 守护线程没有任何机会来关闭文件, 这会导致数据丢失

线程同步

  • 如果多个线程同时读写共享变量, 会出现数据不一致的问题

  • 对变量进行读取和写入时, 结果要正确, 必须保证是原子操作.

  • 原子操作是指不能被中断的一个或一系列操作.

  • 在多线程模型下, 要保证逻辑正确, 对共享变量进行读写时, 必须保证一组指令以原子方式执行.

  • 即某一个线程执行时, 其他线程必须等待

  • 通过加锁解锁的操作, 保证3条指令总是在一个线程执行期间, 不会有其他线程会进入此指令区间

  • 加锁和解锁之间的代码块称之为临界区. 任何时候临界区最多只有一个线程能执行

  • 保证一段代码的原子性, 就是通过加锁和解锁实现的.

  • 使用关键字synchronized对一个对象进行加锁.

      synchronized(Counter.lock) { // 获得锁
        ...
      } // 释放锁
  • synchronized解决了多线程同步访问共享变量变量的正确性问题. 但是性能下降
  • 如何使用:
    1. 找出修改共享变量的线程代码块
    2. 选择一个共享实例作为锁
    3. 使用synchronized(lockObject){...}
  • 无论是否有异常, 都会在代码块结束的时候释放锁
  • 获取到的是哪个锁非常重要

不需要synchronized的操作

  • 原子操作:

    • 基本类型(longdouble除外)赋值
    • 引用类型赋值
  • 单原子操作不需要同步

  • 多行赋值语句, 就必须保证是同步操作

  • 巧妙转换, 就不需要同步操作了

class Pair {
  int first;
  int last;
  public void set(int first, int last) {
    synchronized(this) {
      this.first = first;
      this.last = last;
    }
  }
}

class Pair2 {
  int[] pair;
  public void set(int first, int last) {
    int[] ps = new int[] {
        first,
        last
    }; //  这是`ps`方法内部定义的局部变量, 每个线程都会有各自的局部变量, 互补影响, 不需要同步
    this.pair = ps; // 这是一个原子操作
  }
}

同步方法

  • 方法使用synchronized(this)进行锁定, 就会针对每次操作进行锁定
  • 如果一个类设计允许多线程正确访问, 这个类就是线程安全的
  • 只有提供静态方法, 没有成员变量的类, 也是线程安全的
  • 使用synchronized修饰方法, 表示整个方法使用this加锁, 就是同步方法
  • 静态方法添加, 表示锁住该类的class实例

死锁

  • JVM允许一个线程重复获取同一个锁
  • 能被同一个线程反复获取的锁, 叫做可重入锁.
  • 获取锁的是, 需要记录第几次获取, 每获取一次锁, 记录+1, 退出一次, 记录-1, 一直到0.

死锁介绍

  • 一个线程获取一个锁以后, 再获取另一个锁.
  • 两个线程各自持有不同的锁, 然后试图尝试获取对方手里的锁, 造成爽发无限等待下去, 这就是死锁.
  • 死锁发生活, 没有任何机制能解除死锁, 只能强制结束JVM进程
  • 避免死锁方法: 线程获取锁的顺序保持一致

使用wait和notify

  • synchronized解决了多线程竞争的问题, 但没有解决多线程协助的问题
  • 多线程协调运行原则:
    • 当条件不满足时, 线程进入等到状态;
    • 当条件满足时, 线程被唤醒, 继续执行任务;
  • notifyAll()更安全, 一次性唤醒全部线程.
public class Main {
  public static void main(String[] args) throws InterruptedException {
    TaskQueue q = new TaskQueue();
    ArrayList<Thread> ts = new ArrayList<Thread> ();
    for (int i = 0; i < 5; i++) { // 模拟不同的线程抢着获取任务
      Thread t = new Thread() {
        public void run() {
          while (true) { // 任务不断循环, 因为每次都是唤醒所有, 所有用`while`, 重新上锁并等待
            try {
              String s = q.getTask();
              System.out.println("execute task: " + this.getName() + " " + s);
            } catch (InterruptedException e) {
              return;
            }
          }
        }
      };
      t.start();
      ts.add(t);
    }

    Thread add = new Thread() {
      public void run() {
        for (int i=0; i<10;i++) { // 放了十个task
          // 放入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(Thread tItem : ts) {
      tItem.interrupt();
    }
  }
}

class TaskQueue {
  Queue<String> queue = new LinkedList<>();
  public synchronized void addTask(String s) {
    this.queue.add(s);
    this.notifyAll(); // 唤醒this锁等待的所有线程
  }
  public synchronized String getTask() throws InterruptedException {
    while (queue.isEmpty()) { // 先判断队列是否为空, 为空等待循环, 不为空的时候, 取第一个元素
      // 释放this锁
      this.wait(); // wait方法不会返回, 需要等待从其他线程唤醒, 才会继续执行
      // 重新获取this锁
    }
    return queue.remove();
  }
}

使用ReentrantLock

  • synchronized关键字用于加锁
    • 锁很重,性能不好
    • 获取时必须一直等待, 没有额外的尝试机制
  • ReentrantLock是Java代码实现的锁, 必须先获取锁, 然后再finally中释放
  • ReentrantLock是可重入锁, 一个线程可以多次获取同一个锁
  • 可以尝试获取锁.
    if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取锁, 最多等待1s, 如果1s后未获取, `tryLock`返回`false`
      try {
        // ...
      } finally {
        lock.unlock();
      }
    }

使用Condition

  • 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() throws InterruptedException {
    lock.lock(); // 获取锁
    try {
      while(queue.isEmpty()) { // 判断队列是否为空
        condition.await(); // 队列为空, 释放锁, 进入等待
      }
      return queue.remove();
    } finally {
      lock.unlock();
    }
  }
}
  • synchronized锁对象行为基本保持一致
    • await()会释放当前锁, 进入等待状态
    • signal()会唤醒某个线程的等待
    • signalAll()会唤醒所有等待的锁
    • 线程从await()返回后需要重新获得锁.
  • 可以指定等待唤醒时间
    if (condition.await(1, TimeUnit.SECONDS)) {
      // 被其他线程唤醒
    } else {
      // 指定时间内没有被其他线程唤醒
    }

使用ReadWriteLock

  • ReadWriteLock能力:
    • 只允许一个线程写入(其他线程既不能写入, 也不能读取);
    • 没有写入时, 多个线程允许同时读(提高性能);
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

  • 读的过程中也允许写锁写入. 但需要一些额外的代码判读读的过程中是否有写入

  • 乐观锁: 乐观的估计读的过程中大概率不会有写入

  • 悲观锁: 读的过程中拒绝有写入

  • 读取锁进一步细分未: 乐观读取和悲观读取, 提升并发效率

    • 代码更加复杂
    • 不可重入锁, 不能在一个线程中反复获取同一个锁
  • 还可以让悲观读锁升级为写锁功能, 主要使用在if-then-update的场景:

  • 先读, 如果读的数据不满足条件, 再写入

使用ConCurrent集合

  • BlockingQueue: 当一个线程调用装个TaskQueuegetTask()方法, 内部可能会让线程编程等待状态, 直到队列条件满足不为空, 线程被唤醒后, getTask()方法才返回.
  • 针对List, Map, Set, Deque等, 均有对应的包装类.
  • 这些并发开发集合, 和非线程安全的集合类完全相同.
  • 所有的同步和加锁逻辑都在集合内部实现, 对外部调用者来说, 只需要正常按接口引用.
  • Map threadSafeMap = Collections.synchronizedMap(unsafeMap);包装了非线程安全的Map, 但是读写使用synchronized实现, 获得安全集合的性价比很低.

使用Atomic

  • java.util.concurrent提供了一组原子操作的封装类
  • AtomicInteger主要作用:
    • 增加值并返回新值: int addAndGet(int delta)
    • 加1后返回新值: int incrementAndGet()
    • 获取当前值: int get()
    • 使用CAS方式设置: int compareAndSet(int expect, int update)
  • Atomic类通过无锁方式实现了线程安全访问.
  public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    /**
     * 如果当前的`AtomicInteger`是`prev`, 那么就更新`next`, 返回`true`
     * 如果不是`prev`, 就什么都不干, 返回`fasle`
     */
    do {
      prev = var.get();
      next = prev + 1;
    } while (!var.compareAndSet(prev, next));
    return next;
  }
  • 利用AtomicLong可以一个多线程安全的全局唯一ID生成器
class IdGenerator {
  AtomicLong var = new AtomicLong(0);
  public long getNextId() {
    return var.incrementAndGet();
  }
}
  • 高度竞争的情况下, 使用LongAdderLongAccumulator

线程池

  • 大量线程的创和销毁需要大量时间.
  • 线程池: 复用一组线程, 很多小任务让一组线程来执行, 而不是一个任务对应一个线程.
  • 没有任务时, 线程处于等待状态. 有新任务了, 就分配空闲线程执行.
  • 若所有线程都处于忙碌状态, 新任务要么放入队列等待, 要么增加一个新线程进行处理.
    // 创建固定大小的线程池
    ExecutorService executor = Executors.newFixedThreadPool(3);
    // 提交任务
    executor.submit(task1);
    executor.submit(task2);
    executor.submit(task3);
  • ExecutorService是接口, 常用实现类:
    • FixThreadPool: 线程数固定的线程池
    • CachedThreadPool: 线程数根据任务动态调整的线程池
    • SingleThreadExecutor: 仅单线程执行的线程池
  • 关闭线程池
    • shutdown()方法关闭线程池, 会等待正在执行的任务完成, 然后再关闭
    • shutdownNow()立刻停止正在执行的任务
    • awaitTermination()会等待指定的时间让线程关闭

ScheduledThreadPool

  • ScheduledThreadPool: 解决需要定期反复执行的任务
// 1s后执行指定任务
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2s后开始执行固定任务, 每3s执行
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2s后开始执行, 以3s为间隔执行
ses.scheduleWithFixedDelay(new Task("fix-delay"), 2, 3, TimeUnit.SECONDS);
  • FixedRate模式下, 如果此任务的任何执行时间超过其周期, 后续执行可能会延续, 但不会并发执行
  • 如果任务的任何执行遇到异常, 则将禁止后续任务的执行

使用Future

  • 任务提供Runnable接口, 就可以让线程池执行
  • Callable是一个泛型接口, 多一个返回值
class Task1 implements Callable<String> {
  public String call() throws Exception {
    return longTimeCalculation();
  }
}
  • ExecutorService.submit()返回一个Future类型
  • Future类型的实例代表一个未来能获取结果的对象
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务
Callable<String> task = new Task("task1");
// 提交任务
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果
String result = future.get(); // 可能阻塞
  • 主线程的某个时刻调用get方法, 如果已经执行完毕, 就自动返回, 如果没有执行完, 就会阻塞
  • Future<v>接口表示一个未来可能会返回的结果:
    • get()获取结果(可能会等待)
    • get(long timeout, TimeUnit unit)获取结果, 但只等待指定的时间
    • cancel(boolean mayInterruptIfRunning); 取消当前任务
    • isDone(): 判断任务是否完成

CompletableFuture

  • CompletableFuture: 可以传入回调对象, 当异步任务完成或者发生异常时, 自动调用回调对象的回调方法

  • 优点:

    • 异步任务结束时, 会自动回调某个对象的方法;
    • 异步任务出错时, 会自动回调某个对象的方法;
    • 主线程设置好回调后, 不再关心异步任务的执行;
  • 多个CompletableFuture可以串行执行

  • 多个CompletableFuture并行执行:

    • anyOf: 任意一个异步完成
    • allOf: 全部异步完成
  • xxx(): 表示xxx方法再当前线程执行

  • xxxAsync(): 表示xxx方法将要异步再线程池中运行

public class Main {
  public static void main(String[] args) throws Exception {
    CompletableFuture<String> cfQueryCodeFromSina = CompletableFuture.supplyAsync(() -> {
      return queryCode("石油", "www.sina.com");
    });
    CompletableFuture<String> cfQueryCodeFrom163 = CompletableFuture.supplyAsync(() -> {
      return queryCode("石油", "www.163.com");
    });
    // 使用`anyOf`进行合并
    CompletableFuture<Object> cfQueryCode = CompletableFuture.anyOf(cfQueryCodeFromSina, cfQueryCodeFrom163);

    CompletableFuture<Double> cfFetchPriceFromSina = cfQueryCode.thenApplyAsync((code) -> {
      return fetchPrice((String) code, "www.sina.com");
    });
    CompletableFuture<Double> cfFetchPriceFrom163 = cfQueryCode.thenApplyAsync((code) -> {
      return fetchPrice((String) code, "www.163.com");
    });
    CompletableFuture<Object> cfFetchPrice = CompletableFuture.anyOf(cfFetchPriceFromSina, cfFetchPriceFrom163);

    cfFetchPrice.thenAccept((res) -> {
      System.out.println("price: " + res);
    });
    // 主线程不要立刻结束, 否则CompletableFuture默认使用的线程会立刻关闭
    Thread.sleep(2000);
  }
  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;
  }
}

ForkJoin

  • 把一个大任务拆分成多个小任务执行
  • Fork/Join原理: 判断一个任务是否足够小, 如果是, 直接计算. 如果不是, 就拆分成小任务, 分别计算
  • 在多核CPU上的排序尤为明显
public class Hello {
  public static void main(String[] args) {
    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(10000000);
  }
}

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(10);
        } catch (InterruptedException e) {
        }
      }
      return sum;
    } else { // 任务不够小, 拆分计算
      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会运行两个子任务
      invokeAll(subTask1, subTask2);
      // 获取子任务结果
      Long subresult1 = subTask1.join();
      Long subresult2 = subTask2.join();
      Long result = subresult1 + subresult2;
      System.out.println("result = " + subresult1 + " + " + subresult2 + " ===> " + result);
      return result;
    }
  }

}

使用ThreadLocal

  • Thread.getCurrentThread()获取当前线程
  • 对于多任务, Java可以利用线程池处理这些任务, 同时复用线程
  • Web程序就是典型的多任务, 每个用户请求页面都是一个任务
  public void process(User user) {
    checkPermission(user);
    doWork(user);
    saveStatus(user);
    senResponse(user);
  }
  • 在一个线程中, 很跨若干方法调用, 需要传递的传递的对象, 为上下文(Context)
  • Context是一种状态, 可以是用户身份, 任务信息等
  • ThreadLocal: 在一个线程中传递同一个对象
  • 通常用静态字段初始化
static ThreadLocal<User> threadLocalUser = new ThreadLocal()

void processUser(user) {
  try {
    threadLocalUser.set(user);
    step1();
    step2();
  } finally {
    threadLocalUser.remove()
  }
}
  • 在移除之前, 所有方法都可以随时获取到该User实例, 是同一个对象
  • 相当于每个线程的一个独立的存储空间, 各个线程的threadLocal关联的实例互不干扰
  • 一定要在finally中清除, 因为, 线程结束后, 会放到线程池, 其他线程再取到的时候, 会用到上次的状态
public class Hello {
  public static void main(String[] args) {
    try (var ctx = new UserContext("Bob")) {
      // 可任意调用UserContext.currentUser()
      String currentUser = UserContext.currentUser();
    } // 在此自动调用UserContext.close()方法释放ThreadLocal关联的对象
  }
}

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(){
    // TODO Auto-generated method stub
    ctx.remove();
  }

}

疑问

StampedLock获得œ乐观读取锁后, 为什么还要获取悲观读取锁

posted @ 2020-05-31 15:27  张润昊  阅读(196)  评论(0编辑  收藏  举报