线程安全问题的原因和解决方案

多线程带来的的风险

1.观察线程不安全

// 此处定义⼀个 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, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来的 count 的结果为0
        t1.join();
        t2.join();
        // 预期结果应该是 10w
        System.out.println("count: " + count);
    }

2.线程安全的概念

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:

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

3.线程不安全的原因

线程调度是随机的 这是线程安全问题的罪魁祸⾸ 随机调度使⼀个程序在多线程环境下,执行顺序存在很多的变数. 程序猿必须保证在任意执行顺序下,代码都能正常⼯作.

修改共享数据

多个线程修改同⼀个变量 上⾯的线程不安全的代码中,涉及到多个线程针对count 变量进⾏修改. 此时这个count 是⼀个多个线程都能访问到的"共享数据"

原⼦性

什么是原⼦性

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

⼀条java语句不⼀定是原⼦的,也不⼀定只是⼀条指令

 ⽐如刚才我们看到的count++,其实是由三步操作组成的: 

1. 从内存把数据读到CPU 

2. 进⾏数据更新

3. 把数据写回到CPU 不保证原⼦性会给多线程带来什么问题 如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。

可见性

 可见性指,⼀个线程对共享变量值的修改,能够及时地被其他线程看到.

synchronized关键字

1.synchronized的特性

互斥

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

 • 进⼊synchronized修饰的代码块,相当于加锁

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

synchronized⽤的锁是存在Java对象头⾥的。

可以粗略理解成,每个对象在内存中存储的时候,都存有⼀块内存表⽰当前的"锁定"状态(类似于厕所 的"有⼈/⽆⼈").

 如果当前是"⽆⼈"状态,那么就可以使⽤,使⽤时需要设为"有⼈"状态.

 如果当前是"有⼈"状态,那么其他⼈⽆法使⽤,只能排队

"阻塞等待".

针对每⼀把锁,操作系统内部都维护了⼀个等待队列.当这个锁被某个线程占有的时候,其他线程尝试 进行加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程, 再来获取到这个锁.

"把⾃⼰锁死"

 ⼀个线程没有释放锁,然后⼜尝试再次加锁.

 //第⼀次加锁,加锁成功

 lock();

 //第⼆次加锁,锁已经被占⽤,阻塞等待.

 lock();

 按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待.直到第⼀次的锁被释放,才能获取到第⼆ 个锁.但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进 ⾏解锁操作.这时候就会死锁.

2.synchronized使用示例

synchronized本质上要修改指定对象的"对象头".从使⽤⻆度来看,synchronized也势必要搭配⼀个 具体的对象来使⽤.

修饰代码块:明确指定锁哪个对象. 锁任意对象

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

锁当前对象

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

直接修饰普通⽅法:锁的SynchronizedDemo对象

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

修饰静态⽅法:锁的SynchronizedDemo类的对象

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

我们重点要理解,synchronized锁的是什么.两个线程竞争同⼀把锁,才会产⽣阻塞等待. 两个线程分别尝试获取两把不同的锁,不会产⽣竞争.

public static Object locker1 = new Object();
    public static Object locker2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("t1加锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t2加锁");
                };
            };
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                System.out.println("t2加锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1){
                    System.out.println("t1加锁");
                };
            };
        });
        t1.start();
        t2.start();

当t1进行locker1加锁的同时时,t2也进行locker2加锁操作,这时t1再对locker进行加锁时,此时锁已经被占⽤,阻塞等待.t2也同理,结果为两个线程停留在原地.次操作称为死锁.

如何修复死锁?

只需将上面左图的locker2改为locker1,locker1改为locker2,即右图.

整体代码如下:

    public static Object locker1 = new Object();
    public static Object locker2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("t1加锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t2加锁");
                }
                ;
            }
            ;
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("t1加锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t2加锁");
                }
                ;
            }
            ;
        });
        t1.start();
        t2.start();
    }

运行原理为:

t1对locker1加锁,t2等待t1对locker1解锁后进行加锁,当t1对locker2加锁后就会对locker1解锁,此时t2对locker1加锁,然后等待t1对locker2解锁后对locker2加锁,然后t2对locker2解锁,在对locker1解锁运行结束.

总结-保证线程安全的思路 

1. 使用没有共享资源的模型

2. 适用共享资源只读,不写的模型

a. 不需要写共享资源的模型

b. 使⽤不可变对象

 3. 直面线程安全(重点)

a. 保证原⼦性

b. 保证顺序性

 c. 保证可⻅性

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值