IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 第九章 java多线程——synchronized与锁 -> 正文阅读

[Java知识库]第九章 java多线程——synchronized与锁

首先明确一点:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。
还有一点需要注意的是,我们常常听到的类锁其实也是对象锁。java类只有一个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而Class对象也是特殊Java对象。多以我们常常说的类锁,其实就是Class对象的锁。

9.1 Synchronized关键字

说到锁,我们通常会想到 synchronized这个关键字。它的中文意思是“同步的”。
我们通常使用 synchronized 关键字来给代码或一个方法上锁。他通常有一下三种形式。

// 关键字在实例?法上,锁为当前实例
public synchronized void instanceLock() {
     // code
}

// 关键字在静态?法上,锁为当前Class对象
public static synchronized void classLock() {
     // code
}

// 关键字在代码块上,锁为括号??的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
          // code
    }
}

我们这里介绍一下“临界区”的概念。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。在上面的例字中,如果 synchronized 关键字在方法上,那临界区就是整个方法内部。而如果是使用synchronized代码块,那临界区就指的是代码块内部的区域。

通过上面例子我们可以看到,下面这两个写法其实是等价的作用:

// 关键字在实例?法上,锁为当前实例
public synchronized void instanceLock() {
      // code
}

// 关键字在代码块上,锁为括号??的对象
public void blockLock() {
    synchronized (this) {
       // code
    }
}

同理,下面这两个方法也应该是等价的:

// 关键字在静态?法上,锁为当前Class对象
public static synchronized void classLock() {
     // code
}

// 关键字在代码块上,锁为括号??的对象
public void blockLock() {
    synchronized (this.getClass()) {
       // code
    }
}

9.2 几种锁

Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在Java 6 以前,所有的锁都是”重量级“锁。所以在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它。

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进?安全点的时候,会检查是否有闲置的锁,然后进行降级。

9.2.1 Java对象头

前面我们提到,Java的锁都是基于对象的。首先我们来看看?个对象的“锁”的信息是存放在什么地方的。

每个Java对象都有对象头。如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,?个字宽是32位;在64位虚拟机中,?个字宽是64位。对象头的内容如下表:

长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果是数组)

我们主要来看看Mark Word的格式:

锁状态29bit 或 61bit1 bit是否是偏向锁?2bit 锁标志位
无锁状态001
偏向锁线程ID101
轻量级锁指向栈记录的指针此时这一位不用标识偏向锁00
重量级锁指向互斥量(重量级的指针)此时这一位不用标识偏向锁10
GC标记此时这一位不用标识偏向锁11

可以看到,当对象状态为偏向锁时, Mark Word 存储的是偏向的线程ID;当状态为轻量级锁时, Mark Word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时, Mark Word 为指向堆中的monitor对象的指针。

9.2.2 偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引?了偏向锁。

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

??话就是对锁置个变量,如果发现为true,代表资源?竞争,则?需再?各种加锁/解锁流程。如果为false,代表存在其他线程竞争资源,那么就会?后?的流程。

实现原理

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程ID。当下次该线程进入这个同步块时,会去检查锁的Mark Word里面是不是放的自己的线程ID。

如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁 ;如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,这个时候要

分两种情况:

  • 成功,表示之前线程不存在了,Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁
  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位00,升级为轻量级锁,会按照轻量级锁的方式竞争锁。

CAS: Compare and Swap
?较并设置。?于在硬件层?上提供原?性操作。在 Intel 处理器中,?较并交换通过指令cmpxchg实现。 ?较是否和给定的数值?致,如果?致则修改,不?致则不修改。

线程竞争偏向锁的过程如下:
在这里插入图片描述
图中涉及到了lock record指针指向当前堆栈中的最近?个lock record,是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。

撤销偏向锁

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:

  1. 在一个安全点(这个时间点上没有字节码正在执行)停止拥有锁的线程。
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Work,使其变成无锁状态。
  3. 唤醒被停止的线程,将当前锁升级成轻量级锁。

所以,如果应?程序?所有的锁通常出于竞争状态,那么偏向锁就会是?种累赘,对于这种情况,我们可以?开始就把偏向锁这个默认功能给关闭:

-XX:UseBiasedLocking=false

下?这个经典的图总结了偏向锁的获得和撤销:
在这里插入图片描述

9.2.3 轻量级锁

多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。

轻量级锁的加锁JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word。如果?个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

自旋:不断尝试去获取锁,一般用循环来实现。

自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。

但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

自旋也不是一直进行下去的,如果自旋到?定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

轻量级锁的释放:

在释放锁时,当前线程会使?CAS操作将Displaced Mark Word的内容复制回锁的Mark Word??。如果没有发?竞争,那么这个复制的操作会成功。如果有其他线程因为?旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

?张图说明加锁和释放锁的过程:
在这里插入图片描述

9.2.4 重量级锁

重量级锁依赖于操作系统的互斥量(mutex) 实现的,?操作系统中线程间状态的转换需要相对?较?的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

前?说到,每?个对象都可以当做?个锁,当多个线程同时请求某个对象锁时,对象锁会设置?种状态?来区分请求的线程:

Contention List:所有请求锁的线程将被?先放置到该竞争队列
Entry ListContention List中那些有资格成为候选?的线程被移到Entry List
Wait Set:那些调?wait?法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有?个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程

当?个线程尝试获得锁时,如果该锁已经被占?,则会将该线程封装成?个 ObjectWaiter 对象插?到Contention List的队列的队?,然后调? park 函数挂起当前线程。

当线程释放锁时,会从Contention List或EntryList中挑选?个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承?,假定继承?被唤醒后会尝试获得锁,但 synchronized 是?公平的,所以假定继承?不?定能获得锁。这是因为对于重量级锁,线程先?旋尝试获得锁,这样做的?的是为了减少执?操作系统同步操作带来的开销。如果?旋不成功再进?等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有?个不公平的地?是?旋线程可能会抢占了Ready线程的锁。

果线程获得锁后调? Object.wait ?法,则会将线程加?到WaitSet中,当被 Object.notify 唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调??个锁对象的 wait 或 notify ?法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

9.2.5 总结锁的升级流程

  • 每?个线程在准备获取共享资源时: 第?步,检查MarkWord??是不是放的??的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
  • 第?步,如果MarkWord不是??的ThreadId,锁升级,这时候,?CAS来执?切换,新的线程根据MarkWord??现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
  • 第三步,两个线程都把锁对象的HashCode复制到??新建的?于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为??新建的记录空间的地址的?式竞争MarkWord。
  • 第四步,第三步中成功执?CAS的获得资源,失败的则进??旋 。
  • 第五步,?旋的线程在?旋过程中,成功获得资源(即之前获的资源的线程执?完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果?旋失败 。
  • 第六步,进?重量级锁的状态,这个时候,?旋的线程进?阻塞,等待之前线程执?完成并唤醒??。

9.2.6 各种锁的优缺点对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执??同步?法?仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适?于只有?个线程访问同步块场景。
轻量级锁竞争的线程不会阻塞,提?了程序的响应速度。如果始终得不到锁竞争的线程使??旋会消耗CPU。追求响应时间。同步块执?速度?常快。
重量级锁线程竞争不使??旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执?速度较?。
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-04-30 08:33:07  更:2022-04-30 08:35:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:58:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码