进程是指运行中的程序。 比如我们使用聊天软件,浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源。
线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。
进程是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
多线程是指:单个进程中同时运行多个线程。
多线程的目的是为了提高CPU的利用率。
可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程。这样可以大幅度的提升程序的效率,提高用户的体验。
- 比如Tomcat可以做并行处理,提升处理的效率,而不是一个一个排队。
- 比如要处理一个网络等待的操作,开启一个线程去处理需要网络等待的任务,让当前业务线程可以继续往下执行逻辑,效率是可以得到大幅度提升的。
如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好。
线程安全问题:虽然多线程带来了一定的性能提升,但是在做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。
Java的多线程是指在一个Java程序中同时执行多个线程的能力。线程是程序执行的最小单位,多线程编程允许一个程序在同一时间内执行多个任务。
Java通过java.lang.Thread类和java.util.concurrent 包提供了强大的多线程支持。
串行就是一个一个排队,第一个做完,第二个才能上。
任务一个接一个地执行。
特点:
任务按顺序执行,一个任务完成后下一个任务才开始。
没有任务之间的重叠。
简单易理解,但效率较低,特别是在处理大量任务时。
并行就是同时处理。(一起上!!!)
任务同时执行,需要多核处理器。
特点:
任务在同一时间点上同时执行。
通常需要多核处理器或多台计算机来实现真正的并行。
可以显著提高性能,特别是在处理计算密集型任务时。
这里是多线程中的并发概念(CPU调度线程的概念)。
CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但是只是CPU高速的切换。
任务交替执行,看起来像是同时进行的。
特点:
任务在时间上交替进行,可能并不真正同时运行,但在宏观上看起来是同时进行的。
不需要多核处理器,但可以利用多核处理器提高性能。
适用于I/O密集型任务和需要高响应性的应用。
并行:任务在物理上同时执行,需要多核处理器。
并发:任务在逻辑上同时进行,但在物理上是交替执行的。
并行和并发在某些情况下会重叠,但它们并不是完全相同的概念。
实现线程的方式到底有几种?大部分人会说有 2 种、3 种或是 4 种,很少有人会说有 1 种。
我们接下来看看这些所谓的实现线程的方式具体是什么:
第 1 种方式是通过实现 Runnable 接口实现多线程,如下面代码所示:
public class RunnableThread implements Runnable {
@Override
public void run() {
System.out.println('实现Runnable接口');
}
}
首先通过 RunnableThread 类实现 Runnable 接口,然后重写 run() 方法,之后只需要把这个实现了 run() 方法的实例传到 Thread 类中就可以实现多线程。
第 2 种方式是继承 Thread 类,如下代码所示:
public class ExtendsThread extends Thread {
@Override
public void run() {
System.out.println('用Thread类实现线程');
}
}
与第 1 种方式不同的是它没有实现接口,而是继承 Thread 类,并重写了其中的 run() 方法。
第 3 种方式是通过线程池创建线程,比如我们给线程池的线程数量设置成 10,那么就会有 10 个子线程来为我们工作。如下代码所示:
static class DefaultThreadFactory implements ThreadFactory {
private final ThreadGroup group;
private final String namePrefix;
private final AtomicInteger threadNumber = new AtomicInteger(1);
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
AtomicInteger poolNumber = new AtomicInteger(1);
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
对于线程池而言,本质上是通过线程工厂创建线程的,默认采用 DefaultThreadFactory ,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论怎么设置这些属性,最终它还是通过 new Thread() 创建线程的 ,只不过这里的构造函数传入的参数要多一些,由此可以看出通过线程池创建线程,并没有脱离前面的那两种基本的创建方式,因为本质上还是通过 new Thread() 实现的。
第 4 种线程创建方式是通过有返回值的 Callable 创建线程,Runnable 创建线程是无返回值的,而 Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如下代码所示:
class CallableTask implements Callable {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future future = service.submit(new CallableTask());
实现了 Callable 接口,并且给它的泛型设置成 Integer,然后它会返回一个随机数。但是,无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。
它们可以放到线程池中执行,如上面代码所示, submit() 方法把任务放到线程池中,并由线程池创建线程,不管用什么方法,最终都是靠线程来执行的,而子线程的创建方式仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread 类。
第5种则是通过定时器实现,比如果新建 一个 Timer,令其每隔 10 秒或设置1个小时之后,执行一些任务,那么这时它确实也创建了线程并执行了任务,但如果我们深入分析定时器的源码会发现,本质上它还是会有一个继承自 Thread 类的 TimerThread,所以定时器创建线程最后又绕回到最开始说的两种方式。
class TimerThread extends Thread {
//具体实现
}
其实还有其它的一些其他方式,比如匿名内部类或 lambda 表达式方式,实际上,匿名内部类或 lambda 表达式创建线程,它们仅仅是在语法层面上实现了线程,本质上来说,并不能把它归结于实现多线程的新方式,如匿名内部类实现线程的代码所示,它仅仅是用一个匿名内部类把需要传入的 Runnable 给实例出来。
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();}
关于这个问题,我们先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他 的创建方式,比如线程池或是定时器,它们仅仅是在 new Thread() 外做了一层封装,如果我们把这些都叫作一种新的方式,那么创建线程的方式便会千变万化、层出不穷,比如 JDK 更新了,它可能会多出几个类,会把 new Thread() 重新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质, 打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。
接下来,我们进行更深层次的探讨,为什么说这两种方式本质上是一种呢?
@Override
public void run() {
if (target != null) {
target.run();
}
}
首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法。
这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。 我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于实现线程运行内容的不同。
那么运行内容来自于哪里呢?
运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法,在此基础上我们进行拓展。
总结起来,本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装就可以了。
首先,我们从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容, 在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字, 那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
第三点好处在于 Java 语言不支持多继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。