【日积月累】并发编程(一)

  • 使用并发变成来提高程序的运行效率以及运行速度,也可能带来一系列问题,比如内存泄漏,线程安全问题,死锁等。

线程与进程

进程与线程是非常相似的,首先明确一点,一个进程包含一个或多个线程。进程作为系统运行程序的最基本的单位,是动态的;线程与进程类似,但是他是一个比进程更小的执行单位。
在Java中,启动了main函数就是启动了一个JVM进程,main函数所在的线程就是这个进程的一个线程,也叫主线程。
多个线程共享堆和方法区(jdk8之后的元空间)资源,但是每个线程都有属于自己的 虚拟机栈 本地方法栈 和 程序计数器。
线程是一个比经常更小的执行单位,进程有资源,而线程几乎没有,他只是一个调度单位,资源使用的是进程的。

在这里插入图片描述

程序计数器为什么是私有的?

1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
3.需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置
总结:主要是为了在线程被切换时记录当前线程执行的位置以及帮助实现代码的流程控制

虚拟机栈和本地方法栈为什么是私有的?

1.虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
2.本地方法栈:和虚拟机栈的作用相似,不同的是为使用到的Native方法服务(Native方法指用其他语言,比如c实现的方法
3.所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
总结:虚拟机栈与本地方法栈都是为了避免当前线程的局部变量不被其他线程访问到,虚拟机栈是为java方法服务,而本地方法栈是为native方法服务

堆和方法区(1.8之后的元空间)

堆和方法区(1.8之后的元空间)是所有线程共享的资源,堆是进程中最大的一块内存,主要用于存放新创建的对象,方法去用于存放被加载的类信息,常量,静态变量即时编译器编译后的代码等数据。

线程的生命周期

NEW 初始状态 创建出来还没被调用 srtart()
RUNABLE 运行状态,线程被调用了start()等待运行的状态(包含了 ready 和 runnnig状态 但是由于切换时间太快 JVM就不做区分)
BLOCKED 阻塞状态 需要等待锁释放
WAITING 等待状态 标识该线程需要等待其他线程做出一些特定动作(通知或者中断)调用Object.wait(),Thread.sleep()等
TIME_WAITING 超时等待状态 可以在指定的时间后自行返回而不用像WAITING一样等待
TERMINATED 终止状态 表示线程以及运行完毕
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

Java 线程状态变迁图(图源:挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误):
在这里插入图片描述
总结:在JVM里线程有五种状态 NEW,RUNABLE,BLOCKED,WAITING,TIME_WAITING,TERMINATED

什么是上下文切换?

上下文:线程在执行过程中会有自己的运行条件和状态,比如上文所说到过的程序计数器,栈信息等
上下文切换:在线程切换时保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文
总结:发生在线程切换时,保存当前线程的上下文,等待下次占用cpu时恢复现场,并且加载下一个现场占用cpu的上下文。
线程切换的三种情况1.主动让出cpu 如调用了sleep(),wait()等2.线程时间片用完3.线程被阻塞如 调用了io 4.线程终止或者结束运行

并发与并行的区别

并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
总结:在宏观上,两者都是多个请求同时进行,在微观上,并行也是同时进行,并发则是交替进行。

同步和异步的区别

同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。
总结:同步就是实时处理(如果请求频繁,易造成拥塞),异步就是分时处理(在收到请求后不马上处理,而是等到服务器空闲时再处理,可以避免拥塞)

什么是线程死锁?如何避免死锁?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

在这里插入图片描述

产生死锁的四个条件

在这里插入代码片
public class dmeo {
    private static Object resource1 = new Object();
    private  static Object resource2 = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1){
                System.out.println(Thread.currentThread() + "get resource1");
                

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() +"waiting resource2");
           
                synchronized (resource2){
                    System.out.println(Thread.currentThread() + "get resource2");
                }

            }
        },"resource1").start();
        
        
        new Thread(() ->{
            synchronized (resource2){
                System.out.println(Thread.currentThread() +"get resource2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting resource1");
                 
                synchronized (resource1){
                    System.out.println(Thread.currentThread() +"get resource1");
                }
            }
        },"resource2").start();
    }
}





互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系 以上demo满足线程产生死锁的四个条件

如何预防和避免线程死锁?

破坏产生死锁的条件
破坏请求与保持,一次性申请所以有资源
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放(先申请的后释放)。破坏循环等待条件

如何避免死锁?

银行家算法
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3…Pn> 序列为安全序列
我们对线程 2 的代码修改成下面这样就不会产生死锁了。

new Thread(() -> {
            synchronized (resource1){
                System.out.println(Thread.currentThread() + "get resource1");
                

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() +"waiting resource2");
           
                synchronized (resource2){
                    System.out.println(Thread.currentThread() + "get resource2");
                }

            }
        },"线程111111").start();
        
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程2222222").start();

线程1通过synchronized 获得resource1的监视器锁,这时线程2就获取不到了,然后线程1再去获得resource2的监视器锁,可以获取到,注意:到目前为止线程1占用了resource1 和 resource2的监视器锁 ,接着 线程1释放resource1 和 resource2的监视器锁 ,这时线程2在获取resource1 和 resource2的监视器锁,这样就破坏了破坏循环等待条件,因此避免了死锁

sleep() 方法和 wait() 方法对比

共同点:都可以暂停线程的执行
不同点:1.wait()用于线程间交互/通信,sleep()用于暂停执行2.sleep()方法没有释放锁,wait()释放了锁3.wait方法被调用后不会自动苏醒,需要通知(notify,notifyall等)或者可以使用wait(long timeout)超时后自动苏醒,而sleep会自动苏醒 4.sleep是Thread类的静态本地方法,wait是Object的本地方法

为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。类似的问题:为什么 sleep() 方法定义在 Thread 中?因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

可以直接调用 Thread 类的 run 方法吗?

是一个常见问题,可以直接调用run()方法,但他并不会以多线程的方式执行。
new一个线程后,线程进入了新建状态,使用start()方法,会启动线程使线程进入就绪状态(runable),当分配到时间线就可以开始运行了。start()会执行相应的准备工作,然后自动执行run()方法的内容,这是真正的多线程工作,而如果直接调用run()方法,会把run()方法当做main()线程下一个普通方法去执行,并不会在某个线程执行它,这不是真正的多线程工作。
> 总结:要开始真正的多线程工作,首先要创建一个新的线程,并且调用start()方法使线程进入就绪状态,如果没有准备一个新的线程并且未进入就绪状态,直接调用Thread类的run()方法,只是一个普通方法调用的一个操作

参考

JavaGuide

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顶子哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值