线程的深入理解(二):死锁和更多的并发安全(1)

这篇具有很好参考价值的文章主要介绍了线程的深入理解(二):死锁和更多的并发安全(1)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

// Bug.addStatic();//静态方法同步

}

}

测试代码

public static void main(String[] args) {

BugRunnable bugRunnable = new BugRunnable();

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

new Thread(bugRunnable).start();

}

}

同步代码块

//同步代码块

public synchronized void addBlock() {

synchronized (bugNumber) {

this.bugNumber = ++bugNumber;

System.out.println(“blockSynchronized—>” + getBugNumber());

}

}

测试结果

blockSynchronized—>1

blockSynchronized—>2

blockSynchronized—>3

blockSynchronized—>4

blockSynchronized—>5

blockSynchronized—>6

普通方法同步

//普通同步方法

public synchronized void addNormal() {

bugNumber++;

System.out.println(“normalSynchronized—>” + getBugNumber());

}

测试结果

normalSynchronized—>1

normalSynchronized—>2

normalSynchronized—>3

normalSynchronized—>4

normalSynchronized—>5

normalSynchronized—>6

静态方法同步

//静态同步方法

public static synchronized void addStatic() {

bugNumber++;

System.out.println(“staticSynchronized—>” + getBugNumber());

}

测试结果

staticSynchronized—>1

staticSynchronized—>2

staticSynchronized—>3

staticSynchronized—>4

staticSynchronized—>5

staticSynchronized—>6

对比分析
  • 类的每个实例都有自己的对象锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问同一个实例(因为对象可以有多个实例)同步代码块或同步方法,必须等待当前线程释放掉对象锁才可以,如果是访问类的另外一个实例,则不需要。

  • 如果一个对象有多个同步方法或者代码块,没有获取到对象锁的线程将会被阻塞在所有同步方法之外,但是可以访问非同步方法

  • 对于静态方法,实际上可以把它转化成同步代码块,就拿上面的静态方法,实际上相当于:

//静态同步方法

public static synchronized void addStatic() {

bugNumber++;

System.out.println(“staticSynchronized—>” + getBugNumber());

}

//用同步代码块

public static void changeStatic() {

synchronized (Bug.class) {

++bugNumber;

System.out.println(“blockSynchronized—>” + getBugNumber());

}

}

下面具体来总结一下三者的区别

  • 同步代码块:同步代码块的范围较小,只是锁定了某个对象,所以性能较高

  • 普通同步方法:给整个方法上锁,性能较低

  • 静态同步方法:相当于整个类的同步代码块,性能较低

ReentrantLock

除了synchronized这个关键字外,我们还能通过concurrent包下的Lock接口来实现这种效果,ReentrantLock是lock的一个实现类,可以在任何你想要的地方进行加锁,比synchronized关键字更加灵活,下面看一下使用方式

使用方式

//ReentrantLock同步

public void addReentrantLock() {

mReentrantLock.lock();//上锁

bugNumber++;

System.out.println(“normalSynchronized—>” + getBugNumber());

mReentrantLock.unlock();//解锁

}

运行测试

ReentrantLock—>1

ReentrantLock—>2

ReentrantLock—>3

ReentrantLock—>4

ReentrantLock—>5

ReentrantLock—>6

我们发现也是可以达到同步的目的,看一下ReentrantLock的继承关系

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

ReentrantLock实现了lock接口,而lock接口只是定义了一些方法,所以相当于说ReentrantLock自己实现了一套加锁机制,下面简单分析一下ReentrantLock的同步机制,在分析前,需要知道几个概念:

  • CLH:AbstractQueuedSynchronizer中“等待锁”的线程队列。在线程并发的过程中,没有获得锁的线程都会进入一个队列,CLH就是管理这些等待锁的队列。

  • CAS:比较并交换函数,它是原子操作函数,也就是说所有通过CAS操作的数据都是以原子方式进行的。

成员变量

private static final long serialVersionUID = 7373984872572414699L;

/** Synchronizer providing all implementation mechanics */

