【JUC-2】Synchronized关键字相关知识

news2024/11/16 1:43:07

Synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

java对象内存结构

在这里插入图片描述

Mark Word结构

Mark Word (64bits)State
unused:25 | hashcode:31 | unused:1 | age:4bits | biassed_lock:0 | 01Normal
thread: 54 | epoch:2 | unused:1 | age:4 | biassed_lock:0 | 01Biassed
pre_to_lock_record:62 | 00Lightweight Locked
prt_to_heavyweight_monitor:62 | 10heavyweight Locked
  • hashcode: 对象的hashcode

  • age: 对象年龄, 决定是否去老年代

  • biassed_lock: 偏向锁标志, 0表示非偏向锁, 1表示偏向锁

  • Biassed: 偏向锁

  • thread: 线程id

Monitor(锁)

每个JAVA对象都可以关联一个Monitor对象, 如果使用synchonized给对象上锁(重量级)之后, 该对象头的Mark Word就被设置指向Monitor对象的指针, Mark Word中原本的信息如hashcode就会被存到Monitor对象中, 在释放锁的时候, monitor会将这些属性还原到Mark Word中

在这里插入图片描述

  • 刚开始监视器中所有者为NULL

  • 当T1执行synchronized(obj)就会将Monitor的所有者Owner置为T1,Monitor中只能有一个Owner

  • 当T1上锁后,T2/T3/T4也来执行临界区代码(synchonized(obj))就会进入Monitor的EntryList中阻塞

  • T1执行完临界区代码, 就会唤醒EntryList中的线程, 来进行锁竞争, 竞争时是非公平的.

  • 图中waitSet中的T9/T0是之前获得过锁的, 但是不满足进入WATING状态的线程, 跟wait-notify有关

    注意:

    没有添加synchonized关键字的对象不会关联Monitor

synchonized-轻量级锁

当synchonized为轻量级锁的时候,其锁创建过程如下

在这里插入图片描述

  • 每个线程创建时都会创建一个lock record对象, 内部存储锁对象的Mark Word

  • 当thread0执行到sychonized代码块时, lock record对象中的object reference将指向锁对象, 同时采用cas操作, 将obj锁对象中的mark word与lock record对象中的lock record 地址 00进行交换

  • cas成功, 其状态如下; 锁状态00. 表示轻量级锁

在这里插入图片描述

  • cas失败, 则有两种情况

    • 如果是其他线程已经持有了该obj锁对象, 表示有竞争, 会进入锁膨胀

    • 如果是thread0锁冲入导致cas失败, 那么在栈帧中再添加一个lock record作为锁重入计数

在这里插入图片描述

  • 当退出synchonized代码块时,进行锁释放. 重入几次, 就要释放几次

    • 释放成功, 轻量级锁成功释放
    • 释放失败, 说明轻量级锁进入锁膨胀, 要进入重量级锁解锁流程

锁膨胀

当轻量级锁存在锁竞争的情况时, thread1线程竞争失败, 进入所膨胀流程.

  • 为obj锁对象申请monitor锁, 让obj对象的mark word指向monitor地址

  • 然后thread1线程进入monitor的entryList中进行阻塞等待

    注意: 轻量级锁没有阻塞等待队列

在这里插入图片描述

  • thread0 释放轻量级锁时, cas失败, 进入重量级锁释放流程. 将monitor中的owner置为null, 唤醒entryList中等待线程进行锁竞争

重量级锁-自旋优化

  • 当monitor存在多线程锁竞争的时候, 竞争线程不会立即进入EntryList队列中阻塞, 他会重试多次获取锁, 如果重试的时候获取到了monitor, 则进入临界代码块. 自旋获取失败, 进入monitor 的entryList中阻塞等待
  • java6之后自旋是自适应的, 比如之前的一次锁竞争中自旋成功了, 那么认为当前自旋获取锁的概率大, 就会多进行几次自旋, 反之就少自旋, 甚至不自旋

偏向锁

偏向锁, 锁对象的mark word中记录的是线程id, 当线程重入加锁时, 省去了判断mark word中的地址是否为锁记录的地址.

