前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >J.U.C源码实战:Java多线程基本概念

J.U.C源码实战:Java多线程基本概念

原创
作者头像
怒放吧德德
发布2024-06-03 16:51:48
1040
发布2024-06-03 16:51:48
举报
文章被收录于专栏:知识储备知识储备

Java多线程基本概念

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 更多请看 @一个有梦有戏的人 @怒放吧德德

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息@怒放吧德德 @一个有梦有戏的人

前言

随着计算机硬件性能的不断提升以及计算机软件领域的快速发展,现代计算机系统已经从单核架构演进到了多核甚至多服务器架构。为了充分利用计算机硬件的计算能力,提高软件开发效率,Java语言提供了强大的线程机制。学习JUC知识之前,要先把线程的一些基础知识点掌握,这样有助于后续学习的时候遇到一些相关点,就能够很好的理解。

并发与并行

首先需要了解两个概念,就是并发(concurrent)和并行(parallel),它们都是在多任务处理环境中用来描述可能同时发生事件的术语。它们虽然听上去很相似,但指的是不同的概念。

并发

并发指的是在一个处理器上时间上重叠地处理多个任务的能力。它涉及到任务分割成可以独立执行的子任务的技术,这些子任务可以穿插进行,与此同时,它给用户一种好像事情是同时发生的错觉。它更关注同一时间点上的多个任务的管理。

并行

并行则更关注的是同时执行多个计算。当你有多个处理器或者是处理器核心时,你可以真正意义上同时做多件事。也就是说,在同一时刻,有多个计算过程实际在发生。并行性可以大大加速程序的执行,尤其是在进行大量计算时。

两者之间的一个主要区别是并发并不一定需要多核或多CPU场景,只要能够运行多个进程即可(即使这些进程可能并不是真正在物理上同时执行)。而并行则通常需要在多核或者多CPU的情况下才能实现。

进程线程与管程

作为计算机科学的基础组成部分,线程概念的理解与应用直接影响着我们编写高效、稳健的计算机程序的能力。线程,顾名思义,是进程中独立运行的执行路径,是操作系统进行资源调度和任务执行的基本单位。进程,是系统进行资源分配和调度的一个独立单位。还有一个叫管程, 是一种高级别的同步原语。然而,线程不仅仅是一种执行机制,它更是并发性、并行性、同步性和通信性的综合体现。

进程(Process)

进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。

简单来说,进程就是操作系统运行的某个应用程序,每个进程都有各自独立的内存空间,并拥有一套系统资源,如CPU时间片、系统内存,以及文件等。

如图,我们打开任务管理器,就能够看到运行了多少个进程,如上图的9个应用就代表了9个进程。

线程(Thread)

在多线程编程中,线程作为进程内的执行单位,可以同时执行多个任务。这使得多线程具有并发性,提高了程序的效率和响应速度。然而,线程与进程之间存在着一些区别。进程是操作系统资源分配的基本单位,拥有独立的内存空间和系统资源,而线程是进程中的一个执行单元,共享所属进程的内存空间和其他资源。因此,在使用多线程时需要考虑线程间的同步和通信问题,以确保数据的正确性和一致性。通过合理利用多线程技术,我们能够更好地优化程序的性能和资源利用率。

线程是程序中的一个单一的顺序控制流程,是程序执行流的最小单元,被独立调度和分派的基本单位。

线程有以下几个特点

  • 在同一个进程中的线程共享相同的运行环境。
  • 相比于进程,线程更小,基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
  • 能提高程序并发性,增强用户对应用程序的响应。

管程(Monitor)

管程是一种高级别的同步原语。有别于互斥锁和条件变量这种低级别的同步原语,管程能更好地结构并发程序以及管理对共享资源的访问。

管程包含一个程序组(一种程序员规定的模块)和用于处理同步的数据结构。这些程序组在执行的过程中,会根据要求进行协调操作,只允许一个进程在管程内执行。当一个进程在管程内部执行时,其他想要执行管程内的函数的进程将会被阻塞。这种策略避免了同一时间有多个进程修改数据的问题,也防止了进程之间因资源冲突产生的问题。

代码语言:javascript
复制
object o = new Object();
new Thread(()->{
    synchronized(o) {
        //...
    }
}, "thread-1").start()

使用管程,进程不需要暴露内部的复杂性给其他进程,尤其是竞争同一资源的进程。在工程实践中,管程是一个重要的编程手段,用于控制并发进程对公共资源的访问,比如 Java 中的 synchronized 关键字等就实现了管程的概念。

线程类型

在 Java 中,线程分为两种类型:用户线程和守护线程。在一般情况下,默认是用户线程。

用户线程

用户线程,就是常规所说的主线程,通常是程序的主体部分,做一些核心和关键的工作。用户线程不依赖于任何线程的运行。也就是说,即使它的创建者线程结束,用户线程仍然会继续执行。程序会等待所有的用户线程执行完毕才会停止。

