“全栈2019”Java多线程第十六章:同步synchronized关键字详解

难度

初级

学习时间

30分钟

适合人群

零基础

开发语言

Java

开发环境

  • JDK v11
  • IntelliJ IDEA v2018.3

友情提示

  • 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
  • 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!

1.温故知新

前面在《“全栈2019”Java多线程第五章:线程睡眠sleep()方法详解》一章中介绍了如何暂时停止执行线程。

《“全栈2019”Java多线程第六章:中断线程interrupt()方法详解》一章中介绍了如何停止线程。

《“全栈2019”Java多线程第七章:等待线程死亡join()方法详解》一章中介绍了如何让一个线程等待另一个线程执行完毕再执行

《“全栈2019”Java多线程第八章:放弃执行权yield()方法详解》一章中介绍了如何让一个线程放弃执行权。

《“全栈2019”Java多线程第九章:判断线程是否存活isAlive()详解》一章中介绍了如何判断一个线程是否存活

《“全栈2019”Java多线程第十章:Thread.State线程状态详解》一章中介绍了线程的6种状态

《“全栈2019”Java多线程第十一章:线程优先级详解》一章中介绍了如何设置/获取线程的优先级

《“全栈2019”Java多线程第十二章:后台线程setDaemon()方法详解》一章中介绍了如何将一个线程设置为后台线程

《“全栈2019”Java多线程第十三章:线程组ThreadGroup详解》一章中介绍了线程组ThreadGroup

《“全栈2019”Java多线程第十四章:线程与堆栈详解》一章中介绍了线程与堆栈信息之间的关系

《“全栈2019”Java多线程第十五章:当后台线程遇到finally》一章中介绍了当后台线程遇到finally时,finally代码块不会被执行的情况

现在我们来讲解synchronized关键字

2.单线程

我们来看一个实际生活中的例子。

上图展示的是售票大厅,里面有很多售票窗口,每一个售票窗口都可以看作是一个线程,它们都在同时卖票。

为了弄清卖票的一个流程,我们单独来看一个售票窗口它是怎么工作的

当然了,这是简化版售票流程,实际的应该比这个还要复杂,但那不是我们要关注的重点。

单独的售票流程看完了,我们再来看一个多窗口的售票流程

这个看起来很复杂,其实不然,每个售票窗口都会去查询余票和执行购票这个行为,所以结合起来线条就有点多。

接下来,我们会用程序来描述这个售票流程。

首先,来看一个售票窗口是怎么卖票的。

演示:

请使用程序描述单个售票窗口的售票流程。

请观察程序代码及结果。

代码:

TicketingTaskThread类:

Main类:

结果:

从运行结果来看,符合预期。100张票全部无误卖出。

为什么要说是无误卖出呢?难道还会卖错票?

因为当我们有多个售票窗口同时卖票时就可能卖错票,所以这里就特别强调无误卖出

我们单个售票窗口一定不会卖错票吗?

一定不会卖错票。这里来解释一下为什么单个售票窗口就一定不会卖错票呢?

从售票流程来看,无论是查询余票还是执行购票都是一个人从头执行到尾,中间不会出现车票数量不足还卖出了错误车票以及车票重复售卖等等情况

车票数量不足还卖出了错误车票情况:

上图中,0号车票是错误的车票,属于卖出错误票

车票重复售卖情况:

上图中,两个售票窗口都卖出了0号车票,属于重复售票

看了两个错误售票结果之后,我们发现:之所以会出现这些错误是因为我们有两个或两个以上的售票窗口同时售票造成的

再来回顾我们这个例子中的代码。

首先,我们创建了一个售票任务线程TicketingTaskThread类,该类继承自Thread类:

然后,在售票任务线程TicketingTaskThread类中定义了一个表示车票池的变量quantity:

接着,我们重写了run()方法:

然后,在run()方法内部我们使用while循环不断卖票:

while循环条件是余票大于0:

然后,在循环体中我们打印当前售出的车票:

接着,我们再把车票池内的车票数量减1:

然后,我们在Main类的main()方法中创建了售票任务线程TicketingTaskThread类的对象:

接着,我们启动售票任务线程TicketingTaskThread,开始售票:

执行结果:

结果显示:整个卖票过程准确无误。

我们在前面的教学中讲到:可以将每个售票窗口看作是一个线程

本例中,我们只创建了一个售票任务线程TicketingTaskThread类的对象,所以就是单线程售票。

以上是单个售票窗口售票时的情况,那么多个售票窗口情况又会是怎样呢?

通过上面动画演示可以看出,多个售票窗口同时售票时会出现错误票和重复票等等情况。下面,我们就来着力解决这些问题。

单线程不会出现线程安全问题,而多线程则有可能会出现线程安全问题。

3.多线程访问共享数据

我们现在就来两个售票窗口同时售票。

在开始演示之前,得向大家说明一个情况:售票任务线程TicketingTaskThread类不能再继承Thread类,而应该改为实现Runnable接口

为什么要这么做呢?

我们还是来个实际案例说明情况吧。

演示:

请使用两个售票窗口同时售票。

请观察程序代码及结果。

代码:

TicketingTaskThread类:

Main类:

结果:

从运行结果来看,我们把1-100号车票重复出售了两次。

为什么会出现两次呢?

