程序猿学社的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(Java Memory Model),**是一种基于计算机内存模型,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性(这三个也是多线程的三大特性,划重点,面试经常问)。
本文就了解可见性就可。
可见性:
多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。说了这么多,我们这时候应该知道之前写的模拟抢票demo为什么会有线程安全问题了把。就是因为各自都操作自己的工作内存,拿到主内存的值就开始操作。假设,这时候count为40,同一时间,来了三个线程,那这三个线程的工作内存拿到的值都是40,这样就会导致,这三个线程,都会抢到39这张票。
我们应该如何解决这个问题勒?
要想解决线程安全的问题,我们就需要解决一个问题,就是线程之间进行同步交互。了解可见性后,我们知道是没有办法相互操作对方的工作内存的。
一般有如下几种方法
synchronized关键字(放在方法上)
同步代码块
jdk1.5的Lock
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():释放锁。