文章目录
- 一、线程安全问题
- (1)介绍
- (2)同一个资源问题和线程安全问题
- 1、方式一:实现Runnable接口
- 1.1 票数问题
- 1.2 重票和错票问题
- 2、方式二:继承Thread类
- 二、安全问题分类总结
- (1)局部变量不能共享
- (2)不同对象的实例变量不共享
- (3)静态变量是共享的
- (4)同一个对象的实例变量共享
- (5)抽取资源类,共享同一个资源对象
一、线程安全问题
(1)介绍
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作
,那么不会发生线程安全问题(因为不会对数据进行修改)。
但是如果多个线程中对资源有读和写
的操作,就容易出现线程安全问题。
举例:
“多线程的安全问题”是因为多线程对统一资源进行读和写操作时带来的。
(2)同一个资源问题和线程安全问题
【案例】
火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共100个(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。
注意:不能出现错票、重票。
比如现在要开启三个窗口售票,总票数为100张。
1、方式一:实现Runnable接口
<1> 卖票的票数
首先写一个卖票,用实现的方式建立一个线程,如下:
class SaleTicket implements Runnable{ //卖票
}
因为Runnable接口里面有抽象方法run,所以需要重写抽象方法,Ctrl+i
快捷键调出来,OK即可,如下:
class SaleTicket implements Runnable{ //卖票
@Override
public void run() {
}
}
现在需要卖票,一共是100张票,所以需要有一个变量ticket来表示票的数量。
class SaleTicket implements Runnable{ //卖票 1.创建一个实现Runnable接口的类(实现类)
@Override
public void run() { //2.实现接口中的抽象方法run()方法
int ticket=100;
}
}
1.1 票数问题
🗳️这样写可以吗?
其实是不可以的,不能这样来表示100张票。
下一步我们需要做的是创建这个实现类的对象,具体创建线程的步骤如下:
public class WindowTest {
public static void main(String[] args) {
//3.创建当前实现类的对象
SaleTicket s=new SaleTicket();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
//给三个线程起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。
t1.start();
t2.start();
t3.start();
}
}
当整个程序跑起来之后,就会各自调用run方法,三个线程调用run方法,就会有300张票了。
所以下面的写法不靠谱。
只需要把ticket拿到run()
方法外面定义即可,因为在main方法里面,只创建了一个对象s,它被三个线程所共享了。
现在就没有问题了。
🌱代码
public class WindowTest {
public static void main(String[] args) {
//3.创建当前实现类的对象
SaleTicket s=new SaleTicket();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
//给三个线程起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。
t1.start();
t2.start();
t3.start();
}
}
class SaleTicket implements Runnable{ //卖票 1.创建一个实现Runnable接口的类(实现类)
int ticket=100;
@Override
public void run() { //2.实现接口中的抽象方法run()方法
}
}
<2> 售票
售票的逻辑当然是写在run方法里面的。
一共100张票,3个窗口,每个窗口不一定都拿到100/3张票,所以这里不知道循环了多少次,那就用while
循环吧,只要ticket大于0就说明还有票,若是ticket等于0就说明没有票了,退出循环即可。
如下:
class SaleTicket implements Runnable{ //卖票 1.创建一个实现Runnable接口的类(实现类)
int ticket=100;
@Override
public void run() { //2.实现接口中的抽象方法run()方法
while (true){
if(ticket>0){ //如果票数大于0就可以售票
//哪个窗口卖票了,票卖了多少
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100
ticket--;
}else{
break;
}
}
}
}
🌱代码
package yuyi02.notsafe;
/**
* ClassName: WindowTest
* Package: yuyi02.notsafe
* Description:
*
* @Author 雨翼轻尘
* @Create 2024/1/27 0027 19:28
*/
public class WindowTest {
public static void main(String[] args) {
//3.创建当前实现类的对象
SaleTicket s=new SaleTicket();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
//给三个线程起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。
t1.start();
t2.start();
t3.start();
}
}
class SaleTicket implements Runnable{ //卖票 1.创建一个实现Runnable接口的类(实现类)
int ticket=100;
@Override
public void run() { //2.实现接口中的抽象方法run()方法
while (true){
if(ticket>0){ //如果票数大于0就可以售票
//哪个窗口卖票了,票卖了多少
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100
ticket--;
}else{
break;
}
}
}
}
🍺输出结果(部分)
🔥注意这里显示的票数不是严格递减的,如下:
卖票肯定是先卖票号大的,后面才卖小的,只不过这里输出要显示到控制台,显示的时候看着像单线程维度(显示的先后问题而已),其实是三个线程并发执行的,如下:
这里就不用刻意关注这个事情。
🗳️但是这里有点问题,如下:
三个窗口都卖了100号这张票!这个是线程的安全问题,不应该出现这样的问题。
现在加一个sleep(),可能还会看到错票的情况,代码如下:
if(ticket>0){ //如果票数大于0就可以售票
Thread.sleep(10);
//...
}else{
break;
}
记得处理一下异常:
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
🌱代码
package yuyi02.notsafe;
/**
* ClassName: WindowTest
* Package: yuyi02.notsafe
* Description:
*
* @Author 雨翼轻尘
* @Create 2024/1/27 0027 19:28
*/
public class WindowTest {
public static void main(String[] args) {
//3.创建当前实现类的对象
SaleTicket s=new SaleTicket();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
Thread t3 = new Thread(s);
//给三个线程起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//5.通过Thread类的实例调用start():1.启动线程 2.调用当前线程的run()。
t1.start();
t2.start();
t3.start();
}
}
class SaleTicket implements Runnable{ //卖票 1.创建一个实现Runnable接口的类(实现类)
int ticket=100;
@Override
public void run() { //2.实现接口中的抽象方法run()方法
while (true){
if(ticket>0){ //如果票数大于0就可以售票
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//哪个窗口卖票了,票卖了多少
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100
ticket--;
}else{
break;
}
}
}
}
🍺输出结果(部分)
这里怎么还有-1的票?而且还有一样的票号,这也不行。
1.2 重票和错票问题
🍰对于这种重票和错票的问题,是怎么产生的呢?
卖票逻辑代码如下:
public void run() { //2.实现接口中的抽象方法run()方法
while (true){
if(ticket>0){ //如果票数大于0就可以售票
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//哪个窗口卖票了,票卖了多少
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100
ticket--;
}else{
break;
}
}
}
<1> 理想状态
首先有三个窗口卖100张票,就是三个线程,如下:
然后开始取ticket的票号,在run方法里面,判断票号是否大于0,若大于0就打印票号,如下:
若此时票号变成了0,这时候就执行else里面的break
操作,就结束了,如下:
<2> 极端状态
上面是理想状态,现在来看一下极端状态,以错票来演示一下。
比如现在只有一张票了,如下:
票号是1,线程1
判断票号大于0,判断是true,就进入到if结构里面,此时又有一个sleep,相当于它进入到阻塞状态,还没有实现“票号–”的操作。如下:
此时线程1被阻塞了,在被阻塞的10ms中,线程t2
又进来了,然后发现ticket大于0,它也进入if里面了,也被阻塞,如下:
然后线程t3
也进来了,也被阻塞了,如下:
然后它们相继结束阻塞,但是已经在if里面了,这也就意味着它们都会去卖票,所以这里会出现0号票和-1号票,如下:
注意我们写的程序里面是先打印输出再自减。
sleep相当于放大了线程阻塞的现象。
这里的重票和错票问题,就称为线程的安全问题
。
画个图看看“重票”吧:
Java是高并发的,不会出现并行的情况,在CPU层面只有并发执行。
☕那么对于重票和错票问题,是什么原因导致的呢?又如何解决呢?
如何避免重票和错票问题?
①原因
上面的问题在于,一个线程进入了if结构之后,还没有时间执行后续操作,另一个线程又进入了if结构。
即:线程1操作ticket
的过程中,尚未结束的情况下,其他线程也参与进来,对ticket
进行操作。
②解决
如何解决?
必须保证一个线程a在操作ticket的过程中,其它线程必须等待,直到线程a操作ticket结束以后,其它线程才可以进来继续操作ticket。(ticket就是所有线程共同操作的数据,就是共享数据
)
③Java是如何解决线程的安全问题的?
使用线程的同步机制
。
2、方式二:继承Thread类
卖票
public class WindowTest2 {
public static void main(String[] args) {
//3.创建3个窗口 创建当前Thread的子类的对象
Window w1=new Window();
Window w2=new Window();
Window w3=new Window();
//命名
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类
//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
@Override
public void run() {
}
}
run方法里面,就写逻辑代码。
首先是票的问题,不能定义在run方法里面,那放在外面吗?
class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类
//票
int ticket=100;
//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
@Override
public void run() {
}
}
先来看卖票的细节,和上一个案例逻辑一致,如下:
class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类
//票
int ticket=100;
//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
@Override
public void run() {
while (true){
if(ticket>0){ //如果票数大于0就可以售票
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//哪个窗口卖票了,票卖了多少
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100
ticket--;
}else{
break;
}
}
}
}
🌱代码
package yuyi02.notsafe;
/**
* ClassName: WindowTest2
* Package: yuyi02.notsafe
* Description:
* 使用继承Thread类的方式,实现卖票
* @Author 雨翼轻尘
* @Create 2024/1/27 0027 22:26
*/
public class WindowTest2 {
public static void main(String[] args) {
//3.创建3个窗口 创建当前Thread的子类的对象
Window w1=new Window();
Window w2=new Window();
Window w3=new Window();
//命名
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类
//票
int ticket=100;
//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
@Override
public void run() {
while (true){
if(ticket>0){ //如果票数大于0就可以售票
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//哪个窗口卖票了,票卖了多少
System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket); //最开始票号为100
ticket--;
}else{
break;
}
}
}
}
🍺输出结果(部分)
可以发现,它重票的概率非常高,而且好像每一个都重复了,现在是卖了有300张票。
🎲为啥卖了300张票?
这就要用“面向对象”来解释了。
Window类里面声明了实例变量ticket,如下:
class Window extends Thread{ //卖票 1.创建一个继承于Thread类的子类
//票
int ticket=100;
//2.重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中
@Override
public void run() {
//...
}
}
实例变量是每个对象一份,现在有三个线程,三个对象卖票,意味着各自卖各自的,一共300张票。如下:
public class WindowTest2 {
public static void main(String[] args) {
//3.创建3个窗口 创建当前Thread的子类的对象
Window w1=new Window();
Window w2=new Window();
Window w3=new Window();
//命名
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
//4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法
w1.start();
w2.start();
w3.start();
}
}
所以这里票数不能这样写:int ticket=100;
那么加一个static
呢?这样:static int ticket=100;
现在ticket是静态变量,就是100张票了。
运行一下:
还是有重票和错票的问题出现,这是我们不希望看到的。
下一节来说在Java里面如何解决线程安全问题。
二、安全问题分类总结
(1)局部变量不能共享
示例代码:
package com.atguigu.unsafe;
class Window extends Thread {
public void run() {
int ticket = 100;
while (ticket > 0) {
System.out.println(getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo1 {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
结果:发现卖出300张票。
问题:局部变量是每次调用方法都是独立的,那么每个线程的run()的ticket是独立的,不是共享数据。
(2)不同对象的实例变量不共享
package com.atguigu.unsafe;
class TicketWindow extends Thread {
private int ticket = 100;
public void run() {
while (ticket > 0) {
System.out.println(getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo2 {
public static void main(String[] args) {
TicketWindow w1 = new TicketWindow();
TicketWindow w2 = new TicketWindow();
TicketWindow w3 = new TicketWindow();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
结果:发现卖出300张票。
问题:不同的实例对象的实例变量是独立的。
(3)静态变量是共享的
示例代码:
package com.atguigu.unsafe;
class TicketSaleThread extends Thread {
private static int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
窗口1卖出一张票,票号:100
窗口2卖出一张票,票号:100
窗口3卖出一张票,票号:100
窗口3卖出一张票,票号:97
窗口1卖出一张票,票号:97
窗口2卖出一张票,票号:97
窗口1卖出一张票,票号:94
窗口3卖出一张票,票号:94
窗口2卖出一张票,票号:94
窗口2卖出一张票,票号:91
窗口1卖出一张票,票号:91
窗口3卖出一张票,票号:91
窗口3卖出一张票,票号:88
窗口1卖出一张票,票号:88
窗口2卖出一张票,票号:88
窗口3卖出一张票,票号:85
窗口1卖出一张票,票号:85
窗口2卖出一张票,票号:85
窗口3卖出一张票,票号:82
窗口1卖出一张票,票号:82
窗口2卖出一张票,票号:82
窗口2卖出一张票,票号:79
窗口3卖出一张票,票号:79
窗口1卖出一张票,票号:79
窗口3卖出一张票,票号:76
窗口1卖出一张票,票号:76
窗口2卖出一张票,票号:76
窗口1卖出一张票,票号:73
窗口2卖出一张票,票号:73
窗口3卖出一张票,票号:73
窗口2卖出一张票,票号:70
窗口1卖出一张票,票号:70
窗口3卖出一张票,票号:70
窗口2卖出一张票,票号:67
窗口3卖出一张票,票号:67
窗口1卖出一张票,票号:67
窗口1卖出一张票,票号:64
窗口3卖出一张票,票号:64
窗口2卖出一张票,票号:64
窗口2卖出一张票,票号:61
窗口3卖出一张票,票号:61
窗口1卖出一张票,票号:61
窗口1卖出一张票,票号:58
窗口2卖出一张票,票号:58
窗口3卖出一张票,票号:58
窗口2卖出一张票,票号:55
窗口1卖出一张票,票号:55
窗口3卖出一张票,票号:55
窗口3卖出一张票,票号:52
窗口1卖出一张票,票号:52
窗口2卖出一张票,票号:52
窗口2卖出一张票,票号:49
窗口1卖出一张票,票号:49
窗口3卖出一张票,票号:49
窗口2卖出一张票,票号:46
窗口3卖出一张票,票号:46
窗口1卖出一张票,票号:46
窗口2卖出一张票,票号:43
窗口3卖出一张票,票号:43
窗口1卖出一张票,票号:43
窗口3卖出一张票,票号:40
窗口1卖出一张票,票号:40
窗口2卖出一张票,票号:40
窗口2卖出一张票,票号:37
窗口3卖出一张票,票号:37
窗口1卖出一张票,票号:37
窗口2卖出一张票,票号:34
窗口1卖出一张票,票号:34
窗口3卖出一张票,票号:34
窗口3卖出一张票,票号:31
窗口2卖出一张票,票号:31
窗口1卖出一张票,票号:31
窗口1卖出一张票,票号:28
窗口2卖出一张票,票号:28
窗口3卖出一张票,票号:28
窗口2卖出一张票,票号:25
窗口1卖出一张票,票号:25
窗口3卖出一张票,票号:25
窗口2卖出一张票,票号:22
窗口3卖出一张票,票号:22
窗口1卖出一张票,票号:22
窗口3卖出一张票,票号:19
窗口1卖出一张票,票号:19
窗口2卖出一张票,票号:19
窗口2卖出一张票,票号:16
窗口3卖出一张票,票号:16
窗口1卖出一张票,票号:16
窗口2卖出一张票,票号:13
窗口1卖出一张票,票号:13
窗口3卖出一张票,票号:13
窗口2卖出一张票,票号:10
窗口1卖出一张票,票号:10
窗口3卖出一张票,票号:10
窗口3卖出一张票,票号:7
窗口1卖出一张票,票号:7
窗口2卖出一张票,票号:7
窗口3卖出一张票,票号:4
窗口1卖出一张票,票号:4
窗口2卖出一张票,票号:4
窗口3卖出一张票,票号:1
窗口2卖出一张票,票号:1
窗口1卖出一张票,票号:1
结果:发现卖出近100张票。
问题1:但是有重复票或负数票问题。
原因:线程安全问题
问题2:如果要考虑有两场电影,各卖100张票等
原因:TicketThread类的静态变量,是所有TicketThread类的对象共享
(4)同一个对象的实例变量共享
示例代码:多个Thread线程使用同一个Runnable对象
package com.atguigu.safe;
class TicketSaleRunnable implements Runnable {
private int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo4 {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr, "窗口一");
Thread t2 = new Thread(tr, "窗口二");
Thread t3 = new Thread(tr, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:发现卖出近100张票。
问题:但是有重复票或负数票问题。
原因:线程安全问题
(5)抽取资源类,共享同一个资源对象
示例代码:
package com.atguigu.unsafe;
//1、编写资源类
class Ticket {
private int ticket = 100;
public void sale() {
if (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
} else {
throw new RuntimeException("没有票了");
}
}
public int getTicket() {
return ticket;
}
}
public class SaleTicketDemo5 {
public static void main(String[] args) {
//2、创建资源对象
Ticket ticket = new Ticket();
//3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {
while (true) {
ticket.sale();
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
ticket.sale();
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
ticket.sale();
}
}, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:发现卖出近100张票。
问题:但是有重复票或负数票问题。
原因:线程安全问题。
下一节来介绍如何解决问题。