线程安全和同步--java进阶day15

1.示例:引出线程安全

在引出线程安全之前,我们先写一个案例

我们选择Runnable接口开启线程,票的数量固定为常量100,然后创建一个资源对象,三个线程对象,三个线程对象分别命名,并且共用一个资源对象

右键,运行程序,控制台打印出来的全是负号票

我们修改一下票数的条件,虽然没有负号票了,但是三个窗口各卖了100张票,相当于卖了300张票,不符合题目要求

这就是线程安全的问题,现在我们的数据极其不安全,有负号票还有重复票

2.线程不安全的场景

多条线程共享操作一份资源 时,就会出现线程不安全

[1]出现符号票的原因

下图为示例中我们写的代码,为方便,我们将票的数量改为1

假设线程1先抢到执行权,走到打印语句这,此时ticket=1,线程1打印完,正准备减减时,线程2抢到了执行权

线程2也走到打印语句这,此时ticket没来得及减减,依旧是1,线程2刚要打印,又被线程3抢到了执行权,线程3也走到打印语句这,ticket还是1

当线程3准备打印时,线程1抢走了执行权,线程1继续走未执行的操作(线程1位于红圈,下一步就直接走减减操作)

ticket变为0,因为是共享资源,所以三个线程手里的ticket都会为0

线程1执行完毕后,线程2继续执行,线程2上一次正准备打印,所以这次线程2会继续走打印语句,但此时ticket为0,所以线程2会打印0号票,打印完后,做减减操作,ticket=-1

线程3再操作,打印出来的就是符号票了

出现这个问题的原因,就是因为cpu会在多条线程来回切换,导致数据的不安全,出现重复票也是这个原因

3.线程同步

要解决上面的问题,我们只需要让某个线程在执行操作时上锁,其他线程别来打扰

线程同步就是将多条语句操作的共享数据的代码上锁,让任意时刻只有一个线程可以执行

[1]同步代码块

将要上锁的代码放入{}内,()可以给任意的对象

使用同步代码块后,控制台不再有重复票和负号票,并且每个窗口都有在卖票

细节

如图,我们使用Thread类来开启线程,还是之前的代码,在循环那里上锁,然后创建三个Thread对象,分别命名,右键运行


我们会发现,出现了重复票

锁对象可以是任意对象,但是必须保证多条线程使用的锁对象是同一把锁


上图,我们创建了三次Task对象,也就创建了三把锁(o对象),各自锁各自的,无法达到上锁效果

而且,创建了三次Task对象,也就相当于有三块空间,每块空间都有票,各自卖各自的票,票也不是共享数据


所以,为了保证票和锁都是唯一的,我们需要加上static修饰

此时,控制台打印的票就不再会有重复的了

推荐:以后在给锁对象时,直接给类的字节码对象,这是唯一的


解疑:使用Runnable接口开启线程时,我们只创建了一个资源对象,三个窗口都是线程对象共用的这一个资源对象,所以不需要static修饰。

继承Thread类创建的对象是直接包含了资源的,所以三个对象都有各自的资源,需要用static修饰保证唯一

[2]同步方法

在同步方法里的所有代码都是同步的

如图,还是之前卖票的代码,我们将循环里的逻辑写成同步方法

同步方法里也有锁对象,但是被内部隐藏了

同步方法分为静态和非静态,静态的方法里面的锁对象是类字节码对象,非静态则是this

[3]Lock锁

使用Lock锁,我们可以自定义哪里加锁,哪里释放锁

Lock锁是接口,所以我们使用它的实现类ReentrantLock(互斥锁)

互斥锁常用的方法

如图

上图虽然安全实现了卖票,但是我们会发现控制台还在运行并没有结束

这是因为当ticket=0后,循环结束,还没来得及执行下面的lock.unlock()释放锁,导致程序一直运行,解决方法就是在结束循环前释放锁即可

也可以使用finally强行释放锁

4.死锁

如图,我们通过匿名内部类来实现Runnable开启线程

创建了两个锁对象,然后进行同步嵌套

此时运行就会导致程序卡死

我们用图片来解释

如图,假设线程1抢到了执行权,将A锁关上,因为锁对象都是唯一的,所以线程2那边的A锁也一起关上

当线程2开始执行时,线程2执行自己的任务,把B锁关上,线程1那边的B锁也被关上,导致双方都被锁住,无法释放锁,程序卡死

posted @ 2025-05-16 18:48  直実  阅读(5)  评论(0)    收藏  举报