文章目录
多线程可以帮我们提高效率,但是提高效率的同时它会有一个弊端:不安全。
在学习之前,我们先来看一个小练习。
一、引出问题
需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
这三个窗口在程序中应该是互相独立的,因此三个窗口就可以看做三个线程
MyThread.java
public class MyThread extends Thread {
int ticket = 0;//0 ~ 99
@Override
public void run() {
while (true) {
//同步代码块
if (ticket < 100) {
try {
//方法执行的太快了,这里让它睡眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
测试类
public static void main(String[] args) {
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//开启线程
t1.start();
t2.start();
t3.start();
}
运行上面代码,可以发现三个窗口卖的票好像是互相独立的,即三个窗口总共卖了300张票。
但这不是我想要的,我想要的是总共只有100张票
此时需要在 ticket变量
前面加一个 static
静态关键字。
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;//0 ~ 99
但是改完后还有问题,运行代码,可以发现它还有重复的票
![image-20240506170950878](https://img-blog.csdnimg.cn/img_convert/44ddd0a7b0c0837878d62b5956f456a8.png)
并且将控制台划到最后,发现它还会超出范围。
![image-20240506171217039](https://img-blog.csdnimg.cn/img_convert/1797b04668bead76ff03f4c5cf18a532.png)
二、分析问题
1)为什么票会重复
假设现在有三条线程,三条线程要操作的数据刚开始为0。
线程开启后,三条线程会子在这里抢夺CPU的执行权,谁抢到了,就会继续往下执行。
![image-20240506171541231](https://img-blog.csdnimg.cn/img_convert/2642e6330d38ed734c0a84181c1f59dc.png)
假设线程1在一开始抢到了CPU的执行权,线程1就会继续往下执行,此时是满足这里的判断条件的,所以线程1就会进入到if中,进来后立马睡10毫秒。
睡觉的时候是不会去抢夺CPU的执行权的,CPU的执行权一定会被其他线程抢走。
假设是线程2抢到了,线程2就会继续往下执行,此时也是满足这里的判断条件的,所以线程2也会进入if中,进来后,同样去睡10毫秒。
此时CPU的执行权就会被线程3抢走,线程3也会进来睡10毫秒。
![image-20240506172047011](https://img-blog.csdnimg.cn/img_convert/1dd3e2166ef241398a77338a9279e83d.png)
时间到了后这三条线程就会陆陆续续的醒来,继续执行下面的代码。
假设线程1醒来了,它抢夺到了CPU的执行权,线程1就会继续往下执行:ticket++
,由 0
变成了 1
,但是自增后,它还没来得及去打印,CPU的执行权就被线程2抢走了。
![image-20240506172533061](https://img-blog.csdnimg.cn/img_convert/a8036c0b12fbf86430be6be14431c395.png)
线程2在这里也对 ticket
做了一次自增,ticket
就会从 1
变成了 2
。
![image-20240506172841281](https://img-blog.csdnimg.cn/img_convert/5056cf89fcc6addca9f5f67dc801e0a5.png)
因此,我们需要知道一句话:线程在执行代码的时候,CPU的执行权随时有可能会被其他线程抢走。
假设现在又被线程3抢走了,线程3也做了一次自增,ticket
就变成了 3
。
那么在这个时候,不管是 线程1
还是 线程2
还是 线程3
,继续往下打印票号的时候,它在打印的都是 3号票
。
![image-20240506172935383](https://img-blog.csdnimg.cn/img_convert/71511fb6ec5602f968c9a57d1a83dc3c.png)
这就是重复票的由来,其根本原因是:线程执行时有随机性,CPU的执行权随时有可能会被其他的线程抢走。
2)为什么票会超出范围?
假设 ticket
现在已经到 99
了,此时三条线程在抢夺CPU的执行权。
![image-20240506173255831](https://img-blog.csdnimg.cn/img_convert/2e460d3827e5a5d853592f13a9254e8a.png)
假设线程1抢夺到了CPU的执行权,睡10毫秒。
睡10毫秒的时候CPU的执行权被线程2抢到了,线程2也会进来睡10毫秒。
睡10毫秒的时候CPU的执行权被线程3抢到了,线程3也会进来睡10毫秒。
睡眠完后还是会陆陆续续的醒来,继续执行下面的代码。
假设现在线程1醒来了,抢到了CPU的执行权,执行 ticket自增
,由 99
变成了 100
。
![image-20240506173539157](https://img-blog.csdnimg.cn/img_convert/b1cd85de3a9f8a1219c244b5dff9d763.png)
自增完成后还是一样,还没来得及打印,CPU的执行权被线程2抢走了,线程2也做了一次自增,ticket
变成了 101
。
![image-20240506173641189](https://img-blog.csdnimg.cn/img_convert/221ba1a7ebf9c86599190635ba3befdf.png)
同样的,这个时候它也还没有打印,ticket
执行权又被线程3抢走了,线程3也做了一次自增,ticket
变成了 102
。
那么这个时候不管是线程1,还是线程2,还是线程3,它们打印的都是 102号票
。
这就是超出范围的由来。
![image-20240506173846189](https://img-blog.csdnimg.cn/img_convert/a1b0ef54c0c8dfb933a505a119fb36ab.png)
其根本原因跟刚刚是一样的:线程执行的时候是具有随机性的,CPU的执行权随时都有可能会被其他线程抢走,这个就是买票所产生的原因。
那我们该如何纠正呢?
如果我们能将操作数据的这段代码给锁起来,当有线程进来后,其他的线程就算抢夺到了CPU的执行权,也得在外面等着,它进不来。
只有当线程1出来了,其他的线程才能进去。
三、同步代码块
把操作共享数据的代码锁起来
此时会用到一个关键字 synchronized
,在它的后面写 锁对象
就行了
synchronized (锁){
操作共享数据的代码
}
细节1:在最初锁的状态是默认打开的,如果有一个线程进去了,此时锁就会自动关闭。
细节2:当锁里面所有的代码都执行完毕了,线程出来了,这时候锁才会自动打开。
四、修改代码
小括号中需要写锁对象,这个锁对象是任意的,任意到我在上面创建一个Object的对象都可以,只不过我们一定要切记,这是一个锁对象,一定要是唯一的,即在对象前面加一个静态关键字就可以保证唯一了,即:MyThread这个类不管创建多少对象,这里的obj都是共享的,都是同一个。
public class MyThread extends Thread {
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;//0 ~ 99
static Object obj = new Object();
@Override
public void run() {
//此时我们就可以将obj放在小括号中,当做锁对象
//这样就能解决线程安全的问题,这种解决方式我们会叫做:同步代码块
//利用同步代码块将操作共享数据的代码给锁起来,让同步代码块里的代码轮流进行的
while (obj) {
synchronized (MyThread.class) {//相当于将共享数据的代码锁起来了,此时不管你有多少条线程,这个里面的代码都是轮流执行的。
//同步代码块
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
}
五、同步代码块的两个小细节
1)synchronized同步代码块不能写在循环的外面
如果你写在循环的外面就会有一个小问题:
@Override
public void run() {
synchronized (obj) {
while (true) {
//同步代码块
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
运行上面代码,你会发现窗口1将所有的代码都卖完了,压根就没有窗口2,窗口3什么事。
![image-20240506175243124](https://img-blog.csdnimg.cn/img_convert/baac078defcca94877a548858b8cb7a5.png)
程序刚开始运行的时候,窗口1、窗口2、窗口3它会去抢夺CPU的执行权。
如果说窗口1抢到了,此时它就会进来,进来后,这里的锁就关闭了。
就算窗口2、窗口3抢到CPU的执行权,它也得在synchronized的外面等着,一直等到synchronized里面所有的代码全都执行完毕后,线程1出来了,2跟3才有可能进去。
由于synchronized里面是循环,窗口1只有将100张票全部卖完了,线程1才会出来,此时就会导致一个线程会将所有的票卖完。
@Override
public void run() {
synchronized (obj) {
///----------------------------------------------------
while (true) {
............
}
}
}
2)synchronized里面的锁对象一定要是唯一的
那如果不是唯一的,会出现什么效果呢?
如果现在有两条线程,两条线程看的是不同的锁,那么这把锁还有意义吗?
![image-20240506180139784](https://img-blog.csdnimg.cn/img_convert/f5fb37d3c68909aa581a724384e07599.png)
就没意义了,如果每条线程对应的锁不一样,那这个synchronized代码块就等于没有写。
下面代码将之前的obj改成this,当线程1进来后,这里的this表示的就是线程1本身,线程2进来后,this表示的就是线程2本身,此时这里的锁对象就是不一样,我们可以来运行一下。
@Override
public void run() {
while (true) {
synchronized (this) {
//同步代码块
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
程序运行完毕,可以发现卖重复票的情况和超票的情况依旧存在。
![image-20240506180454350](https://img-blog.csdnimg.cn/img_convert/901debc56f71329b14984f243e330f25.png)
所以 synchronized里面的锁对象一定要是唯一的。
但一般我们会怎么写呢?一般我们会在这写当前类的字节码文件对象。
其实说的就是那个class的对象,这个对象其实是唯一的,因为在同一个文件夹里面只能有一个 MyThread.class
,因此字节码文件对象一定是唯一,那么既然你是唯一的,那我就把你拿过来当做是锁对象就行了。
@Override
public void run() {
while (true) {
synchronized (this) {
//同步代码块
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}