上一节我们说到了线程的基本概念和java中构造线程对象的两种方式。
今天我们来介绍以下线程之间的关系。这里说的线程是指交互线程(若并发执行的多个线程之间需要共享资源或交换数据,则称这组线程为交互线)。并发执行的交互线程之间存在与时间有关的错误,这是因为线程的执行是根据操作系统自动调度的,如果一个线程修改了共享资源,那么另一个线程再使用这个共享资源的时候,并不知道它被另外的线程使用过,会导致bug。所以有必要研究交互线程之间存在的问题,并针对性解决。
交互线程之间存在两种关系:竞争和协同
线程之间的竞争是指两个线程要访问同一资源,一个线程通过操作系统分配得到该资源,那么另一个线程将不得不等待,这时一个线程的执行就可能影响到同其竞争资源的其他线程。
补充知识:资源竞争可能会出现的两个问题:死锁和饥饿。死锁是说一组线程如果都获得了部分资源,还想得到其他线程所占用的资源,最终所有线程都将陷入死锁。饥饿是当一个线程由于其他线程总是优先于它而被无限期延后。
为了协调好线程对资源的竞争,保证线程能互斥的访问临界资源尤为重要。
线程互斥和临界区管理
线程互斥是指若干个线程要使用同一共享资源时,任何时刻最多允许一个线程使用,其他线程必须等待,直到占有资源的线程释放该资源。
我们把共享变量代表的资源称为临界资源,并发线程中与共享变量有关的程序段称为临界区。
由于线程时并发执行,启动后的先后执行顺序不可预知,所以,操作系统规定了对于交互线程各自进入临界区的3个调度原则:
1.一次至多一个线程能在他的临界区。
2.不能让一个线程无限期地停留在它的临界区内。
3.不能强迫一个线程无限地等待进入它的临界区。特别地,进入临界区的任意线程不能妨碍正在等待进入的其他线程的进展。
总结该调度原则就是:无空等待、有空让进、择一而入、算法可行。算法可行时说所选策略不能造成线程饥饿甚至死锁。
java的线程互斥实现
java中采用关键字synchronized用于声明程序段为临界区,使线程对临界资源采用互斥原则。synchronized有两种用法:声明一条语句或声明一个方法。
1.同步语句
synchronized(对象)
语句(可为复合语句(大括号包括的一串语句))
2.同步方法
synchronized 方法声明
该方法与以下方法效果相同:
方法声明
synchronized(this)
{
方法体
}
同步语句的执行过程如下:
说完线程之间的竞争与互斥,接下来我们说一下线程之间的协作关系及相应处理。
所谓线程协作是说多个线程为完成同一任务而分工协作时,它们彼此之间有联系,知道其他线程的存在而且受到其他线程的影响。(PS:这点是和线程互斥不同的地方,线程互斥是并发执行的线程不知道其他线程的存在,但是共用了同一资源,使用互斥避免干扰,而线程协调知道其他线程的存在,并且要利用这种信号协调多个线程共同完成一个操作。)但是由于线程启动后都是以不可知的速度运行,所以需要相互协作的线程在某些协调点上协调各自的工作。当合作线程中的一个到达协调点,在尚未得到其他伙伴线程发来的信号之前应阻塞自己,直到收到其他伙伴线程发来的协调信号后方能被唤醒并继续执行。这种方式称为线程同步。
由于线程同步是基于某个条件来协调多个线程之间的活动,线程互斥是根据互斥区域来协调多个线程,所以将线程互斥和线程同步统称为线程同步。
下面说一下线程同步机制:
操作系统实现线程同步有一种工具称为信号量和PV操作,描述如下:
1.背景
多个线程需要同一个共享变量进行操作,所以多个线程间必须互斥执行。
2.设置信号值
为共享变量设置一个信号量,信号量有多种状态,就像交通信号灯有多种颜色。设置信号量状态可以有多种状态。测试信号量称为P操作,改变信号量称为V操作,这两种操作互斥的执行,并且执行时不能被打断。
3.线程根据信号量状态执行
当一个线程执行时,它先测试信号量的状态,若状态合适,则执行,并且改变信号量的状态;否则等待,阻塞自己,直至被唤醒。
上面提到线程阻塞后需要被唤醒,这种操作需要线程间的通信,java中线程通信的方法有:java.lang.Object类提供了以下方法:
public final void wait()throws InterruptedException //等待,最终方法
public final void wait(long timeout)throws InterruptedException //等待指定时间
public final void notify() //唤醒一个等待线程
public final void notifyAll() //唤醒所以等待线程
实战演练:
斗地主的游戏相信大家都玩过,共有54张牌,一位地主20张牌,2位农民各17张牌,然后按照出牌规则进行游戏。我们选择斗地主的发牌过程,基于多线程的相关知识,模仿以下斗地主的发牌过程。
主要分为两部分发牌线程和收牌线程。一个发牌线程和3个收牌线程需要进行同步机制处理。说明如下:
1.约定一个缓冲区存放一张牌,为缓冲区是否为空设置一个信号量(两种状态)
2.有一个发牌线程,随机产生1-54中的一个数,每次发一张牌。
3.对于发出的每一张牌,同时有3个收牌线程在争抢。所以需要为每个收牌线程约定一个收牌次序,就是再设置一个信号量,信号量有三种状态,约定为0,1,2,每个值指定一个线程可执行。
4.农民线程在收到17张牌后应该终止,剩余的收发牌过程应该由发牌线程和地主收牌线程实现,这时候就不再需要为区别取牌次序而设置的信号量。
5.发牌线程的优先级应该高于收牌线程。因为每次取走一张牌,接下来应该执行发牌线程。
程序如下:
1package ThreadTest;
2
3import java.awt.Font;
4import java.util.ArrayList;
5import java.util.Collections;
6
7import javax.swing.JFrame;
8import javax.swing.JTextArea;
9
10public class DealCard {
11
12 public DealCard(int cardMax, int number) {
13 CardBuffer buffer = new CardBuffer(number);//创建缓冲区
14 new SendCardThread(buffer, cardMax, number).start();//启动发牌线程
15 String[] titles = {"地主","农民1","农民2"};
16 int[] xs = {300,550,300},ys = {200,320,440};
17 for(int i=0;i<number;i++)
18 new ReceiveCardJframe(buffer, i, titles[i], xs[i], ys[i]);//启动收牌线程
19
20 }
21
22 public static void main(String[] args) {
23 new DealCard(54, 3);
24 }
25}
26
27class CardBuffer{
28 private int value;
29 private boolean isEmpty = true;
30 private int order = 0;
31 private int number;
32 public CardBuffer(int number) {
33 super();
34 this.number = number;
35 }
36
37 synchronized void put(int i) {
38 while(!isEmpty) {
39 try {
40 this.wait();//阻塞当前线程
41 } catch (InterruptedException e) {
42 }
43 }
44
45 this.value = i;
46 isEmpty = false;
47 this.notifyAll();//唤醒所有等待线程
48 }
49
50 synchronized int get(int order) {
51 while(isEmpty || this.order != order) {
52 try {
53 this.wait();
54 } catch (InterruptedException e) {
55 }
56 }
57
58 isEmpty = true;
59 this.notifyAll();
60 this.order = (this.order + 1) % this.number;
61 return this.value;
62 }
63
64 synchronized int get() {
65 while(isEmpty) {
66 try {
67 this.wait();
68 } catch (InterruptedException e) {
69 }
70 }
71
72 isEmpty = true;
73 this.notify();
74 return this.value;
75 }
76}
77
78class SendCardThread extends Thread{
79
80 private CardBuffer buffer;
81 private ArrayList<Integer> cardlist;
82
83 public SendCardThread(CardBuffer buffer, int cardMax, int number) {
84 this.buffer = buffer;
85 cardlist = new ArrayList<>();
86 for(int i=1;i<=cardMax;i++) {
87 cardlist.add(i);
88 }
89 Collections.shuffle(cardlist);//打乱卡牌
90
91 this.setPriority(MAX_PRIORITY);
92 }
93
94 @Override
95 public void run() {
96 for(int i : cardlist) {
97 this.buffer.put(i);
98 }
99 }
100}
101
102class ReceiveCardJframe extends JFrame implements Runnable{
103
104 /**
105 *
106 */
107 private static final long serialVersionUID = 1L;
108 private CardBuffer buffer;
109 private JTextArea text;
110 private int order;
111 private Thread thread;
112 private ArrayList<Integer> cardlist;
113
114
115 public ReceiveCardJframe(CardBuffer buffer, int order, String title, int x, int y){
116 super(title);
117 this.setBounds(x, y, 270, 250);
118 this.setDefaultCloseOperation(EXIT_ON_CLOSE);
119 this.buffer = buffer;
120 this.order = order;
121 this.cardlist = new ArrayList<>();
122 this.text = new JTextArea();
123 this.getContentPane().add(text);
124 this.text.setLineWrap(true);
125 this.text.setEditable(false);
126 this.text.setFont(new Font("Dialog",Font.PLAIN,20));
127 this.setVisible(true);
128 thread = new Thread(this);
129 thread.start();
130 }
131
132
133 @Override
134 public void run() {
135 String[] numbers = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};
136 String[] colors = {"方块","梅花","红桃","黑桃"};
137 String[] wangs = {"小王","大王"};
138 String card = "";
139 String title = this.getTitle();
140
141 while(true) {
142 if((title.startsWith("农民") && cardlist.size() < 17)
143 ||(title.equals("地主") && cardlist.size() < 20)) {
144 int value;
145 if(cardlist.size() > 17)
146 value = this.buffer.get();
147 else
148 value = this.buffer.get(order);
149
150 if(value <= 52)
151 card = colors[(value-1) % 4] + numbers[(value-1) / 4];
152 else
153 card = wangs[value - 53];
154 cardlist.add(value);
155 this.text.append(String.format("%-8s", card));
156
157 try {
158 Thread.sleep(100);
159 } catch (InterruptedException e) {
160 }
161 continue;
162 }
163 cardlist.sort((O1,O2)->O2-O1);
164 this.text.setText("");
165 for(int i: cardlist) {
166 if(i <= 52)
167 card = colors[(i-1) % 4] + numbers[(i-1) / 4];
168 else
169 card = wangs[i - 53];
170 this.text.append(String.format("%-8s", card));
171 }
172 break;
173 }
174 }
175}
到这里呢,我们的多线程回顾部分就结束了,多线程的深入知识有很多,有兴趣的同学可以看看《JAVA并发编程实践》这本书。