浅谈线程安全
1.什么是线程安全?
多个线程同一时刻对同一个全局变量(同一份资源)做写操作(读操作不会涉及线程安全)时,如果跟我们预期的结果一样,我们就称之为线程安全,反之,线程不安全。
2.举个栗子
同学们在过年时候买火车票、 那家伙、那场面那是相当大呀!那真是:锣鼓喧天,鞭炮齐鸣,红旗招展,人山人海呀。 我们下面用代码模拟一下业务场景。
package com.example.snailthink;
/**
* @program: snailthink
* @description:
* @author: SnailThink
* @create: 2021-09-03 14:19
**/
public class MyThread implements Runnable {
private int ticketCount=50;
@Override
public void run() {
try {
while (ticketCount<50){
Thread.sleep(50);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 抢到第"+ticketCount--+"张");
}
}
测试类
package com.example.snailthink;
/**
* @program: snailthink
* @description:
* @author: SnailThink
* @create: 2021-09-03 14:09
**/
public class MyThreadTests{
private int count=50;
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();
}
}
运行结果
通过上面的测试结果,三个线程,同时抢票,有时候会抢到同一张票?为什么会有这种问题发现?
在回答这个问题之前,我们应该了解一下 Java 的内存模型(JMM)
3.什么是JMM?
**JMM(Java Memory Model),**是一种基于计算机内存模型,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性
可见性: 多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。
我们通过图,把之前抢票的业务画出来。
同一进程下的多个线程,内存资源是共享的。主内存的count才是共享资源。 小李、小张、小王 实际上不是直接对主内存的count进行写入操作 程序运行过程中,他们每个人,都有各自的工作内存。实际上就是把主内存的count,每个人,都copy一份,对各自的工作内存的变量进行操作。操作完后,再把对应的结果通知到主内存。
模拟抢票demo为什么会有线程安全问题的原因就是:
因为各自都操作自己的工作内存,拿到主内存的值就开始操作。假设,这时候count为23,同一时间,来了俩个线程,那这俩个线程的工作内存拿到的值都是23,这样就会导致,这俩个线程,都会抢到23这张票。
4. 怎么解决线程安全问题?
要想解决线程安全的问题,我们就需要解决一个问题,就是线程之间进行同步交互。了解可见性后,我们知道是没有办法相互操作对方的工作内存的。
一般有如下几种方法
synchronized
关键字(放在方法上)- 同步代码块
- jdk1.5的Lock
4.1 synchronized关键字(放在方法上)
package com.example.snailthink.test.thread;
/**
* @program: snailthink
* @description:
* @author: SnailThink
* @create: 2021-09-03 14:43
**/
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());
}
}
}
测试结果
通过图片我们可以发现,同一时间,抢票的间隔差不多都是50ms . 为什么多线程会失效???
因为在抢票的方法上,增加了synchronized,导致同一时候,只能有一个线程运行,需要等这个线程运行完后,下一个线程才能运行。
4.2 同步代码块
-
这种方式就是利用synchronized+锁对象
-
相对于
synchronized
放在方法上方式,性能有提升,只锁了代码块,而不是把这个方法都锁咯。
package com.example.snailthink.test.thread;
/**
* @program: snailthink
* @description:
* @author: SnailThink
* @create: 2021-09-03 14:41
**/
public class SynchronizedBlockThreadTest 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());
}
}
}
}
4.3 jdk1.5的Lock
package com.example.snailthink.test.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: snailthink
* @description:
* @author: SnailThink
* @create: 2021-09-03 14:40
**/
public class LockThreadTests 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();
}
}