并发编程总结(一)

本文详细介绍了进程和线程的基本概念,以及它们在并发和并行编程中的应用。讲解了如何在Java中创建和运行线程,涉及start和run的区别,sleep和yield的作用,以及主线程和守护线程的特点。此外,还探讨了线程的不同状态和临界区的管理,帮助读者理解多线程环境下的同步和资源访问问题。
摘要由CSDN通过智能技术生成

目录

进程与线程

并行与并发

创建和运行线程

查看进程线程的方法

栈与栈帧

线程上下文切换(Thread Context Switch)

start 与 run

sleep 与 yield

主线程与守护线程

线程六种状态

临界区 Critical Section


进程与线程

        进程
        程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU ,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
        当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
        进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
        线程
        一个进程之内可以分为一到多个线程。
        一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
        Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
        二者对比
                进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
                进程拥有共享的资源,如内存空间等,供其内部的线程共享
                进程间通信较为复杂
                同一台计算机的进程通信称为 IPC( Inter-process communication
                不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
                线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
                线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

并行与并发

        并发(concurrent )是同一时间应对( dealing with )多件事情的能力
        并行(parallel )是同一时间动手做( doing )多件事情的能力
        例子
                家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
                家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待)

创建和运行线程

        方法一,直接使用 Thread

// 创建线程对象
Thread t = new Thread() {
     public void run() {
         // 要执行的任务
     }
};
// 启动线程
t.start();
        方法二,使用 Runnable 配合 Thread
        把【线程】和【任务】(要执行的代码)分开
                Thread 代表线程
                Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
     public void run(){
         // 要执行的任务
     }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
        小结
                方法1 是把线程和任务合并在了一起,方法 2 是把线程和任务分开了
                用 Runnable 更容易与线程池等高级 API 配合
                用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
        方法三,FutureTask 配合 Thread
                FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

        输出:

19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100

查看进程线程的方法

        windows
                任务管理器可以查看进程和线程数,也可以用来杀死进程
                tasklist 查看进程
                taskkill 杀死进程
        linux
                ps -fe 查看所有进程
                ps -fT - p <PID> 查看某个进程( PID )的所有线程
                kill 杀死进程
                top 按大写 H 切换是否显示线程
                top -H - p <PID> 查看某个进程( PID )的所有线程
        Java
                jps 命令查看所有 Java 进程
                jstack <PID> 查看某个 Java 进程( PID )的所有线程状态
                jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

栈与栈帧

        Java Virtual Machine Stacks ( Java 虚拟机栈)
        我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
        每个栈程由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存
        每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch

        因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
                线程的 cpu 时间片用完
                垃圾回收
                有更高优先级的线程需要运行
                线程自己调用了 sleep、 yield wait join park synchronized lock 等方法
        当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态, Java 中对应的概念就是程序计数器(Program Counter Register ),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
                状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
                Context Switch 频繁发生会影响性能

start run

        直接调用 run 是在主线程中执行了 run ,没有启动新的线程
        使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep yield

        sleep
                1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
                2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
                3. 睡眠结束后的线程未必会立刻得到执行
                4. 建议用 TimeUnit sleep 代替 Thread sleep 来获得更好的可读性
        yield
                1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
                2. 具体的实现依赖于操作系统的任务调度器

        线程优先级

        线程优先级会提示(hint )调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
        如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

主线程与守护线程

        默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
 log.debug("开始运行...");
 sleep(2);
 log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
        输出
08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束...
        注意
                垃圾回收器线程就是一种守护线程
                Tomcat 中的 Acceptor Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

线程六种状态

        【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
        【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
        【运行状态】指获取了 CPU 时间片运行中的状态
                当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
        【阻塞状态】
                如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU ,会导致线程上下文切换,进入
        【阻塞状态】
                等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
                与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
        【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

临界区 Critical Section

        一个程序运行多个线程本身是没有问题的
        问题出在多个线程访问共享资源
                多个线程读共享资源其实也没有问题
                在多个线程对共享资源读写操作时发生指令交错,就会出现问题
        一段代码块内如果存在对共享资源 的多线程读写操作,称这段代码块为 临界区
  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值