多线程的安全问题
什么是Java多线程安全问题?
我们首先来看一个例子
案列演示
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
Public class MyTest {
public static void main(String[] args) {
//A:
//案例演示
//需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
//通过继承Thread类实现
CellThred th1 = new CellThred();
CellThred th2 = new CellThred();
CellThred th3= new CellThred();
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
public class CellThred extends Thread {
static int piao = 100;
//th1 th2 th3
@Override
public void run() {
while (true) {
if (piao > 0) {
try {
Thread.sleep(100);//模拟电影卖票延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + "正在售卖 " + (piao--) + " 张票");
}
}
}
}
-
程序运行结果
请注意看最后一条数据出现了负值,这就是多线程安全问题,原因在于Java多线程对cpu的调用是抢占式,多个线程之间的时间如果发生冲突,就会造成数据的混乱有的时候会出现数字重复运算,或者是顺序混乱,你可以多运行几次看看; -
线程安全问题的产生环境
- 多线程环境
- 有共享数据
- 存在多条语句同时操作共享数据
-
如何解决线程安全问题
基本思想:让线程没有安全环境,即打破上述三个条件,1,2条件是不能打破的,这样的话就只能打破第三个条件了,那么如何实现呢?我们只需要将多个语句操作数据的代码给锁起来,让任一时刻只有一个线程执行即可。
我们这里使用同步代码块就能够做到这一点,格式:
- synchronized(对象){//不能在括号了直接new 对象 new 了 就没效果 要被同步的代码 ; }
使用同步代码块改造刚才的卖票代码
public class CellThred extends Thread {
static int piao = 100;
//创建一个锁对象
static Object obj=new Object();
// th2
@Override
public void run() {
while (true) {
synchronized (obj){ //obj 就相当于一把锁 哪个线程一进入同步代码块,就会持有锁,那么这个把所不释放,其他线程就阻塞状态
if (piao > 0) {
//模拟延迟
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//i-- i++ 先使用 后运算 //th3 //th2 100
System.out.println(Thread.currentThread().getName() + "正在售卖 " + (piao--) + " 张票");
}
}
//哪个线程出了同步代码块就会释放锁
}
}
}
-
注意事项:
注意这个对象 要定义为静态成员变量 才能被所有线程共享不能在括号了直接new 对象 new 了 就没效果
-
同步代码块解决线程安全问题的好处和弊端
同步的好处:解决了线程的安全问题,弊端:当线程的数量增多时,因为每一个线程都需要去判断同步上的锁,这是很耗费资源的,无形中会降低程序的利用效率。
-
我们不仅可以通过锁对象来进行同步还可以直接将 synchronized 加到方法上,我们叫做同步方法,这时使用的锁对象是this
如果使用的是静态方法那么此时的锁对象是当前类的字节码文件对象。
@Override
public void run() {
while (true) {
maiPiao();
}
}
//将 synchronized 加到方法上,我们叫做同步方法
//同步方法用的是this 这个多对象
public synchronized void maiPiao() {
//System.out.println(this);
// 相当于一把锁 哪个线程一进入同步代码块,就会持有锁,那么这个把所不释放,其他线程就阻塞状态
if (piao > 0) {
//模拟延迟
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//i-- i++ 先使用 后运算 //th3 //th2 100
System.out.println(Thread.currentThread().getName() + "正在售卖 " + (piao--) + " 张票");
}
}
- JDK5之后的Lock锁的概述和使用
虽然我们可以理解同步代码块和同步方法的锁对象问题,
但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
注意ReentrantLock是Lock接口的实现 - synchronized与Lock的区别
1、我把两者的区别分类到了一个表中,方便大家对比:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
synchronized与Lock的两者详细的区别可以参考这篇
public class CellThred extends Thread {
static int piao = 100;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //加锁
if (piao > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖 " + (piao--) + " 张票");
}
lock.unlock(); //释放锁
}
}
}
- 多线程的死锁问题
死锁问题概述
如果出现了同步嵌套,就容易产生死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
同步代码块的嵌套案例
死锁: 两个或者两个以上的线程,在抢占CPU的执行权的时候,都处于等待状态
举例: 中国人和美国人一起吃饭
中国人使用的筷子
美国人使用的刀和叉
中国人获取到了美国人的刀
美国人获取到了中国人的一根筷子
代码演示:
public class MyTest {
public static void main(String[] args) {
//死锁:
//两个或者两个以上的线程, 在抢占CPU的执行权的时候, 都处于等待状态
MyThread th1 = new MyThread(true);
MyThread th2 = new MyThread(false);
th1.start();
th2.start();
}
}
public class MyThread extends Thread {
boolean b;
public MyThread(boolean b) {
this.b = b;
}
@Override
public void run() {
if (b) {
synchronized (LockeInterface.objA) {
System.out.println("true objA 进来了");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LockeInterface.objB) {
System.out.println("true objB 进来了");
}
}
} else {
synchronized (LockeInterface.objB) {
System.out.println("false objB 进来了");
synchronized (LockeInterface.objA) {
System.out.println("false objA 进来了");
}
}
}
}
}
public interface LockeInterface {
//定义两把锁
public static final Object objA=new Object();
public static final Object objB=new Object();
}