Java高并发之线程不安全

转载自:https://zhuanlan.zhihu.com/p/108880731?from_voters_page=true

前言

  在学习多线程的道路上,我们会经常看到线程安全这类词汇,面试官也经常问,本文就来说一说什么是线程安全。

1.什么是线程安全?

  多个线程同一时刻对同一个全局变量(同一份资源)做写操作(读操作不会涉及线程安全)时,如果跟我们预期的结果一样,我们就称之为线程安全,反之,线程不安全

  git应该大家都用过吧,有github仓库,还有本地库,在项目开发过程中,我们经常会遇到冲突的问题,就是因为,多个人同时对同一份资源进行了操作。

2.经典案例

代码模拟业务

  大家都抢过票,知道一到春运、过节的时候,票就很难抢,下面我们通过一段代码,来模拟一下抢票的业务。

public class MyThread implements  Runnable {
    private  int count=50;

    @Override
    public void run() {
        while (count > 0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":  抢到第"+count--+"张");
        }
    }
}

测试类

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"程序猿学社");
        Thread thread1 = new Thread(myThread,"隔壁老王");
        Thread thread2 = new Thread(myThread,"小张");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

测试结果

小张:  抢到第50张
隔壁老王:  抢到第48张
程序猿学社:  抢到第49张
程序猿学社:  抢到第47张
小张:  抢到第46张
隔壁老王:  抢到第45张
程序猿学社:  抢到第44张
隔壁老王:  抢到第43张
小张:  抢到第42张
程序猿学社:  抢到第41张
隔壁老王:  抢到第40张
小张:  抢到第40张
程序猿学社:  抢到第39张
小张:  抢到第38张
隔壁老王:  抢到第38张
程序猿学社:  抢到第37张
隔壁老王:  抢到第36张
小张:  抢到第35张
程序猿学社:  抢到第34张
隔壁老王:  抢到第33张
小张:  抢到第33张
程序猿学社:  抢到第32张
小张:  抢到第31张
隔壁老王:  抢到第30张
程序猿学社:  抢到第29张
小张:  抢到第28张
隔壁老王:  抢到第27张
程序猿学社:  抢到第26张
隔壁老王:  抢到第25张
小张:  抢到第24张
程序猿学社:  抢到第23张
隔壁老王:  抢到第22张
小张:  抢到第21张
隔壁老王:  抢到第20张
程序猿学社:  抢到第19张
小张:  抢到第18张
程序猿学社:  抢到第17张
隔壁老王:  抢到第16张
小张:  抢到第15张
程序猿学社:  抢到第14张
隔壁老王:  抢到第13张
小张:  抢到第12张
隔壁老王:  抢到第11张
程序猿学社:  抢到第10张
小张:  抢到第9张
隔壁老王:  抢到第8张
小张:  抢到第7张
程序猿学社:  抢到第6张
隔壁老王:  抢到第5张
小张:  抢到第4张
程序猿学社:  抢到第3张
隔壁老王:  抢到第2张
程序猿学社:  抢到第1张
小张:  抢到第1张
隔壁老王:  抢到第0张

   通过上面的测试结果,三个线程,同时抢票,有时候会抢到同一张票?为什么会有这种问题发现?
  在回答这个问题之前,我们应该了解一下 Java 的内存模型(JMM),划重点,也是面试官经常会问的一个问题。

什么是JMM?

  JMM(Java Memory Model),是一种基于计算机内存模型,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性(这三个也是多线程的三大特性,划重点,面试经常问)。
本文就了解可见性就可。

可见性:

  多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。
  看到这里是不是还是有点懵,别急,我们通过图,把之前抢票的业务画出来。
