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

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

1.示例代码

class Counter {
    public int count = 0;
    public void add(){
            count++;
    }
}

public class Demo {

    public static void main(String[] args) {
        Counter counter = new Counter();
        //两个线程,两个线程分别调用counter来进行5W次的add方法
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

代码的原理是两个线程分别调用counter来进行5W次的add方法,按照理想情况来说,最后输出的count值应该为10W,但是实际上运行上面的代码,会发现每次输出count的值可能都不一样,而这就是典型的线程安全问题。

2. 原因

1.根本原因: 是抢占式执行,随机调度

2.代码结构: 多个线程同时修改同一个变量

解决方法: 可以通过调整代码结构来规避这个问题,但是这种调整,并不一定都可以使用,值得注意的是 String是不可变对象,不可变对象,是天然是线程安全的

3.原子性

如果修改操作是非原子的,就可能出现线程安全问题,比如上面的代码中count的值不一样,其原因是修改操作是非原子性的,因为count++的++操作本质上分成三步:
1.先把内存中的值,读取到CPU的寄存器中 load
2.把CPU寄存器里的数值进行+1运算 add
3.把得到的结果写在内存中 save

如果两个线程并发执行count++,此时相当于两组load add save进行执行
那么线程调度顺序的不一致,就可能产生结果上的差异。

解决方法: 可以通过把这个非原子的操作变成原子的操作

4.内存可见性问题
是指一个线程读变量,一个线程改变量,从而产生线程安全问题

5.指令重排序问题
是指编译器对所写的代码进行优化,从而产生线程安全问题

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

从原子性入手,利用加锁,把非原子的改成原子的,即利用synchronized进行加锁

4.synchronized的介绍

1.synchronized使用方法

1.修饰方法

1)修饰普通方法

//修饰普通方法
    synchronized public void print() {
        System.out.println("A");
    }

2)修饰静态方法

// 修饰静态方法
    synchronized public static void print(){
        System.out.println("A");
    }

2.修饰代码块

//修饰代码块
    public static void print(){
        synchronized (this){
            System.out.println("A");
        }
    }

2.synchronized的特性

1) 互斥

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待
1.进入synchronized修饰的代码块,就相当于加锁
2.退出synchronized修饰的代码块,就相当于解锁
在这里插入图片描述

synchronized用的锁是存在Java对象里头的

2)可重入

synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题

理解"把自己锁死":即一个线程没有释放锁,然后又开始尝试再加锁

//第一次加锁,加锁成功
lock();
//第二次加锁,锁已经被占用,阻塞等待
lock();

按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才可以获得第二个锁,但是释放第一个锁也是由该线程来完成的,所以就无法进行解锁操作,从而产生死锁,这种锁就叫做不可重入锁

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值