1. 程式人生 > >如何高效實現多視窗賣票

如何高效實現多視窗賣票

多視窗賣票是常見的多執行緒問題,來看看要怎麼搞

方法1,不建議的使用方式

package concurrent.me.ticket;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 多執行緒賣票:5個視窗(執行緒)進行賣票,一共100張票
 * 方法1 - 沒有使用任何同步,執行緒是不安全的
 * @author BarryLee
 * @2018年11月6日@下午11:09:49
 */
public class Ticket1 {
	//初始化票
	private static List<String>tickets = new ArrayList<>();
	static {
		for(int i = 0;i<100;i++) {
			tickets.add("票:"+i);
		}
	}
	//測試
	public static void main(String[] args) {
		for(int i = 0;i<5;i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while(true) {
						//判斷是否還有票
						if(tickets.size()>0) {
							//有的話就睡一下,模擬這個位置有很多工,將問題暴露出來
							try {
								TimeUnit.MILLISECONDS.sleep(2);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							System.out.println(tickets.remove(0));
						}else break;
					}
				}
			}).start();;
		}
	}
}
/*
票:2
票:4
票:0
票:0
票:3
票:5
票:9
票:7
票:6
票:5
票:9
票:14
票:13
票:10
票:10
票:15
票:18
票:17
票:16
票:15
票:20
票:22
票:24
票:21
票:21
票:25
票:28
票:28
票:25
票:25
票:29
票:33
票:29
票:29
票:33
票:34
票:34
票:36
票:34
票:34
票:37
票:41
票:39
票:37
票:37
票:41
票:47
票:46
票:44
票:41
票:48
票:52
票:50
票:50
票:50
票:52
票:59
票:58
票:54
票:52
票:60
票:63
票:62
票:62
票:61
票:65
票:66
票:65
票:65
票:65
票:70
票:72
票:70
票:70
票:73
票:75
票:78
票:76
票:76
票:75
票:79
票:84
票:80
票:80
票:79
票:84
票:88
票:85
票:84
票:89
票:89
票:92
票:90
票:89
票:93
票:94
票:98
票:97
票:96
票:97
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
票:99
Exception in thread "Thread-0" Exception in thread "Thread-4" Exception in thread "Thread-2" Exception in thread "Thread-3" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at concurrent.me.ticket.Ticket1$1.run(Ticket1.java:36)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at concurrent.me.ticket.Ticket1$1.run(Ticket1.java:36)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at concurrent.me.ticket.Ticket1$1.run(Ticket1.java:36)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at concurrent.me.ticket.Ticket1$1.run(Ticket1.java:36)
	at java.lang.Thread.run(Thread.java:748)

*/

方法2,使用比較原始的同步容器Vector,同樣也會出問題

package concurrent.me.ticket;

import java.util.Vector;
import java.util.concurrent.TimeUnit;

/**
 * 多執行緒賣票:5個視窗(執行緒)進行賣票,一共100張票
 * 方法2 - 使用了Vector,Vector是執行緒安全的,但是在這裡也會出問題
 * @author BarryLee
 * @2018年11月6日@下午11:09:49
 */
