synchronized锁的认知

生命不息,写作不止

继续踏上学习之路,学之分享笔记

总有一天我也能像各位大佬一样

一个有梦有戏的人 @怒放吧德德

分享学习心得,欢迎指正,大家一起学习成长!

简介

synchronized 是 Java 语言的一个关键字,它允许多个线程同时访问共享的资源,以避免多线程编程中的竞争条件和死锁问题。synchronized可以用来给对象或者方法进行加锁,当对某个对象或者代码块加锁时,同时就只能有一个线程去执行。这种就是互斥关系,被加锁的区域称为临界区,而里面的资源就是临界资源。当一个线程进入临界区的时候,另一个线程就必须等待。



synchronized可以限制对某个资源的访问,但是它锁的并不是资源本身,可以锁住某个对象,只有线程拿到这把锁之后才能够去访问临界资源。如下代码,在我们想执行对count变量进行操作的时候,线程需要拿到o这个对象。

public class T1_Synchronized01 {
private int count = 1;
private Object o = new Object(); public void m1() {
synchronized (o) { // 必须先拿到o这个锁
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}

synchronized基础用法

1、通过对象进行锁

在代码里,可以通过创建一个对象,这样要想拿到临界资源,就必须先获得到这个对象的锁。

public class T1_Synchronized01 {
private int count = 1;
private Object o = new Object(); public void m1() {
synchronized (o) { // 必须先拿到o这个锁
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}

2、通过this

使用this代表锁住的是当前对象,这种方法等同直接把synchronized关键字加在方法前。

public class T1_Synchronized01 {
private int count = 1; public void m2() {
synchronized (this) { // 必须先拿到this的锁
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
} public synchronized void m3() { // 与m2一样
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
} }

3、锁定静态方法

锁定静态方法需要通过类.class,或者直接在静态方法上加上关键字。但是,类.class不能使用this来代替。注:在同一个类加载器中,class是单例的,这也就能保证synchronized能够只让一个线程访问临界资源。

public class T1_Synchronized01 {
public static void m4() { // 静态方法
synchronized (T1_Synchronized01.class) {
System.out.println(Thread.currentThread().getName());
}
} public synchronized static void m5() {
System.out.println(Thread.currentThread().getName());
}
}

4、实验测试

①、首先测试一下,同步和非同步是否可以相互调用

定义两个线程,一个执行同步方法,一个执行非同步方法,如果不能够互相调用,那么,非同步线程就需要等待同步线程执行完之后在继续执行。

public class T3_Synchronized03 {
public synchronized void one() {
System.out.println(Thread.currentThread().getName() + " start one method");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " end one method");
} public void two() {
System.out.println(Thread.currentThread().getName() + " execute two method");
} public static void main(String[] args) {
T3_Synchronized03 t = new T3_Synchronized03();
new Thread(t::one, "第一个线程").start();
new Thread(t::two, "第二个线程").start();
}
}

从运行的结果可以看出是可以的。



②、读写不全加锁会怎样

通过购票与查询票数来进行模拟读写加锁问题。

首先,看以下代码是给读写都进行加锁了,在扣掉票数的时候,休眠了2秒,当线程执行了购票之后,通过多个线程去查询票数,每次启动线程会睡眠0.5秒。

public class T4_Synchronized {
private int ticket = 100; // 模拟100张票 public synchronized int getTicket() { // 读
return this.ticket;
} public synchronized void buy(int number) { // 写
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket = ticket - number;
} public static void main(String[] args) throws InterruptedException {
T4_Synchronized bus = new T4_Synchronized();
System.out.println("刚开始有票数:" + bus.getTicket());
new Thread(() -> bus.buy(1)).start();
for (int i = 1; i <= 10; i++) {
Thread.sleep(500);
int finalI = i;
new Thread(() -> System.out.println("第" + finalI + "次查询余票数:" + bus.getTicket())).start();
} }
}

运行之后,我们可以发现,数据是正确的,尽管是在查询的时候并没有睡眠0.5秒,显示数据依然是期望数据。



然而,当我们把读的锁去掉,运行代码,会发现,数据读出来了脏数据,为了更好的显示,查询票数的睡眠不要去掉。



③、synchronized的可重入性

定义一个类,类中有两个同步方法,他们锁的对象都是当前类,如果不能够重入,在one线程启动的时候就会死锁。在同步方法one中去调用同步方法two,当线程启动的时候,已经获取了对象的锁,等调用two方法的时候,同样是拿到了这个对象的锁。所以synchronized是可重入的。

public class T5_Synchronized {

    synchronized void one() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
two();
System.out.println("one - thread-" + Thread.currentThread().getName() + " end");
} synchronized void two() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("two - thread-" + Thread.currentThread().getName() + " end");
} public static void main(String[] args) {
T5_Synchronized t5 = new T5_Synchronized();
new Thread(t5::one, "one1").start();
new Thread(t5::one, "one2").start();
new Thread(t5::one, "one3").start();
}
}

实验结果



④、异常会释放锁

当线程执行过程中出现了异常,synchronized的锁会被释放,这样其他需要访问这个临界资源的线程就能进入执行。

public class T6_Synchronized {
int count = 0;
synchronized void add() {
System.out.println("线程 " + Thread.currentThread().getName() + " start");
while (true) {
count++;
System.out.println("线程 " + Thread.currentThread().getName() + " now count = " + count);
if (count == 3) {
throw new NullPointerException("人为异常");
}
if (count == 10) {
throw new NullPointerException("测试结束");
}
}
} public static void main(String[] args) throws InterruptedException {
T6_Synchronized t = new T6_Synchronized();
new Thread(t::add, "1").start();
Thread.sleep(1000);
new Thread(t::add, "2").start();
}
}

当第一次异常抛出时,线程2就立即进入执行。

synchronized锁的底层原理

synchronized实现锁的基础就是Java对象头,synchronized锁会将线程ID存入mark word(对象头由标记字)。关于mark word,先简要了解一下Java对象。

在Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。synchronized主要是跟对象头有关系,在对象头中包含了标记字(mark word)、类指针(klass word)和 数组长度(array length)。也就是通过mark word的字节位数来表示各种锁状态。



synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,这种就是偏向锁,默认是认为不会有多个线程抢着用,mark word是通过64bit来表示的,通过最低2位也就是锁标志位,偏向锁与无锁的值是01,轻量级锁用00表示,重量级锁用10表示,标记了GC的用11表示,无锁与偏向锁低2位是一致的,在倒数第3位有1位来表示偏向锁位:值为1表示偏向锁。

这里引用一张掘金博客上的图:https://juejin.cn/post/6978882583492821023

synchronized锁升级

● synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,默认也就是使用偏向锁。

● 当第二个线程来争用的时候,此时第二个线程会占用cpu,循环等待锁的释放,这时候偏向锁也就升级为自旋锁。

● 当自旋10次之后,就会升级为重量级锁,重量级锁是不占用cpu,他是使用OS的。

当线程数较少、运行时间较短的时候是比较适合使用自旋锁,反之则比较适合重量级锁。

博文推荐

对于synchronized的学习,我也查阅了许多的相关信息,确实很不容易,我将认为不错的文章都推荐出来:

【并发编程系列2】synchronized锁升级原理分析(偏向锁-轻量级锁-重量级锁)_双子孤狼的博客-CSDN博客_java锁降级和升级

深入理解Java并发之synchronized实现原理_zejian_的博客-CSDN博客_javasynchronized原理

深入理解synchronized底层原理,一篇文章就够了! - 腾讯云开发者社区-腾讯云

Java 对象、对象头mark word、锁升级、对象占内存大小 - 掘金

深入理解Java的对象头mark word_了不起的盖茨比。的博客-CSDN博客_撖寡情憭尋ark word

Java知识进阶-程序员未接触的知识点之对象模型Klass Word-知识铺_实例

创作不易,如有错误请指正,感谢观看!记得点赞哦!

【多线程与高并发】- synchronized锁的认知的更多相关文章

  1. 多线程与高并发(一)—— 自顶向下理解Synchronized实现原理

    一. 什么是锁? 在多线程中,多个线程同时对某一个资源进行访问,容易出现数据不一致问题,为保证并发安全,通常会采取线程互斥的手段对线程进行访问限制,这个互斥的手段就可以称为锁.锁的本质是状态+指针,当 ...

  2. 互联网大厂高频重点面试题 (第2季)JUC多线程及高并发

    本期内容包括 JUC多线程并发.JVM和GC等目前大厂笔试中会考.面试中会问.工作中会用的高频难点知识.斩offer.拿高薪.跳槽神器,对标阿里P6的<尚硅谷_互联网大厂高频重点面试题(第2季) ...

  3. Redis实现高并发分布式锁

    分布式锁场景在分布式环境下多个操作需要以原子的方式执行首先启一个springboot项目,再引入redis依赖包: <!-- https://mvnrepository.com/artifa . ...

  4. 【多线程与高并发原理篇:4_深入理解synchronized】

    1. 前言 越是简单的东西,在深入了解后发现越复杂.想起了曾在初中阶段,语文老师给我们解说<论语>的道理,顺便给我们提了一句,说老子的无为思想比较消极,学生时代不要太关注.现在有了一定的生 ...

  5. 多线程与高并发(二)—— Synchronized 加锁解锁流程

    前言 上篇主要对 Synchronized 的锁实现原理 Monitor 机制进行了介绍,由于 Monitor 基于操作系统调用,上下文切换导致开销大,在竞争不激烈时性能不算很好, 在 jdk6 之后 ...

  6. web开发中的两把锁之数据库锁:(高并发--乐观锁、悲观锁)

    这篇文章讲了 1.同步异步概念(消去很多疑惑),同步就是一件事一件事的做:sychronized就是保证线程一个一个的执行. 2.我们需要明白,锁机制有两个层面,一种是代码层次上的,如Java中的同步 ...

  7. 一篇博客带你轻松应对java面试中的多线程与高并发

    1. Java线程的创建方式 (1)继承thread类 thread类本质是实现了runnable接口的一个实例,代表线程的一个实例.启动线程的方式start方法.start是一个本地方法,执行后,执 ...

  8. JAVA多线程下高并发的处理经验

    java中的线程:java中,每个线程都有一个调用栈存放在线程栈之中,一个java应用总是从main()函数开始运行,被称为主线程.一旦创建一个新的线程,就会产生一个线程栈.线程总体分为:用户线程和守 ...

  9. java后端知识点梳理——多线程与高并发

    进程与线程 进程是一个"执行中的程序",是系统进行资源分配和调度的一个独立单位 线程是进程的一个实体,一个进程中一般拥有多个线程. 线程和进程的区别 进程是操作系统分配资源的最小单 ...

  10. Redis高并发分布式锁详解

    为什么需要分布式锁 1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock. 2.但是随 ...

随机推荐

  1. 搭建一个Hexo个人博客系统

    0x01 前言 虽然说前两天折腾了一下博客园(自己之前也有做过自己的博客,奈何维护费用太贵了,真的消耗不起,钱要花在刀刃上.) 网上有些教程有些参差不齐,今天给自己的真实搭建过程呈现给大家. 0x02 ...

  2. 能将三次握手讲到这个程度,不给你offer给谁!

    摘要:在后端相关岗位的入职面试中,三次握手的出场频率非常的高,甚至说它是必考题也不为过. 本文分享自华为云社区<能将三次握手理解到这个深度,面试官拍案叫绝~>,作者:龙哥手记. 在后端相关 ...

  3. Isaac Sim 机器人仿真器介绍、安装与 Docker [1]

    前言与参考 此文书写于: January 6, 2023, 更新于 January 6, 2023 :可能会随着时间的变化 此教程会有过时概念哦 Isaac Sim 相关参考链接: 官方文档地址 官方 ...

  4. elasticsearch实现简单的脚本排序(script sort)

    目录 1.背景 2.分析 3.构建数据 3.1 mapping 3.2 插入数据 4.实现 4.1 根据省升序排序 4.1.1 dsl 4.1.2 运行结果 4.2 湖北省排第一 4.2.1 dsl ...

  5. ACWJ_00扫描器

    第一部分:词法扫描介绍 ​ 我们从一个简单的词汇扫描器开始我们的编译器编写之旅.正如我在之前部分所提到的,扫描器的任务是从输入语言中(用来编译的语句)识别词法元素或者是符号. ​ 我们将定义一个只有5 ...

  6. 01-Tcl基本知识

    1 Tcl基本知识 1.1 Tcl是什么? Tcl全称是Tool Command Language,是一种基于字符串的命令语言. Tcl是一种解释性语言,类似于其他脚本语言一样,直接对每条语句顺次解释 ...

  7. Caddy-用Go写的新一代可扩展WebServer

    前几天用 Netmaker 的时候发现它用 Caddy 替换掉了 Nginx,用了后发现确实简单好用,就安利一下. Caddy 是一个强大的.可扩展的平台,用 Go 编写,可以为你的站点.服务和应用程 ...

  8. 關於ctype.h頭文件的一些函數

  9. 请务必注意精度不一样,就不相等(float 与double)

    即使两个小数显示的值是一样的,但是类型不一要样,精度不一样实际上是不一样的 就比如下面这段代码: #include <iostream> #include <cstring> ...

  10. 手把手教你使用HBuilderX调试项目-windows系统

    准备工作 一个可以运行的uniapp项目 HBuilderX工具 官方 IDE 下载地址 快速上手 step1: 安装 HBuilderX 官方 IDE 下载地址 step2: 双击运行HBuilde ...