在《【Java】线程并发、互斥与同步》(点击打开链接)中利用了操作系统通过操作信号量控制的原始方法,完成了线程的互斥与同步,说句题外话,其实这个信号量的算法,是著名的迪杰斯特拉创造的,也就是数据结构、计算机网络上面最短路径算法、迪杰斯特拉算法、Dijkstra算法的贡献人。其实Java里面根本就不需要自己定义一个信号量来实现临界区,Java对于临界区的实现早已封装好了,而且synchronized还是Java的关键字。
那么,到底怎么来使用这个关键字呢?下面就对上次《【Java】线程并发、互斥与同步》(点击打开链接)中的程序进行改进,不用信号量,同样来实行里面相应的功能。
一、基本目标
首先来回顾一下《【Java】线程并发、互斥与同步》(点击打开链接)中的程序,说的是有四个黄牛,分别是线程1、线程2、线程3、线程4,目的是要刷光票站里面的20张票,而且各自抢各自的票,不会出现多个黄牛抢一张票的情况,也就是说一个黄牛对应一张票
而且,每次运行结果不同:
这个程序上次是以信号量来实现的,下面就用利用synchronized(this)完成线程的临界区,在这里也就是买票的部分,只有一个售票窗口,每次只能容纳一个黄牛去抢票,而且这次每个线程都是以cpu的频率去刷票,刷2000只不虚,每次完成的结果不同,且一张票只落到一个黄牛的手里,
不会产生以下的无序、乱序的冲突:
甚至刷20000票都不虚:
由于程序是健壮的,你改到二十万张也是照样能跑:
每次运行结果不同是由于CPU分配的资源不同。
二、基本思想:
所谓的利用synchronized(this)完成线程的临界区,就是在一个implements Runnable的进程类里面有如下的一段代码,形成所谓的线程临界区:
至于许多不知道在写什么的操作系统,对于临界区是这样说的:每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
其实,在这里,票就是临界资源,临界区就是黄牛如何买票。
三、制作过程
1、首先和《【Java】线程并发、互斥与同步》(点击打开链接)一样在主函数中开一个进程,其中这个进程里面有四个线程:
public class Threadtest { public static void main(String[] args) throws Exception { GetTicket getTicket = new GetTicket(); new Thread(getTicket, "线程1").start(); new Thread(getTicket, "线程2").start(); new Thread(getTicket, "线程3").start(); new Thread(getTicket, "线程4").start(); } }
2、这个进程类如下:
class GetTicket implements Runnable { //ticket是票数、isNotRead的设置是为了下面对于统计的输出只输出一次 private int ticket = 200000; private boolean isNotRead = true; private int count1 = 0; private int count2 = 0; private int count3 = 0; private int count4 = 0; public void run() { while (this.ticket > 0) { //临界区开始,所谓的临界区就是仅能有一个线程访问的部分 synchronized (this) { if (this.ticket > 0) { //对票的操作 this.ticket--; System.out.println("票" + (200000 - this.ticket) + "被" + Thread.currentThread().getName() + "买走,当前票数剩余:" + this.ticket); //Thread.currentThread().getName()取当前线程的名字 switch (Thread.currentThread().getName()) { case "线程1": this.count1++; break; case "线程2": this.count2++; break; case "线程3": this.count3++; break; case "线程4": this.count4++; break; } } else { //这4个线程无论怎么样都会经过这里的,所以为了只输出一次,必须设置一个布尔值 //这个isNotRead某种程度上也是一个信号量 if (isNotRead) { System.out.println("^_^票已卖光,明天请早,都各自散吧!(杀死所有进程)"); System.out.println("=========得票统计========="); System.out.println("线程1:" + count1 + "张"); System.out.println("线程2:" + count2 + "张"); System.out.println("线程3:" + count3 + "张"); System.out.println("线程4:" + count4 + "张"); isNotRead = false; } //这段与Thread.currentThread.stop()等价,eclipse在JDK1.7中推荐这样写 Thread.yield(); } } //临界区结束 } } }
因此,整个进程如下:
class GetTicket implements Runnable { //ticket是票数、isNotRead的设置是为了下面对于统计的输出只输出一次 private int ticket = 200000; private boolean isNotRead = true; private int count1 = 0; private int count2 = 0; private int count3 = 0; private int count4 = 0; public void run() { while (this.ticket > 0) { //临界区开始,所谓的临界区就是仅能有一个线程访问的部分 synchronized (this) { if (this.ticket > 0) { //对票的操作 this.ticket--; System.out.println("票" + (200000 - this.ticket) + "被" + Thread.currentThread().getName() + "买走,当前票数剩余:" + this.ticket); //Thread.currentThread().getName()取当前线程的名字 switch (Thread.currentThread().getName()) { case "线程1": this.count1++; break; case "线程2": this.count2++; break; case "线程3": this.count3++; break; case "线程4": this.count4++; break; } } else { //这4个线程无论怎么样都会经过这里的,所以为了只输出一次,必须设置一个布尔值 //这个isNotRead某种程度上也是一个信号量 if (isNotRead) { System.out.println("^_^票已卖光,明天请早,都各自散吧!(杀死所有进程)"); System.out.println("=========得票统计========="); System.out.println("线程1:" + count1 + "张"); System.out.println("线程2:" + count2 + "张"); System.out.println("线程3:" + count3 + "张"); System.out.println("线程4:" + count4 + "张"); isNotRead = false; } //这段与Thread.currentThread.stop()等价,eclipse在JDK1.7中推荐这样写 Thread.yield(); } } //临界区结束 } } } public class Threadtest { public static void main(String[] args) throws Exception { GetTicket getTicket = new GetTicket(); new Thread(getTicket, "线程1").start(); new Thread(getTicket, "线程2").start(); new Thread(getTicket, "线程3").start(); new Thread(getTicket, "线程4").start(); } }
好了,既然临界区都怎么写,那么之后的生产者、消费者问题、读者写者问题、理发师问题、哲学家进餐问题,还是问题吗?就是来秀逗、开玩笑、送分的!都是一个道理,就是来看你,怎么处理好多个进程对同一资源提出请求时,你怎么安排好,避免出现以下的情况: