详解线程安全的原因及解决方案

前言

本篇博客博主将详细地介绍线程安全产生的原因及怎么解决

一.线程安全产生的原因

1.1线程安全的概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,那么我们说这个程序是安全的

1.2线程不安全的原因

1.线程调度是随机的,这是线程安全问题的罪魁祸首,随机调度使一个程序在多线程环境下,执行顺序存在很多变数;

2.修改共享数据,多个线程修改同一个变量;

3.原子性。

注:什么是原子性呢?

       我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。
       那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。有时也把这个现象叫做同步互斥,表⽰操作是互相排斥的。

注:不保证原子性会给多线程带来什么问题?

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果可能就是错误的。

1.3线程安全的实例

代码如下:

// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
 Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 count++;
 }
 });
 Thread t2 = new Thread(() -> {
 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 count++;
 }
 });
 t1.start();
 t2.start();
 // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了
 t1.join();
 t2.join();
 // 预期结果应该是 10w
 System.out.println("count: " + count);
}

结果显而易见的不是,结果会随机产生,我们接着往下看。

二.线程不安全问题的解决方案

加锁

代码如下:

// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
 Object locker = new Object();
 Thread t1 = new Thread(() -> {
 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 synchronized (locker) {
 count++;
 }
 }
 });
 Thread t2 = new Thread(() -> {
 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 synchronized (locker) {
 count++;
 }
 }
 });
 t1.start();
 t2.start();
 // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了.
 t1.join();
 t2.join();
 // 预期结果应该是 10w
 System.out.println("count: " + count);
}

2.1 synchronized关键字

synchronized关键字也被称为 监视器锁 monitor lock

2.1.1synchronized的特性

1.互斥 

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待

进入synchronized修饰的代码块,相当于加锁;

退出synchronized修饰的代码块,相当于解锁。

注:

synchronized用的锁是存在Java对象里面的;

上⼀个线程解锁之后, 下⼀个线程并不是⽴即就能获取到锁. ⽽是要靠操作系统来 "唤醒". 这也就
是操作系统线程调度的⼀部分⼯作;
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B ⽐ C 先来的, 但是 B 不⼀定就能获取到锁, ⽽是和 C 重新竞争, 并不遵守先来后到的规则。
synchronized的底层是使⽤操作系统的mutex lock实现的。
2.可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
理解“把自己锁死”
⼀个线程没有释放锁, 然后⼜尝试再次加锁.
// 第⼀次加锁, 加锁成功
lock();
// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.
lock();
按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进⾏解锁操作. 这时候就会死锁.

Java中的 synchronized是可重入锁,因此没有上面的问题

for (int i = 0; i < 50000; i++) {
 synchronized (locker) {
 synchronized (locker) {
 count++;
 }
 }
}

在可重入锁的内部,包含了“线程持有者”和“计数器”两个信息:

如果某个线程加锁的时候, 发现锁已经被⼈占⽤, 但是恰好占⽤的正是⾃⼰, 那么仍然可以继续获取
到锁, 并让计数器⾃增
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

2.1.2 synchronized使用实例

synchronized本质上要修改对象的“对象头”,从使用角度来看,synchronized也势必要搭配一个具体的对象来使用

1)修饰代码块:明确指定锁哪个对象

锁任意对象

public class SynchronizedDemo {
 private Object locker = new Object();
 
 public void method() {
 synchronized (locker) {
 
 }
 }
}

锁当前对象

public class SynchronizedDemo {
 public void method() {
 synchronized (this) {
}
}
}

2)直接修饰普通方法:锁的SynchronizedDemo对象

public class SynchronizedDemo {
 public synchronized void methond() {
 }
}

3)修饰静态方法:锁的SynchronizedDemo类的对象

public class SynchronizedDemo {
 public synchronized static void method() {
 }
}

注:我们重点理解,synchronized锁的是什么,两个线程竞争同一把锁,才会产生阻塞等待

两个线程分别尝试获取不同的锁,不会产生竞争。

尾语

这篇博客到这里就结束啦,希望可以给大家带来帮助~~

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值