第十八章 java并发常见问题总结(上)

线程和进程

进程概念

进行是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

当我们在Java中启动main函数时,就说启动了一个JVM的进程,而main函数所在的线程就说这个进程中的一个线程,也称为主线程。

线程概念

线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话至少有一个进程

进程与线程的关系

一个进程中有:运行时数据区域和多个线程,多个线程共享进程的 运行时数据区域(堆和方法区,在JDK1.8之后是 本地内存的元空间。本地内存:元空间+直接内存),但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈
总结:
线程使进程划分成更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程不一定,因为同一个进程中的线程可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程相反。

程序计数器的作用

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到的位置。
    注意:
    如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时,程序计数器记录的才是下一条指令的地址
    私有的目的:线程切换后能恢复到正确的执行位置

虚拟机栈的作用

每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

本地方法栈的作用


和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。


Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C

虚拟机栈和本地方法栈私有的原因

为了保证线程中的局部变量不被别的线程访问。

堆和方法区

堆和方法区是所有线程共享的资源。堆是进程中最大的一块内存,主要是用于存放新创建的对象,几乎所有的对象都在这里分配内存。方法区主要存放已经被虚拟机加载的类型信息,常量以及一些静态变量等信息。

并发和并行的区别

并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
最关键的点是:是否是 同时 执行

同步和异步的区别

同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。

使用多线程的目的

  1. 从总体上来看
    从计算机底层:
    线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销
    从当代互联网发展趋势:
    现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能
  2. 深入到计算机底层
    单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
    多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)
    注意:
    并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。BLOCKED :阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。

线程创建后状态变化:

  1. 创建后处于NEW状态,调用start()方法后开始运行,处于READY状态。可运行状态的线程获得了CPU时间片后处于RUNNING状态。
    在操作系统层面,线程有可运行和运行状态;而在JVM层面,只有运行状态。所以Java系统一般把操作系统中的可运行和运行状态 统称为 运行中 状态
  2. 当线程执行wait()方法后,线程进入 等待状态。进入等待状态的线程需要依靠其他的线程通知才能返回到运行状态。
  3. 超时等待状态相当于在等待状态的基础上增加了超时限制。比如通过sleep(long millis)方法或者是wait(long millis)方法,可以将线程置于超市等待状态。当超时时间结束后,线程会返回到 运行 状态。
  4. 当线程进入synchronized方法/块,或者调用wait后(被notify)重新进入synchronized方法/块,但是锁被其他线程占有,这个时候线程会进入到 阻塞状态。
  5. 线程在执行完run()方法后会进入到 终止状态。

JVM不区分 可运行和运行 状态

当前的 时分多任务操作系统架构通常是“时间分片” 进行抢占式轮转调度。这个时间分片通常比较小,一个线程一次最多只能在CPU上运行10-20ms,此时处于运行状态,这个时间非常短,时间片用完后切换下来放到调度队列末尾等待再次调度,即回到了可运行状态。
线程切换的很快,区分这两种状态没有什么意义

上下文切换

线程在执行过程中会有自己的运行条件和状态(也称上下文)。比如程序计数器,栈信息等。
当出现如下情况的时候,线程会从占用 CPU 状态中退出。主动让出 CPU:

  1. 比如调用了 sleep(), wait() 等。
  2. 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
  3. 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  4. 被终止或结束运行
    这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

线程死锁

定义

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止

产生死锁的四个必要条件

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

预防和避免线程死锁

预防死锁

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

避免死锁

避免死锁是在资源分配时,借助算法,例如:银行家算法。对资源分配进行计算评估,使其进入安全状态。

安全状态:系统能够按照某种线程推进顺序(p12345…n)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可以顺利完成。称:<p12345…n>序列为安全序列。

sleep()和wait()方法对比

共同点

两者都可以暂停线程的执行

区别

  1. sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  2. wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  3. wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。
    sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  4. sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

wait()方法不是定义在Thread

wait()是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象都拥有对象锁,既然要释放当前线程占有的对象锁,并让其进入等待状态,自然是操作对应的对象,而不是当前的线程。

sleep()方法定义在Thread的原因

sleep()是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁

不能直接调用Thread类的run方法

new一个Thread,线程进入新建状态。
调用start()方法,会启动一个线程并使线程进入就绪状态,当分配时间片后就可以开始运行。
start()会执行线程的相应准备工作,然后自动执行run()方法的内容,是多线程工作。
但是,直接执行run()方法,会把run()方法当成一个main线程下的普通方法进行执行,而不是在某个线程中执行,不是多线程工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值