卖票的例子
上一节讲到卖票的例子,现在来实现一下。
Runnable runnable = new Runnable() {
int ticket = 100;//100张票
public void run() {
while (true) {
if (ticket > 0) {
//卖出一张票并且打印
System.out.println(Thread.currentThread().getName() + "__" + ticket--);
}
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
嗯,看上去没问题。其实你仔细检查会发现有几个票数是一样的,这不符合现实,100张票卖出了101张、102张、103张……这就是多线程并发存在的问题。有些同学会有疑问,为什么最后的票数不是1,而是其他数。我在第一节讲过,CPU调度工作是乱序的,所以不是按顺序也是正常的。
不安全的原因
多线程存在漏洞,我们是不是不用了呢。也许你是对的,但是开发程序避免不了多线程,到最后发现线程是多么好的一个东西。多线程并发为什么会存在这问题呢。主要执行代码块是run方法,我们来分析run方法。
if (ticket > 0) {//第一站
System.out.println(Thread.currentThread().getName() + "__" + ticket--);//第二站
}
在第一节中提到,CPU调度工作是不断切换的,执行到某地方就会切换线程继续接下来的工作。我们放慢工作,比如上面线程1执行判断语句(假设票数还剩下50张),这时CPU切换线程2执行判断语句(票数还剩下50张),然后执行打印(票数还剩下49张),可是又切换回线程1(这时线程1的票数还是50张),然后执行打印(49张票数)。简单来说就是多条语句操作线程中的共享数据,一线程没执行完就切换其他线程,导致共享数据错误。
同步锁
不用共享数据是一个解决方法,但是多个用户访问一个数据库,就会存在线程安全问题,这就是多线程访问共享数据的一个例子。Java提供一个synchronized关键字,意为同步,这是为了解决线程安全问题。使用方法很简单。在操作共享数据的多条语句用synchronized括起来。
synchronized(this){
if (ticket > 0) {//第一站
System.out.println(Thread.currentThread().getName() + "__" + ticket--);//第二站
}
}
这又有一个疑问,括号里面是什么呢。这就是同步需要的锁。简单来说,哪个线程先拿到这锁就会锁上整个方法,等执行完方法再把锁给下一个线程。那这锁里面一定要this吗,并不是的,这个锁任意对象都是可以的。但不要在括号中new对象,否则它会一直停留在那,因为每次new的对象地址都是不一样的。
synchronized(new Object()){//杜绝
...
}
还有另一种姿势。
//同步方法
private void synchronized saleTicket(){
...
}
//等价与
synchronized(this){
saleTicket();
}
private void saleTicket(){
...
}
同步方法的锁就是this,两种方法执行是一致的。
终止线程
当我们run方法执行的是无限循环的代码,这就要注意终止该线程。如果你用Eclipse执行代码,你会发现电脑会开始响起来,这是因为CPU还在不断执行你的循环语句。Java API提供start方法,不是也有对应的stop方法吗?是有的,但是已经标记过时方法了,因为存在很多问题。那有什么方法终止。
boolean flag = true;
whlie(flag){
}
最简单的方式就是用flag判断,flag为flase就视为退出,该线程释放了。
还有一种情况是线程阻塞,在while里面是动不了,无法判断flag,一直无法终止线程。这时就要用到interupt方法,也就是下一节的内容。