因为我们创建了两个售票任务线程TicketingTaskThread类的对象,而每个售票任务线程TicketingTaskThread对象里面都有一个车票池(车票数量都是100张),再加上两个售票任务线程TicketingTaskThread都启动了,两同时开始出售各自的1-100号车票,所以就出现了重复票。

怎么解决这个问题?

之所以会出现把1-100号车票重复出售了两次情况,是因为我们每个线程里面都各自持有一个车票池,所以才会出现重复票的情况。要是车票池只有一个,售票线程有多个的话,问题就解决了

在之前的教学中,我们写过这样的代码:

从代码中可以看到,我们创建了一个Runnable对象,两个Thread对象,并把Runnable对象传递给了两个Thread对象。

如果我们把车票池放入Runnable对象中,这个问题就得到了解决

于是,我们改写程序代码。

演示:

请使用两个售票窗口同时售票。

请观察程序代码及结果。

代码:

TicketingTaskThread类:

Main类:

结果:

从运行结果来看,多个线程共用一个车票池我们成功实现了。

此次程序改动有两处,第一处就是我们的售票任务TicketingTaskThread类从之前的继承Thread类改为了实现Runnable接口:

第二处就是我们在Main类里的main()方法中创建了一个TicketingTaskThread类的实例,两个线程同时开始售票:

针对类似的多个线程同时拥有资源的情况,只需把公共资源设为唯一即可解决问题。以上问题专业术语叫作:多个线程访问共享数据

4.非同步

不知道大家发现了没有,上一小节最后程序执行结果中,出现了两个100号车票。

这是第一张100号车票:

这是第二张100号车票:

怎么会出现两张100号车票呢?这不就出现了重复票了吗?

对,我们卖出了重复票。错误原因通过下面这个动画来展示给大家看:

两个线程同时拿到100号车票,所以才造成出售两张100号车票现象

怎么解决这个问题呢?

之所以会出现这个问题,是因为我们两个或以上的线程同时拿到100号车票

也就是说如果让线程们一个一个的拿,就不会出现这类情况。也就是让线程们同步执行即可

解决此类问题需要用到一个关键字:synchronized

下面我们就来介绍synchronized关键字。

5.同步代码块

单词synchronized中文是同步的意思。

synchronized关键字表明一段代码需要同步执行。

synchronized关键字可以作用在代码块上:

synchronized关键字作用在代码块上时叫作同步代码块。将需要同步的代码写在同步代码块中。

下面,我们就使用synchronized关键字来改写程序。

演示:

请使用两个售票窗口同时售票。

请观察程序代码及结果。

代码:

TicketingTaskThread类:

Main类:

结果:

从运行结果来看,当多个线程遇到同步代码块时,会变得井然有序,它们之间不再互相竞争。

简单的回顾程序代码。

Main类没有做改动,所以不用讲解。

TicketingTaskThread类加入了同步代码块,所以来看看我们的同步代码块是怎么写的:

可以看到的是,我们写了一个synchronized关键字:

然后,在synchronized关键字后面跟上了一对小括号:

接着,在小括号里面写上了this关键字:

这代表什么意思呢?

这叫同步锁。下面是同步锁的定义:

每个Java对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁,当消费者线程试图执行以带有synchronized(this)标记的代码块时,消费者线程必需先获得this关键字引用的Stack对象的锁。

本章节只介绍同步锁,不详细讲解同步锁,下一章详细讲解。

最后,我们在小括号后面跟上一对花括号来声明代码块作用域:

好了,程序至此回顾完毕。

6.同步方法

synchronized关键字除了作用在代码块上,还可以作用在方法上:

synchronized关键字作用在方法上时叫作同步方法。

下面我们就使用同步方法来改写程序。

演示:

请使用两个售票窗口同时售票。

请观察程序代码及结果。

代码:

TicketingTaskThread类:

Main类:

结果:

从运行结果来看,符合预期。

简单地来回顾代码。

首先,Main类没有改动,所以不用再赘述。

然后,我们在TicketingTaskThread类中添加了一个出售车票的同步方法sellTickets()

执行结果如上面动图所示。

同步方法和同步代码块执行的结果就本例而言,是一样的,同步方法和同步代码块都有各自的应用场景,希望大家可以选择合适的来用。

总结

  • 单线程不会出现线程安全问题,而多线程则有可能会出现线程安全问题。
  • 针对类似的多个线程同时拥有资源的情况,只需把公共资源设为唯一即可解决问题。这叫多个线程访问共享数据。
  • synchronized关键字表明一段代码需要同步执行。
  • synchronized关键字作用在代码块上时叫作同步代码块。将需要同步的代码写在同步代码块中。
  • synchronized关键字作用在方法上时叫作同步方法。

至此,Java中synchronized关键字相关内容讲解先告一段落,更多内容请持续关注。

答疑

如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。

上一章

“全栈2019”Java多线程第十五章:当后台线程遇到finally

下一章

“全栈2019”Java多线程第十七章:同步锁详解

学习小组

加入同步学习小组,共同交流与进步。

  • 方式一:关注头条号Gorhaf,私信“Java学习小组”。
  • 方式二:关注公众号Gorhaf,回复“Java学习小组”。

全栈工程师学习计划

关注我们,加入“全栈工程师学习计划”。

版权声明

原创不易,未经允许不得转载!

了解更多
举报
评论 0