这一节我们讲什么是线程、什么是进程、线程与进程的关系、以及并发与并行的概念。
目录
线程与进程
线程
线程是程序运行的基石,是 CPU 调度的最小单位,必须依赖于进程而存在,运行程序需要分配至少一个线程。我们写的Java程序就分配在Java
多线程
主线程中。而多线程就是指几个程序一起执行,这种也称为并行。就像我们点完外卖在家看剧一样,外卖是一个单独的线程,我们看剧也是一个线程,这就是多线程的思想。另外Java本身就是一个多线程的语言,比如Java程序在运行时不仅有主线程,还有其他线程如Reference Handler(清理Renference的线程)、GC(垃圾回收线程)。所以说,线程无处不在。
在Java的内存模型(JMM)中,每一个线程有其独有的工作内存,如果该线程想拿到其他资源的内存就要去主内存中去拷贝到自己的工作内存中,同样的,如果自己修改了变量,该线程需要将这一块内存拷贝到主内存中供其他线程调用。

进程
进程是程序运行资源分配的最小单位,是由操作系统直接分配并管理的,操作系统管理的资源包括CPU、内存空间、磁盘等。一个进程可能有多个线程同时执行,而不同进程之间是相互独立的。当你运行了一个程序后,操作系统就为这个程序分配了一个进程。在任务管理器中能看到计算机所运行的所有进程并且可控。由此可见,程序是静态的,进程是动态的。
进程又可分为系统进程和用户进程,凡是用于完成系统功能的进程都是系统进程,用户进程就是由用户自己启动的进程,如qq。
核心数
目前很多计算机都采用多核CPU,核心数决定了线程数的多少。一般核心数与线程数为1:2的关系
多核心:也指单芯片多处理器( Chip Multiprocessors,简称 CMP),CMP 是由美国
斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成
到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地
运行程序是实现超高速计算的一个重要方向,称为并行处理
核心数、线程数:目前主流 CPU 有双核、三核和四核,六核也在 2010 年发布。
增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般
情况下它们是 1:1 对应关系,也就是说四核 CPU 一般拥有四个线程。但 Intel 引入
超线程技术后,使核心数与线程数形成 1:2 的关系
时间片轮转机制
我们的计算机都是一个CPU运行,而几乎所有程序的运算都在CPU中执行,那为什么我们在添加了多个进程后计算机还能运行呢。这就用到了时间片轮转机制,计算机的CPU运行效率很快,为了运行多个进程,CPU在执行一个进程一段时间后会转到另一个进程中运行。速度太快所以对用户来说就好像是同时执行一样。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
有趣的是CPU在进行进程切换时也耗费性能,假如进程切( processwitch),有时称为上下文切换( context switch),需要
5ms,再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来
进行进程切换。CPU 时间的 20%被浪费在了管理开销上了。如果轮转时间设置的太快会导致性能耗费严重,太慢就会引起用户运行进程的卡顿。
另外主频是衡量CPU执行速度的标准主频越高,性能越好,计算速度越快。
并发与并行
并发
并发:指多个任务在同一时间段内交替执行(宏观上看起来同时进行),但在微观层面可能共享同一资源(如 CPU 核心),通过快速切换任务来模拟 “同时进行” 的效果。
例如:一个 CPU 核心同时处理多个进程,通过时间片轮转机制每个任务执行一小段时间后切换到下一个,让用户感觉多个任务在同时运行。
我们常说并发量,是指单位时间内有多少任务需要执行。
并行
并行:指多个任务在同一时刻真正同时执行(微观上也同时进行),需要多个资源(如多个 CPU 核心、多个处理器) 分别处理不同任务。
例如:一个 4 核 CPU 同时运行 4 个任务,每个核心独立处理一个任务,此时任务是真正 “并行” 的。
在日常生活中,并发和并行有如下场景:一个人边吃饭边看手机,吃一口饭,看一眼手机。这就是并发执行。两个人,一个人看手机,一个人吃饭,这属于并行。值得注意的是并发不是真正意义上的同时执行,而并行是并发的一种实现方式。
高并发编程
高并发编程的意义
1、提高CPU的利用率:在同一时间里,如果一个程序仅仅使用一个线程,那么就会造成其他线程空闲等待的情况,导致CPU仅仅运算当前线程,使得CPU的利用率变低,就像我们可以坐地铁看书,效率翻倍。
2、加快用户响应时间:比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个
线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。
我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升 1s,如果流量大的话,就能增加不少转换量。做过高性能 web 前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。
3、使得代码模块化,异步化,简单化:
我们在开发过程中可能会分模块,我们可以让每个模块单独运行,让他们进行异步运行,这既使得代码模块化,又使用异步充分利用CPU,还提高了代码的可读性,让结构变得清晰简单。
多线程下的问题
1、线程安全问题:
设想有一个全局变量,A线程和B线程同时持有并修改,首先A将全局变量进行+1操作,在此同时由于JMM内存模型来说,B的工作内存还是原来的数,所以B进行+1操作后就会导致A、B都将变量+1,而最后只加了1。但是如果两个操作都只有读操作而没有写操作的话是没有线程安全问题的,这个可以自己设想。
2、死锁:
为了解决刚才的线程安全问题,我们引入锁的概念,就是给资源上锁,必须等某一个线程使用完后才能被调用变量,就像是去公共厕所给门上锁,代表这个位置有人了,需要等待别人出来才能进入。但是这样的话又引起了新的问题就是死锁,当两个线程竞争资源时,A线程持有了资源a,给a上了锁,B线程持有了b,给b上锁,而A线程需要b资源才能往下运行,这时候b已经被B线程锁住了,同理A线程将a也锁住了,这两个线程就进入了循环等待,谁也不让谁。解决死锁的方案和预防死锁也需要系统学习,但这里不再展开。
3、导致资源耗尽
我们知道,如果我们创建了超出计算机核心数可以持有的最大线程数后,CPU就需要频繁切换执行,一方面导致内存压力过大,另一方面CPU切换频繁导致效率变低。不过这样我们可以用一个池子存放线程,需要线程就从池子里拿出来线程,不需要了在将线程放回去供其他人使用,这样就不用再重新创建线程了,等池子线程不够时就在外面阻塞等待,这就是线程池的原理。
玩转Java多线程
线程三种创建方式及启动
继承Thread创建
public class T1 extends Thread{
@Override
public void run() {
while(true) {
System.out.println("T1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
由于继承自线程类,那么T1就可以称之为线程了,那么他的创建和启动:
Thread t1 = new T1();
t1.start();
这样就开启了一个线程,注意一点,调用的方法时start方法。
继承Runnable接口实现:
public class T2 implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("T2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
由于实现的是接口,所以T2不是真正意义上的线程,所以他的创建方式是:
Runnable runnable = new T2();
Thread t2 = new Thread(runnable);
t2.start();
创建线程后,将我们创建的继承Runnable接口的对象直接作为参数传入线程中即可获取线程。
继承Callable接口:
public class T3 implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T3");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "run success";
}
}
这里我们发现与之前接口不同的是,call方法又返回值,而这个返回值的类型与Callable的泛型保持一致,在线程执行完毕后可以获取到该返回值。
他的创建方式如下:
FutureTask<String> futureTask = new FutureTask<String>(new T3());
Thread t3 = new Thread(futureTask);
t3.start();
这是为什么,我们进入到Thread的构造函数中发现只能传入Runnable接口,但是为什么要传入FutureTask类呢。找到FutureTask的关系图:

原来是FutureTask已经继承了Runnable接口了,这样做为的就是和Runnable做一个直接的区分,而从他的命名来看,这是一个任务,需要线程来执行并返回结果。
在我们运行主函数就发现,T1,T2,T3已经交替执行了。
这里还要提到一个小点,我们的线程可以调用run和start函数,可别弄混了这两个函数,run函数是我们进行重写的一个方法,他只是一个方法的调用,而start函数会调用run函数将本线程进行执行
线程的状态

线程的各种状态是我们学习多线程的必备知识,在该图中,Java其实内部隐藏了就绪态和运行态之间的转化,是其内部自己实现的,让我们不再关心。下面我们通过几个例子和讲解来解释各种状态。
interrupt
interrupt是一种线程停止方式。在此还有一种停止方式为stop方法,这是一种暴力的停止方式,发出stop信号后该线程立即停止,在日常开发中绝对不会用这个方法,因为这就好像你在做饭,刚打开天然气还没点火,你妈就叫你去下楼买瓶酱油,后果可想而知。而interrupt不同的是它可以让程序员自己分配合理的终止时机,就好像你跟你妈说等我把天然气先关了再去。下面用代码介绍用法:
下面介绍两个前置知识:当线程接收到interrupt信号后,将isInterrupt函数返回值置为true,这是第一点,第二点是当线程sleep或wait的阻塞状态时,interrupt信号发出后线程会立即捕获到然后执行catch代码块,并且将isInterrupt信号置为false,也就是消费掉了interrupt信号,这时我们再想让外层函数收到就要再次发送一次。
还是t1,t2这两个线程:
//t1
public class T1 extends Thread{
@Override
public void run() {
while(!isInterrupted()) {
System.out.println("T1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("线程被中断");
System.out.println("before: " + isInterrupted());
Thread.currentThread().interrupt();// 重新设置中断标志,以便外层代码可以检测到
System.out.println("after: " + isInterrupted());
}
}
}
}
//t2
public class T2 implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("T2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//主线程
public class Run {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread t1 = new T1();
Runnable runnable = new T2();
Thread t2 = new Thread(runnable);
FutureTask<String> futureTask = new FutureTask<String>(new T3());
Thread t3 = new Thread(futureTask);
t1.start();
Thread.sleep(200);
t2.start();
t1.interrupt();
}
}
解释:当住线程发出interrupt信号的时候让t1停止,这会让t1的isInterrupt信号变为true,在t1的while条件上我们检测如果为true我们就停止循环。而在t1中消费interrupt信号后再次发送。主函数我们让 t1 先执行200ms确保 t1 在睡眠中被中断。
运行结果:

wait与notify
wait和notify方法为配套使用的,wait方法可以让当前线程阻塞,直到获得到锁对象的notify信号。notifyAll可以唤醒所有的正在等待该对象的线程。如下代码:
// t1
public class T1 extends Thread{
@Override
public void run() {
synchronized (Run.lock) {
while(true) {
System.out.println("T1");
try {
Run.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
// t2
public class T2 implements Runnable{
@Override
public void run() {
synchronized (Run.lock) {
while(true) {
System.out.println("T2");
try {
Run.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
// 主线程
public class Run {
public static final String lock = "lock";
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread t1 = new T1();
Runnable runnable = new T2();
Thread t2 = new Thread(runnable);
FutureTask<String> futureTask = new FutureTask<String>(new T3());
Thread t3 = new Thread(futureTask);
t1.start();
t2.start();
while (true) {
Thread.sleep(500);
synchronized (lock) {
lock.notifyAll();
}
}
}
}
我们让T1, T2线程的对象每次打印后都等待主线程中的锁对象,而主线程每500ms唤醒所有线程,最后就使得每0.5秒打印T1和T2。值得注意的是,无论在wait还是notify的时候,都要获取到lock的锁对象才能对其进行唤醒和等待操作,因为这也是对多线程下的线程安全处理,wait和notify对同一个全局变量进行了读写操作,则需要将lock加锁,还有一点就是,我们看刚才的线程状态图可以看到,当线程调用wait方法后进入阻塞状态,阻塞状态自然会失去CPU的执行权,那么这个线程也会丢掉锁对象的持有权,当被nofity的时候会进入就绪态,就绪态再去竞争CPU的执行权。这就好像在上公共厕所,你先抢到了坑位,关上了门,结果一个环卫工人敲门告诉你,这个坑位坏了,你需要出来等着我修好...,你只能打开门让环卫工人进去锁上门,谁也不让进。等修好了,环卫工人出来,你和厕所的几个好大哥又得去竞争这个坑位。
wait和noitfy多适用于生产者与消费者模式,生产者生产后通知(nofity)消费者来消费,如果生产满了则进入等待(wait), 消费者同理,消费了通知生产者生产,不够消费了等生产者生产。代码示例放在下面:
class ProducerConsumer {
private static int count = 0;
private static final Object lock = new Object();
// 生产者线程
static class Producer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (lock) {
while (count >= 10) { // 假设缓冲区满了
try {
lock.wait(); // 生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("生产者生产,当前数量: " + count);
lock.notify(); // 唤醒消费者线程
}
}
}
}
// 消费者线程
static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (lock) {
while (count <= 0) { // 假设缓冲区空了
try {
lock.wait(); // 消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("消费者消费,当前数量: " + count);
lock.notify(); // 唤醒生产者线程
}
}
}
}
public static void main(String[] args) {
Thread producerThread = new Thread(new Producer());
Thread consumerThread = new Thread(new Consumer());
producerThread.start();
consumerThread.start();
}
}
守护线程
守护线程比较简单,如果设置守护线程,那么调用该守护线程的线程在结束后会想终止守护线程的运行,这种线程典型运用于后台服务操作,例如Java的垃圾回收,当Java程序终止后垃圾回收线程也跟着终止。
public class Run {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread t1 = new T1();
Runnable runnable = new T2();
Thread t2 = new Thread(runnable);
FutureTask<String> futureTask = new FutureTask<String>(new T3());
Thread t3 = new Thread(futureTask);
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
Thread.sleep(3000);
}
}
以上的t1,t2线程运行三秒后会终止运行。
Callable接口
下面我们演示实现Callable的接口实现的线程如何使用以及注意事项。
callable在被实现后必须有一个泛型类型,目的是用来确定线程结束后的返回值。需要注意的是,这个返回值是被异步获取的,也就是在启动该线程后,依然可以运行其他任务,但是注意:如果你用到了这个返回值,也就是说需要拿到这个返回值,当前线程会被阻塞,直到整个返回值被拿到,看如下代码理解:
// T3
public class T3 implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("T3");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "run success";
}
}
// 主线程
public class Run {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread t1 = new T1();
Runnable runnable = new T2();
Thread t2 = new Thread(runnable);
FutureTask<String> futureTask = new FutureTask<>(new T3());
Thread t3 = new Thread(futureTask);
t3.start();
String s = futureTask.get();
System.out.println(s);
t1.start();
t2.start();
}
}
我们在主线程中开启 t3后直接获取返回值,但是线程T3会等待3秒才能返回这个值,所以主线程就阻塞到这个地方不开启t1和t2线程。执行结果:

join
join方法是用于当前线程想在其他线程执行完后再执行当前线程,该方法具有阻塞性,也可以设置最长等待时间,只有线程执行完后或者等待时间完成后才能继续执行线程。
// 主线程
public class Run {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread t1 = new T1();
Runnable runnable = new T2();
Thread t2 = new Thread(runnable);
FutureTask<String> futureTask = new FutureTask<>(new T3());
Thread t3 = new Thread(futureTask);
t1.start();
t1.join(3000);
t2.start();
}
}
当t1执行三秒后才开始执行t2。
yield
yield其实在平时的生产环境中几乎用不到,它主要是用来平衡线程执行的顺序,让线程执行的更加均衡
在 Java 中,Thread.yield() 是一个静态方法,它的核心作用是让当前正在执行的线程主动让出 CPU 资源,给其他具有相同优先级的线程提供执行机会。虽然它的功能看似简单,但在特定场景下能帮助优化线程调度。
如下代码场景:
// 线程A
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程A执行: " + i);
Thread.yield(); // 主动让出CPU
}
}).start();
// 线程B(与A优先级相同)
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程B执行: " + i);
Thread.yield(); // 主动让出CPU
}
}).start();
这样使得两个线程交替规律执行。
yield的特点有:
仅为 “建议”,非强制
yield() 只是向操作系统的线程调度器发送一个 “礼让” 信号,调度器可以忽略该信号。如果没有其他可运行的同等优先级线程,当前线程可能会立即再次获得 CPU。
不释放锁资源
若线程持有同步锁(如在 synchronized 块中),调用 yield() 不会释放锁,其他线程仍无法进入同步代码块。
对低优先级线程影响有限
yield() 更可能让同等优先级的线程获得机会,低优先级线程获得 CPU 的概率仍然较低。
1482

被折叠的 条评论
为什么被折叠?



