不懂并发?看完就超神!!!Java并发基础(一)

多线程基本概念

参考课程:全面深入学习java并发编程,java基础进阶中级必会教程

进程与线程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
在这里插入图片描述

并行与并发

并行:在同一时刻一个进程的一条指令执行,只是由于多个进程被快速轮转,使得在宏观上好像是多个进程同时进行。
在这里插入图片描述
并行:在同一时刻内多个进程的多条指令在多个处理器上同时执行,这种情况下才是多个进程真正同时进行。
在这里插入图片描述
并发可以在多处理器和单处理器中存在,而并行只能在单处理器中存在。

线程上下文与开销

在多道程序算法下,既多核CPU,CPU的一个核心在任意时刻只能被一个线程使用,根据CPU的RR(Round-Robin)调度算法,CPU会为每个线程分配时间片,当时间片执行完后就会去执行其他线程,在这个切换的过程中,CPU需要保存当前任务的状态后再去执行其他任务,这个过程就是上下文切换(thread context switch),上下文切换是操作系统内核优化的一个关键参数指标。在任务间发生切换需要花费大量的时间用于处理诸如:保存和恢复寄存器和内存页表、更新内核相关数据结构等操作。上下文切换通常是计算密集型的,也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的CPU时间。

在操作系统上的体现

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。 在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态。
在这里插入图片描述
内核态可以执行任意命令,调用系统的一切资源,而用户态只能执行简单的运算,不能直接调用系统资源。用户态必须通过系统接口(System Call),才能向内核发出指令。比如,当用户进程启动一个 bash 时,它会通过 getpid() 对内核的 pid 服务发起系统调用,获取当前用户进程的 ID;当用户进程通过 cat 命令查看主机配置时,它会对内核的文件子系统发起系统调用。
在这里插入图片描述
在这里插入图片描述
互斥锁的开销主要在内核态与用户态的切换:

申请锁时,从用户态进入内核态,申请到后从内核态返回用户态(两次切换);没有申请到时阻塞睡眠在内核态。使用完资源后释放锁,从用户态进入内核态,唤醒阻塞等待锁的进程,返回用户态(又两次切换);被唤醒进程在内核态申请到锁,返回用户态(可能其他申请锁的进程又要阻塞)。所以,使用一次锁,包括申请,持有到释放,当前进程要进行四次用户态与内核态的切换。同时,其他竞争锁的进程在这个过程中也要进行一次切换。进程上下文切换的直接消耗包括CPU寄存器保存和加载,需要调度时有内核调度代码的执行。

在JVM中对同优先级的采用RR调度算法,且高优先级的可以抢占。

Monitor

对象头

object header分为mark word,Klass word两个字段,其中klass word就是指明对象的类型。
在这里插入图片描述
mark word中可以分为5种:

  • normal状态:hashcode,age用于jvm中,biased_lock偏向锁标志位,最后两位就是status位,normal的status为01,表示没有于任何锁关联。001
  • baised状态:101
  • lightweight locked状态:舍去了hashcode等字段,变为指向lock record的指针,00。
  • heavyweight locked状态:同理,指向重量级锁的指针,10。
  • gc:垃圾回收,11。
    在这里插入图片描述

重量级锁

synchronized

java每个对象都可以关联一个Monitor对象,如果使用sychronized加锁,对象头中的mark word就被指向了monitor对象的指针,monitor又是依赖操作系统的mutexlock,这种锁就叫重量级锁,或者悲观锁,阻塞同步。
其中这个对象obj的mark word就会从normal变为heavyweight locked状态,状态从01变为10,指针指向monitor,而thread2也就会变为这个monitor的owner。
在这里插入图片描述
在这里插入图片描述
sychronized采用互斥的方式让同一时间内只有一个线程能有对象锁,其他线程想要获取时就会被阻塞,从而保证线程安全。实际上就是使用对象锁保证了临界区内的原子性。

