多线程是我们学习java的一个重点,但总是看着别人的案例就会做,一离开案例敲多线程就各有各的奇葩结果。解决问题的关键,还是要更深入理解多线程和线程锁的特性。
什么时候应该用多线程呢?
以下两种情况下建议使用多线程处理:
- 当需要同时做多件事情:一边播放背景音乐,一边显示画面,或者一边上传文件,一边响应用户的操作,这种需要多线程。
- 当某个操作耗时很久:比如与服务器通信,从数据库获取数据,即使很快,我们都默认他是耗时很久的任务,需要用到多线程的。
因为线程池的引入,现在已经很少有人用继承Thread的方式来开启多线程,我也推荐使用实现Runnable的方式来开启多线程,这有3个好处:
- 可以让线程多一个继承的机会:如果通过继承thread来继承任务,就无法再继承其他类了。
- 可以让线程间多了一种共享数据的方式:一个Runnable对象可以生成多个线程,这多个线程之间可以共享Runnable对象的实例成员。如果用继承thread方式实现,则只能把共享资源变成静态,才能实现共享。
- 可以使用线程池:线程池只接受实现了Runnable或Callable的对象,不接受thread对象作为参数。
使用多线程时应该思考些什么
在定义Runnable对象的时候,我们应该把Runnable当成一份任务清单,一个线程是一个任务的执行人。
- 当new了不同的Runnable对象,就是生成了不同的任务,不同的任务之间基本没什么联系了,最多就共用同一个静态成员变量,因为实例成员变量已经不在共享了,各有各的任务信息。
- 而用同一个Runnable对象生成不同的线程时,就说明是要执行同一个任务,会共享Runnable对象的成员变量的。
举一个售票员卖票的例子,两位售票员要卖出一共5张电影票:
public class SellingTicket implements Runnable {
private int count = 5;
@Override
public void run() {
String name = Thread.currentThread().getName();
while (true) {
//synchronized (this) {
if (count > 0) {
try {
Thread.sleep(((int) (Math.random() * 10 + 10))); //睡10~20毫秒模拟售票耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(name + "卖出了一张票," +
"目前还有票" + count + "张");
} else {
System.out.println(name + "发现票卖完了!下班!");
break;
}
//}
}
}
}
定义好Runnable以后,就在main中调用,我分两种调用的方式:
//方式一:
public class TestDemo {
public static void main(String[] args) {
SellingTicket sellingTicket = new SellingTicket();
new Thread(sellingTicket, "售票员B").start();
new Thread(sellingTicket, "售票员A").start();
}
}
运行结果:
售票员A卖出了一张票,目前还有票3张
售票员B卖出了一张票,目前还有票4张
售票员A卖出了一张票,目前还有票2张
售票员B卖出了一张票,目前还有票1张
售票员A卖出了一张票,目前还有票0张
售票员B卖出了一张票,目前还有票-1张
售票员B发现票卖完了!下班!
售票员A发现票卖完了!下班!
//方式二
public class TestDemo {
public static void main(String[] args) {
new Thread(new SellingTicket(), "售票员B").start();
new Thread(new SellingTicket(), "售票员A").start();
}
}
运行结果:
售票员A卖出了一张票,目前还有票4张
售票员B卖出了一张票,目前还有票4张
售票员B卖出了一张票,目前还有票3张
售票员A卖出了一张票,目前还有票3张
售票员B卖出了一张票,目前还有票2张
售票员A卖出了一张票,目前还有票2张
售票员B卖出了一张票,目前还有票1张
售票员A卖出了一张票,目前还有票1张
售票员B卖出了一张票,目前还有票0张
售票员B发现票卖完了!下班!
售票员A卖出了一张票,目前还有票0张
售票员A发现票卖完了!下班!
出现了两个问题:
- 第二个调用方法中,一共卖出了10张票的问题。
- 第一种调用方法出现了还剩下-1张票
我们一个一个来处理:
问题一:第二个调用方法中,一共卖出了10张票。
第一种调用方式,用了一个Runnable对象,生成两个线程,第二种调用方式,new了两个Runnable对象,生成两个线程,从他们的运行结果我们可以看出,第二种调用方式一共卖出了10张票,每个线程(售票员)各自卖出了5张票。
这是因为new了两个Runnable对象,等于是告诉线程(售票员B)进行一共全新的任务:卖出5张票。
而两个线程共用同一个Runnable对象,则是同时告诉两个线程(售票员A和售票员B)要卖出5张票
从内存图上看,我们也能看出区别来:
调用方式1:两个线程共用一个Runnable对象
另外,还有因为一个线程一个栈,所以被放在run方法中的变量都是不能共享的,是线程自己私有的变量。如果Runnable对象的成员变量:count 被定义在run方法中,同样也会造成共卖出10张票的问题。
这是很多新接触多线程的朋友可能出错的地方,一定要注意的。
多线程会遇到的问题
那么接下来解决第二个问题,为什么会出现剩下-1张票的情况呢?这就是所谓的线程同步问题。让我们想象一下下面的场景:、
- 售票员A和售票员B共同接收到一个任务:卖出5张票。
- 这5张票被放在一个箱子里,两个售票员在卖票之前先要来看看箱子还有没有票,确认过有票才卖(票数-1),没有就停止售票了
- 卖啊卖啊卖剩一张了,这时候售票员A的窗口来客人了,他先到箱子里看有没有票:还剩一张。卖!
- 就在售票员A在给客人做卖票手续的时候,售票员B的窗口也来了客人,售票员B先到箱子里看还有没有票:还剩一张!
- 售票员A做完卖票手续,把票数-1,票数变成0。这时售票员B也做完售票手续,把票数-1。于是票数变成-1了。
就是因为在售票员A做售票手续的时候,售票员B来看有没有票剩余,才出现这个问题,那解决的思路就很简单了:在售票员A从看有没有票剩余开始,一直到售票员A把票数-1结束,都禁止售票员B来碰这个箱子。问题就解决了。
这也是同步锁的思想,我将再开一篇文章来讲这个问题。
本章所有源码已经上传github:https://github.com/huheman/Blog,
本人水平有限,如您发下有错漏请在评论不吝指教。