多线程带来的风险以及解决办法

多线程虽然能大大提高CPU执行效率,但是也并不是百利无一害,也有线程不安全的情况存在,这也是多线程并发中涉及到的最重要也是最复杂的问题。

那么什么是线程不安全呢?

概括来说,线程不安全的原因在于多并发执行某行代码的时候,产生了逻辑上的错误,这就叫做线程不安全。那么什么是逻辑上的错误呢?我们以具体代码来解释

class counter {//计数器
    public int count = 0 ;
    public void incr() {
        count++;
    }
}
public class ThreadDemo12 {//验证线程不安全
    public static void main(String[] args) throws InterruptedException {
        counter co  = new counter();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i<5000;i++){
                    co.incr();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i<5000;i++){
                    co.incr();
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((co.count));
    }
    //t1这个线程会去修改计数器的count,t2这个线程也会去修改。并且它们是并行执行的,这样的结果会导致最终count
    //并没有达到t1的5000和t2的5000.这就叫做线程不安全

通过上面代码我们会发现,当多个线程同时去修改或者访问一个变量的时候,会出现错误的结果。原因主要有以下几点

  1. 多线程并发执行的时候是抢占式调度的,也就意味着我们用户层无法控制也无法知道线程的执行顺序。完全交由调度器来决定。有可能某个线程正在CPU执行着就被拉下来了。
  2. 自增的操作不是原子性的,也就是说自增的时候并不是一次就执行完,或者根本不执行。而是分为三个步骤去执行,第一步首先要把需要计算的数据从内存中读取到CPU中,然后再由CPU进行计算,最后再把结果返回到内存中去。
    以上两个原因一结合,就造成了这个错误的结果。假设此时有一个CPU,t1线程和t2线程同时启动,当CPU上执行完t1中的读取操作之后,切换到了t2线程,又读取到CPU。然后又切换到t1继续执行。这样一来t1和t2都各自得到了一次+1的结果。最终返回的结果就是1而不是2了。当然也有可能会出现t1刚好执行完返回操作,t2刚好执行读取操作。这样一串联起来结果就是刚好的。

通过上面的例子我们会发现,线程不安全的原因主要有三个

  1. 线程的调度是抢占式的,这是线程不安全的主要原因。
  2. 有的操作不是原子性的,也就是说不能一次性在CPU上执行完,或者直接先暂时不去CPU上执行
  3. 多个线程尝试去修改同一个变量,但是如果线程和变量之间是一对一修改,多对一读取,一对一读取,多对多修改这些都是安全的。

除此之外,线程不安全的原因还有两个

  1. 内存可见性导致的
  2. 指令重排序:Java的编译器在编译代码时会对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,提高程序的运行效率。

为了解决线程不安全问题,引入了获取锁释放锁这样的办法,首先我们来认识以下什么是锁

锁的特点:互斥的,同时时刻只有一个线程能获取到同一对象的锁。其他的线程如果想尝试获取,就会发生阻塞等待,一直等到刚才的线程释放锁,才会重新去参与竞争。

锁的基本操作:

  1. 加锁
  2. 解锁
    在Java中锁的使用需要借助关键字synchronized
class counter {//计数器
    public int count = 0 ;
    synchronized  public void incr() {
        count++;
    }
}
public class ThreadDemo12 {//验证线程不安全
    public static void main(String[] args) throws InterruptedException {
        counter co  = new counter();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i<5000;i++){
                    co.incr();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i<5000;i++){
                    co.incr();
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((co.count));
    }

我们知道,一个对象被new出来之后,会在堆上分配内存,保存这个对象的一些信息,其中也包括一些隐藏信息,其中就有加锁状态,我们可以想象成这个状态是boolean类型。当线程1和线程2抢占式执行的时候,谁先被调度谁就先调用被synchronized修饰的方法或者类,这样一来这个线程就把这个对象的加锁状态置为了true,此时其他线程要想来尝试修改,就会进入阻塞状态,必须等线程1中调用的incr方法执行完才会重新出来参与竞争。可以理解为把不是原子性的操作变为了是原子性的操作。

需要注意的是。如果线程1获取到锁之后,出现了一些意外导致长时间不能解锁,就会导致参与竞争锁的线程都不会继续执行,只能继续阻塞。极端情况下会出现死锁的情况。程序就凉凉了。并且一旦使用了锁,这个程序基本就和高性能无缘了,因为等待解锁的过程很消耗时间。

synchronized的几种常见用法

  1. 加在一个方法前面,代表给这个方法所在类加锁,也就是this
  2. 加在静态方法前,表示锁这个类的类对象。
  3. 加到某个代码块之前,显示指定给某个对象加锁。需要注意的是线程之间竞争的锁是同一把锁才会出现互斥性,也就是同一个对象或者类对象的加锁状态。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值