Java || 线程 || 大一新生 || 一篇synchronized锁的知识点总结

简介: Java || 线程 || 大一新生 || 一篇synchronized锁的知识点总结

(一)概述

在多线程的程序执行中,有可能会出现多个线程会同时访问一个共享并且可变资源的情况,这种时候由于线程的执行是不可控的,所以必须采用一些方式来控制该资源的访问,这种方式就是“加锁”。

我们把那些可能会被多个线程同时操作的资源称为临界资源,加锁的目的就是让这些临界资源在同一时刻只能有一个线程可以访问。

(二)CAS的介绍

CAS:compare and swap,比较且交换。使用CAS操作可以在没有锁的情况下完成多线程对一个值的更新。CAS的具体操作如下:

当要更新一个值时,先获取当前值E,计算更新后的结果值V(先不更新),当要去更新这个值时,比较此时这个值是否还是等于E,如果相等,则将E更新为V,如果不相等,则重新进行上面的操作。

以i++操作为例,在没有锁的情况下,这个操作是线程不安全的,假设i的初始值为0,CAS操作先获取原值E=0,计算更新后的值V=1,要更新之前先比较这个值是否还是等于0,如果等于0则将E更新为1,如果不等于0则说明有线程已经更新了,重新获取E值=1,继续执行。

ABA问题

CAS操作可能会出现ABA问题,ABA问题即我们要去比较的这个值E,经过多个线程的操作后从0变成1又变成了0。此时虽然E值和更新前相等,但是还是已经被更新了。

ABA问题的解决办法

对E值增加一个版本号,每次要获取数据时将版本号也获取,每次更新完数据之后将版本号递增,这样就算值相等通过版本号也能知道是否经过修改。

java在很多地方都用到了CAS操作,比如Atomic的一些类:

AtomicInteger i=new AtomicInteger();

进入AtomicInteger方法中,可以看到有个叫Unsafe的类,进入这个类中,可以看到CAS的几个操作方法

02364c7ced4d05a32eee5ebdb0d7f48.png

(三)对象在内存中的存储布局

要想学会synchronized,首先要理解Java对象的内存布局,或者称为内存结构。

e4a029c8b6c7a5e57261fd8fb5070de.png

一个对象分为对象头、实例数据和对其填充。

其中对象头Header占12个字节:Mark Word占8个字节,类型指针class pointer占4个字节(默认经过了压缩,如果不开启压缩占8个字节)

实例对象按实际存储有不同大小,对象为空时等于0。

Padding表示对齐,当此时内存所占字节不能被8整除时补上相应字节数。

以Object o=new Object()为例,我们先导入一个jol依赖,通过jol可以看到具体的内存布局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

运行以下代码:

public static void main(String[] args) {
    Object o=new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

观察结果,OFFSET表示偏移量的起始点,SIZE表示所占字节,前两行是Mark Word一共占8个字节,第三行是class pointer占4个字节,此时对象为空,实例对象等于0,最后padding补齐,一共16个字节。

6ce347d84aee95ce6d8e8c74312a5d1.png

(四)synchronized

synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,synchronized把锁信息存放在对象头的MarkWord中。

synchronized作用在非静态方法上是对方法的加锁,synchronized作用在静态方法上是对当前的类加锁。

在早期的jdk版本中,synchronized是一个重量级锁,保证线程的安全但是效率很低。后来对synchronized进行了优化,有了一个锁升级的过程:

无锁态(new)–>偏向锁–>轻量级锁(自旋锁)–>重量级锁

通过MarkWord中的8个字节也就是64位来记录锁信息。也有人将自旋锁称为无锁,因为自选操作并没有给一个对象上锁,这里只要理解意思即可。

b57472fea8666d3a03e575080648544.png

4.1 锁升级过程详解:

当给一个对象增加synchronized锁之后,相当于上了一个偏向锁。

当有一个线程去请求时,就把这个对象MarkWord的ID改为当前线程指针ID(JavaThread),只允许这一个线程去请求对象。

当有其他线程也去请求时,就把锁升级为轻量级锁。每个线程在自己的线程栈中生成LockRecord,用CAS自旋操作将请求对象MarkWordID改为自己的LockRecord,成功的线程请求到了该对象,未成功的对象继续自旋。

如果竞争加剧,当有线程自旋超过一定次数时(在JDK1.6之后,这个自旋次数由JVM自己控制),就将轻量级锁升级为重量级锁,线程挂起,进入等待队列,等待操作系统的调度。

4.2 加锁的字节码实现

synchronized关键字被编译成字节码之后会被翻译成monitorenter和monitorexit两条指令,进入同步代码块时执行monitorenter,同步代码块执行完毕后执行monitorexit

(五)锁消除

在某些情况下,如果JVM认为不需要锁,会自动消除锁,比如下面这段代码:

public void add(String a,String b){
    StringBuffer sb=new StringBuffer();
    sb.append(a).append(b);
}

StringBuffer是线程安全的,但是在这个add方法中stringbuffer是不能共享的资源,因此加锁只会徒增性能消耗,JVM就会消除StringBuffer内部的锁。

(六)锁粗化

在某些情况下,JVM检测到一连串的操作都在对同一个对象不断加锁,就会将这个锁加到这一连串操作的外部,比如:

StringBuffer sb=new StringBuffer();
while(i<100){
    sb.append(str);
    i++;
}

上述操作StringBuffer每次添加数据都要加锁和解锁,连续100次,这时候JVM就会将锁加到更外层(while)部分。

(七)逃逸分析

首先问一个经常基础的虚拟机问题,实例对象存放在虚拟机的哪个位置?按以前的回答,示例对象放在堆上,引用放在栈上,示例的元数据等存放在方法区或者元空间。

但这是有前提的,前提是示例对象没有线程逃逸行为。

JDK1.7开始默认开启了逃逸分析,所谓逃逸分析,就是指如果一个对象被编译器发现只能被一个线程访问,那么这个对象就不需要考虑同步。JVM就对这种对象进行优化,将堆分配转化为栈分配,归根结底就是虚拟机在编译过程中对程序的一种优化行为。

开启逃逸分析:­ XX:+DoEscapeAnalysis
关闭逃逸分析: ­XX:­-DoEscapeAnalysis
目录
相关文章
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
6天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
18 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
43 1
C++ 多线程之初识多线程
|
23天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
16 3
|
23天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
23天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
23天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1