java基础之线程同步机制(适合纯小白)

上文我们聊到了如何创建多线程的两种方式,现在有如下场景。

假设存在一个电影院,现在有100张票等待出售,同时有三个窗口出售电影票。

代码如下:

java基础之线程同步机制(适合纯小白)

为了让效果更明显,这里让线程等待了10millis。

接下来我们在main方法中创建三个线程来“买票”

java基础之线程同步机制(适合纯小白)

运行代码,得到如下结果:

java基础之线程同步机制(适合纯小白)

售票竟然出现了重复的票,还出现了不存在的票。这是为什么呢?

票  我们可以看作是一个公共资源。为了让问题简化,我们假设现在只有A线程和B线程访问它。

--------------------------------------以下场景可能出现

当A线程抢夺到CPU资源时运行run方法,当运行完

System.out.println(Thread.currentThread().getName() + "当前正在出售第" + ticket + "张票");

这一句代码后,这时候可能A线程失去了CPU资源(注意:此时ticket--这一句代码未运行,此时ticket的值仍然是100,控制台打印的值为"Thread-A当前正在出售第100张票"),B线程成功抢夺到CPU资源,运行run方法。当运行到

System.out.println(Thread.currentThread().getName() + "当前正在出售第" + ticket + "张票");

这一句代码时,控制台成功输出"Thread-B当前正在出售第100张票"(出现重复的票),然后此时B线程同样可能失去CPU资源,由A再次成功抢夺到执行权,运行A线程的

ticket--;

ticket此时的值为99,A线程失去资源,B线程获得资源,B线程中再次运行

ticket--;

ticket此时的值为98。经历多次运行后,此时假设票还剩余1张。

A线程获得CPU资源,运行到if(ticket>0)这一句后,由于1>0为true,成功进入循环,此时A线程失去资源,由B成功上位。ticket此时等于1,B线程也成功进入while循环。

然后B失去CPU资源,A重新上位。A打印:Thread-A当前正在出售第1张票,A结束,ticket此时的值为0。B重新获得运行资源,B由于if条件已经为true成功进入分支,直接打印:Thread-A当前正在出售第0张票。(不存在的票)A线程结束。

那么问题来了。在正常的生产环境中,我们一般不会允许这样的情况出现,如何才能避免线程安全问题呢?

以下给出解决方案。

方案1:使用synchronized关键字同步代码块

废话不多数,直接上代码:

java基础之线程同步机制(适合纯小白)

现在加入synchronize关键字后,再次运行main方法:

java基础之线程同步机制(适合纯小白)

可以看出此次运行,票数连续且正常(未出现重复票和不存在的票)。

原理:现在有两个线程A和B

当线程A获取CPU资源后进入run方法,遇到了synchronize修饰的代码块,此时会A会检查synchronize代码块中是否存在锁对象。如果存在锁对象,就会获取锁对象并成功进入同步中执行。

此时A失去了CPU资源,B成功上位。B遇到synchronize修饰的代码块。此时B同样也会检查synchronize代码块中是否有锁对象。当然没有,因为锁对象现在被A线程持有。这时,B线程会进入到阻塞状态,会一直等待A线程归还锁对象,然后B才能获取锁对象并进入同步中执行。

当A线程运行出被synchronize修饰的代码块后,锁对象会被归还。此时B线程如果成功抢夺到资源,就能成功进入同步。

同步保证了只能有一个线程在同步中操作共享数据。但是程序需要频繁的判断锁,获得锁,释放锁。程序的效率会下降。

方案2:使用synchronized关键字同步方法

java基础之线程同步机制(适合纯小白)

运行main方法买票:

java基础之线程同步机制(适合纯小白)

同样运行正确。

这个办法的原理和上种方法是一样的。同样是通过控制锁对象来实现代码的同步。这里的锁对象其实就是Runnable的实现类(即this)

如果同步方法使用的static关键字修饰(静态同步方法),那么此时的锁对象就是本类的class属性(class文件对象)。

方案3:使用Lock锁(推荐)

java基础之线程同步机制(适合纯小白)

强烈建议把解锁的代码放入到finally块中去。