线程安全问题
通过一个案例引出线程安全问题
三个窗口卖100张票,也就是三个线程卖100张票
代码实现
package org.westos.demo3;
//这里通过实现Runnable接口来创建线程,其他方式也可以
public class SellRunnable implements Runnable{
//定义多个线程的共享数据--票,用static修饰即可
static int num = 100;
@Override
public void run() {
while(true){
if(num>0){
//模拟网络延迟,更容易观察
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":还剩余"+(num--)+"张票");
}
}
}
}
package org.westos.demo3;
//实现多线程
public class MyTest {
public static void main(String[] args) {
SellRunnable runnable = new SellRunnable();
Thread th1 = new Thread(runnable);
Thread th2 = new Thread(runnable);
Thread th3 = new Thread(runnable);
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
运行结果:
从运行结果可看出,票数是错乱的,会出现剩余0张票,也会出现-1票,多观察几次,还会出现多个窗口出售同一张票的情况,这些都是不符合实际情况的,这就是线程安全问题
整理一下,出现线程安全问题的标准:
(1)多线程环境
(2)有共享数据
(3)有多条语句操作共享数据
以上的案例满足标准:3个窗口代表多线程环境,票是共享数据,而票–代表着读,改,写三条语句,所以出现了线程安全问题,那么,如何解决?
多线程环境和共享数据是我们的需求,不能更改,那么只能通过修改第三个标准让线程安全
首先,我们要先知道为什么会出现这些情况:
比如还剩1张票,第一个线程先抢占到CPU执行权,执行票–操作,将票改为0,但在写之前被第二个线程抢到了CPU时间片,第二个线程就会开启,此时票依然是1,线程进入后,又被第一个线程抢到时间片,将票改为0,然后结束第一个线程,第二个线程继续执行,这时票为0,执行票–,也就出现了-1票的情况,这是由于线程具有随机性导致
而出现多个窗口出售同一张票是因为非原子性操作导致
要想修改第三个标准,只需给出现安全问题的代码让个锁,让这段代码变为单线程,只有一个线程在操作共享数据即可
解决方法一:通过同步代码块和synchronized关键字
package org.westos.demo3;
//这里通过实现Runnable接口来创建线程,其他方式也可以
public class SellRunnable implements Runnable{
//定义多个线程的共享数据--票,用static修饰即可
static int num = 100;
//获取一把锁,锁可以是任意对象
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){
if(num>0){
//模拟网络延迟,更容易观察
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":还剩余"+(num--)+"张票");
}
}
}
}
}
可以看出,数据不会错乱了,因为每个线程只有等上一个线程运行完后释放锁才能再获取锁执行代码块,也就解决了第三个标准,这样就解决了线程安全问题
同步代码块的好处:解决的线程安全问题
弊端:当线程很多时,因为每个线程都会去判断锁,降低了性能,浪费底层资源
解决方法二:通过同步方法
package org.westos.demo3;
//这里通过实现Runnable接口来创建线程,其他方式也可以
public class SellRunnable implements Runnable{
//定义多个线程的共享数据--票,用static修饰即可
static int num = 100;
@Override
public void run() {
while(true){
if(num>50){
//想要获取同一把锁,就要跟同步方法的锁一致
//synchronized (this){
synchronized (SellRunnable.class){
if(num>0){
//模拟网络延迟,更容易观察
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":还剩余"+(num--)+"张票");
}
}
}else{
maipiao();
}
}
}
//同步方法,给代码块抽取成一个方法,用synchronized修饰
//此时的锁对象是this
//如果是静态同步方法,锁对象是字节码文件:.class
private static synchronized void maipiao() {
if(num>0){
//模拟网络延迟,更容易观察
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":还剩余"+(num--)+"张票");
}
}
}
分两部分为了更好理解同步代码块的锁对象,运用这两部分要特别注意同步代码块的锁this是不是同一个对象,如果是用继承Thread实现多线程的方式,这里的this就不是指同一个对象了,而是3个,因为new了三次,而SellRunnable只在主方法中new了一次,所以this是同一对象
解决方式三:加上锁lock
package org.westos.demo3;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//这里通过实现Runnable接口来创建线程,其他方式也可以
public class SellRunnable implements Runnable{
//定义多个线程的共享数据--票,用static修饰即可
static int num = 100;
//获取锁对象
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
//获取锁
lock.lock();
try{
if(num>0){
//模拟网络延迟,更容易观察
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":还剩余"+(num--)+"张票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
//无论有没有异常都要释放锁
lock.unlock();
}
}
}
}
这一方式可以更加清晰表达获取锁和释放锁,同样也可以解决线程安全问题
死锁
- 概述:指两个或者两个以上的线程在执行过程中,因互相持有对方需要的锁,导致双方都处于等待状态,如果出现了同步嵌套,就容易出现这类问题
- 演示:
package org.westos.demo3;
public class ObjectUtils {
//定义两把锁
public static final Object obj1=new Object();
public static final Object obj2=new Object();
}
package org.westos.demo3;
public class MyThread extends Thread {
private boolean flag;
public MyThread(boolean flag) {
this.flag=flag;
}
@Override
public void run() {
if(flag){
synchronized (ObjectUtils.obj1){
System.out.println("持有锁obj1");
synchronized (ObjectUtils.obj2){
System.out.println("持有锁obj2");
}
}
}else{
synchronized (ObjectUtils.obj2){
System.out.println("持有锁obj2");
synchronized (ObjectUtils.obj1){
System.out.println("持有锁obj1");
}
}
}
}
}
package org.westos.demo3;
public class MyTest2 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread(true);
MyThread myThread2 = new MyThread(false);
myThread1.start();
myThread2.start();
}
}
持有的锁只有在同步代码块执行完之后才会释放,而在释放锁之前,其他线程无法获得这个锁