Java 中的线程

Java 各版本新特性 - 虚拟线程 Linux 理论知识

线程的状态

JDK映射的状态

参考 java.lang.Thread.State 的枚举状态

/**
 * A thread state.  A thread can be in one of the following states:
 * <ul>
 * <li>{@link #NEW}<br>
 *     A thread that has not yet started is in this state.
 *     </li>
 * <li>{@link #RUNNABLE}<br>
 *     A thread executing in the Java virtual machine is in this state.
 *     </li>
 * <li>{@link #BLOCKED}<br>
 *     A thread that is blocked waiting for a monitor lock
 *     is in this state.
 *     </li>
 * <li>{@link #WAITING}<br>
 *     A thread that is waiting indefinitely for another thread to
 *     perform a particular action is in this state.
 *     </li>
 * <li>{@link #TIMED_WAITING}<br>
 *     A thread that is waiting for another thread to perform an action
 *     for up to a specified waiting time is in this state.
 *     </li>
 * <li>{@link #TERMINATED}<br>
 *     A thread that has exited is in this state.
 *     </li>
 * </ul>
 *
 * <p>
 * A thread can be in only one state at a given point in time.
 * These states are virtual machine states which do not reflect
 * any operating system thread states.
 *
 * @since   1.5
 * @see #getState
 */
public enum State {
	/**
	 * Thread state for a thread which has not yet started.
	 */
	NEW,
 
	/**
	 * Thread state for a runnable thread.  A thread in the runnable
	 * state is executing in the Java virtual machine but it may
	 * be waiting for other resources from the operating system
	 * such as processor.
	 */
	RUNNABLE,
 
	/**
	 * Thread state for a thread blocked waiting for a monitor lock.
	 * A thread in the blocked state is waiting for a monitor lock
	 * to enter a synchronized block/method or
	 * reenter a synchronized block/method after calling
	 * {@link Object#wait() Object.wait}.
	 */
	BLOCKED,
 
	/**
	 * Thread state for a waiting thread.
	 * A thread is in the waiting state due to calling one of the
	 * following methods:
	 * <ul>
	 *   <li>{@link Object#wait() Object.wait} with no timeout</li>
	 *   <li>{@link #join() Thread.join} with no timeout</li>
	 *   <li>{@link LockSupport#park() LockSupport.park}</li>
	 * </ul>
	 *
	 * <p>A thread in the waiting state is waiting for another thread to
	 * perform a particular action.
	 *
	 * For example, a thread that has called {@code Object.wait()}
	 * on an object is waiting for another thread to call
	 * {@code Object.notify()} or {@code Object.notifyAll()} on
	 * that object. A thread that has called {@code Thread.join()}
	 * is waiting for a specified thread to terminate.
	 */
	WAITING,
 
	/**
	 * Thread state for a waiting thread with a specified waiting time.
	 * A thread is in the timed waiting state due to calling one of
	 * the following methods with a specified positive waiting time:
	 * <ul>
	 *   <li>{@link #sleep Thread.sleep}</li>
	 *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
	 *   <li>{@link #join(long) Thread.join} with timeout</li>
	 *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
	 *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
	 * </ul>
	 */
	TIMED_WAITING,
 
	/**
	 * Thread state for a terminated thread.
	 * The thread has completed execution.
	 */
	TERMINATED;
}

参考: https://zhuanlan.zhihu.com/p/82769548

1.NEW

NEW: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

2.RUNNABLE

  • 调用start(),进入可运行态
  • 当前线程sleep()结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态
  • 当前线程时间片用完,调用当前线程的yield()方法,当前线程进入可运行状态
  • 锁池里的线程拿到对象锁后,进入可运行状态
  • 正在执行线程必属于此状态

这时候线程处于等待CPU分配资源阶段,谁拿到CPU资源,谁执行; 注意: Java线程是映射到操作系统的线程, 真正决定权在操作系统调度; 它表示线程在JVM层面是执行的,但在操作系统层面不一定

3.BLOCKED

  • 当前线程调用Thread.sleep(),进入阻塞态
  • 运行在当前线程里的其它线程调用join(),当前线程进入阻塞态。
  • 等待用户输入的时候,当前线程进入阻塞态。

