5分钟搞懂多线程安全问题

前言

线程相关的面试题经久不衰,尤其是在JAVA领域

曾经某架构师对我说,不懂线程的是初级,懂线程的是高级,半懂不懂的是中级。

可见线程在面试中是一个怎样的角色了。但是,面试造火箭,入职拧螺丝是常态。我们真的有必要学线程吗?

有,很有必要!

就算是拧螺丝,你拿着比别人多的薪水拧,难道不舒服吗?

线程

什么是线程?

你就是一个线程

或者说,你拧螺丝的时候真像一个线程.

线程在计算机中是CPU的最小调度单位,啥意思呢?

程序要运行就会在内存中占用内存,那么计算机要怎么给程序分配内存呢,这个就由操作系统来管理分配,操作系统要怎么统一分配呢?

这时就产生了进程的概念。

操作系统会为每一个程序分配一块地盘,并加上标识,记录这是哪个程序的地盘。

地盘分完了得干活呀。谁来干呢?线程就出马了,每个进程都会有一个线程在工作。

而且一个进程可以有N个线程存在.

但同一时间,只能执行一个线程的工作(单核CPU中)。

这个就是所谓的最小调度单位,也可以说是最小工作单位。

所以说,你在拧螺丝的时候就是个线程。而在公司里有千千万万个这样的线程。

线程安全问题

假设,你接到一个需求,要拧200个螺丝,你一看文档现在还有200个整没拧。 这时候你的同事也接到这个需求,一看文档剩200个没拧。 这时候你们都去拧了一个,各自记录-1,还剩199个没拧。 但其实已经拧了2个了,这就有问题了。

用代码来演示一下,300个人拧200个螺丝会出现什么情况。


package Thread;

public class MyRun implements Runnable {

    public static int luosi = 200;
    @Override
    public  void run() {
        for (int i = 1;i<=100;i++) {   // 100人去拧这200个螺丝
            if (luosi > 0) {
                try {
                    Thread.sleep(50);   // 假设每个人耗费50毫秒去拧
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                luosi-= 1;  // 拧成功了,总数减去1
                System.out.println(Thread.currentThread()+"拧了螺丝,还剩:"+luosi+"个没拧");
            }
        }
    }
}

再来个入口函数去执行:


package Thread;

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        MyRun mr = new MyRun();

        Thread mh = new Thread(mr);   // 第一个地方 100个人拧
        Thread mh2 = new Thread(mr);  // 第二个地方
        Thread mh3 = new Thread(mr);  // 第三个地方
        mh.start();
        mh2.start();
        mh3.start();
    }

}

结果:

Thread[Thread-1,5,main]拧了螺丝,还剩:2个没拧
Thread[Thread-0,5,main]拧了螺丝,还剩:2个没拧
Thread[Thread-1,5,main]拧了螺丝,还剩:0个没拧
Thread[Thread-2,5,main]拧了螺丝,还剩:-1个没拧
Thread[Thread-0,5,main]拧了螺丝,还剩:0个没拧

再来看下总共拧了多少螺丝

可以看到拧了222次螺丝。

这个就是传说中的线程安全问题。

多个线程操作同一个数据,出现的数据紊乱现象

为什么会出现问题

回想一下,在拧螺丝的时候,是否都要在拧之前查看一下还有多少个螺丝没拧.

因为每个人都有一份文档,各自优先更新自己的那份,没有及时同步给其他人.

这个例子和JAVA工作模式很是相近,画个图让大家理解一下:

每个线程都是优先操作自己的工作区,而主内存更新有可能不及时。

如何解决

最经典的一个办法

  • 加锁

保证同一时间只有一个人在操作,并且直接更新主内存数据,拿到锁的一方也必须从主内存读取最新数据进行操作.

JAVA如何解决:

  • synchronized

package Thread;

public class MyRun implements Runnable {

    public static int luosi = 200;
    @Override
    public  void run() {
        for (int i = 1;i<=100;i++) {   // 100人去拧这200个螺丝
            synchronized (MyRun.class) {  // 加锁,保证原子性,可见性操作
                if (luosi > 0) {
                    try {
                        Thread.sleep(50);   // 假设每个人耗费50毫秒去拧
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    luosi-= 1;  // 拧成功了,总数减去1
                    System.out.println(Thread.currentThread()+"拧了螺丝,还剩:"+luosi+"个没拧");
                }
            }

        }
    }
}

结果:

Thread[Thread-0,5,main]拧了螺丝,还剩:4个没拧
Thread[Thread-0,5,main]拧了螺丝,还剩:3个没拧
Thread[Thread-2,5,main]拧了螺丝,还剩:2个没拧
Thread[Thread-1,5,main]拧了螺丝,还剩:1个没拧
Thread[Thread-1,5,main]拧了螺丝,还剩:0个没拧

拧了多少:

这次正常了,我们完美的解决了线程安全问题。

总结

线程安全问题只会出现在多个线程操作同一个数据上,否则不会出现线程安全问题。 而一般解决这种问题的方式就是加锁。我们回想一下,这个锁是不是很熟悉,在MySQL中,多个事务操作同一条数据也是通过加锁来隔离的。而这都会造成一个共同的问题,性能下降,甚至死锁问题。

加锁是否是解决线程安全问题的最优解呢?

而且我们在做业务需求时,真的有必要开启多线程吗?我觉得这200个螺丝,我一个人拧的更快!

PS:你的赞是我创作的动力!

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 1
wanghan

哈哈哈,那你就一个人拧~

3年前 评论
维C (楼主) 3年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!