public class Ticket2 {
	//初始化票
	private static Vector<String> tickets = new Vector<String>();
	static {
		for(int i = 0;i<100;i++) {
			tickets.add("票:"+i);
		}
	}
	//測試
	public static void main(String[] args) {
		for(int i = 0;i<5;i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while(true) {
						//判斷是否還有票
						if(tickets.size()>0) {
							//有的話就睡一下,模擬這個位置有很多工,將問題暴露出來
							try {
								TimeUnit.MILLISECONDS.sleep(2);
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
							/* 問題就在這裡:tickets.size()是安全的,tickets.remove()也是安全的
							 * 但是這兩個方法加起來的時候並沒有原子性
							 * 當容器還有最後一個元素的時候,一個執行緒來訪問,發現還有,然後就去拿,還沒拿到
							 * 然後這時CPU被第二個執行緒佔用的,他也去訪問,也發現容器中還有票,也去拿票
							 * 然後其中一個拿到了票,其餘的執行緒去拿的時候容器已經是空的了,然後就出bug了!!
							 */
							System.out.println(tickets.remove(0));
						}else break;
					}
				}
			}).start();;
		}
	}
}
/*
票:0
票:4
票:3
票:2
票:1
票:5
票:9
票:8
票:7
票:6
票:10
票:14
票:13
票:12
票:11
票:15
票:19
票:18
票:17
票:16
票:20
票:24
票:23
票:21
票:22
票:25
票:29
票:28
票:27
票:26
票:30
票:33
票:34
票:32
票:31
票:35
票:39
票:38
票:37
票:36
票:40
票:43
票:41
票:44
票:42
票:45
票:49
票:48
票:47
票:46
票:50
票:54
票:51
票:52
票:53
票:55
票:59
票:58
票:57
票:56
票:60
票:64
票:63
票:61
票:62
票:65
票:66
票:68
票:69
票:67
票:70
票:71
票:74
票:72
票:73
票:75
票:79
票:78
票:77
票:76
票:80
票:84
票:83
票:82
票:81
票:85
票:86
票:87
票:89
票:88
票:90
票:91
票:93
票:92
票:94
票:95
票:96
票:97
票:98
票:99
Exception in thread "Thread-3" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
	at java.util.Vector.remove(Vector.java:834)
	at concurrent.me.ticket.Ticket2$1.run(Ticket2.java:35)
	at java.lang.Thread.run(Thread.java:748)
*/

方法3,這個方法OK,但是效率較低

package concurrent.me.ticket;

import java.util.Vector;
import java.util.concurrent.TimeUnit;

/**
 * 多執行緒賣票:5個視窗(執行緒)進行賣票,一共100張票
 * 方法3 - 使用synchronized鎖來解決上一個例子的問題
 * 缺點是相對來講比較慢
 * @author BarryLee
 * @2018年11月6日@下午11:09:49
 */
public class Ticket3 {
	//初始化票
	private static Vector<String> tickets = new Vector<String>();
	static {
		for(int i = 0;i<100;i++) {
			tickets.add("票:"+i);
		}
	}
	//測試
	public static void main(String[] args) {
		for(int i = 0;i<5;i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while(true) {
						synchronized (tickets) {
							if(tickets.size()>0) {
								try {
									TimeUnit.MILLISECONDS.sleep(2);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
								System.out.println(tickets.remove(0));
							}else break;
						}
					}
				}
			}).start();;
		}
	}
}

方法4,最後一個了,也就是建議的寫法,上一個能找到工作,但這個能找到好工作哈哈哈

package concurrent.me.ticket;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

/**
 * 多執行緒賣票:5個視窗(執行緒)進行賣票,一共100張票
 * 方法4,這個是正確的操作了,效率相對synchronized較高而且不會出現前面的問題
 * @author BarryLee
 * @2018年11月6日@下午11:30:36
 */
public class Ticket4 {
	//初始化容器 - 使用jdk1.5之後新增的併發容器ConcurrentLinkedQueue
	//底層的add方法有一個checkNotNull(e)方法,所以當add(null)的時候會丟擲異常,null是新增不進去的
	private static Queue<String>queue = new ConcurrentLinkedQueue<>();
	static {
		for(int i = 0;i<100;i++) {
			queue.add("票:"+i);
		}
	}
	public static void main(String[] args) {
		for(int i = 0;i<10;i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while(true) {
						String s = queue.poll();
						/*
						 * 這裡為什麼不會出現問題呢?
						 * 因為這裡是先取出queue中的元素,然後再判斷
						 * 而queue中一定是不能存放null值的
						 * 頂多就s==null而break出去
						 * (想想Ticket1、2,這兩個都是先判斷,然後再拿)
						 */
						try {
							TimeUnit.MILLISECONDS.sleep(2);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						if(s==null) {
							break;
						}
						System.out.println(s);
					}
				}
			}).start();
		}
	}
}