如下就是一个简单的主线程创建与启动:

代码语言:javascript
复制
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)方法设置线程为守护线程。

如下就是守护线程的创建与启动工作的例子:

代码语言:javascript
复制
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...”的输出。如果不去掉这一行,可能看不到任何输出,因为在没有用户线程的情况下,守护线程会立即结束。

Demo演示

接下来,通过Java代码来演示用户线程和守护线程,并且通过观察运行结果来直接学习什么是用户线程什么是守护线程。

代码语言:javascript
复制
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不会立即结束。

输出结果:

代码语言:javascript
复制
thread-1	 开始运行,其是 用户线程
main	 线程结束

我们能够看到,在没声明守护线程的时候,其就是个用户线程。接下来我们观察一下控制台。

我们发现,这个程序依然是运行的,但是main线程已经是结束。这就说明了用户线程都是互不相干的,在thread-1线程中,加入了死循环,这就不会让这个线程结束掉,即使main线程结束了,整个程序还有thread-1存活,所以JVM并不会退出。

接着我们再来看另一段代码:

代码语言:javascript
复制
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,将线程设置为守护线程,我们看输出结果:

代码语言:javascript
复制
thread-1	 开始运行,其是 守护线程
main	 线程结束

Process finished with exit code 0

我们会发现,当主线程结束时候,守护线程就会消亡,整个程序也就退出了。

这里需要注意的是,设置守护线程一定是在调用start方法之前,否者会报IllegalThreadStateException异常。

在Java源码中就已经提示了,This method must be invoked before the thread is started.

Java线程的创建

继承Thread类

Java线程的创建可以通过继承Thread类来实现。在子类中重写父类的run()方法,即可完成线程的具体业务逻辑。这种方式简单易用,但存在一定的局限性,因为子类必须继承自Thread类,无法满足某些特定场景下的需求。

实现Runnable接口

除了继承Thread类外,还可以通过实现Runnable接口来创建线程。在实现Runnable接口的类中,编写run()方法来实现线程的业务逻辑。相比于继承Thread类,这种方式更加灵活,可以满足更多复杂的需求。

Java线程的状态转换

1. 新建状态(NEW)

当线程对象刚刚创建时,其状态为新建状态。此时,线程尚未被启动,也没有占用任何资源。

2. 就绪状态(RUNNABLE)

当线程对象被启动后,其状态将变为就绪状态。此时,线程已经准备好执行任务,但尚未获得CPU时间片。

3. 运行状态(RUNNING)

当线程成功获取到CPU时间片后,其状态将变为运行状态。此时,线程正在执行任务,占用着CPU资源。

4. 阻塞状态(BLOCKED)

当线程在执行过程中遇到了某种阻塞条件(如I/O操作、锁竞争等),其状态将变为阻塞状态。此时,线程暂时停止执行,等待阻塞条件解除。

5. 消亡状态(TERMINATED)

当线程执行完毕或者因异常退出时,其状态将变为死亡状态。此时,线程不再占用任何资源,也不会再次被调度执行。

Java线程的同步控制

synchronized关键字

Java中的synchronized关键字可以用来实现线程之间的同步控制。当某个对象上加锁之后,只有持有该锁的线程才能够访问该对象。这样可以有效地避免多线程环境下的数据竞争问题。

ReentrantLock锁

ReentrantLock锁是Java中的另一种常用的同步控制工具。相较于synchronized关键字,ReentrantLock锁具有更高的灵活性和可扩展性,可以支持多种不同的锁策略和解锁方式。

AtomicInteger类

AtomicInteger类是Java中的一个原子变量类,可以用来实现线程之间的无锁同步控制。通过使用AtomicInteger类,可以保证多个线程对同一个变量的读写操作是原子性的,从而避免了数据竞争问题。

ThreadLocal类

ThreadLocal类是Java中的一个本地线程存储类,可以用来实现线程之间的隔离性。通过使用ThreadLocal类,可以为每个线程单独维护一份私有的变量副本,从而避免了多线程环境下的变量污染问题。

总结

本章介绍了Java多线程的一些基本概念,主要是为了后续学习JUC内容进行一个铺垫,掌握概念,后面遇到的一些内容就不会显得不明白。


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java多线程基本概念
    • 前言
      • 并发与并行
        • 并发
        • 并行
      • 进程线程与管程
        • 进程(Process)
        • 线程(Thread)
        • 管程(Monitor)
      • 线程类型
        • 用户线程
        • 守护线程
        • Demo演示
      • Java线程的创建
        • 继承Thread类
        • 实现Runnable接口
      • Java线程的状态转换
        • Java线程的同步控制
          • synchronized关键字
          • ReentrantLock锁
          • AtomicInteger类
          • ThreadLocal类
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档