Java多线程基础知识总结,36岁老码农现身说法

简介: Java多线程基础知识总结,36岁老码农现身说法

CAS操作的流程为:

  • 读取原值。
  • 通过原子操作比较和替换。

虽然比较和替换是原子性的,但是读取原值和比较替换这两步不是原子性的,期间原值可能被其它线程修改。

ABA问题有些时候对系统不会产生问题,但是有些时候却也是致命的。

ABA问题的解决方法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。

(2)自旋次数过多

CAS操作在不成功时会重新读取内存值并自旋尝试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。

(3)只能保证一个变量的原子性

当对一个变量操作时,CAS可以保证原子性,但同时操作多个变量时CAS就无能为力了。

可以封装成对象,再对对象进行CAS操作,或者直接加锁。

四、多线程锁的升级原理是什么?


锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。

1、无锁

没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其它修改失败的线程会不断重试直到修改成功。

无锁总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,CAS是无锁技术的关键。

2、偏向锁

对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁。

如果线程处于活动状态,升级为轻量级锁的状态

3、轻量级锁

轻量级锁是指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从er提升性能。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定次数时,轻量级锁边会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访时,轻量级锁也会升级为重量级锁。

注:自旋是什么?

自旋(spinlock)是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

4、重量级锁

指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

5、锁状态对比

  偏向锁 轻量级锁 重量级锁
使用场景 只有一个线程进入同步块 虽然很多线程,但没有冲突,线程进入时间错开因而并未争抢锁 发生了锁争抢的情况,多条线程进入同步块争用锁
本质 取消同步操作 CAS操作代替互斥同步 互斥同步
优点 不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId) 不会阻塞 不会空耗CPU
缺点

适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗

长时间获取不到锁空耗CPU 阻塞,上下文切换,重量级操作,消耗操作系统资源

6、锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,别切不会被其它线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

五、Synchronized的特性


1、可重入性

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;

可重入的好处:

(1)可以避免死锁;

(2)可以让我们更好的封装代码;

synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

2、不可中断性

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;

synchronized 属于不可被中断;

Lock lock方法是不可中断的;

Lock tryLock方法是可中断的;

六、Synchronized保证了原子性、可见性、有序性


1、Synchronized保证原子性

public class Test {
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0;i<1000;i++){
number++;
}
};
List list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("number = " + number);
}
}

Synchronized保证原子性
Runnable increment = () -> {
for (int i = 0;i<1000;i++){
synchronized (obj){
number++;
}
}
};

2、Synchronized保证可见性

public class Test1 {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag = false;
System.out.println(“线程修改了变量的值为false”);
}).start();
}
}

volatile即可解决这个问题!

public static volatile boolean flag = true;

Synchronized保证可见性

Synchronized保证可见性的原理,执行Synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。

3、Synchronized保证有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

七、synchronized的三种应用方式


1、修饰普通方法

作用于当前方法加锁,进入同步代码前要获得当前实例的锁。

(1)synchronized保证线程安全

(2)synchronized什么情况下无法保证线程安全

(3)这时synchronized修饰在静态方法上,就可以解决这个问题了。

start()、run()、join()的区别:

start():线程不会立即启动。相当于是在就绪队列里面;

run():启动线程;

join():主要作用是同步,它可以使得线程之间的并行执行变为串行执行。

join方法的作用:

在A线程中调用了B线程的join方法,表示只有当B线程执行完毕后,A线程才能继续执行。注意调用的join方法是没有传参的,join方法其实可以传递一个参数给它,如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。

2、修饰静态方法

作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。

但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

3、修饰同步代码块

指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁。

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

从代码可以看出,将synchronized作用域一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其它线程正持有该对象锁,那么新到的线程必须等待,这样也就保证了每次只有一个线程执行i++操作。当然除了使用instance作为对象外,还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下:

//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}

八、理解Java对象头与Monitor


在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

内存填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

Java头对象是是实现synchronized锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字节来存储对象头(数组是三个字节),其主要结构是由Mark Word和Class Metadata Address组成,其结构说明如下表:

| 头对象结构 | 说明 |

| — | — |

| Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |

| Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |

| 锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit 锁标志位 |

| — | — | — | — | — |

| 无锁状态 | 对象HashCode | 对象分代年龄 | 0 | 01 |

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:


目录
打赏
0
0
0
0
80
分享
相关文章
|
2月前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
212 60
【Java并发】【线程池】带你从0-1入门线程池
|
19天前
|
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
56 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
99 23
|
1月前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
158 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
189 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
80 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
3月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
165 17
|
4月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
116 1
Java—多线程实现生产消费者