一个对象创建时:

  • 如果开启了偏向锁(默认开启) ,那么对象创建后,markword值为0x05即最后3位为101, 这时它的thread、epoch、 age 都为0

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数

    -XX:BiasedLockingStartupDelay=e来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,markword 值为0x01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值

偏向锁-撤销偏向

会撤销偏向的场景

  • 当调用锁对象的hashcode方法时, 偏向锁会撤销, 将mark word中的线程id替换为正常对象的信息(可看mark word结构). 即将biassed状态撤销为normal状态
  • 当多个线程都要使用偏向锁时(不是竞争也会升级), 偏向锁升级为轻量级锁.
  • 调用wait/notify; 这两个方法只有重量级锁, 不管当前用的什么锁, 都会升级重量级锁

偏向锁-批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID

当撤销偏向锁阈值超过20次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

偏向锁-批量撤销

当撤销偏向锁阈值超过40次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有

对象都会变为不可偏向的,新建的对象也是不可偏向的

Synchronized锁升级过程

伪代码:

private Object lock = new Object();
synchronized (lock) {
    // 代码逻辑   
}

当java程序第一次运行一个上述synchronized代码块儿时, 假设当前线程为t0, 锁变化如下

  1. 加偏向锁, lock对象的mark word记录t0线程ID, 将锁标志位修改为01表示当前为偏向锁. 如果后续执行该代码块的线程仍然为t0线程, 那么只需要比较lock对象中的mark word存的是不是t0线程的ID即可, 不用像轻量级锁那样获取地址值进行比较.
  2. 当访问代码块的线程为t1线程时, lock对象的mark word中线程ID与当前线程不相等, 升级为轻量级锁, 并且将lock对象的mark word存入t1线程的栈帧中的lock record对象, 将lock record对象的指针存入lock对象的mark word, 完成交换.
  3. 在场景2的基础上, 又来了线程t3要执行同步代码块, 此时t2没有执行完成, t3无法获得锁, 锁升级为重量级锁, lock对象的mark word记录monitor的地址值, 设置t3线程进入monitor的entryList中等待t2线程释放锁.

锁消除

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:

public String method() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

StringBuffer对象是局部变量, 且不会从该方法中逃逸出去. 所以StringBuffer对象是没有并发环境的, 所以JVM对其进行了锁消除. 编译时, 将StringBuffer转换成了StringBuilder

锁粗化

锁粗化是指,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

public String method() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        // 伪代码:加锁操作
        sb.append( + i);
        // 伪代码:解锁操作
    }
    return sb.toString();
}

在循环中每次都需要加锁, 浪费性能, 将锁放到循环外, 只用加锁一次

public String method() {
    StringBuilder sb = new StringBuilder();
    // 伪代码:加锁操作
    for (int i = 0; i < 10; i++) {
        sb.append( + i);
    }
    // 伪代码:解锁操作
    return sb.toString();
}

wait-notyfy

  • obj.wait() 让进入object监视器的线程到waitSet等待
  • obj.wait(long n) n毫秒之后, 自动唤醒
  • obj .notify()在object. 上正在waitSet等待的线程中挑一 个唤醒
  • obj . notifyAll()让object. 上正在waitSet等待的线程全部唤醒
  • 它们都是线程之间进行协作的手段,都属于Object对象的方法。obj对象必须被线程锁住,才能调用这几个方法
  • wait调用之后, 会释放锁. 其他线程可获取当前锁进入临界代码区

在这里插入图片描述

如果没有synchonized(obj), 直接执行obj.wait()会报错.

public class Test7 {
    private static final Object obj = new Object();

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    try {
                        System.out.println("执行wait");
                        obj.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("执行释放");
            }
        });
    }
}

sleep与wait方法的区别

  • sleep是Thread类的静态方法; wait是Object类的普通方法
  • sleep不释放锁; wait释放锁
  • wait需要synchonized才能使用

同步模式-保护性暂停

概念: 即Guarded Suspension,用在一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者消费者)
  • JDK中, join的实现、Future的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

保护性暂停案例, 这也是join()方法的实现原理

package com.heima.test;

import java.io.Serializable;
import java.util.List;

public class GuardedObject implements Serializable {

    private List<String> response;

    public List<String> getResponse() {
        return response;
    }

