今天是我自学Java的第33天。
感谢你的观看,谢谢你。
话不多说,开始今天的学习:线程同步。
想必很多小伙伴应该都经历过去火车站买票的情况。
现有一个案例:火车站有3个售票窗口,一共有100张票要卖,3个窗口同时卖。
对于这种情形,如何使用Java代码来实现?
一、多窗口卖票案例
根据我们这几天的学习,很显然要创建三个线程来解决这种情况,我们选择使用实现Runnable接口的这种方式来创建线程:
创建一个类MyRunnable,实现Runnable接口。
有100张票要售卖。
也就是说类中有一个成员变量是100。
重写run方法。
创建一个循环语句:
因为需要一直卖票,直到票被卖完为止,所以使用循环语句,每循环一次卖一张票,打印卖票信息并且将ticket减一。
创建MyRunnable对象。
创建三个线程。
将MyRunnable对象初始化赋值给它,并且给各个窗口命名。
启动线程。
根据我们前几天学习的线程,我们可以写出这样的代码来实现卖票的需求。
现在看看打印结果:
咦,发现结果怎么和我想象的不一样?
票确实是在卖票,但怎么会是无序的呢?并且有时还会出现重复票。
这是为什么?
因为Java虚拟机的抢占式调度,我窗口壹先进来了,但是我还没有执行完,就切换到窗口贰了。
run方法中的while循环语句执行也是需要时间的,虽然它执行起来很快,需要的时间也很少,但是线程切换更加地快。
于是就会出现:窗口贰还在打印第6张票,窗口叁连续卖了好几张都卖完了这种情况。
当我们使用多个线程访问同一资源的时候,且多个线程对该资源都有操作,就容易出现线程安全问题。
什么叫线程安全问题?
通俗点理解就是:线程一在执行任务的时候,还没有执行完,线程二也进来执行线程一还没执行完的任务,这就乱套了呀,这种情况就是线程不安全。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与票无序的问题,Java中提供了同步机制(synchronized)来解决
二、同步代码块
用synchronized这个关键字来修饰代码块:
Object是Java里的顶层父类,它代表了任意对象。
同步代码块:锁住了lock这个对象,里面的代码就只允许执行一个线程的代码。
什么意思呢?
就是现在进来了一个线程,只要你进入了我锁住的这块代码,你就必须得将我锁住的这块代码执行完。在执行完之前,别的线程根本就进不来。
其中lock可以是任意对象,但要保证它的唯一性。
这又是什么意思呢?
lock就好比是一把锁,线程一、线程二,无论哪个线程进来,面对的lock都是同一把锁,一次就只能进入一个。
如果我们将里面创建Object对象的代码放入到run方法里面,这样就会出现一个问题:
线程启动就会执行run方法,这样的话启动三个线程就会执行三次run方法,如果在run方法里面,就会new三个Object对象,也就是三个不同的lock,这样的话就相当于有三把不同的锁,还是会乱序。
这就是lock要保证它的唯一性的原因。
我们再看看控制台输出情况:
票的打印确实是有序的了,也没有重复卖票。
但是现在问题又来了:第0张票和第-1张票怎么来的?
为什么会出现这种情况?
我们仔细看看while循环的代码:
现在窗口壹在售卖第1张票,卖完之后票数ticket等于0了。
但是窗口贰和窗口叁它们两个线程在干嘛?
它们早就已经进入while循环了,只不过因为先前synchronized锁住的代码块,窗口壹在里面,它们没法进去,只能等在synchronized外面,但是它们已经在while循环里面。
我们仔细分析下这个流程:
(1)窗口壹打印完第1张票,ticket变成了0,通过while循环的判断语句(ticket>0),窗口壹无法进入while循环了。
(2)窗口贰在while循环里面等着,看到窗口壹出来了立马就抢先进去了,这个时候ticket已经为0了,所以它打印第0张票,于是ticket变成了-1,窗口贰循环结束出来了,通过while循环的判断语句(ticket>0),窗口贰也无法进入while循环了。
(3)窗口叁在while循环里面等着,看到刚才抢先自己一步进入的窗口贰出来了,自己终于可以进去了,这个时候ticket已经为-1了,所以它打印第-1张票,于是ticket变成了-2,窗口叁循环结束出来了,通过while循环的判断语句(ticket>0),窗口叁也无法进入while循环了。
以上就是第0张票和第-1张票的由来。
除了这个问题还有一个问题:窗口壹会一直售卖好多张票。我们如何让窗口壹卖第一张,窗口贰卖第二张,窗口叁卖第三张,窗口壹卖第四张……这样一直循环依次卖票?
面对这两个问题,我们将代码进一步优化:
加一个判断语句:如果票数小于等于0,就直接结束循环,不执行后面的语句了。
所以当窗口壹打印完第1张票,ticket变成了0。这时就算窗口贰、窗口叁这两个线程进入了synchronized里面,也会有一个if判断语句中的break直接将循环结束掉。
让该线程睡眠10毫秒:
Thread有一个静态方法sleep(),sleep是睡眠的意思,也就是说窗口壹执行完语句后,会让它睡眠10毫秒,这样的话窗口贰就能进去执行,不然的话根据Java虚拟机的抢占式调度,下一次执行语句的可能还是窗口壹。
以上就是对同步代码块的说明,除了同步代码块,还有同步方法和Lock锁也可以实现同样的功能。
三、同步方法和Lock锁
1.同步方法
同步代码块里面的代码,我们可以将其提取成一个方法,而用synchronized这个关键字来修饰的方法就叫做同步方法:
线程进来遇到同步方法后,就只能进去一个线程,其它线程得等这个线程执行完后才能进去。
同步方法:格式就是在方法声明上加上synchronized这个关键字。
如果ticket大于0就打印输出。
这个同步方法的作用和同步代码块是一样的。
2.Lock锁
lock是一个接口,它提供了比同步代码块和同步方法更广泛的锁定操作,更加地强大和体现了面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了。
Lock是一个接口,无法实例化创建对象,需要其实现类创建对象。
lock方法:顾名思义就是上锁,也就是说上锁后的空间线程只能进去一个,其他线程就不去。
unlock方法:顾名思义就是解锁,将锁解开了,其他线程也就能进去了。
这用我们现实里的一个例子来理解就是:
就相当于我们去上厕所,将门给锁上,这样其他人就进不来了。
就相当于我们上完厕所,将门给解开,这样其他人就能进去了。
一个人就相当于是一个线程。以上就是同步方法和Lock锁,它们和同步代码块的作用其实是大同小异的。
总结:
领取专属 10元无门槛券
私享最新 技术干货