从Java中String类分析开始,深度解析Java线程安全机制

1.String类

想必大家都知道Java中String类是一个不可变类,天然支持线程安全,但是有没有思考过为啥是不可变呢?以及为啥不可变类就是天然支持线程安全呢?下面就从这两个问题来解析String类。
(1).不可变性体现在哪里?
下面借助demo来简要阐述下:

        String str = new String();
        str = "abc";
        str = str + "def";
        System.out.println(str); //结果是abcdef

乍看一下字符串str变量不是已经发生了变化吗?怎么是不可变呢?

下面来观察下两个str前后的地址变化,对象判别方式首先是根据对象hashCode进行判断,如果hashCode在进行equals 地址判断,如果hashCode不同,则直接判断对象不同。可见,前后str hashCode的值发生了变更,也就是说这个str 对象的引用【C++语言称为地址】发生了变化,而所引用的那块地址的值仍然是不变的。
在这里插入图片描述
实际上str = str + “def”; 背后是通过 StringBuild类创建了一个新对象然后重新赋值给str对象

因此要修改string 类型的值,背后都是创建一个新对象然后重新赋值来实现的,给使用者看起来是可变的,但是内部实现并不是直接在原对象上进行修改。

天然支持线程安全,这个是为什么呢?

讲述之前先看个关键字final的用法,
final修饰类,表示该类不可被继承
final 修饰方法,方法不可被修改,相当于私有方法;
final 修饰对象,声明该对象不可被修改基本内类值不可变,对象/引用 类型构建对象的地址不可变】;

String类内部使用了大量的final 关键字在这里插入图片描述
final 关键字确保类线程安全的底层逻辑就是:多线程并发执行过程,CPU只允许同一时刻对同一块内存地址进行写操作。 也就是说,由于final关键字的作用,内存地址不可变性,同一时间只允许一个线程对某个String对象进行修改操作,执行完成后,会重新赋值暴露给其他线程,这个过程对其他线程是隔离的,也就天然保证了并发执行的线程安全。

2.探讨Java线程安全机制

1.锁类型
(1).根据线程不安全情况发生的概率的高低,大致分为悲观锁、乐观锁。
(2).根据锁能否被多线程同时占用情况,分为共享锁和独占锁。
(3).根据锁能否被多次获取的情况,分为可重入锁和不可重入锁。
(4).组合情况:
读不加锁,写时加锁,写时复制锁,典型 CopyOnWriteArrayList,思想就是写操作前,先进行复制,然后修改完后,重新赋值。
读共享锁,写独占锁,可重入读写锁,典型 ReentrantReadWriteLock 16为高位用于读锁,16为低位用于写锁。

2.线程安全实现几种方式
(1).使用不可变类诸如String、LocalDate、LocalDateTime、ThreadLocalRandom等,多线程情况下不要使用simpleDateFormat类
(2).加锁,读多写少使用乐观锁,读少写多使用悲观锁。
1) 乐观锁实现:
CAS 【AtomicInteger/AtomicLong/LongAddr等】【借助硬件实现,采用的是CPU原语操作】 / 使用版本号或者是时间戳来实现
2) 悲观锁实现:ReentrantLock/ synchronized 关键字修饰方法/同步代码块,synchronized 关键字实现的是可重入内置锁。这两个可重入锁底层都是基于AQS实现。AQS是抽象队列同步器。
(3) 自旋锁:不断循环竞争获取锁操作,线程不直接陷入阻塞,适用于线程竞争较少的情景,一旦自旋次数过多,CPU资源会大大消耗,性能下降明显。

AQS:
抽象队列同步器,整个是使用双向链表构成的队列,是CLH队列的变体,线程从自旋转为阻塞机制。 理论上没有长度限制。

在这里插入图片描述

特点:

1.共享、独占锁

Share共享:Semaphore信号量、CountDownLatch
Exclusive独占:ReentrantLock 可重入锁

2.公平锁、非公平锁

公平锁,可以按照竞争获取锁的顺序,依次唤醒阻塞线程
非公平锁,即获取锁过程中,可以跳过阻塞等待,直接参与竞争获取锁。存在后来居上的情况。

3.可重入锁

内置使用一个state变量来记录当前获取锁的次数,每获取一次进行+1。

4.包含阻塞等待队列

定义了两种队列分别是同步等待队列和条件等待队列

CAS:CompareAndSwap,从名称就大致清楚知道运行原理,则在进行对象更新之前,先比较之前存储的对象的值和目前对象最新的值是不是一致,如果是一致,
说明这段时间对象没有被其他线程进行修改操作,(当然有可能出现ABA情况,即更新了两次,从原值度过一个中间状态又回到原值,为了解决这个问题,可以引入自增版本号或者是自增唯一id来进行比较),则直接进行swap交换操作,然后将交换后的值赋值为对象新的值。

synchronized 和ReentrantLock的区别:
ReentrantLock:可使用条件队列、公平锁、锁释放开启需要手动释放。功能相对强大。
synchronized:关键字,只能实现非公平锁,功能较单一,但是性能相对而言要好点。锁释放获取无需手动释放。使用简单。

死锁问题
1.什么情况发生死锁?
1. 一种最简单的情况,当一个方法中出现获取两个不同的独占锁,另外一个方法获取这两个锁,只是获取锁顺序刚好和这个相反,此时多线程执行过程中同时调用这两个方法,就会发生死锁。
A线程 获取a锁、再获取b锁,在执行同步代码
B线程 获取b锁 再获取a锁 ,在执行同步代码
此时如果A/B两个线程同时执行,就会发生死锁现象,A线程需要等待B线程释放b锁,B线程需要等待A线程释放a锁,出现循环依赖等待对方释放锁,这就陷入死锁。
2.一个线程持有锁,如遇到网络崩溃、服务器宕机等其他不可控故障时,持有锁的线程一直阻塞不释放锁,也会陷入死锁。
如何解决死锁
1.第一种情况就是避免出现获取锁顺序不同的情况、即使出现也可以通过在外层在加一层锁或者是自增版本序列号保证获取线程锁顺序一致性。
2.第二种情况就是要让线程持有锁的时间可控,给锁加个过期时间,不能让一个线程一直持有一把锁而不释放。
锁作用域
普通锁只能在同一台机器JVM进程内生效,在不同机器上锁是无效的。为了解决分布式场景服务调用之间的线程安全,于是就有了分布式锁。
分布式锁实现的几种方式:
1.redis 自增 自减实现
2.Redisson 【给锁加了个过期时间,开启子线程动态监听过期时间,自动续费】
3.zookeeper 【构建临时有序节点 】

3.小结

本文通过分析String数据不可变性和线程安全性,然后引申简述了Java线程安全机制的几种实现方式,对多线程并发安全做了较为详细的介绍。希望对读者有点帮助,如有错误,还望大家评论区交流指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值