概述
并发编程笔记
基础知识
1)程序、进程、线程、协程(纤程);
2)线程有哪些状态;
3)进程间的通信方法、线程间的通信方法。
程序,进程,线程,协程
程序:
编译好的二进制文件,不占用资源。是含有指令和数据的文件 比如qq.exe
进程:
进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
进程是**操作系统进行资源分配的最小单位,**其中资源包括:CPU、内存空间、磁盘IO等, 同一进程中的多条线程共享该进程中的全部系统资源, 而进程和进程之间是相互独立的。
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身, 用户进程就是所有由你启动的进程。
线程:
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
线程无处不在,任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。
在 windows 中进程是不活动的,只是作为线程的容器
进程和线程的区别:
根本区别:进程是操作系统资源分配的基本单位,线程是处理器任务调度,执行的基本单位。
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
包含关系:进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集。
内存分配:同一进程的线程共享本进程的地址和资源(方法区,堆),而进程之间的地址和资源是相互独 立的。
//使用JMX来查看一个普通的Java程序包含哪些线程,如代码清单4-1所示。
public static void main(String[] args) {
//获取java线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//不需要获取同步的monitor 和 synchronizer 信息,仅获取线程和程序堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
//遍历线程信息,仅打印线程id和线程名称
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId()+"=============="+threadInfo.getThreadName());
}
}
//结果:
6==============Monitor Ctrl-Break
5==============Attach Listener
4==============Signal Dispatcher
3==============Finalizer
2==============Reference Handler
1==============main
线程上下文切换:
在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
什么是线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会- -直相互等待而无法继续运行下去,如图1-2所示。
在图1-2中,线程A已经持有了资源2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。
那么为什么会产生死锁呢?
死锁的产生必须具备以下四个条件。
互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合(TO, TI, T2, …, Tn);中的TO正在等待一个TI占用资源, T1正在等待T2占用的资源, .T正在等待已被TO占用的资源。
public class DeadLockTest2//创建资源
private static Object resourceA = new Object ();
private static Object resourceB = new Object ();
public static void main (string [] args) {
//创建线程A
Thread threadA = new Thread (
new Runnable(){
public void run () synchronized (resourceA) {
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread. sleep (1000);
}cath (InterruptedException e) {
e.printstackTrace();
System.out.println(Thread.currentThread() + "waiting get sourceB") ;
synchronized (resourceB){
System.out.println(Thread.currentThread() + "get esourceB");
}
}
}
//创建线程B
Thread threadB = new Thread (new Runnable() {
public void run() {
synchronized (resourceB) {
System.out.printIn (Thread.currentThread() + " get ResourceB");
try {
Thread.sleep (1000);
}catch (InterruptedException e){
e.printStackTrace();
System.out.println (Thread.currentThread () + "waiting get esourceA");
synchronized (resourceA) {
System. out.println (Thread.currentThread() + "get ResourceA");
}
}
});
threadA.start();
threadB.start();
}
}
如何避免死锁:
目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程B的代码进行如下修改。
让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致,其·实资源分配有序性就是指,假如线程A和线程B都需要资源1, 2, 3,…, n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。
一些线程的方法:
Thread.sleep()的使用
Thread类中有–个静态的sleep方法,当–个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但.是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
Thread.yield()的使用:
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
总结: sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回(也就是线程A必须要等Thread线程执行完之后才可以继续执行)。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
在下面代码所示的例子中,创建了10个线程,编号0~9,每个线程调用前一个线程的join()方法,也就是线程0结束了,线程1才能从join()方法中返回,而线程0需要等待main线程结束。
package com.pingfa.demo.course;
/**
* @Author: gyy
* @Description:
* @Date: 2021/6/7 16:43
*/
public class Join {
public static void main(String[] args) {
Thread pre = Thread.currentThread();
for (int i = 1; i < 10; i++) {
Thread thread = new Thread(new JoinThread(pre),"joinThread=="+i);
pre = thread;
thread.start();
}
System.out.println(Thread.currentThread().getName()+" terminate");
}
static class JoinThread implements Runnable{
private Thread last;
public JoinThread(Thread last){
this.last = last;
}
public void run() {
try {
last.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" terminate");
}
}
}
//结果:
main terminate
joinThread==1 terminate
joinThread==2 terminate
joinThread==3 terminate
joinThread==4 terminate
joinThread==5 terminate
joinThread==6 terminate
joinThread==7 terminate
joinThread==8 terminate
joinThread==9 terminate
从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。
看一下join中的源码:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
//核心代码
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
}
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。可以看到join()方法的逻辑结构和等待/通知经典范式一致,即加锁、循环和处理逻辑3个步骤。
- synchronized 关键字很关键,表明此方法是线程安全的,线程进入此方法需要获取锁。
那么这里的锁是什么呢?是本线程实例对象,也就是语法上synchronized 关键字省略(this)的写法。
InterruptedException 说明此过程可以被打断,如有必要需要处理InterruptedException异常
isAlive()判断线程的存活状态,如果线程存活则是true,否则是false,也就是不能是终结态TERMINATED,是一个final 的 native方法- while,而不是if,这里是为了防上虚假唤醒,虚假唤醒是操作系统的权衡性能和健壮性之后的处理方案,具体参考虚假唤醒(spurious wakeup)
核心代码逻辑:
线程A调用线程B.join()方法,重载方法调用到join(long millis)方法
循环检查条件线程B.isAlive(),如果是存活状态则释放锁,线程A进入等待状态,等待线程B执行完成
为防止虚假唤醒,使用while循环方式来检查条件
如果线程B终结了,则跳出while循环,join方法执行结束,回到线程A继续执行
注意:
另外,线程A调用线程B的join方法后会被阻塞,当其他线程调用了线程A的
interrupt()方法中断了线程A时,线程A会抛出InterruptedException 异常而返回。下 面通
过一个例子来加深理解。
协程:
**协程,英文Coroutines,是一种比线程更加轻量级的存在。**正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。
来看例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r =
最后
以上就是迅速鱼为你收集整理的并发编程---基础基础知识的全部内容,希望文章能够帮你解决并发编程---基础基础知识所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复