进程是系统运行程序的基本单位,我们计算机启动的每一个应用程序都是一个进程。如下图所示,在 Windows
中这一个个 exe
文件,都是一个进程。而在 JVM
下,每一个启动的 Main
方法都可以看作一个进程。
线程是一个比进程更小的执行单位,是 CPU 调度的基本单位。一个进程在其执行的过程中可以产生多个线程。所以在进行线程切换时的开销会远远小于进程,线程也常常被称为轻量级进程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
堆和方法区是所有线程共享的资源。
最关键的点是:是否是 同时 执行。
主要是为了提高程序的性能和并发能力。
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
start()
。start()
等待运行的状态。线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
具体来说:
线程创建之后它将处于 **NEW(新建)**状态,
调用 start()
方法后开始运行,线程这时候处于 **READY(可运行)**状态。
可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)**状态。
sleep(long millis)
方法或 wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。synchronized
方法/块或者调用 wait
后(被 notify
)重新进入 synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。线程在执行完了 run()
方法之后将会进入到 TERMINATED(终止) 状态。
常见的有 5 种方式:
class MyThread extends Thread {
@Override
public void run() {
// 线程的执行逻辑
}
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程的执行逻辑
}
}
// 创建并启动线程
Thread thread = new Thread(new MyRunnable());
thread.start();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程的执行逻辑
}
});
thread.start();
Thread thread = new Thread(() -> {
// 线程的执行逻辑
});
thread.start();
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程的执行逻辑
return "Hello from Callable";
}
}
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(new MyCallable());
String result = future.get(); // 获取线程执行结果
线程上下文切换是指:CPU 从一个线程中断执行转而执行另一个线程的过程。
在多线程编程中,线程上下文切换是非常常见的操作。
这个过程需要耗费一定的时间和资源,因此线程上下文切换的频繁发生会导致系统的性能下降。
1、什么是上下文
线程在执行过程中会有自己的运行条件和状态(也称上下文)。
比如程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。(上下文切换通常发生在以下几种情况)
sleep()
, wait()
等。2、为了减少线程上下文切换带来的性能损失,可以采取以下措施:
两者都可以暂停线程的执行。
sleep()
方法没有释放锁,而 wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout)
超时后线程会自动苏醒。sleep()
是 Thread
类的静态本地方法,wait()
则是 Object
类的本地方法。因为 wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。
- 这句话指出了
wait()
方法的两个关键作用:等待和释放对象锁。- 当一个线程调用了对象的
wait()
方法,它会进入等待状态,等待其他线程通过notify()
或notifyAll()
方法唤醒它。- 同时,该线程会自动释放它当前占有的对象锁,这使得其他等待这个对象锁的线程有机会获得锁并执行临界区代码。
每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)。
wait() 方法是针对对象锁进行操作,而不是针对线程本身的操作。
- 这句话强调了对象锁是和对象绑定的,而不是与线程绑定的。
- 每个 Java 对象都有一个关联的对象锁(监视器锁),这个锁用于对该对象的同步访问。
- 当线程调用了某个对象的
wait()
方法,它会让出这个对象的锁,让其他线程有机会进入临界区或执行同步代码。- 释放的是对象锁,而不是当前线程的锁。这也是为什么在使用
wait()
时需要明确调用的是哪个对象的锁。
对象锁是一种多线程同步机制,它用于保护对象的状态和操作,以确保在多线程环境下对象的数据一致性和线程安全性。
在 Java 中,每个对象都有一个关联的对象锁,也称为监视器锁或内置锁。对象锁的作用是防止多个线程同时访问一个对象的临界区代码,从而避免并发访问造成的数据错误和不一致性。
可用 synchronized
关键字来实现。
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
Thread
,线程进入了新建状态。start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:
调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。
在 JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程,守护线程是用来服务用户线程的线程,比如垃圾回收线程。多用于执行后台任务。
那么守护线程和用户线程有什么区别呢?
区别之一是当最后一个非守护线程束时,JVM 会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM 退出。换而言之,只要有一个用户线程还没结束,正常情况下 JVM 就不会退出。
简单来说就是用户线程会阻止 JVM 的退出,而守护线程不会。
Java 内存模型(Java Memory Model,JMM)是 Java 虚拟机规范中的一部分,它定义了 Java 程序中各种变量的访问方式和存储方式。
JMM 的作用是:解决并发编程中的线程安全问题,确保多线程环境下程序的正确性和稳定性。
主要包括以下几个方面:
主内存和工作内存:Java 内存模型将内存分为主内存和工作内存两部分。
主内存是所有线程共享的内存区域,而每个线程都有自己的工作内存,工作内存中保存了该线程使用到的变量的副本。
线程不能直接对主内存进行操作,而是需要先将变量的副本从主内存中读取到工作内存中,然后再对变量进行操作,操作完成后再将变量的副本写回到主内存中。
内存屏障:内存屏障(Memory Barrier)是一种机制,用于确保线程之间的内存可见性和操作的有序性。
happens-before 关系:happens-before 是 Java 内存模型中的一个概念,用于描述变量之间的先后顺序和可见性。
原子性、可见性和有序性:JMM 保证了原子性、可见性和有序性的内存操作。
原子性指的是一个操作是不可分割的整体,要么全部执行,要么全部不执行;
可见性指的是一个线程对变量的修改对其他线程是可见的;
有序性指的是指令的执行顺序是有序的,保证了程序的正确性。
AQS,全称为 AbstractQueuedSynchronizer,是 Java 并发编程中的一个重要组件。
它提供了一种灵活的框架,可以用来实现各种同步工具,比如锁、信号量、倒计时门栓等。
AQS 的核心思想是使用一个 FIFO 的等待队列来管理线程的获取和释放资源。
AQS 维护一个 state 变量,用来表示同步状态,同时通过一个双向链表来实现等待队列,并提供了 acquire、release、tryAcquire、tryRelease 等方法,允许子类通过重写这些方法来实现特定的同步逻辑。
概念
CountDownLatch 是 Java 并发编程中的一个同步工具,它允许一个或多个线程等待其他线程完成操作后再执行。
原理
CountDownLatch 的核心思想是:通过一个计数器来实现,计数器初始值为线程数,每个线程完成操作后会将计数器 -1,当计数器减为 0 时,所有等待的线程都会被唤醒。
用法
CountDownLatch 的用法如下:
应用场景
CountDownLatch 的应用场景包括: