【Java多线程-6】synchronized同步锁,2024年最新京东物流面试问题及答案

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

  • 2.2 同步方法

  • 2.3 同步静态方法

  • 2.4 同步类

前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。

例如,下面模拟这样一个场景:一个售票处有3个售票员,出售20张票。

public class SellTickets {

public static void main(String[] args) {

TicketSeller seller = new TicketSeller();

Thread t1 = new Thread(seller, “窗口1”);

Thread t2 = new Thread(seller, “窗口2”);

Thread t3 = new Thread(seller, “窗口3”);

t1.start();

t2.start();

t3.start();

}

}

class TicketSeller extends Thread {

private static int tickets = 20;

@Override

public void run() {

while (true) {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

}

if (tickets > 0) {

System.out.println(Thread.currentThread().getName() + “正在出售第” + (tickets–) + “张票”);

}

}

}

}

运行后,发现会出现多个售票员出售同一张票的现象:

在这里插入图片描述

为了解决线程安全的问题,Java提供了多种同步锁。

1 synchronized 原理概述

==============================================================================

1.1 操作系统层面


synchronized的底层是使用操作系统的mutex lock实现的。下面先了解一些相关的概念。

  • 内存可见性:同步块的可见性是由以下两个规则获得的:
  1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。

  2. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)。

  • 操作原子性:持有同一个锁的两个同步块只能串行地进入

锁的内存语义:

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

锁释放和锁获取的内存语义:

  • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

  • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。

  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息

在这里插入图片描述

Mutex Lock

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

mutex的工作方式:

在这里插入图片描述

  1. 申请mutex,如果成功,则持有该mutex,如果失败,则进行spin自旋. spin的过程就是在线等待mutex, 不断发起mutex gets, 直到获得mutex或者达到spin_count限制为止

  2. 依据工作模式的不同选择yiled还是sleep

  3. 若达到sleep限制或者被主动唤醒或者完成yield, 则重复1-2步,直到获得为止

由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中。

synchronized与java.util.concurrent包中的ReentrantLock相比,由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

1.2 JVM层面


synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?

Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述。

| 长度 | 内容 | 说明 |

| — | — | — |

| 32/64bit | Mark Word | 存储对象的hashCode 或锁信息 |

| 32/64bit | Class Metadata Address | 存储对象类型数据的指针 |

| 32/64bit | Array length | 数组的长度(如果当前对象是数组) |

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):

在这里插入图片描述

Monitor

什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。

与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

其结构如下:

在这里插入图片描述

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL。

  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

  • RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。

  • Nest:用来实现重入锁的计数。HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

2 synchronized 使用

============================================================================

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

| 序号 | 类别 | 作用范围 | 作用对象 |

| — | — | — | — |

| 1 | 同步代码块 | 被synchronized修饰的代码块 | 调用这个代码块的单个对象 |

| 2 | 同步方法 | 被synchronized修饰的方法 | 调用该方法的单个对象 |

| 3 | 同步静态方法 | 被synchronized修饰的静态方法 | 静态方法所属类的所有对象 |

| 4 | 同步类 | 被synchronized修饰的代码块 | 该类的所有对象 |

2.1 同步代码块


同步代码块就是将需要的同步的代码使用同步锁包裹起来,这样能减少阻塞,提高程序效率。

同步代码块格式如下:

synchronized(对象){

同步代码;

}

同样对于文章开头卖票的例子,进行线程安全改造,代码如下:

public class SellTickets {

public static void main(String[] args) {

TicketSeller seller = new TicketSeller();

Thread t1 = new Thread(seller, “窗口1”);

Thread t2 = new Thread(seller, “窗口2”);

Thread t3 = new Thread(seller, “窗口3”);

t1.start();

t2.start();

t3.start();

}

}