线程在阻塞等待 monitor lock (监视器锁)
一个线程在进入 synchronized 修饰的临界区的时候,或者在synchronized临界区中调用Object.wait然后被唤醒重新进入 synchronized 临界区都对应该态。

4.WAITING

线程拥有对象锁后进入到相应的代码区后,调用相应的“锁对象”的wait()后产生的一种结果 in short 已经拥有对象锁后, 主动释放并等待其他线程通知; 它的场景是, 依赖其他资源, 主动放弃的结果

  • 变相的实现
    LockSupport.park()
    LockSupport parkNanos( )
    LockSupport parkUntil( )
    Thread join( )

和 BLOCKED 状态的区别?

  • BLOCKED是虚拟机认为程序还不能进入某个区域,因为同时进去就会有问题,这是一块临界区
  • WAITING 的先决条件是将要进入临界区,也就是线程已经拿到了“门票”,自己可能进去做了一些事情,但此时通过判定某些业务上的参数(由具体业务决定),发现还有一些其他配合的资源没有准备充分,那么自己就等等再做其他的事情

这里的临界区可以理解为, JVM 在这连接两者之间切换的临界状态

当一个线程试图获取一个对象的锁,而该锁已经被其他线程占用时,该线程会进入BLOCKED状态。 当一个线程在等待另一个线程执行特定操作时,它会进入WAITING状态。

WAITING 为什么需要某个对象锁?

既然要等,就要考虑等什么,这里等待的就是一个对象发出的信号,所以要基于对象而存在。
不用对象也可以实现,比如 suspend()/resume()就不需要,但是它们是反面教材,表面上简单,但是处处都是问题

5.TIMED_WAITING

相当于使用某个时间资源作为锁对象,进而达到等待的目的,当时间达到时触发线程回到工作状态。 下列带超时的方式:
Thread.sleep、0bject.wait、 Thread.join、 LockSupport.parkNanos、 LockSupport.parkUntil

注意, 该状态不会释放锁 !!!

6.TERMINATED

终止线程的线程状态。线程正常完成执行或者出现异常。 在一个死去的线程上调用start()方法,会抛java.lang.IllegalThreadStateException.

线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

run()走完了,线程就处于这种状态。其实这只是Java 语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java 代码看到的线程状态而已。

线程的中断

捕获线程中断

线程中断, 并非字面意义上的中断, 而是一个线程标记; 每个线程都有一个Interrupted标记, 它标记着线程已经被中断.

interrupt() 操作只对处于WAITING 和TIME_WAITING 状态的线程有用,让它们]产生实质性的异常抛出。 在通常情况下,如果线程处于运行中状态,也不会让它中断,如果真的不给CPU时间,可能会导致正常的业务运行出现问题。

调用线程实例的interrupted() 方法可以触发一次该线程中断标记, 以下Thread.sleep(1000);会抛出中断(InterruptedException)异常:

