飞道的博客

【多线程并发编程】六 什么是线程安全?

221人阅读  评论(0)

程序猿学社的GitHub,欢迎Star
https://github.com/ITfqyd/cxyxs
本文已记录到github,形成对应专题。

前言

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

1.什么是线程安全?

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

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

2.经典案例

代码模拟业务

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

package com.cxyxs.thread.six;

/**
 * Description:转发请注明来源  程序猿学社 - https://ithub.blog.csdn.net/
 * Author: 程序猿学社
 * Date:  2020/2/24 14:56
 * Modified By:
 */
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--+"张");
        }
    }
}
package com.cxyxs.thread.six;

/**
 * Description:转发请注明来源  程序猿学社 - https://ithub.blog.csdn.net/
 * Author: 程序猿学社
 * Date:  2020/2/24 14:58
 * Modified By:
 */
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.5的Lock

synchronized关键字(放在方法上)

package com.cxyxs.thread.six;

/**
 * Description:通过同步代码块解决线程安全问题
 * 转发请注明来源  程序猿学社 - https://ithub.blog.csdn.net/
 * Author: 程序猿学社
 * Date:  2020/2/24 14:56
 * Modified By:
 */
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());
        }
     }
}
package com.cxyxs.thread.six;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Description:转发请注明来源  程序猿学社 - https://ithub.blog.csdn.net/
 * Author: 程序猿学社
 * Date:  2020/2/24 14:58
 * Modified By:
 */
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,导致同一时候,只能有一个线程运行,需要等这个线程运行完后,下一个线程才能运行。

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

同步代码块

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

package com.cxyxs.thread.six;

/**
 * Description:通过同步代码块解决线程安全问题
 * 转发请注明来源  程序猿学社 - https://ithub.blog.csdn.net/
 * Author: 程序猿学社
 * Date:  2020/2/24 14:56
 * Modified By:
 */
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());
            }
        }
     }
}
  • 这种方式相对于前一种方式,性能有提升,只锁了代码块,而不是把这个方法都锁咯。

jdk1.5的Lock

package com.cxyxs.thread.six;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Description:转发请注明来源  程序猿学社 - https://ithub.blog.csdn.net/
 * Author: 程序猿学社
 * Date:  2020/2/24 16:52
 * Modified By:
 */
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():释放锁。

转载:https://blog.csdn.net/qq_16855077/article/details/104477155
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场