    public List<String> get(long n) throws InterruptedException {

        synchronized (this) {
            long start = System.currentTimeMillis();
            long passed = 0L;
            while (null == response) {
                passed = System.currentTimeMillis() - start;
                System.out.println("获取进入等待,等待时间:" + n);
                wait(n - passed);
            }
            return response;
        }
    }

    public void complete(List<String> list) {
        synchronized (this) {
            response = list;
            System.out.println("唤醒其他线程");
            this.notifyAll();
        }
    }
}

public class Test7 {

    public static void main(String[] args) {
        GuardedObject guarded = new GuardedObject();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    List<String> list = guarded.get(2000);
                    if (null != list) {
                        list.forEach(System.out::println);
                    } else {
                        System.out.println("null");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
            HttpURLConnection conn = null;
            try {
                conn = (HttpURLConnection) new URL("http://www.baidu.com").openConnection();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            List<String> list = new ArrayList<>();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    list.add(line);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            guarded.complete(list);
        }, "t2");

        t.start();
        t2.start();
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/711413.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++2】进程 信号 dbus

文章目录 1.进程&#xff1a;fork()&#xff0c;ps -ef (同-aux) | more2.信号&#xff1a;signal&#xff08;, EXIT&#xff09;&#xff0c;jps2.1 捕捉信号&#xff1a;ctrlc&#xff1a;22.2 捕捉信号&#xff1a;kill -9&#xff1a;92.3 捕捉信号&#xff1a;kill&#…

欧几里得算法

0x00 前言 改补的内容是一点都不会少。本章来看欧几里得算法 0x01 概述 欧几里得算法又称为辗转相除法&#xff0c;指用于计算两个非负整数a和b的最大公约数。 两个整数的最大公约数是能够同时整除他们的最大的正整数。 基本原理&#xff1a;两个整数的最大公约数等于其中…

【动态规划】子数组系列(上)

子数组问题 文章目录 【动态规划】子数组系列&#xff08;上&#xff09;1. 最大子数组和1.1 题目解析1.2 算法原理1.2.1 状态表示1.2.2 状态转移方程1.2.3 初始化1.2.4 填表顺序1.2.5 返回值 1.3 代码实现 2. 环形子数组的最大和2.1 题目解析2.2 算法原理2.2.1 状态表示2.2.2 …

C++2(表达式和关系运算)

目录 1.表达式基础 1.表达式基础 运算符重载&#xff0c;就是自己定义 - * / 之类的运算符怎么运算 C中的左值和右值 C语言左值在左侧&#xff0c;右值在右侧 在cpp中要复杂的多 能取到地址的表达式是左值 不能取到地址的表达式是右值 常量对象为代表的左值不能作为赋值语句的左…

【Linux】网络相关概念概述以及原理简单分析介绍

文章目录 [toc] Linux 网络概述网络发展独立模式网络互联局域网LAN 和 广域网WAN 认识 "协议"协议的分层网络协议栈OSI七层模型TCP/IP五层(四层)模型TCP/IP网络协议栈 与 操作系统 的关系 **重新以计算机的视角看待 网络协议栈 局域网内部通信原理简单介绍不同局域网…

mybatis web使用02

处理 transfer 请求的 servlet package com.wsd.web;import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRe…

GAMES101 笔记 Lecture08 Shading 2(Shading, Pipeline and Texture Mapping)

目录 Specular Term(高光项)Ambient Term(环境光照项)Blinn-Phong Reflection ModelShading Frequencies(着色频率)Shade each triangle(flat shading)在每个三角形上进行着色Shade each vertex (Gouraud shading)(顶点着色)Shade each pixel (Phong shading)Defining Per-Vert…

【C++详解】——哈希

目录 unordered系列关联式容器 unordered_map unordered_map的接口说明 1.unordered_map的构造 2.unordered_map的容量 3.迭代器相关 4.unordered_map的元素访问 5. unordered_map的查询 6.unordered_map的修改操作 unordered_set 性能测试 底层结构——Hash 哈希…

copula简介

二元正态copula最为重要

MySQL - 自连接查询

1. 测试数据 创建 category 表 : CREATE TABLE category(categoryid INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主题id,pid INT(10) NOT NULL COMMENT 父id,categoryName VARCHAR(50) NOT NULL COMMENT 主题名字,PRIMARY KEY(categoryid) ) ENGINEINNODB AUTO_INCREM…

cmd的学习

目录 常用的cmd命令 使用cmd的例子 常用的cmd命令 指令作用盘符名称:盘符切换dir查看当前路径下的内容tree以树形结构输出当前路径下的内容cd进入单级目录cd ..回退到上一级目录cd 目录1\目录2\...进入多级目录cd \回退到盘符目录cls清屏exit退出窗口 &#xff08;值得注意的…

Android AlertDialog setView,kotlin

Android AlertDialog setView&#xff0c;kotlin <?xml version"1.0" encoding"utf-8"?> <com.google.android.material.textfield.TextInputLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width…

MySQL数据库——主从复制和读写分离

MySQL数据库——主从复制和读写分离 一、主从复制和读写分离的相关知识1.什么是读写分离&#xff1f;2.为什么要读写分离呢&#xff1f;3.什么时候要读写分离&#xff1f;4.主从复制与读写分离5.mysql支持的复制类型6.主从复制的工作过程7.MySQL 读写分离原理8.目前较为常见的 …

前端实现俄罗斯方块游戏(内含源码)

目录 一、前言 二、功能介绍 三、页面搭建 四、样式设置 五、逻辑部分 一、前言 今天带领大家完成俄罗斯方块游戏&#xff0c;功能也比较简单&#xff0c;也是想借助这样一个简单的功能&#xff0c;然后来帮助大家了解我们JavaScript在前端中的作用&#xff0c; 后续也会带…

【服务器】ASP.Net Core(C#)创建Web站点

简单几步实现本地ASP.Net.Core web 站点结合cpolar内网穿透工具实现远程访问 1. 创建站点 *环境搭建,这边测试,使用.NET 6.0 SDK,可以点击跳转到官网下载,下载后安装即可. 安装完成后,进入到某个文件夹,打开powershell执行下面命令,创建新的 Web 应用,名称叫:aspnetcoreapp …

机器学习 day22(ReLU激活函数)

ReLU激活函数 如果想让a取更大的非负数&#xff0c;激活函数g(z)可以选用ReLU激活函数&#xff0c;他在z&#xff1c;0时取0&#xff0c;在z ≥ 0时取z 常见的激活函数 左侧的为线性激活函数&#xff0c;因为f(x) wxb&#xff0c;使用激活函数后f(x) g(z)&#xff0c;此…

综合评价算法 | Matlab实现基于TOPSIS法的综合评价算法

文章目录 效果一览文章概述研究内容源码设计参考资料效果一览 文章概述 综合评价算法 | Matlab实现基于TOPSIS法的综合评价算法 研究内容 C.L.Hwang 和 K.Yoon 于1981年首次提出 TOPSIS (Technique for Order Preference by Similarity to an Ideal Solution)。TOPSIS 法是一种…

卷积神经网络--猫狗系列【CNN】

数据集&#xff0c;这次这个是分了类的【文末分享】 各12500张&#xff1a; 两点需要注意&#xff1a; ①猫狗分类是彩色图片&#xff0c;所以是3个channel&#xff1b; ②猫狗分类的图片大小不一&#xff0c;但是CNN的输入要求是固定大小&#xff0c;所以要resize。 划分训练…

【动态规划】子数组系列(下)

子数组问题 文章目录 【动态规划】子数组系列&#xff08;下&#xff09;1. 等差数组划分1.1 题目解析1.2 算法原理1.2.1 状态表示1.2.2 状态转移方程1.2.3 初始化1.2.4 填表顺序1.2.5 返回值 1.3 代码实现 2. 最长湍流子数组2.1 题目解析2.2 算法原理2.2.1 状态表示2.2.2 状态…

初学spring5(五)使用注解开发

学习回顾&#xff1a;初学spring5&#xff08;四&#xff09;自动装配 一、使用注解开发 二、说明 在spring4之后&#xff0c;想要使用注解形式&#xff0c;必须得要引入aop的包 在配置文件当中&#xff0c;还得要引入一个context约束 <beans xmlns"http://www.sprin…