赞
踩
操作系统在运行一个程序的时候,就会为它创建一个进程。比如说我们启动一个java程序,操作系统就会创建一个java进程,在这个进程里面,我们可以创建很多线程,每个线程都拥有自己的程序计数器和栈,并且还能够访问共享的变量。线程是操作系统调度的最小单元,CPU在这些线程上高速切换,让使用者感觉这些线程是在同时执行
并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升
创建多少线程这个问题的本质是:尝试通过增加线程来提升 I/O 的利用率和 CPU 的利用率,努力将硬件的性能发挥到极致
对于单核 CPU,如果只有一个线程,执行 CPU 计算的时候,I/O 设备空闲;执行 I/O 操作的时候,CPU 空闲,所以 CPU 的利用率和 I/O 设备的利用率都是 50%,我们可以通过多线程去平衡 CPU 和 I/O 设备
对于多核 CPU,我们可以增加线程数提高 CPU 利用率,来降低响应时间(一个方法内使用多线程执行可并行的部分或者并行执行多个方法)
但是一味的增加线程数并可取
如果一个程序只有 CPU 计算,而没有 I/O 操作的话,多线程不但不会提升性能,还可能会使性能变差,原因是增加了线程切换的成本
我们首先要区分具体场景
CPU 密集型:纯 CPU 计算,不涉及 I/O 操作的场景
I/O 密集型:只要涉及 I/O 操作的场景都属于 I/O 密集型,因为 I/O 设备的速度相对于 CPU 计算来说都非常长
对于 CPU 密集型:多线程的本质就是提升多核 CPU 的利用率,理论上 线程的数量 = CPU 核数 就是最合适的,工程上,线程的数量会设置为 CPU 核数+1,这样的话,当线程因为偶尔的内存页失效或者其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率
对于 I/O 密集型:最佳的线程数是与程序中 CPU 计算和 I/O 操作的耗时比相关的,对于单核CPU,最佳线程数 =1 +(I/O 耗时 / CPU 耗时),对于多核 CPU,最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
在java线程中,会通过一个整形变量 priority 来控制线程优先级,范围是一到十,默认是5,可以通过对应的set方法修改。理论上优先级高的线程分配时间片数量要多于优先级低的线程,但实际上操作系统可能不太会理会java线程对于优先级的设定
Java的线程分为两类:
daemon = false
daemon = true
时,就是守护线程用户线程与守护线程的关系:
用户线程就是运行在前台的线程,守护线程就是运行在后台的线程,一般情况下,守护线程是为用户线程提供一些服务,比如在Java中,GC内存回收线程就是守护线程
JVM 与用户线程共存亡:
当所有用户线程都执行完成,只存在守护线程在运行时,JVM就退出,Java程序在main线程执行退出时,会触发执行JVM退出操作,但是JVM退出方法destroy_vm()
会等待所有非守护线程都执行完,里面时用变量numberofnondaemonthreads
统计非守护线程的数量,这个变量在新增线程和删除线程时会做增减操作
当JVM退出时,所有还存在的守护线程会被抛弃,既不会执行finally部分代码,也不会执行catch异常
main线程可以比子线程先退出:
main 线程退出前,通过 LEAVE()
方法,调用了 destroy_vm()
方法,但是在 destroy_vm()
方法里面等待着非守护线程执行完,子线程如果是非守护线程,则 JVM 会一直等待,不会立即退出。
线程在还没有通过start()方法启动前,可以通过修改daemon属性来将用户线程转变为守护线程,启动之后就不能修改了
其他特性:
操作系统采用时分的形式调度运行的线程,操作系统会分出一个个的CPU时间片,线程会分到若干个CPU时间片,当线程的CPU时间片用完了就会发生线程调度,也就是线程切换,等待着下次分配。线程分配到CPU时间片的多少就决定了使用CPU资源的多少。
当发生线程切换的时候,操作系统要保存当前线程状态,并恢复另外一个线程的状态,这个时候就要使用程序计数器,记住下一条JVM指令的执行地址,这也是程序计数器必须线程私有的原因
线程切换的原因:
CPU指令集:指令集是 CPU 实现软件指挥硬件执行工作的媒介,具体来说每一条汇编语句都对应了一条 CPU 指令,CPU 指令不止一条的,而是由非常非常多的 CPU 指令集合在一起,组成了一个、甚至多个的集合,每个指令的集合叫:CPU 指令集
CPU指令权限分级:CPU 指令是可以直接操作硬件的,要是因为指令操作的不规范,造成的错误是会影响整个 计算机系统 的,所以对CPU指令进行了分装操作,对CPU指令设置了权限,不同级别的权限可以使用的CPU指令是有限的
ring0:权限最高,可以使用所有的CPU指令
ring1
ring2
ring3:权限最低,仅能使用常规的CPU指令,这个级别的权限不能使用访问硬件资源的指令,比如 IO 读写、网卡访问、申请内存都不行,都没有权限
ring 0
被叫做 内核态
,完全在 操作系统内核 中运行,由专门的 内核线程 在 CPU 中执行其任务
ring 3
被叫做 用户态
,在 应用程序 中运行,由 用户线程 在 CPU 中执行其任务
Linux 中默认采用 1:1 线程模型
,就是有一个 用户线程,就得在内核中启动一个对应的 内核线程,然后把这个 用户线程 绑定到 内核线程 上。
JVM 采用 Linux 默认函数库,也就是 PThread,1:1 线程模型。java new 一个 Thread 时,是创建了 1个用户线程和内核线程的,然后把用户线程绑定到内线线程中,ring3 的代码在 用户线程中执行,ring0 的代码切换到 内核线程 中去执行,然后使用 内核线程 接受 系统内核的调度,内核线程抢到 CPU 时间片后,用户线程就会激活执行代码
用户态和内核态的切换的实质就是用户线程和内核线程的切换
当在一系统中执行一个程序时,大部分时间时运行在用用户态下的,在其需要操作系统帮助的完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态
用户态切换到内核态的三种方式
这三种方式是系统在运行时由用户态切换到内核态的最主要方式,其中系统调用可以认为时用户进程主动发起的,异常和外围设备中断则是被动的。从触发方式上看,切换方式都不一样,但是从最终实际完成由用户态到内核态的切换操作来看,步骤又是一样的,都相当于执行了一个中断响应的过程。系统调用实际上最终是中断机制实现的,而异常和中断的处理机制基本一致。
回答思路:死锁的定义-----死锁产生的原因(使用细粒度锁)-----死锁形成的条件-----如何预防死锁
死锁的定义:一组互相竞争资源的线程因互相等待,导致 “永久” 阻塞的现象
在使用锁的过程中我们会选择用不同的锁对受保护资源进行精细化管理,也就是使用细粒度锁,能够提升性能,但是使用细粒度锁是有代价的,这个代价就是可能会导致死锁
死锁形成的条件:
我们只用破坏一个条件就可以避免死锁的发生
解决方案:
互斥:互斥这个条件没有办法避免
占用且等待:我们一次性申请所有的资源,这样就不存在等待了(使用等待 - 通知机制,线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁)
具体实现:使用 synchronized 配合 wait()、notify()、notifyAll() 这三个方法来进行是实现,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,调用 wait()方法,然后线程会被阻塞,进入等待队列中,同时释放线程持有的互斥锁,让其他线程有机会获得锁,进入临界区,当线程要求满足时,通知等待队列中的线程,被通知的线程想要重新执行,仍然需要获取到互斥锁(进入锁等待队列中 因为之前调用wait方法的时候释放了互斥锁)
不可抢占条件:破坏不可抢占条件的核心是能够主动释放它占有的资源。synchronized 做不到这一点,synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,不能主动释放已经抢占的资源(java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的)
循环等待:破坏循环等待,需要对资源进行排序,然后按序申请资源(可以给资源加一个 id 属性,这个 id 属性可以作为排序的字段,申请资源时,我们可以按照从小到大的顺序来申请)
协程可以理解为线程的线程。线程虽然提升了资源的利用率,但是也存在线程资源有限,而且大多数线程资源处于阻塞的状态,线程之间的开销虽然对比进程少了不少,但是上下文切换的切换开销也不小的问题。协程的出现在一定程度上解决了一些问题。协程的核心在于调度那块由他来负责解决,遇到阻塞操作,立刻放弃掉,并且记录当前栈上的数据,阻塞完后立刻再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑,等达到一定条件后,再恢复原来的栈信息继续执行。协程在Java原生库上不支持的,要引入三方的库才支持。Kotlin和Go语言是原生支持协程的。
因为 CPU 和 内存的速度差异非常的大,为了合理的利用 CPU,平衡这两者的速度差异,CPU 难题wem加了缓存来均衡与内存的速度;同时会在编译的时候,进行指令重排序,使得缓存能够得到更加合理地利用,但是这样会带来两个问题,就是可见性问题和指令重排序导致的问题
Java 内存模型其实是一套规范,这套规范解决了可见性和有序性问题,能够使 JVM 按需禁用 CPU 缓存和禁止指令重排序。这套规范包括对 volatile、synchronized、final 三个关键字的解析,和 7 个Happens-Before 规则
JMM 通过添加内存屏障来禁止指令重排序,编译器的内存屏障会告诉编译器,不要对指令进行重排序,当编译完成后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在
JMM通过 happens-before 关系向程序员提供跨线程的内存可见性保证。如果一个操作happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
继承Thread类
//构造方法的参数是给线程指定名字
Thread t = new Thread("t1") {
@Override
//run方法内实现了要执行的任务
public void run() {
System.out.printLn("hello");
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。