【IT168技术】最近一直在看提升javascript代码性能的资料,看来看去就是为了如何提升网站的并发能力或者是软件如何对海量数据进行处理,这里我不想探讨解决这些难题的解决方案,但要解决并发或者是海量数据的问题一定离不开线程,线程的确相当的重要,当你要满足日新月异的用户需求而你所做的软件没有好好利用线程的知识绝对是一件无法让人接受的,就算你是一个关注前端技术的工程师也不会幸免对线程的运用,我相信不到三年我们的主流浏览器都会或多或少有类似的线程开发模型。同时线程也是面试最爱问的问题,线程的确可以很好的衡量一个程序员水平的技术难点,对于一个关注技术的工程师,想把软件真正做好,一定要熟练掌握好线程,我就从今天开始好好复习下线程的知识。
我主要的工作语言是java,我就从java里的线程技术讲起。
在java里创建线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。
第一种创建线程的方法:继承Thread类
Thread类最重要的方法是run(),你如果想实现线程你就得覆盖这个方法,run里面的内容就是你要实现线程所具有的功能。而Thread里的run方法总会以某种形式进行循环,如果run方法里没有跳出循环的条件,线程将会一直运行下去。编写好一个线程对象后,对象要通过start()方法启动线程,构建的线程对象如果不调用start()方法,线程将永远都不会执行run方法里写好的业务逻辑。
下面是一个最简单的线程实例:
public class SimpleThread extends Thread {
private int countDown = 5;//构建的Thread对象内部使用的数据
private static int threadCount = 0;//静态变量属于类,目的是为了每个构建的线程定义一个名称
public SimpleThread(){
super("" + ++threadCount);
start();
}
public String toString(){
return "#" + getName() + ": " + countDown;
}
public void run(){
while(true){
System.out.println(this);
if (--countDown == 0)
return;
}
}
public static void main(String[] args) {
for (int i = 0;i < 5;i++)
{
new SimpleThread();
}
}
}
结果如下:
#2: 5
#1: 4
#3: 5
#2: 4
#2: 3
#3: 4
#1: 3
#3: 3
#2: 2
#5: 5
#3: 2
#4: 5
#1: 2
#4: 4
#3: 1
#5: 4
#2: 1
#5: 3
#4: 3
#1: 1
#4: 2
#5: 2
#4: 1
#5: 1
我们写的代码里都是直接new一个继承了Thread的类,这里会有一个奇怪的问题,构建的这个对象没有被任何变量所引用,按照java里垃圾回收机制,thread对象会被马上进行回收,但是事实上构建的对象并没有被及时回收,这是为什么呢?因为每一个新建的thread对象都只有离开了run方法后才能把它清理掉,换句话说只要thread的run方法还在执行时候不管这个thread对象处在什么状态下(是被引用了还是没被引用),thread对象都不会被回收。
Java里用Thread实现线程是标准的做法,上面我写了个很简单的例子,在这里我想回过头来讲讲线程的原理,学技术,我一个经验一定要知其然还要知其所以然,只是会用而不懂其原理那你就是把自己变成编写代码的工具了,我们应该学会如何是运用技术来解决实际发生的问题,那么前提一定要知其然还要知其所以然。
当我们走进线程时候,我们会发现我们将进入一个完全不同的编程领域。我常常把线程技术当做计算机编程语言里的克隆技术,线程技术可以将程序转换成彼此分离,内容相同,但又可以独立运行的计算任务。现实中多线程技术通常的用法就是在你程序里的某一个部分会与一个特定的资源或事件相联系,但是这种联系你又不希望主程序专门抽时间去解决它,那么你就可以拷贝一份代码独立运行这个计算任务,以至于该计算任不会影响到主程序的运行。
学习线程也是一种摆脱传统编程思维的奇妙之旅,这也是我想好好复习线程的重要原因之一,对线程的学习完全可以当做一门新语言来进行,而且各种不同语言实现线程的差异度都不大,这也是跳出语言框框的一次重要实践。
让我们回归到程序里吧,上面的例子里我们不断执行程序会发现:每次运行的结果都不一样,名称不同的线程会交替运行,这到底是什么原因造成的呢?产生的原因是java虚拟机里线程的调度机制,到底哪个线程先执行那个线程后执行,线程调度是使用一个非常随机的方式进行管理的,导致结果输出的混乱。解决这个问题的方法就是使用Thread所带的join()方法,大家请看下面的代码:
private int countDown = 5;//构建的Thread对象内部使用的数据
private static int threadCount = 0;//静态变量属于类,目的是为了每个构建的线程定义一个名称
public SimpleThread(){
super("" + ++threadCount);
start();
}
public String toString(){
return "#" + getName() + ": " + countDown;
}
public void run(){
while(true){
System.out.println(this);
if (--countDown == 0)
return;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0;i < 5;i++)
{
new SimpleThread().join();
}
}
}
结果如下:
#1: 4
#1: 3
#1: 2
#1: 1
#2: 5
#2: 4
#2: 3
#2: 2
#2: 1
#3: 5
#3: 4
#3: 3
#3: 2
#3: 1
#4: 5
#4: 4
#4: 3
#4: 2
#4: 1
#5: 5
#5: 4
#5: 3
#5: 2
#5: 1
大家看到了输出的结果是按线程构建的顺序进行的,这个现象说明了join()方法可以改变java里线程随机调度的现象,join方法会让使用join方法的线程处于等待的状态,只有当正在执行线程结束运行后后才会调用使用到join方法的线程。
前面讲到了,线程对象要被销毁要满足两个条件:一个是该线程对象没有被引用,一个是run方法停止运行了。但是当线程跳出了run方法,java虚拟机不一定知道线程已经执行完毕,这个需要一个过程,因此java中有了个yield方法,这个方法告诉java虚拟机,线程的任务已经完成了,可以释放该线程的CPU留给其他线程使用了,下面是使用yield方法的例子:
private int countDown = 5;
private static int threadCount = 0;
public YieldingThread()
{
super("" + ++threadCount);
start();
}
public String toString(){ return "#" + getName() + ": " + countDown;
}
public void run()
{
while(true)
{
System.out.println(this);
if (--countDown == 0)
return;
yield();
}
}
public static void main(String[] args)
{
for (int i = 0;i < 5;i++)
{
new YieldingThread();
}
}
}
结果如下:
#2: 5
#1: 4
#3: 5
#1: 3
#2: 4
#3: 4
#2: 3
#1: 2
#4: 5
#1: 1
#2: 2
#5: 5
#3: 3
#5: 4
#2: 1
#4: 4
#5: 3
#3: 2
#5: 2
#4: 3
#5: 1
#3: 1
#4: 2
#4: 1
我们发现使用了yield对程序的输出结果并没有任何影响,原因何在了?原因还是线程的调度,线程的调度是以抢占式的方式进行,有时调度花去的时间太多,导致线程执行到yield时候线程对象可能已经被java虚拟机所回收了,因此在实际运用中yield用途还真的不大。
我在实际运用中使用最多的Thread方法是sleep,让线程休息一段时间,换句话说就是让线程暂停固定的时间。使用sleep方法的代码如下:
public class SleepingThread extends Thread{
private int countDown = 5;
private static int threadCount = 0;
public SleepingThread(){
super("" + ++threadCount);
start();
}
public String toString(){
return "#" + getName() + ": " + countDown;
}
public void run(){
while(true)
{ System.out.println(this);
if (--countDown == 0)
return;
try {
sleep(100);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
for (int i = 0;i < 5;i++)
{
new SleepingThread();
}
}
}
结果如下:
#2: 5
#3: 5
#4: 5
#5: 5
#1: 4
#3: 4
#2: 4
#4: 4
#5: 4
#2: 3
#1: 3
#3: 3
#5: 3
#4: 3
#2: 2
#3: 2
#1: 2
#4: 2
#5: 2
#1: 1
#3: 1
#2: 1
#5: 1
#4: 1
我们发现调用sleep方法的地方要抛出一个异常InterruptedException,为什么会有这个异常呢?原因很简单,休眠的线程也有可能被终止,例如使用thread里的interrupt方法就会导致线程的终止,这种情况违反了sleep函数设计的初衷,因此会有导致在此抛出。我们回到上面join方法,join方法使用时候也会抛出InterruptedException,为什么呢?其实我们在调用join方法时候,被调用join方法的线程也是像调用sleep方法那样被休眠起来,它等待前一个线程执行结束的时候才能唤醒它,但是只要是休眠的线程同样也有可能被中断,这种结果也是方法不愿意接受的情况,因此会有InterruptedException异常的抛出。
使用了sleep方法的代码程序输出的结果任然是无序的,我修改上面的代码:
private int countDown = 5;
private static int threadCount = 0;
public SleepingThread(){
super("" + ++threadCount);
start();
}
public String toString(){
return "#" + getName() + ": " + countDown;
}
public void run(){
while(true)
{
System.out.println(this);
if (--countDown == 0)
return;
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0;i < 5;i++)
{
new SleepingThread().join();
}
}
}
结果如下:
#1: 4
#1: 3
#1: 2
#1: 1
#2: 5
#2: 4
#2: 3
#2: 2
#2: 1
#3: 5
#3: 4
#3: 3
#3: 2
#3: 1
#4: 5
#4: 4
#4: 3
#4: 2
#4: 1
#5: 5
#5: 4
#5: 3
#5: 2
#5: 1
以上实例表明sleep,yield方法都不是决定线程执行顺序的因素,而join可以有序的定义线程执行的顺序。
作为程序员的我很喜欢按次序的输出结果,但是让线程输出的结果变得有规律真的有这个必要吗?我们回顾下我们使用过的语言,假如编程语言里没有线程,那么一切程序应该都是按顺序进行的,有了线程的语言我们可以打破这个固有的顺序,让程序随时随地进行执行,这样就可以开发并行的计算任务了,按顺序输出的线程是完全没有使用价值的程序,而且线程机制总会消耗到一定的系统资源,编写有序的线程也是极大的资源浪费。
我曾经在一次面试时候说出了上面的理解,但是面试官马上问了一个问题难住了我,线程本来就该是无序的,那么我们就任意让线程随便执行,这个你认为合理吗?当时我从join角度解释这个问题,现在复习线程知识发现通过join阐述答案是自己思路太窄了,线程虽然注定是无序,但并不是说所有线程都可以让它肆意的暂用宝贵的CPU资源,java语言里还有方法可以控制它,线程还有一个功能:线程的优先级,大家看下面的代码:
private int countDown = 5;
private volatile double d = 0;
public SimplePriorities(int priority){
setPriority(priority);
start();
}
public String toString(){
return super.toString() + ": " + countDown;
}
public void run(){
while(true)
{
for (int i = 1;i < 100000;i++)
{
d = d + (Math.PI + Math.E);
System.out.println(this);
if (--countDown == 0)
return;
}
}
}
public static void main(String[] args)
{
new SimplePriorities(Thread.MAX_PRIORITY);
for (int i = 0;i < 5;i++)
{
new SimplePriorities(Thread.MIN_PRIORITY);
}
}
}
结果如下:
Thread[Thread-0,10,main]: 4
Thread[Thread-2,1,main]: 4
Thread[Thread-1,1,main]: 4
Thread[Thread-3,1,main]: 5
Thread[Thread-3,1,main]: 4
Thread[Thread-4,1,main]: 5
Thread[Thread-4,1,main]: 4
Thread[Thread-3,1,main]: 3
Thread[Thread-0,10,main]: 2
Thread[Thread-3,1,main]: 2
Thread[Thread-4,1,main]: 2
Thread[Thread-1,1,main]: 2
Thread[Thread-5,1,main]: 3
Thread[Thread-0,10,main]: 1
Thread[Thread-1,1,main]: 1
大家可以到线程1的优先级最高,先被打印出来,其余线程都是随机进行的。虽然java虚拟机里面线程有10个优先级别,但是现实中在不同的操作系统里都是映射的不是特别好,因此使用者三个静态变量是最保险的做法。
下面我要讲到另外一种实现java里线程的方式接口Runnable。
{
private int countDown = 5;
public String toString(){
return "#" + Thread.currentThread().getName() + ": " + countDown;
}
@Override
public void run() {
while(true){
System.out.println(this);
if (--countDown == 0)
return;
}
}
public static void main(String[] args) {
for (int i = 1;i <= 5;i++)
{
new Thread(new RunnableThread(),"" + i).start();
}
}
}
为什么java会提供另外一种线程实现的方式呢?我们知道java里没有多继承,一个类继承了某一个父类后就不能再继承别的类了,这就产生了一个问题,在实际的开发中,子类所要继承的父类往往都是包含业务信息的,而Thread类仅仅是为了实现线程功能而定义的,没有必要让定义好的类直接继承Thread类,为了解决java多继承给线程实现带来的问题,java里使用了一个变通的方案,实现了一个Runnable接口从而解决了上面的问题,让我们定义的类既能继承父类的业务信息,又能具有线程的特点。大家看下面的代码:
结果如下:
#2: 5
#1: 4
#2: 4
#3: 5
#1: 3
#4: 5
#3: 4
#2: 3
#2: 2
#3: 3
#4: 4
#4: 3
#1: 2
#4: 2
#3: 2
#2: 1
#5: 5
#3: 1
#4: 1
#1: 1
#5: 4
#5: 3#5: 2#5: 1
Runnable接口只有一个run方法,只要实现这个run方法就可以了,构造线程对象,只要把实现Runnable接口的类创建的对象作为Thread类的构造参数传入就行了。
虽然实现Runnable接口创建线程同样可以产生线程实例,不过使用Runnable接口定义的线程还是有些特别的地方,我总结了下,有以下两种:
①每个线程都有一个名字,前面通过继承Thread来创建线程类里有一个getName方法来获得线程的名称,但是用Runnable接口,必须通过调用Thread.currentThread().getName()方法来获取线程的名称。Thread.currentThread()获取当前运行线程的引用。
②由于Runnable只是一个接口,里面不包含任何和线程相关的信息,所以用Runnable构建的Thread对象使用时候要调用start方法才能让线程调用run方法
通过对Runnable接口实现线程让我有了一个新的想法:我们使用线程往往是需要线程提供的功能,我们不一定因为需要线程而去改变整个程序的组织结构,所以直接继承Thread类往往不可取,但是Runnable是不是唯一的解决方案了?想让类授予线程的功能是不是还可以有别的实现方式了?这种想法让我想到了两种替代方案,一个是单独构造一个继承了Thread类,然后把它设为业务类的一个属性,这种做法想想就觉得是多此一。
那么还有一种方法就是使用内部类,下面是我从书本上抄写下来的一个例子:
private int countDown = 5;
private Inner inner;
private class Inner extends Thread{
Inner(String name) {
super(name);
start();
}
public void run(){
while(true){
System.out.println(this);
if (--countDown == 0)
return;
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public String toString(){
return getName() + ": " + countDown;
}
}
public InnerThread1(String name) {
inner = new Inner(name);
}
}
class InnerThread2{
private int countDown = 5;
private Thread t;
public InnerThread2(String name) {
t = new Thread(name){
public void run(){
while(true)
{
System.out.println(this);
if (--countDown == 0)
return;
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public String toString(){
return getName() + ": " + countDown;
}
};
t.start();
}
}
class InnerRunnable1{
private int countDown = 5;
private Inner inner;
private class Inner implements Runnable{
Thread t;
public Inner(String name) {
t = new Thread(this,name);
t.start();
}
@Override
public void run() {
while(true){
System.out.println(this);
if (--countDown == 0)
return;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public String toString(){
return t.getName() + ": " + countDown;
}
}
public InnerRunnable1(String name) {
inner = new Inner(name);
}
}
class InnerRunnable2{
private int countDown = 5;
private Thread t;
public InnerRunnable2(String name)
{
t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println(this);
if (--countDown == 0)
return;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} } }
public String toString(){
return Thread.currentThread().getName() + ": " + countDown;
}
},name);
t.start();
}
}
class ThreadMethod{
private int countDown = 5;
private Thread t;
private String name;
public ThreadMethod(String name){
this.name = name;
}
public void runThread(){
if (t == null){
t = new Thread(name){
public void run(){
while(true)
{
System.out.println(this);
if (--countDown == 0)
return;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} public String toString(){
return getName() + ": " + countDown;
}
}; t.start();
}
}
}public class ThreadVariations {
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerThread2("InnerThread2");
new InnerRunnable1("InnerRunnable1");
new InnerRunnable2("InnerRunnable2");
new ThreadMethod("ThreadMethod").runThread();
}
}
结果如下:
InnerThread2: 5
InnerRunnable1: 5
InnerRunnable2: 5
InnerThread1: 4
ThreadMethod: 5
InnerRunnable1: 4
InnerRunnable2: 4
InnerThread1: 3
ThreadMethod: 4
InnerRunnable1: 3
InnerRunnable2: 3
InnerThread1: 2
ThreadMethod: 3
InnerRunnable1: 2
InnerRunnable2: 2
InnerThread1: 1
ThreadMethod: 2
InnerRunnable1: 1
InnerRunnable2: 1
ThreadMethod: 1
InnerThread2: 4
InnerThread2: 3
InnerThread2: 2
InnerThread2: 1
第一个InnerThread1类里面创建了一个内部类,在InnerThread1的构造函数里构建了这个内部类,其实我们往往只是希望某一个对象具有线程的功能,因此单独构造一个继承Thread类的类是不是有这个必要是值得商榷的。所以我们有了第二个类的做法了,我们在InnerThread2的构造函数里建立了一个匿名内部类,他继承了Thread,这样我们既让自定义的类具有了线程的功能,又隐藏了不必要的细节,这种做法更合理了。至于第三个和第四个类是换了Runnable来实现同样的功能,但是看起来比较繁琐哈。而ThreadMethod类展示了如何在方法内部创建线程,这种做法的应用场景是,一个类主干可能不需要多线程的,但是它的某个功能可能需要多线程,那么把线程放到方法里是很有必要的,这种方法很值得推广,我们将线程的功能精确到方法级别也许会让我们的程序更加好控制也更加的健壮。
线程的基础篇上篇内容结束了,下一篇我将讲多线程共享资源的问题。