private final Sync sync;//同步器

成员变量除了序列化ID之外,只有一个Sync,那就看一看具体是什么

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

Sync有两个实现类,一个是FairSync,一个是NonfairSync,从名字可以大致推断出一个是公平锁,一个是非公平锁,

FairSync(公平锁)

lock方法:

final void lock() {

acquire(1);

}

ReentrantLock是独占锁,1表示的是锁的状态state。对于独占锁而言,如果所处于可获取状态,其状态为0,当锁初次被线程获取时状态变成1,acquire最终调用的是tryAcquire方法

protected final boolean tryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

if (c == 0) {

// 当c==0表示锁没有被任何线程占用

(hasQueuedPredecessors),

if (!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

else if (current == getExclusiveOwnerThread()) {

//锁已经被线程占用

int nextc = c + acquires;

if (nextc < 0)

throw new Error(“Maximum lock count exceeded”);

setState(nextc);

return true;

}

return false;

}

tryAcquire主要是去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false

NonfairSync(非公平锁)

非公平锁NonfairSync的lock()与公平锁的lock()在获取锁的流程上是一直的,但是由于它是非公平的,所以获取锁机制还是有点不同。通过前面我们了解到公平锁在获取锁时采用的是公平策略(CLH队列),而非公平锁则采用非公平策略它无视等待队列,直接尝试获取。

final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

lock()通过compareAndSetState尝试设置锁的状态,若成功直接将锁的拥有者设置为当前线程(简单粗暴),否则调用acquire()尝试获取锁,对比一下,公平锁跟非公平锁的区别在于tryAcquire中

//NonfairSync

if (c == 0) {

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

//FairSync

if (c == 0) {

if (!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

公平锁中要通过hasQueuedPredecessors()来判断该线程是否位于CLH队列头部,是则获取锁;而非公平锁则不管你在哪个位置都直接获取锁。

unlock

public void unlock() {

sync.release(1);//释放锁

}

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

对比分析
等待可中断
  • synchronized:线程A跟线程B同时竞争同一把锁,如果线程A获得锁之后不释放,那么线程B会一直等待下去,并不会释放。

  • ReentrantLock:可以在线程等待了很长时间之后进行中断,不需要一直等待。

锁的公平性

公平锁:是指多个线程在等待同一个锁时,必须按照申请的时间顺序来依次获得锁;非公平锁:在锁被释放时,任何一个等待锁的线程都有机会获得锁;

  • synchronized:是非公平锁

  • ReentrantLock:可以是非公平锁也可以是公平锁

绑定条件
  • synchronized中默认隐含条件。

  • ReentrantLock可以绑定多个条件

可见性

volatile
内存语义

由于多个线程方法同一个变量,导致了线程安全问题,主要原因是因为线程的工作副本的变量跟主内存的不一致,如果能够解决这个问题就可以保证线程同步,而Java提供了volatile关键字,可以帮助我们保证内存可见性,当我们声明了一个volatile关键字,实际上有两层含义;

  • 禁止进行指令重排序。

  • 一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。

原理

在使用volatile关键字的时候,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

使用场景

这里需要强调一点,volatile关键字并不一定能保证线程同步,如果非要采用volatile关键字来保证线程同步,则需要满足以下条件:

  • 对变量的写操作不依赖于当前值

  • 该变量没有包含在具有其他变量的不变式中

其实看了一些书跟博客,都是这么写的,按照我的理解实际上就是只有当volatile修饰的对象是原子性操作,才能够保证线程同步,为什么呢。

测试代码:

class Volatile {

volatile static int count = 0;

public static void main(String[] args) {

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

new Thread(new Runnable() {

@Override

public void run() {

Volatile.add();

}

}).start();

}

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“count—>” + ++count);

}

private static void add() {

count++;

}

}

运行结果

count—>1001

理论上是1000才对,但是输出的值是1001,为什么呢,这个其实在之前的JMM中已经分析过了,下面再贴一张图

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

跟之前一样,我们每次从主内存中获取到的count确实是最新的,但是由于对count的操作不是原子性操作,假如现在有两个线程,线程1跟线程2,如果线程1读取到了count值是5,然后read—>load进内存了,然后现在被线程2抢占了CPU,那么线程2就开始read—>load,并且完成了工作副本的赋值操作,并且将count 的值回写到主内存中,由于线程1已经进行了load操作,所以不会再去主内存中读取,会接着进行自己的操作,这样的话就出现了线程不安全,所以volatile必须是原子性操作才能保证线程安全。

基于以上考虑,volatile主要用来做一些标记位的处理:

volatile boolean flag = false;

//线程1

while(!flag){

doSomething();

}

//线程2

public void setFlag() {

flag = true;

}

当有多个线程进行访问的时候,只要有一个线程改变了flag的状态,那么这个状态会被刷新到主内存,就会对所有线程可见,那么就可以保证线程安全。

automatic

automatic是JDK1.5之后Java新增的concurrent包中的一个类,虽然volatile可以保证内存可见性,大部分操作都不是原子性操作,那么volatile的使用场景就比较单一,然后Java提供了automatic这个包,可以帮助我们来保证一些操作是原子性的。

使用方式

替换之前的volatile代码

public static AtomicInteger atomicInteger = new AtomicInteger(0);

private static void add() {

atomicInteger.getAndIncrement();

}

测试一下:

AtomicInteger: 1000

原理解析

AtomicInteger既保证了volatile保证不了的原子性,同时也实现了可见性,那么它是如何做到的呢?

成员变量

private static final long serialVersionUID = 6214790243416807050L;

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

private volatile int value;

运算方式

public final int getAndIncrement() {

return unsafe.getAndAddInt(this, valueOffset, 1);

}

public final int getAndAddInt(Object var1, long var2, int var4) {

int var5;

do {

var5 = this.getIntVolatile(var1, var2);

} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

}

int compare_and_swap(int reg, int oldval, int newval) {

ATOMIC();

int old_reg_val = reg;

if (old_reg_val == oldval)

reg = newval;

END_ATOMIC();

return old_reg_val;

}

分析之前需要知道两个概念:

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

compare_and_swap这个才是核心方法,也就是上面提到的CAS,因为CAS是基于乐观锁的,也就是说当写入的时候,如果寄存器旧值已经不等于现值,说明有其他CPU在修改,那就继续尝试。所以这就保证了操作的原子性。

变量私有化

这种方式实际上指的就是ThreadLocal,翻译过来是线程本地变量,ThreadLocal会为每个使用该变量的线程提供独立的变量副本,但是这个副本并不是从主内存中进行读取的,而是自己创建的,每个副本相互之间独立,互不影响。相对于syncronized的以时间换空间,ThreadLocal刚好相反,可以减少线程并发的复杂度。

简单使用

class ThreadLocalDemo {

public static ThreadLocal local = new ThreadLocal<>();//声明静态的threadlocal变量

public static void main(String[] args) {

local.set(“Android”);

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

SetThread localThread = new SetThread();//创建5个线程

new Thread(localThread).start();

}

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(local.get());

}

static class SetThread implements Runnable {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!文章来源地址https://www.toymoban.com/news/detail-847811.html

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

2024392853)]

[外链图片转存中…(img-JyPVZCdX-1712024392853)]

[外链图片转存中…(img-u3icbk1D-1712024392853)]

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

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

线程的深入理解(二):死锁和更多的并发安全(1),程序员,安全,java,jvm

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-car4Vw4I-1712024392853)]

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