在这里插入图片描述
  同一进程下的多个线程,内存资源是共享的。主内存的count才是共享资源。程序猿学社、隔壁老王、小张,实际上不是直接对主内存的count进行写入操作。实际上,程序运行过程中,他们每个人,都有各自的工作内存。实际上就是把主内存的count,每个人,都copy一份,对各自的工作内存的变量进行操作。操作完后,再把对应的结果通知到主内存。

  • 再回顾一下我之前git案例。有本地库(工作内存),有github库(主内存)。
  • 在多个人同时过程中,组长会新建一个项目,其他的组员,是不是需要把代码拉取下来,到本地。
  • 我们开发完一个功能后,需要先提交本地库,再提交到github(把工作内存的结果,提交给github。
  • 提交代码的时候,我们根本就无法知道,我这份代码是不是最新的,所有有时候一提交,就报错(可见性)。
  • 说了这么多,我们这时候应该知道之前写的模拟抢票demo为什么会有线程安全问题了把。就是因为各自都操作自己的工作内存,拿到主内存的值就开始操作。假设,这时候count为40,同一时间,来了三个线程,那这三个线程的工作内存拿到的值都是40,这样就会导致,这三个线程,都会抢到39这张票。

我们应该如何解决这个问题呢?

怎么解决线程安全问题?

要实现线程安全,需要保证数据操作的两个特性:

  • 原子性:对数据的操作不会受其他线程打断,意味着一个线程操作数据过程中不会插入其他线程对数据的操作。
  • 可见性:当线程修改了数据的状态时,能够立即被其他线程知晓,即数据修改后会立即写入主内存,后续其他线程读取时就能得知数据的变化。

  以上两个特性结合起来,其实就相当于同一时刻只能有一个线程去进行数据操作并将结果写入主存,这样就保证了线程安全。

一般有如下几种方法:

  • synchronized关键字(放在方法上)
  • 同步代码块
  • jdk1.5Lock

1. synchronized关键字(放在方法上)

public class MySynchronizedThread implements  Runnable {
    private  int count=50;

    @Override
    public  void run() {
        while(true){
            buy();
        }
    }
    public synchronized  void  buy(){
        if(count>0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":  抢到第"+count--+"张,"+System.currentTimeMillis());
        }
     }
}

测试类

public class Test {
    public static void main(String[] args) {
        //线程不安全
        //MyThread myThread = new MyThread();
        MySynchronizedThread myThread = new MySynchronizedThread();
        Thread thread = new Thread(myThread,"程序猿学社");
        Thread thread1 = new Thread(myThread,"隔壁老王");
        Thread thread2 = new Thread(myThread,"小张");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

测试结果
在这里插入图片描述
  通过图片我们可以发现,同一时间,抢票的间隔差不多都是50ms,为什么,不是说多线程吗(前提不是单核)

  因为在抢票的方法上,增加了synchronized,导致同一时候,只能有一个线程运行,需要等这个线程运行完后,下一个线程才能运行。

  • 可以理解为,有一个茅坑,里面有四个坑,隔壁小王这个人,就怕别人偷窥他,直接把进茅坑的们直接锁上,意思就是我在茅坑的时候,其他的都不能进茅坑,需要等隔壁小王,出来后,其他人才能进入。这样的结果就会导致,大家都有意见,所以这种方式,一般很少使用。

2. 同步代码块

这种方式就是利用synchronized+锁对象

public class SynchronizedBlockThread implements  Runnable {
    private  int count=50;
    private Object object = new Object();

    @Override
    public  void run() {
        while(true){
            buy();
        }

    }
    public   void  buy(){
        synchronized (object){
            if(count>0){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":  抢到第"+count--+"张,"+System.currentTimeMillis());
            }
        }
     }
}

  这种方式相对于前一种方式,性能有提升,只锁了代码块,而不是把这个方法都锁了。

3. jdk1.5的Lock

public class LockThread implements Runnable {
    private  int count=50;
    //定义锁对象
    private Lock lock = new ReentrantLock();
    @Override
    public  void run() {
        while(true){
            buy();
        }

    }
    public   void  buy(){
        lock.lock();
        if(count>0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":  抢到第"+count--+"张,"+System.currentTimeMillis());
        }
        lock.unlock();
    }
}

jdk1.5lock重要的两个方法

  • lock(): 获取锁。
  • unlock():释放锁。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值