@Override
public  void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println(this.getName() + " excute " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Object 的 wait(); Thread 的join(); yield(); sleep();注意一点的是无论是哪一个方法, 在抛出该异常后,中断标记会被清除

in short 当调用一次interrupted() 它的效果是发生一次InterruptedException异常, 在循环内被捕获, 继续下一个循环, 换句话说 该代码的线程永远无法被中断停止!

中断的核心方法​​

  • interrupt()​:
    中断目标线程。如果线程处于阻塞状态(如 sleep()wait()join()),会触发 InterruptedException;否则,仅设置中断标志为 true
  • isInterrupted()​:
    检查线程的中断状态,​​不会清除中断标志​​(非静态方法)。
  • Thread.interrupted()​:
    检查​​当前线程​​的中断状态,并​​清除中断标志​​(静态方法)。例如,第一次调用返回 true,第二次可能返回 false(如果中间未被再次中断)。

RentrantLock 重入锁

JAVA的java.util.concurrent框架中提供了ReentrantLock类(于JAVA SE 5.0时引入), ReentrantLock实现了lock接口

尝试获取锁的时候, 最多等待1秒; 如果1秒后仍未获取到锁, tryLock()返回false

ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

判断当前线程是否获得锁?

Reentrantlock 主要是基于 AQS, 而 AQS 继承一个抽象类 AbstractOwnableSynchronizer, 有个变量 private transient Thread exclusiveOwnerThread;, 用来存储锁持有的线程, 然后比较, 比如源码中你会看到这个 (current == getExclusiveOwnerThread())

java.util.concurrent.locks.ReentrantLock#isHeldByCurrentThread

 
/**
 * 
 *  Queries if this lock is held by the current thread.
 *
 * <p>Analogous to the {@link Thread#holdsLock(Object)} method for
 * built-in monitor locks, this method is typically used for
 * debugging and testing. For example, a method that should only be
 * called while a lock is held can assert that this is the case:
 *
 * <pre> {@code
 * class X {
 *   final ReentrantLock lock = new ReentrantLock();
 *   // ...
 *
 *   public void m() {
 *       assert lock.isHeldByCurrentThread();
 *       // ... method body
 *   }
 * }}</pre>
 *
 * <p>It can also be used to ensure that a reentrant lock is used
 * in a non-reentrant manner, for example:
 *
 * <pre> {@code
 * class X {
 *   final ReentrantLock lock = new ReentrantLock();
 *   // ...
 *
 *   public void m() {
 *       assert !lock.isHeldByCurrentThread();
 *       lock.lock();
 *       try {
 *           // ... method body
 *       } finally {
 *           lock.unlock();
 *       }
 *   }
 * }}</pre>
 *
 * @return {@code true} if current thread holds this lock and
 *         {@code false} otherwise  如果当前线程持有此锁 返回 true, 否则其他
 */
public boolean isHeldByCurrentThread() {
	return sync.isHeldExclusively();
}

基于字符串 JVM 串行代码

在WEB单体服务中, 可以基于用户ID实现串行代码;

通常ID字符串不大可能在常量池参考: Jvm 内存区域 - 常量池, 下面代码无效

//userId 不在常量池, 无效..
String userId = user.getId()
System.out.println("addrs ===>"+ System.identityHashCode(userId) );
synchronized (userId){
    ...
}

String.intern()

当你调用一个字符串的 intern()方法时,Java 首先检查字符串常量池中是否已经存在一个等于该字符串内容的字符串对象:

如果在常量池中已经存在相同内容的字符串,intern() 方法将返回常量池中的字符串对象的引用。 如果常量池中不存在相同内容的字符串,intern() 方法则会将该字符串对象包含的字符串添加到常量池中,并返回它的引用。

正解是使用 String.intern() 把它放到常量池;

//userId 先放到常量池中, 再拿到其引用
String userId = user.getId()
userId = userId.intern();//(用户多的话, 要考虑内存的问题...)
System.out.println("addrs ===>"+ System.identityHashCode(userId) );
synchronized (userId){
    ...
}

全局字符串池大小

在 Java 6 中, 这个 HashTable 固定的 bucket 数量是 1009, 后来添加了选项(-XX:StringTableSize=N) Java 7(7u40) 以后的版本字符串常量池大小被增大到60013(60013也是质数); 默认值改为60013可以让你保存大概30000个不同的字符串到常量池中而不会发生任何Hash碰撞;

可通过命令: java -XX:+PrintFlagsFinal -version 找到这个值

...
uintx StringTableSize                           = 60013                               {product}
 
...

线程池

  1. 创建一个固定大小的线程池,该线程池中的线程数量始终保持不变。
ExecutorService executor = Executors.newFixedThreadPool(5);
Executors.newCachedThreadPool():
  1. 创建一个可缓存的线程池,线程数量根据需要自动增加或减少,适用于执行大量短期异步任务的场景。
ExecutorService executor = Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor():
  1. 创建一个单线程的线程池,确保所有任务按照顺序执行。
ExecutorService executor = Executors.newSingleThreadExecutor();
Executors.newScheduledThreadPool(int corePoolSize):
  1. 创建一个固定大小的线程池,可用于执行定时任务和周期性任务。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
Executors.newWorkStealingPool()///(Java 8及以上版本):

5.创建一个工作窃取线程池,该线程池使用ForkJoinPool实现,并根据可用处理器的数量动态创建工作线程,适用于任务分割和合并的场景。

ExecutorService executor = Executors.newWorkStealingPool();

线程池(ThreadPoolExecutor)构造参数

ExecutorService

//ThreadPoolExecutor 构造方法
public ThreadPoolExecutor(
        int corePoolSize,// 线程池长期维持的线程数, 即使线程处于Idle状态, 也不会回收; 
        int maximumPoolSize, // 线程数的上限
        long keepAliveTime, TimeUnit unit,//超过corePoolSize的线程的idle时长, 超过这个时间, 多余的线程会被回收; 
        BlockingQueue<Runnable> workQueue,//任务队列
        ThreadFactory threadFactory,// 新线程的产生方式 Executors.defaultThreadFactory(),
        RejectedExecutionHandler handler) // 拒绝策略
//默认拒绝策略, 抛出的异常 RejectedExecutionException
 
//////实例
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1024);
RejectedExecutionHandler policy = new ThreadPoolExecutor.AbortPolicy();
ExecutorService excutor  =  new ThreadPoolExecutor(poolSize, poolSize*2, 
    0, TimeUnit.SECONDS,  queue,  policy);
 

