Java 中的线程
约 5748 字大约 19 分钟
2025-04-10
一、介绍
1. 进程和线程
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
2. 程序运行原理
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
3. 并发和并行
并行:指两个或多个事件在同一时刻发生,强调的是时间点。
并发:指两个或多个事件在同一时间段内发生, 强调的是时间段。
二、创建线程的方式
1. 继承Thread类
创建线程的步骤
定义一个类继承 Thread。
重写 run 方法。(该线程要执行的操作)
创建该类对象,就是创建 thread 子类对象。
调用 start 方法,开启线程并让线程执行,同时还会告诉 jvm 去调用 run 方法。
线程对象调用 run 方法和调用 start 方法区别?
- 线程对象调用 run 方法不开启线程,仅是对象调用方法。
- 线程对象调用 start 开启线程,并让 jvm 调用 run 方法在开启的线程中执行。
概述
- Thread 类本质也 Runnable 接口的一个实例,启动线程的唯一方法是通过对象的 start() 方法。
- start() 方法是一个 native (本地)方法,它将启动一个新线程,并执行 run() 方法。( Thread 中提供的 run() 方法是一个空方法)。
- 通过自定义类直接 extends Thread,并重写 run() 方法,就可以启动新线程,并执行自定义的 run() 方法。
- 需要注意的是:当 start() 方法调用后不是立即执行多线程代码,而是使得该线程变为可运行状态(Runnable),什么时候执行还得由操作系统决定。
示例
class MyThread extends Thread{ public void run(){ System.out.println("多线程体"); } } public class Test1 { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 开启线程 } }
2. 实现Runnable接口
创建线程的步骤
定义类实现Runnable接口。
覆盖接口中的run方法。
创建Thread类的对象。
将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
调用Thread类的start方法开启线程。
继承Thread类和实现Runnable接口有啥区别呢?
- 第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
- 第一种方式继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。
- 实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
示例
class MyThread implements Runnable { @Override public void run() { System.out.println("线程方法体"); } } public class Test2 { public static void main(String[] args) { MyThread t = new MyThread(); Thread thread = new Thread(t); thread.start(); } }
3. 实现Callable 接口,重写call()方法
Callable 对象实际是属于 Executor框架中的功能类,Callable接口和Runnable接口类似,但是提供了比Runnable更强大的功能,主要表现在:
Callable可以在任务结束后提供一个返回值,Runnable接口无法实现这个功能。
Callable中的call()方法可以抛异常,而Runnable的run() 方法不能抛异常。
运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果。它提供了检查计算是否完成的方法。由于线程属于异步计算模型,所以无法从其他线程中得到方法返回值,这种情况下,可以使用Future来监视目标线程调用Call()方法的情况,当调用Futrue的get()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。
// ⾃定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要⼀秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使⽤
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调⽤get⽅法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使⽤可以设置超时时间的重载get⽅法。
System.out.println(result.get());
}
}
三、Future和FutureTask
1. Future接口
Future接口位于concurrent包中,只有几个简单的方法:
package java.util.concurrent;
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)throwsInterruptedException, ExecutionException, TimeoutException;
}
cancel ⽅法是 试图 取消⼀个线程的执⾏。
注意是 试图取消 ,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些其它因素不能取消,存在取消失败的可能。 boolean 类型的返回值是“是否取消成功”的意思。参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。
所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable。如果为了可取消性⽽使⽤ Future 但⼜不提供可⽤的结果,则可以声明 Future<?> 形式类型并返回 null 作为底层任务的结果。
2. FutureTask类
FutureTask 是实现的 RunnableFuture 接⼝的,⽽ RunnableFuture 接⼝同时继承了 Runnable 接⼝ 和 Future 接⼝。
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
Future 只是⼀个接⼝,⽽它⾥⾯的 cancel 、 get 、 isDone 等⽅法要⾃⼰实现 起来都是⾮常复杂的。所以JDK提供了⼀个 FutureTask 类来供我们使⽤。
package java.util.concurrent;
import java.util.concurrent.locks.LockSupport;
public class FutureTask<V> implements RunnableFuture<V> {}
使用示例:
// ⾃定义Callable,与上⾯⼀样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要⼀秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 使⽤
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
四、线程池
《阿里巴巴JAVA开发手册》有这样一条强制规定:线程池不允许使用Executors去创建,而应该通ThreadPoolExecutor方式,这样处理方式更加明确线程池运行规则,规避资源耗尽风险。
//1:创建线程类
class MyRunnable implements Runnable{
public void run() {
System.out.println("我是线程:" + Thread.currentThread().getName());
}
}
//2:创建线程池
ExecutorService pooled = Executors.newFixedThreadPool(3);
//3:给线程池分配任务
pooled.submit(new MyRunnable());
pooled.submit(new MyRunnable());
pooled.submit(new MyRunnable());
pooled.submit(new MyRunnable());
//4:打印结果
pool-1-Thread-1
pool-1-Thread-2
pool-1-Thread-3
pool-1-Thread-1
✒️ RejetedExecutionHandler:饱和(拒绝)策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中略:
AbortPolicy:直接抛出异常
CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
五、线程状态
1. Java 中线程共6种状态:
new 初始状态
线程被构建,但是还没有调用start方法
RUNNABLE 运行状态
Java线程把操作系统中就绪和运行两种状态统一称为“运行中”
BLOCKED 阻塞
表示线程进入等待状态,也就是线程因为某种原因放弃了CPU的使用权阻塞也分为几种情况(当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。)
等待阻塞:
运行的线程执行了Thread.sleep、wait、join等方法,JVM会把当前线程设置为等待状态,当sleep结束,join线程终止或者线程被唤醒后,该线程从等待状态进入阻塞状态,重新占用锁后进行线程恢复
同步阻塞:
运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么JVM会把当前项城放入到锁池中
其他阻塞:
发出I/O请求,JVM会把当前线程设置为阻塞状态,当I/O处理完毕则线程恢复
WAITING 等待
等待状态,没有超时时间(无限等待),要被其他线程或者有其他的中断操作,如执行wait()、join()、LockSupport.park()
TIME_WAITING 超时等待
与等待不同的是, 不是无限等待 ,超时后自动返回,执行sleep,带参数的wait等可以实现
TERMINATED 终止
线程执行完毕
/**
* 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 <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* 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;
}
2. 线程状态的转换
值得注意的是,调用wait()方法前线程必须持有对象的锁。线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
六、控制线程的方法
1. Sleep(long millis)
使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。
需要注意的是,sleep 的时候要对异常进行处理。sleep方法不会释放锁。
try {//sleep会发生异常要显示处理
Thread.sleep(20);//暂停20毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
2. join()
等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。
join(long millis)
等待这个线程死亡最多 millis毫秒。
//创建MyRunnable类
MyRunnable mr = new MyRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "张飞");
Thread t2 = new Thread(mr, "貂蝉");
Thread t3 = new Thread(mr, "吕布");
//启动线程
t1.start();
try {
t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();
3. yield()
yield() 方法是一个静态方法,用于暗示当前线程愿意放弃其当前的时间片,允许其他线程执行。然而,它只是向线程调度器提出建议,调度器可能会忽略这个建议。具体行为取决于操作系统和 JVM 的线程调度策略。
class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(YieldExample::printNumbers, "刘备");
Thread thread2 = new Thread(YieldExample::printNumbers, "关羽");
thread1.start();
thread2.start();
}
private static void printNumbers() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i)
// 当 i 是偶数时,当前线程暂停执行
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " 让出控制权...");
Thread.yield();
}
}
}
}
4. interrupt()
只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。
如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。
Thread myThread = new Thread(() -> {
while (!Thread.interrupted()) {
// 线程的业务逻辑
}
// 线程被中断后的处理
});
// 启动线程
myThread.start();
// 中断线程
myThread.interrupt();
5. isInterrupted()
检查中断状态: isInterrupted() 是 Thread 类的另一个实例方法,用于检查线程的中断状态。通过调用该方法,可以判断线程是否被中断,并根据需要执行相应的逻辑。
// 检查线程中断状态
boolean isInterrupted = myThread.isInterrupted();
6. interrupted()
Thread 类还提供了一个静态方法 interrupted(),用于检查当前线程的中断状态,并清除中断状态。与 isInterrupted() 不同,interrupted() 是一个静态方法,检查的是调用该方法的线程的中断状态, 执行后具有将状态标识清除为false的功能 ,连续调用两次,第一次返回true,第二次返回false。
javaCopy code// 检查当前线程中断状态
boolean isInterrupted = Thread.interrupted();
七、TreadLocal
1. ThreadLocal介绍
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是个Thread,而是Thread的一个局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理一些。
线程安全问题的核心在于多个线程会对同一个临界区的共享资源进行访问,ThreadLocal运用“空间换时间”的思想,每个线程拥有自己的“共享资源”,虽然内存占用变大了,但由于不需要同步,也就减少了线程可能存在的阻塞问题,从而提高时间上的效率。
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一线程内多个函数或者组件之间一些公共变量的传递的复杂度。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
2. ThreadLocal是如何做到为每一个线程维护变量的副本的呢?
在ThreadLocal类中有一个static声明的Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
package java.lang;
import jdk.internal.misc.TerminatingThreadLocal;
import java.lang.ref.*;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class ThreadLocal<T> {
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*
*/
/**
* 翻译:
* ThreadLocalMap 是一个自定义的哈希图,仅适用于维护线程本地值。
* 不会在 ThreadLocal 类之外导出任何操作。
* 该类是包私有的,以允许在类 Thread 中声明字段。
* 为了帮助处理非常大且长期存在的用法,哈希表条目对键使用 WeakReferences。
* 但是,由于不使用引用队列,因此可以保证仅在表开始耗尽空间时删除过时的条目。
*/
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
虽然它定义了这样一个内部类,但ThreadLocal本身真的没有持有ThreadLocalMap的变量,这个ThreadLocalMap的持有者是Thread。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
* 与此线程相关的 ThreadLocal 值。此映射由 ThreadLocal 类维护。
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
}
3. Threadlocal会不会造成OOM
从上面的源代码,我们知道ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap。
但如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收ThreadLocalMap 的内存了,不然的话,Entry对象越多,那ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量就应该清理掉其对应的Entry对象。
使用的方式是,Entry对象的key是WeakReference 的包装,ThreadLocalMap 的 private Entry[] table ,已经被占用达到了三分之二时 threshold = 2/3(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象。我们可以看到ThreadLocalMap.set()方法中有下面的代码:
public class ThreadLocal<T> {
static class ThreadLocalMap {
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
}
其中,cleanSomeSlots就是回收方法。
如上图(虚线为弱引用),每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap。
Map中的key为一个Threadlocal实例。 这个Map的确使用了弱引用,不过弱引用只是针对key 。每个key都弱引用指向threadlocal。 当把threadlocal实例置为 null 以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。
但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。
但是,当我们使用线程池的时候,就意味着当前线程未必会退出(比如固定大小的线程池,线程总是在的)。如果这样的话,将一些很大的对象设置到ThreadLocal中(这个很大的对象实际保存在Threa的threadLocals属性中),这样的话就可能会出现内存溢出的情况。
一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释Thread 中的ThreadLocalMap对象,造成内存溢出。
也就是说,ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我在使用线程池的时候,使用ThreadLocal要格外小心!
重要
Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get(),set()的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get(),set()方法,那么这个期间就会发生真正的内存泄露。
public class ThreadLocal<T> {
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
}