class TicketSeller implements Runnable {

private static int tickets = 100;

@Override

public void run() {

while (true) {

synchronized (this) {

try {

Thread.sleep(10);

if (tickets > 0) {

System.out.println(Thread.currentThread().getName() + “正在出售第” + (tickets–) + “张票”);

}

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

同步代码块的关键在于锁对象,多个线程必须持有同一把锁,才会实现互斥性。

将上面代码中的 synchronized (this) 改为 synchronized (new Objcet()) 的话,线程安全将得不到保证,因为两个线程的持锁对象不再是同一个。

又比如下面这个例子:

public class SyncTest implements Runnable {

// 共享资源变量

int count = 0;

@Override

public void run() {

synchronized (this) {

for (int i = 0; i < 5; i++) {

System.out.println(Thread.currentThread().getName() + “:” + count++);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

public static void main(String[] args) {

// test1();

test2();

}

public static void test1() {

SyncTest syncTest1 = new SyncTest();

Thread thread1 = new Thread(syncTest1, “thread-1”);

Thread thread2 = new Thread(syncTest1, “thread-2”);

thread1.start();

thread2.start();

}

public static void test2() {

SyncTest syncTest1 = new SyncTest();

SyncTest syncTest2 = new SyncTest();

Thread thread1 = new Thread(syncTest1, “thread-1”);

Thread thread2 = new Thread(syncTest2, “thread-2”);

thread1.start();

thread2.start();

}

}

从输出结果可以看出,test2() 方法无法实现线程安全,原因在于我们指定锁为this,指的就是调用这个方法的实例对象,然而 test2() 实例化了两个不同的实例对象 syncTest1,syncTest2,所以会有两个锁,thread1与thread2分别进入自己传入的对象锁的线程执行 run() 方法,造成线程不安全。

如果要使用这个经济实惠的锁并保证线程安全,那就不能创建出多个不同实例对象。如果非要想 new 两个不同对象出来,又想保证线程同步的话,那么 synchronized 后面的括号中可以填入SyncTest.class,表示这个类对象作为锁,自然就能保证线程同步了。

synchronized(xxxx.class){

//todo

}

一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

例如下面的例子:

public class SyncTest {

public static void main(String[] args) {

Counter counter = new Counter();

Thread thread1 = new Thread(counter, “线程-1”);

Thread thread2 = new Thread(counter, “线程-2”);

thread1.start();

thread2.start();

}

}

class Counter implements Runnable {

private int count = 0;

public void countAdd() {

synchronized (this) {

for (int i = 0; i < 5; i++) {

try {

System.out.println(Thread.currentThread().getName() + " 同步计数:" + (count++));

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

public void printCount() {

for (int i = 0; i < 5; i++) {

try {

System.out.println(Thread.currentThread().getName() + " 非同步输出:" + count);

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

public void run() {

String threadName = Thread.currentThread().getName();

if (threadName.equals(“线程-1”)) {

countAdd();

} else if (threadName.equals(“线程-2”)) {

printCount();

}

}

}

我们也可以用synchronized 给对象加锁。这时,当一个线程访问该对象时,其他试图访问此对象的线程将会阻塞,直到该线程访问对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码,,例如下例:

public class SyncTest {

public static void main(String args[]) {

Account account = new Account(“zhang san”, 10000.0f);

AccountOperator accountOperator = new AccountOperator(account);

final int THREAD_NUM = 5;

Thread threads[] = new Thread[THREAD_NUM];

for (int i = 0; i < THREAD_NUM; i++) {

threads[i] = new Thread(accountOperator, “Thread-” + i);

threads[i].start();

}

}

}

class Account {

String name;

double amount;

public Account(String name, double amount) {

this.name = name;

this.amount = amount;

}

//存钱

public void deposit(double amt) {

amount += amt;

最后总结我的面试经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

BAT面试经验

实战系列:Spring全家桶+Redis等

其他相关的电子书:源码+调优

面试真题:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

[外链图片转存中…(img-FbpgxmdH-1713662007323)]

BAT面试经验

实战系列:Spring全家桶+Redis等

[外链图片转存中…(img-KGwxAaAG-1713662007324)]

其他相关的电子书:源码+调优

[外链图片转存中…(img-YwmWkz1c-1713662007324)]

面试真题:

[外链图片转存中…(img-30ko1d0D-1713662007325)]

[外链图片转存中…(img-x2RnPxzi-1713662007325)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-6SwkSIYm-1713662007326)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 8
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值