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

Gwen ·
更新时间:2024-11-10
· 964 次阅读

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

文章目录前言1.什么是线程安全?2.经典案例代码模拟业务什么是JMM?怎么解决线程安全问题?synchronized关键字(放在方法上)同步代码块jdk1.5的Lock 前言

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

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():释放锁。
作者:程序猿学社



并发编程 并发 线程安全 多线程 线程

需要 登录 后方可回复, 如果你还没有账号请 注册新账号