1、售票程序V1
public class TicketSoldV1 {
public static void main(String[] args) {
TicketWindowV1 r1 = new TicketWindowV1();
TicketWindowV1 r2 = new TicketWindowV1();
TicketWindowV1 r3 = new TicketWindowV1();
new Thread(r1, "A窗口").start(); //启动三个线程窗口
new Thread(r2, "B窗口").start();
new Thread(r3, "C窗口").start();
}
}
class TicketWindowV1 implements Runnable {
private static int ticketNumber = 100; //静态变量,所有售票窗口共享该
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始运行...");
while(true) { //循环卖票
if(ticketNumber > 0) {
//打印所卖票的票号
System.out.println(Thread.currentThread().getName() + ":" + ticketNumber);
ticketNumber--;
}
else {
break;
}
}
System.out.println(Thread.currentThread().getName() + "线程结束运行...");
}
}
上述程序运行结果如下所示:
A窗口线程开始运行...
C窗口线程开始运行...
B窗口线程开始运行...
B窗口:100
C窗口:100
A窗口:100
A窗口:97
A窗口:96
C窗口:98
B窗口:99
C窗口:94
A窗口:95
C窗口:92
B窗口:93
B窗口:89
C窗口:90
A窗口:91
C窗口:87
B窗口:88
C窗口:85
A窗口:86
C窗口:83
B窗口:84
B窗口:80
C窗口:81
A窗口:82
A窗口:77
C窗口:78
B窗口:79
B窗口:74
B窗口:73
C窗口:75
A窗口:76
C窗口:71
B窗口:72
B窗口:68
C窗口:69
A窗口:70
C窗口:66
B窗口:67
C窗口:64
A窗口:65
A窗口:61
A窗口:60
C窗口:62
B窗口:63
B窗口:57
C窗口:58
A窗口:59
C窗口:55
B窗口:56
C窗口:53
A窗口:54
A窗口:50
C窗口:51
B窗口:52
C窗口:48
A窗口:49
C窗口:46
B窗口:47
C窗口:44
A窗口:45
C窗口:42
B窗口:43
C窗口:40
A窗口:41
C窗口:38
B窗口:39
C窗口:36
A窗口:37
C窗口:34
B窗口:35
C窗口:32
A窗口:33
C窗口:30
B窗口:31
C窗口:28
A窗口:29
C窗口:26
B窗口:27
C窗口:24
A窗口:25
C窗口:22
B窗口:23
C窗口:20
A窗口:21
C窗口:18
B窗口:19
C窗口:16
A窗口:17
C窗口:14
B窗口:15
C窗口:12
A窗口:13
C窗口:10
B窗口:11
C窗口:8
A窗口:9
A窗口:5
C窗口:6
B窗口:7
C窗口:3
A窗口:4
C窗口:1
B窗口:2
C窗口线程结束运行...
A窗口线程结束运行...
B窗口线程结束运行...
从上述结果中可以看到三个售票窗口线程交替卖票,符合我们的需求,但是从中也看到了该程序存在线程安全问题。
2、线程安全
问题1:
A、B、C窗口重复卖出了100号票;
问题2:
有可能出现卖出0号票的情况(票号:1-100);
问题3:
有可能出现某些票号没有卖出的情况;
我们可以人为验证线程安全问题,在Java中在Thread类中提供了sleep方法,可以使线程交出cpu执行权,“睡一下”,模拟真实的卖票场景。
售票程序V2(验证线程不安全):
//线程不安全验证
public class TicketSoldV2 {
public static void main(String[] args) {
TicketWindowV2 r1 = new TicketWindowV2();
TicketWindowV2 r2 = new TicketWindowV2();
TicketWindowV2 r3 = new TicketWindowV2();
new Thread(r1, "A窗口").start(); //启动三个线程窗口
new Thread(r2, "B窗口").start();
new Thread(r3, "C窗口").start();
}
}
class TicketWindowV2 implements Runnable {
private static int ticketNumber = 100; //静态变量,所有售票窗口共享该
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始运行...");
while(true) { //循环卖票
if(ticketNumber > 0) {
//打印所卖票的票号
System.out.println(Thread.currentThread().getName() + ":" + ticketNumber);
ticketNumber--;
try { //sleep方法会抛出异常
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
break;
}
}
System.out.println(Thread.currentThread().getName() + "线程结束运行...");
}
}
上述程序运行结果如下所示:
A窗口线程开始运行...
A窗口:100
C窗口线程开始运行...
C窗口:99
B窗口线程开始运行...
B窗口:98
B窗口:97
C窗口:96
A窗口:95
C窗口:94
A窗口:93
B窗口:92
B窗口:91
C窗口:90
A窗口:90
B窗口:88
A窗口:87
C窗口:86
B窗口:85
C窗口:85
A窗口:85
B窗口:82
C窗口:81
A窗口:80
C窗口:79
A窗口:78
B窗口:77
B窗口:76
C窗口:75
A窗口:74
B窗口:73
C窗口:72
A窗口:71
B窗口:70
C窗口:69
A窗口:68
A窗口:67
C窗口:66
B窗口:65
B窗口:64
C窗口:64
A窗口:64
B窗口:61
A窗口:61
C窗口:61
B窗口:58
A窗口:58
C窗口:56
B窗口:55
A窗口:54
C窗口:53
B窗口:52
A窗口:51
C窗口:50
B窗口:49
A窗口:48
C窗口:47
A窗口:46
C窗口:45
B窗口:44
B窗口:43
A窗口:43
C窗口:41
A窗口:40
C窗口:40
B窗口:40
A窗口:37
B窗口:36
C窗口:36
A窗口:34
C窗口:34
B窗口:34
B窗口:31
C窗口:30
A窗口:30
C窗口:28
A窗口:27
B窗口:26
C窗口:25
B窗口:25
A窗口:24
B窗口:22
C窗口:21
A窗口:20
B窗口:19
A窗口:19
C窗口:19
B窗口:16
A窗口:16
C窗口:14
A窗口:13
B窗口:13
C窗口:12
B窗口:10
A窗口:9
C窗口:9
C窗口:7
A窗口:6
B窗口:5
C窗口:4
A窗口:3
B窗口:2
B窗口:1
C窗口:0
A窗口线程结束运行...
B窗口线程结束运行...
C窗口线程结束运行...
可以看到有些票被卖了多次(90,85,64等),有些票没有被卖出(89,63,39等),且出现了0号票。
3、线程安全的解决:同步
可以看到线程安全问题是由于多个线程同时操作一个共享的资源造成的,那么我们如果在一个线程使用这个资源时,禁止其他线程再获取这个资源,也就是一次只能有一个线程操作这个资源,这种状态成为同步。Java专门为多线程安全问题提供了解决机制:同步代码块,将操作共享资源的代码放到同步代码块中。
synchronized(锁) {
需要被同步的代码
}
4、售票程序V3
找到所有"读取、修改"共享资源的语句,将他们放在同步代码块中。
public class TicketSoldV3 {
public static void main(String[] args) {
TicketWindowV3 r1 = new TicketWindowV3();
TicketWindowV3 r2 = new TicketWindowV3();
TicketWindowV3 r3 = new TicketWindowV3();
new Thread(r1, "A窗口").start(); //启动三个线程窗口
new Thread(r2, "B窗口").start();
new Thread(r3, "C窗口").start();
}
}
class TicketWindowV3 implements Runnable {
private static int ticketNumber = 100; //静态变量,所有售票窗口共享该
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始运行...");
while(true) { //循环卖票
synchronized (TicketWindowV3.class) {
if(ticketNumber > 0) {
//打印所卖票的票号
System.out.println(Thread.currentThread().getName() + ":" + ticketNumber);
ticketNumber--;
try { //sleep方法会抛出异常
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
break;
}
}
}
System.out.println(Thread.currentThread().getName() + "线程结束运行...");
}
}
上述程序运行结果正常,如下所示:
A窗口线程开始运行...
A窗口:100
C窗口线程开始运行...
B窗口线程开始运行...
A窗口:99
B窗口:98
B窗口:97
B窗口:96
B窗口:95
C窗口:94
B窗口:93
A窗口:92
B窗口:91
B窗口:90
B窗口:89
B窗口:88
B窗口:87
B窗口:86
B窗口:85
B窗口:84
B窗口:83
B窗口:82
C窗口:81
C窗口:80
C窗口:79
B窗口:78
B窗口:77
B窗口:76
B窗口:75
B窗口:74
B窗口:73
B窗口:72
B窗口:71
A窗口:70
B窗口:69
B窗口:68
B窗口:67
B窗口:66
B窗口:65
B窗口:64
B窗口:63
B窗口:62
B窗口:61
C窗口:60
B窗口:59
B窗口:58
B窗口:57
B窗口:56
B窗口:55
B窗口:54
B窗口:53
B窗口:52
B窗口:51
B窗口:50
B窗口:49
B窗口:48
A窗口:47
B窗口:46
B窗口:45
B窗口:44
B窗口:43
B窗口:42
B窗口:41
B窗口:40
B窗口:39
B窗口:38
B窗口:37
B窗口:36
C窗口:35
B窗口:34
A窗口:33
B窗口:32
C窗口:31
B窗口:30
A窗口:29
A窗口:28
B窗口:27
B窗口:26
B窗口:25
B窗口:24
B窗口:23
B窗口:22
C窗口:21
B窗口:20
B窗口:19
B窗口:18
B窗口:17
B窗口:16
A窗口:15
A窗口:14
B窗口:13
B窗口:12
B窗口:11
B窗口:10
B窗口:9
C窗口:8
B窗口:7
B窗口:6
B窗口:5
B窗口:4
B窗口:3
B窗口:2
B窗口:1
B窗口线程结束运行...
A窗口线程结束运行...
C窗口线程结束运行...
扩展问题:如果把整个run方法中的代码都放到同步代码块里会出现什么情况呢?
public class TicketSoldV4 {
public static void main(String[] args) {
TicketWindowV4 r1 = new TicketWindowV4();
TicketWindowV4 r2 = new TicketWindowV4();
TicketWindowV4 r3 = new TicketWindowV4();
new Thread(r1, "A窗口").start(); //启动三个线程窗口
new Thread(r2, "B窗口").start();
new Thread(r3, "C窗口").start();
}
}
class TicketWindowV4 implements Runnable {
private static int ticketNumber = 100; //静态变量,所有售票窗口共享该
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始运行...");
synchronized (TicketSoldV4.class) {
while(true) { //循环卖票
if(ticketNumber > 0) {
//打印所卖票的票号
System.out.println(Thread.currentThread().getName() + ":" + ticketNumber);
ticketNumber--;
try { //sleep方法会抛出异常
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
break;
}
}
}
System.out.println(Thread.currentThread().getName() + "线程结束运行...");
}
}
上述程序运行结果正常,如下所示:
A窗口线程开始运行...
A窗口:100
C窗口线程开始运行...
B窗口线程开始运行...
A窗口:99
A窗口:98
A窗口:97
A窗口:96
A窗口:95
A窗口:94
A窗口:93
A窗口:92
A窗口:91
A窗口:90
A窗口:89
A窗口:88
A窗口:87
A窗口:86
A窗口:85
A窗口:84
A窗口:83
A窗口:82
A窗口:81
A窗口:80
A窗口:79
A窗口:78
A窗口:77
A窗口:76
A窗口:75
A窗口:74
A窗口:73
A窗口:72
A窗口:71
A窗口:70
A窗口:69
A窗口:68
A窗口:67
A窗口:66
A窗口:65
A窗口:64
A窗口:63
A窗口:62
A窗口:61
A窗口:60
A窗口:59
A窗口:58
A窗口:57
A窗口:56
A窗口:55
A窗口:54
A窗口:53
A窗口:52
A窗口:51
A窗口:50
A窗口:49
A窗口:48
A窗口:47
A窗口:46
A窗口:45
A窗口:44
A窗口:43
A窗口:42
A窗口:41
A窗口:40
A窗口:39
A窗口:38
A窗口:37
A窗口:36
A窗口:35
A窗口:34
A窗口:33
A窗口:32
A窗口:31
A窗口:30
A窗口:29
A窗口:28
A窗口:27
A窗口:26
A窗口:25
A窗口:24
A窗口:23
A窗口:22
A窗口:21
A窗口:20
A窗口:19
A窗口:18
A窗口:17
A窗口:16
A窗口:15
A窗口:14
A窗口:13
A窗口:12
A窗口:11
A窗口:10
A窗口:9
A窗口:8
A窗口:7
A窗口:6
A窗口:5
A窗口:4
A窗口:3
A窗口:2
A窗口:1
C窗口线程结束运行...
A窗口线程结束运行...
B窗口线程结束运行...
执行可以发现所有的票都是从A窗口卖出的:这是因为窗口A继承进入同步代码块,锁上锁,执行while循环,循环过程中cpu切换到其他线程,例如窗口B线程执行run,发现锁处于锁定状态,不执行操作,cpu再切换,最后cpu只能继续执行窗口A线程,实际上这就变成了单线程了。
从这里可以发现:不应该将过多的代码放到同步代码块中。只将所有"读取、修改"共享资源的语句放在同步代码块中。