到了这里,关于线程的深入理解(二):死锁和更多的并发安全(1)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 华为云出品《深入理解高并发编程:Java线程池核心技术》电子书发布

    系统拆解线程池核心源码的开源小册 透过源码看清线程池背后的设计和思路 详细解析AQS并发工具类 点击下方链接进入官网,右上角搜索框搜索“《深入理解高并发编程:Java线程池核心技术》” 即可获取下载。 https://auth.huaweicloud.com/authui/login.html?locale=zh-cnservice=https%3A%2F%2F

    2024年02月16日
    浏览(38)
  • 【并发专题】单例模式的线程安全(进阶理解篇)

    最近学习了JVM之后,总感觉知识掌握不够深,所以想通过分析经典的【懒汉式单例】来加深一下理解。(主要是【静态内部类】实现单例的方式)。 如果小白想理解单例的话,也能看我这篇文章。我也通过了【前置知识】跟【普通懒汉式】、【双检锁懒汉】、【静态内部类】

    2024年02月14日
    浏览(40)
  • 深入理解 Java 多线程、Lambda 表达式及线程安全最佳实践

    线程使程序能够通过同时执行多个任务而更有效地运行。 线程可用于在不中断主程序的情况下在后台执行复杂的任务。 创建线程 有两种创建线程的方式。 扩展Thread类 可以通过扩展Thread类并覆盖其run()方法来创建线程: 实现Runnable接口 另一种创建线程的方式是实现Runnable接口

    2024年03月15日
    浏览(54)
  • ChatGPT在连续追问下对多线程和双重检查锁模式的理解--已经超越中级程序员

    一、问: 二、ChatGPT答:  在多线程情况下,可能会出现GZHttpClientResultModel model为null的情况,因为CACHE_RESULT_MODEL是一个ConcurrentHashMap对象,虽然它本身是线程安全的,但是它内部的操作不是完全线程安全的。 比如在cacheResultMode方法中,如果两个线程同时接近CACHE_RESULT_MODEL.con

    2023年04月27日
    浏览(39)
  • 内网安全信息收集与并发编程挑战:深入理解PowerShell脚本执行与域环境控制

    本文详细介绍了如何收集内网安全信息,以及如何面对并发编程的挑战。内容涵盖了判断域环境、定位域IP和管理员账户、理解域内权限、控制主机判断、域渗透思路、本地主机与域成员主机的区别,以及如何使用PowerShell脚本执行策略进行操作。

    2024年04月11日
    浏览(40)
  • Golang Channel详解:安全并发通信与避免死锁方法

    深入了解Golang中的Channel,探讨其线程安全性、类型特性以及避免死锁的方法。学习如何正确初始化、存取数据,关闭Channel以及处理只读只写情况。

    2024年02月10日
    浏览(47)
  • 【Linux】线程互斥 -- 互斥锁 | 死锁 | 线程安全

    我们写一个多线程同时访问一个全局变量的情况(抢票系统),看看会出什么bug: 假如创建4个线程同时抢票,总票数有10000张,每个线程抢到票以后减一,按照正常情况我们应该是抢票到0截至。 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换 线程一般

    2024年02月14日
    浏览(42)
  • 【Linux】线程安全-死锁

    死锁的两种场景: 线程加锁之后一直没有将锁释放,在上一篇文中,我们模拟过这种场景,某个线程拿到锁,进行加锁,线程退出之前没有释放锁,导致后面的线程不能拿到锁,一直在等待加锁,导致程序一直不退出,这也是为什么条件变量等待函数中第二个参数是锁的原因

    2024年02月10日
    浏览(39)
  • 【线程安全】死锁问题及解决方案

    比如上一次讲到 synchronized 的时候,一个线程,对同一个对象连续加锁两次,如果出现阻塞等待,代表这个锁是不可重入锁,这样的线程,也就称为死锁! 一旦程序进入死锁了就会导致线程僵住了,无法继续执行后续的工作了,程序也就出现了严重的 BUG! 而死锁这样的情况

    2024年02月06日
    浏览(45)
  • 【JavaEE】多线程之线程安全(synchronized篇),死锁问题

    线程安全问题 观察线程不安全 线程安全问题的原因  从原子性入手解决线程安全问题 ——synchronized synchronized的使用方法  synchronized的互斥性和可重入性 死锁 死锁的三个典型情况  死锁的四个必要条件  破除死锁 在前面的章节中,我们也了解到多线程为我们的程序带来了

    2024年02月01日
    浏览(55)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包