当有其他也是用相同对象obj作为锁时,发现其mark word表示正在被其他线程使用中,这些线程就会进入entrylist阻塞队列并进入blocked状态。

当thread-2执行完了,其他线程再通过竞争获取锁(owner),此时假设thread1竞争到了,就变为owner。
在这里插入图片描述
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

自旋优化

通过不断自旋来避免进入blocked状态,如果自旋成功了就表示锁已经释放了,如果失败就进入blocked状态,自旋的次数是自适应的。
在这里插入图片描述

syn使用方式

  1. 普通方法:作用于当前对象实列,既方法的调用者。
synchronized void method() {
    
      
        //业务代码
}
  1. 静态方法:给当前类模板加锁,作用于类所有的对象实列。
synchronized void staic method() {
    
      
        //业务代码
}
  1. 代码块:对指定的对象、类加锁,synchronized(this、object)表示对象,synchronized(类.class)类的锁。
synchronized(this|object|.class) {
    
      
     //业务代码
}

轻量级锁

轻量级锁是指在没有竞争的情况下就会采取轻量级锁的方式加锁。
在这里插入图片描述
使用创建锁记录lock record对象来代替使用monitor,每个线程栈都会包含一个锁记录的结构,用于存储锁定对象的mark word。
在这里插入图片描述
此时让object reference指向锁对象,并会尝试使用cas(compare and set)来交换lock record和object的mark word。01表示未加锁状态,00表示为轻量级锁。
在这里插入图片描述
如果交换成功了,就表示加上了锁,如果失败了,进入锁膨胀或者锁重入。
解锁时又会使用尝试cas将mark word恢复给对象头,失败了就会进入重量级的解锁流程。
在这里插入图片描述

锁重入

同一线程多次使用同一object锁时,在自己线程栈里就会不断创建lock record作为重入的计数,就可以继续执行。
在这里插入图片描述
解锁时:一直清除null这条lock record,直到不为null,然后就按照轻量级锁的解锁方式解锁。
在这里插入图片描述

锁膨胀

在这里插入图片描述
当thread1进行加锁时,发现thread0已经使用了锁了,这时就产生了竞争,这时thread1就会进入锁膨胀,变为重量级锁。
在这里插入图片描述
thread1使用重量级锁加锁的方式进行加锁。

而当thread0开解锁时,发现object已经变为重量级锁了,进入重量级锁解锁。
在这里插入图片描述

偏向锁

轻量级锁在没有竞争时,每次重入还需要执行CAS操作,而偏向锁:
在这里插入图片描述
释放锁后,lock record的字段任然会保留在锁的mark word里,一般多使用竞争较少的。
在这里插入图片描述
在这里插入图片描述

偏向状态

在这里插入图片描述
biased_lock就表示偏向锁状态,偏向锁默认开启且是延迟的,也就是不会马上生效,可以看下面列子,延迟4s后,就变为101。

在这里插入图片描述
在这里插入图片描述

撤销-hashcode

使用hashcode会导致偏向锁失效,既101变为001。

撤销-其他线程

t1使用偏向锁,t1释放后,d对象仍然保持着t1的lock record,当t2使用锁时就会升级成为轻量级锁,最后释放锁后偏向状态变为normal lock。

撤销-调用wait、notify

wait、notify属于重量级的方法,调用这个方法会变为重量级锁。

撤销-批量重偏向
在这里插入图片描述
有30个对象,使用t1线程将这30个对象分别作为对象锁,这时由于没有竞争会变为偏向锁,于是这30个对象锁全处于偏向状态,当使用t2线程时,前20个对象都会逐个进行偏向撤销,当超过20次后,jvm会自动将剩下的全改为偏向于t2。

批量撤销
在这里插入图片描述

锁消除

java会使用JIT即时编译器对字节码进行优化,它认为这个不用加锁,于是就将其消除。
在这里插入图片描述
由于并发篇幅较长,本人打算分成几篇,这样也方便阅读。

猜你喜欢

转载自blog.csdn.net/TheCarol/article/details/112981737