😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🌝分享学习心得,欢迎指正,大家一起学习成长!
转发请携带作者信息@怒放吧德德 @一个有梦有戏的人
随着计算机硬件性能的不断提升以及计算机软件领域的快速发展,现代计算机系统已经从单核架构演进到了多核甚至多服务器架构。为了充分利用计算机硬件的计算能力,提高软件开发效率,Java语言提供了强大的线程机制。学习JUC知识之前,要先把线程的一些基础知识点掌握,这样有助于后续学习的时候遇到一些相关点,就能够很好的理解。
首先需要了解两个概念,就是并发(concurrent)和并行(parallel),它们都是在多任务处理环境中用来描述可能同时发生事件的术语。它们虽然听上去很相似,但指的是不同的概念。
并发指的是在一个处理器上时间上重叠地处理多个任务的能力。它涉及到任务分割成可以独立执行的子任务的技术,这些子任务可以穿插进行,与此同时,它给用户一种好像事情是同时发生的错觉。它更关注同一时间点上的多个任务的管理。
并行则更关注的是同时执行多个计算。当你有多个处理器或者是处理器核心时,你可以真正意义上同时做多件事。也就是说,在同一时刻,有多个计算过程实际在发生。并行性可以大大加速程序的执行,尤其是在进行大量计算时。
两者之间的一个主要区别是并发并不一定需要多核或多CPU场景,只要能够运行多个进程即可(即使这些进程可能并不是真正在物理上同时执行)。而并行则通常需要在多核或者多CPU的情况下才能实现。
作为计算机科学的基础组成部分,线程概念的理解与应用直接影响着我们编写高效、稳健的计算机程序的能力。线程,顾名思义,是进程中独立运行的执行路径,是操作系统进行资源调度和任务执行的基本单位。进程,是系统进行资源分配和调度的一个独立单位。还有一个叫管程, 是一种高级别的同步原语。然而,线程不仅仅是一种执行机制,它更是并发性、并行性、同步性和通信性的综合体现。
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。
简单来说,进程就是操作系统运行的某个应用程序,每个进程都有各自独立的内存空间,并拥有一套系统资源,如CPU时间片、系统内存,以及文件等。
如图,我们打开任务管理器,就能够看到运行了多少个进程,如上图的9个应用就代表了9个进程。
在多线程编程中,线程作为进程内的执行单位,可以同时执行多个任务。这使得多线程具有并发性,提高了程序的效率和响应速度。然而,线程与进程之间存在着一些区别。进程是操作系统资源分配的基本单位,拥有独立的内存空间和系统资源,而线程是进程中的一个执行单元,共享所属进程的内存空间和其他资源。因此,在使用多线程时需要考虑线程间的同步和通信问题,以确保数据的正确性和一致性。通过合理利用多线程技术,我们能够更好地优化程序的性能和资源利用率。
线程是程序中的一个单一的顺序控制流程,是程序执行流的最小单元,被独立调度和分派的基本单位。
线程有以下几个特点
管程是一种高级别的同步原语。有别于互斥锁和条件变量这种低级别的同步原语,管程能更好地结构并发程序以及管理对共享资源的访问。
管程包含一个程序组(一种程序员规定的模块)和用于处理同步的数据结构。这些程序组在执行的过程中,会根据要求进行协调操作,只允许一个进程在管程内执行。当一个进程在管程内部执行时,其他想要执行管程内的函数的进程将会被阻塞。这种策略避免了同一时间有多个进程修改数据的问题,也防止了进程之间因资源冲突产生的问题。
object o = new Object();
new Thread(()->{
synchronized(o) {
//...
}
}, "thread-1").start()
使用管程,进程不需要暴露内部的复杂性给其他进程,尤其是竞争同一资源的进程。在工程实践中,管程是一个重要的编程手段,用于控制并发进程对公共资源的访问,比如 Java 中的 synchronized 关键字等就实现了管程的概念。
在 Java 中,线程分为两种类型:用户线程和守护线程。在一般情况下,默认是用户线程。
用户线程,就是常规所说的主线程,通常是程序的主体部分,做一些核心和关键的工作。用户线程不依赖于任何线程的运行。也就是说,即使它的创建者线程结束,用户线程仍然会继续执行。程序会等待所有的用户线程执行完毕才会停止。
如下就是一个简单的主线程创建与启动:
public class MainThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("User Thread running...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
守护线程是在后台运行的线程,主要用于为其他线程(通常是用户线程)提供服务或者执行一些“垃圾回收”、“清理”等辅助工作。一旦所有的用户线程都结束了,守护线程也就没有存在的必要了,因此它会被自动终止。所以当系统只剩下了守护线程,Java虚拟机就会自动退出。
简单来说,守护线程的生命周期依赖于任何用户线程。可以通过调用Thread类的setDaemon(true)方法设置线程为守护线程。
如下就是守护线程的创建与启动工作的例子:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Daemon Thread running...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.setDaemon(true);
thread.start();
}
}
这段代码中,如果去掉thread.setDaemon(true);这一行,你会看到“Daemon Thread running...”的输出。如果不去掉这一行,可能看不到任何输出,因为在没有用户线程的情况下,守护线程会立即结束。
接下来,通过Java代码来演示用户线程和守护线程,并且通过观察运行结果来直接学习什么是用户线程什么是守护线程。
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 开始运行,其是 " +
(Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
while (true) {
// 为了不让thread-1线程结束
}
}, "thread-1").start();
// 睡眠2秒
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "\t 线程结束");
}
}
以上代码,当我们启动main方法时候,就会有两个线程,一个是main线程,一个就是创建的thread-1线程,在thread-1线程中,加了个死循环,这样就能够使得thread-1不会立即结束。
输出结果:
thread-1 开始运行,其是 用户线程
main 线程结束
我们能够看到,在没声明守护线程的时候,其就是个用户线程。接下来我们观察一下控制台。
我们发现,这个程序依然是运行的,但是main线程已经是结束。这就说明了用户线程都是互不相干的,在thread-1线程中,加入了死循环,这就不会让这个线程结束掉,即使main线程结束了,整个程序还有thread-1存活,所以JVM并不会退出。
接着我们再来看另一段代码:
public class DaemonThread2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 开始运行,其是 " +
(Thread.currentThread().isDaemon() ? "守护线程" : "用户线程"));
while (true) {
// 为了不让thread-1线程结束
}
}, "thread-1");
t1.setDaemon(true);
t1.start();
// 睡眠2秒
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "\t 线程结束");
}
}
这段代码,我们加上了daemon=true,将线程设置为守护线程,我们看输出结果:
thread-1 开始运行,其是 守护线程
main 线程结束
Process finished with exit code 0
我们会发现,当主线程结束时候,守护线程就会消亡,整个程序也就退出了。
这里需要注意的是,设置守护线程一定是在调用start方法之前,否者会报IllegalThreadStateException异常。
在Java源码中就已经提示了,This method must be invoked before the thread is started.
Java线程的创建可以通过继承Thread类来实现。在子类中重写父类的run()方法,即可完成线程的具体业务逻辑。这种方式简单易用,但存在一定的局限性,因为子类必须继承自Thread类,无法满足某些特定场景下的需求。
除了继承Thread类外,还可以通过实现Runnable接口来创建线程。在实现Runnable接口的类中,编写run()方法来实现线程的业务逻辑。相比于继承Thread类,这种方式更加灵活,可以满足更多复杂的需求。
1. 新建状态(NEW)
当线程对象刚刚创建时,其状态为新建状态。此时,线程尚未被启动,也没有占用任何资源。
2. 就绪状态(RUNNABLE)
当线程对象被启动后,其状态将变为就绪状态。此时,线程已经准备好执行任务,但尚未获得CPU时间片。
3. 运行状态(RUNNING)
当线程成功获取到CPU时间片后,其状态将变为运行状态。此时,线程正在执行任务,占用着CPU资源。
4. 阻塞状态(BLOCKED)
当线程在执行过程中遇到了某种阻塞条件(如I/O操作、锁竞争等),其状态将变为阻塞状态。此时,线程暂时停止执行,等待阻塞条件解除。
5. 消亡状态(TERMINATED)
当线程执行完毕或者因异常退出时,其状态将变为死亡状态。此时,线程不再占用任何资源,也不会再次被调度执行。
Java中的synchronized关键字可以用来实现线程之间的同步控制。当某个对象上加锁之后,只有持有该锁的线程才能够访问该对象。这样可以有效地避免多线程环境下的数据竞争问题。
ReentrantLock锁是Java中的另一种常用的同步控制工具。相较于synchronized关键字,ReentrantLock锁具有更高的灵活性和可扩展性,可以支持多种不同的锁策略和解锁方式。
AtomicInteger类是Java中的一个原子变量类,可以用来实现线程之间的无锁同步控制。通过使用AtomicInteger类,可以保证多个线程对同一个变量的读写操作是原子性的,从而避免了数据竞争问题。
ThreadLocal类是Java中的一个本地线程存储类,可以用来实现线程之间的隔离性。通过使用ThreadLocal类,可以为每个线程单独维护一份私有的变量副本,从而避免了多线程环境下的变量污染问题。
本章介绍了Java多线程的一些基本概念,主要是为了后续学习JUC内容进行一个铺垫,掌握概念,后面遇到的一些内容就不会显得不明白。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。