RejectedExecutionHandler 的实现JDK自带的默认有4种:

  1. AbortPolicy: 丢弃任务,抛出运行时异常
  2. CallerRunsPolicy: 由提交任务的线程来执行任务
  3. DiscardPolicy: 丢弃这个任务,但是不抛异常
  4. DiscardOldestPolicy: 从队列中剔除最先进入队列的任务,然后再次提交任务

Future 结果的状态

提交到线程池后, 返回的 Future 可以获取结果及状态

 
import java.util.concurrent.*;
 
public class FutureStatusDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
 
        // 场景1:任务正常完成
        Future<Integer> normalFuture = executor.submit(() -> {
            Thread.sleep(1000);
            return 100;
        });
 
        // 场景2:任务执行中抛出异常(中断)
        Future<Integer> exceptionFuture = executor.submit(() -> {
            Thread.sleep(1500);
            throw new RuntimeException("任务执行出错");
        });
 
        // 场景3:任务被主动取消
        Future<Integer> cancelFuture = executor.submit(() -> {
            try {
                Thread.sleep(3000); // 模拟长时间任务
            } catch (InterruptedException e) {
                System.out.println("取消任务时线程被中断");
            }
            return 200;
        });
 
        // 主动取消第三个任务
        cancelFuture.cancel(true);
 
        // 检查并输出各任务状态
        checkFutureStatus("正常完成", normalFuture);
        checkFutureStatus("执行异常", exceptionFuture);
        checkFutureStatus("主动取消", cancelFuture);
 
        executor.shutdown();
    }
 
    private static void checkFutureStatus(String name, Future<Integer> future) {
        try {
            Thread.sleep(2000); // 等待任务执行/取消完成
            System.out.println("\n===== " + name + " =====");
            System.out.println("是否完成: " + future.isDone());
            System.out.println("是否取消: " + future.isCancelled());
 
            if (!future.isCancelled()) {
                Integer result = future.get();
                System.out.println("任务结果: " + result);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            System.out.println("任务异常原因: " + e.getCause().getMessage());
        }
    }
}

输出

取消任务时线程被中断

===== 正常完成 =====
是否完成: true
是否取消: false
任务结果: 100

===== 执行异常 =====
是否完成: true
是否取消: false
任务异常原因: 任务执行出错

===== 主动取消 =====
是否完成: true
是否取消: true

Process finished with exit code 0

方法作用是否阻塞
isDone()判断任务是否完成(正常结束 / 异常 / 取消都算完成)
isCancelled()判断任务是否被取消
get()获取任务执行结果,若未完成则阻塞等待
get(long timeout, TimeUnit unit)带超时的获取结果,超时未完成则抛 TimeoutException是(最多阻塞指定时间)

异步编程 CompletableFuture

CompletableFuture 是java8 提供的基于异步操作的封装

