到底什么时候该用多线程?


问题情景[0]:设计一个简单的UI:包括一个文本标签和一个按钮,在点击按钮时文本显示由0~10的增长,每秒增长量为1。

问题情景[1]:某同学编写的坦克大战程序中,每一个坦克和子弹均使用一个独立的线程,这是否合理?(当然不合理。。。)如果是你,你会怎么编写这个程序?

    说到这,多线程归根究底是为了解决"等"的问题,那我们这样定义一个阻塞过程:程序运行该过程所消耗的时间有可能在运行上下文间产生明显的卡顿;这里使用“可能”是因为有些情况下,诸如Socket通信,如果数据源源不断的进入,那么阻塞的时间可能非常小,但我们还是使用了一条线程(nio另说)来处理它,因为我们无法保证数据到来的持续性和有效性;"卡顿"带有主观臆想,也就是说是使用者(人或一些自动化程序)不可接受的。

接下来,对什么时候使用多线程做一个回答:编写程序过程中需要使用某些阻塞过程时,我们才使用多线程,或者更进一步讲,使用多线程的目的是对阻塞过程中的实际阻塞的抽象提取。前半句话应该很好理解,而后面的一句虽然不太好懂,不过它对一个程序应具有的合理线程数量进行了阐释(这点接下来解释)。


好了,接下来我们回顾一些两个问题,并对它们做出解答:

问题情景[0]

这个问题是笔者还是一名小菜时遇到的,当时笔者是这么写的:

public class MyFrame
{
    Label textShower;
    Button textChanger;
    public MyFrame//实例化等省略
    {
       textChanger.setOnClickListener(new OnClickListener(){
	public void onClick(MouseEvent e){
		for(int i = 1;i <= 10;i++){
			textShower.setText(i+"");//设置文字
			Thread.sleep(1000);//等待一秒
		}
	}
	});
    }
}


当时老师是这么教给我的:

public class MyFrame
{
    Label textShower;
    Button textChanger;
    public MyFrame//实例化等省略
    {
       textChanger.setOnClickListener(new OnClickListener(){
	public void onClick(MouseEvent e){
	new Thread(){
		public void run()
		{
			for(int i = 0;i < 10;i++){
				textShower.setText(i+"");//设置文字
				Thread.sleep(1000);//等待一秒
			}	
		}
	}.start();
	}
	});
    }
}





问题情景[1]:在这个问题中,将主要讨论实际阻塞的抽象和合理线程数量的问题。


坦克类:

public class Tank extends Thread{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 1f;
	public void run()
	{
		drawtank();//清除上一次的绘制,根据横坐标x画一个坦克
		x+=speed;
		Thread.sleep(17);//约合一秒60次
	}
}



子弹类:

public class Bullet extends Thread{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 10f;
	public void run()
	{
		drawbullet();//清除上一次的绘制,根据横坐标x画一个子弹
		x+=speed;
		Thread.sleep(17);//约合一秒60次
	}
}






    其实这样异步的绘制会使画面产生明显的抖动,而且用于同步的逻辑也十分复杂,并不是一个好的方案。

其实上面两个类中的run方法中,只有sleep属于实际阻塞,也就是说是可以被抽象出来的,我们只要一个线程,每过17毫秒执行一些列非阻塞过程即可。

上述过程中,绘制及坐标的运算属于非阻塞过程,我们将其抽象为一个接口:

public interface Drawable
{
	public void draw();
}

之后我们书写抽象实际阻塞的线程类:

public class BlockThread extends Thread 
{
	Collection<Drawable> c = new Collection<Drawable>();
	public void run()
	{
		for(Drawable d:c)
		{
			d.draw();
		}
		Thread.sleep(17);
	}
	//封装对成员c的同步CRUD不赘述
	public void addDrawable(Drawable d);
	public void removeDrawable(Drawable d);
	...
}

最后,坦克和子弹的改动:


坦克类:

public class Tank implements Drawable{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 1f;
	@Override
	public void draw()
	{
		drawtank();//清除上一次的绘制,根据横坐标x画一个坦克
		x+=speed;
	}
}

子弹类:

public class Bullet implements Drawable{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 10f;
	@Override
	public void draw()
	{
		drawbullet();//清除上一次的绘制,根据横坐标x画一个子弹
		x+=speed;
	}
}


    我们可以发现:原有的实际阻塞过程已经被抽象到一个线程之中,而非阻塞过程,诸如绘制和坐标运算依然作为方法保留到对应类中,这样,无论有多少坦克和炮弹,只要非阻塞过程的运算压总和力不至于逼近阻塞的程度,使用一个线程即可完成所有工作。