public class Main{
  public static void main(String[] args) throws Exception {
        // 创建异步执行任务:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
        // 如果执行成功:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 如果执行异常:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        // 主线程不要立刻结束, 否则CompletableFuture默认使用的线程池会立刻关闭:
        Thread.sleep(200);
    }
 
    static Double fetchPrice() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("fetch price failed!");
        }
        return 5 + Math.random() * 20;
    }
}

多线程 - 廖雪峰的官方网站

Object 的所有方法

方法签名描述注意事项
protected Object clone()创建并返回当前对象的一个拷贝。需要实现 Cloneable接口,否则会抛出 CloneNotSupportedException浅拷贝默认行为;如需深拷贝,需重写此方法。在 JDK17 中仍可用,但推荐使用复制构造函数或工具类(如 Apache Commons 或 Jackson)实现对象复制。
public boolean equals(Object obj)比较当前对象与指定对象是否相等。默认实现比较对象引用(即 ==)。重写时需遵守契约:自反性、对称性、传递性、一致性,且必须与 hashCode()方法一致。
protected void finalize()在对象被垃圾回收器(GC)回收时会回调这个方法。JDK9 后标记为弃用(deprecated)。看官方的态度不推荐使用。
public final Class<?> getClass()返回当前对象的运行时类(Class对象)。用于反射操作,但注意性能开销。
public int hashCode()返回对象的哈希码值,常用于哈希表(如放在 HashMap的对象一定要重写 hashcode 和equals )。必须与 equals()方法一致:如果两个对象相等,则哈希码必须相同。默认实现基于对象地址。
public final void notify()唤醒在此对象监视器上等待的单个线程。用于线程间通信,必须在线程持有对象锁时调用(即在 synchronized块内)。在 JDK17 中,推荐使用 java.util.concurrent包的高级别同步工具。
public final void notifyAll()唤醒在此对象监视器上等待的所有线程。同 notify(),但唤醒所有等待线程。注意可能导致的“惊群效应”。
public String toString()返回对象的字符串表示形式。默认实现返回类名和哈希码。一般用于调试和日志记录,控制台打印 idea调试查看对象时就是调用此方法。
public final void wait()释放当前线程持有的对象锁,并使线程进入等待状态,直到其他线程调用此对象的 notify()或 notifyAll()方法。必须在 synchronized块内调用,否则会抛出 IllegalMonitorStateException。考虑使用 Lock和 Condition接口替代。
public final void wait(long timeout)同 wait(),但指定超时时间(毫秒)。超过超时时间后,线程会自动继续。同上,不过可以指定等待超时的时间。
public final void wait(long timeout, int nanos)同 wait(long timeout),但添加纳秒级额外时间(范围 0-999999 纳秒)。同上,不过可以指定等待超时的时间。

Equal 和 HasCode 重写原则

重写原则

自反性: 对于任意的对象x, x.equals(x) 返回true(自己一定等于自己); 对称性: 对于任意的对象x和y, 若x.equals(y)为true, 则y.equals(x)亦为true; 传递性: 对于任意的对象x, y和z, 若x.equals(y)为true且y.equals(z)也为true, 则x.equals(z)亦为true; 一致性: 对于任意的对象x和y, x.equals(y)的第一次调用为true, 那么x.equals(y)的第二次, 第三次, 第n次调用也均为true, 前提条件是没有修改x也没有修改y; 对于非空引用x, x.equals(null) 永远返回为false;

如何编写hashCode

  1. 把某个非零的常数值result, 比如 7
  2. boolean 则计算f ? 1:0
  3. byte, char, short或者int类型, 则c计算(int) f
  4. long类型, 则c计算(int) (f ^ (f >>> 32))
  5. float类型, 则c计算Float.floatToIntBits(f);
  6. double类型, 则c计算Double.doubleToLongBits(f), 再按 long类型 处理
  7. 如果是一个数组, 则要把每一个元素当做单独来处理
  8. 如果是一个对象先调用EQ比较 ,调用hashCode
  9. 按照如下公式计算

int result = 7 result = 31 * result + c; result = 31 * result + c;

放到Map, Set 等用到对象 hashcode的 必须重写这两个方法!!