Java高并发编程-剖析线程启动的start方法

这篇文章,带着大家来分析一下线程启动的start()方法,并且简单地讲解一下模板设计模式在线程类中的使用。

在之前分析中,我们知道线程从NEW状态进入到RUNNABLE装需要调用start()方法,但是当线程调用完start()方法之后到底进行了那些操作呢?在之前的例子中,我们将业务逻辑代码写到了run()方法中,但是实际上执行线程调用的时候却是执行的是线程的start()方法,两者之间又有什么联系呢?

下面我们就来分析一下。

Thread start方法源码分析

Thread 类的start方法源码如下所示。

public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } }

通过源码可以看出来,源码逻辑相对比较简单,其中最主要的一个代码就是start0()方法。在源码中可以看到start0(),代码如下所示。

private native void start0();

也就是说,在我们调用了start()方法之后,会调用start0()方法,还是没有发现run()方法是什么时候被执行的,但是查看JDK的文档之后,我们会知道,其实这里的start0()方法其实是通过JNI技术进行调用的,而run()方法也是被JNI的start0()方法。阅读源码之后会有如下的一些问题。

  • 当我们new一个线程之后,threadStatus 属性的值应该是默认为0的,当不等于0的时候就会抛出异常。这个在源码中说得比较清楚。threadStatus为零表示这个当前状态是NEW
  • 根据threadStatus的状态可以知道,不能进行两次启动线程操作,也就是说同一个线程对象,不能两次重复调用start()方法,否则就会抛出异常。
  • 线程启动之后就会被加入到一个线程组中,后续会说到线程组相关的概念。
  • 当一个线程结束,进入到TERMINATED状态之后,是没有办法通过调用start()方法再次进入到RUNNABLE状态或者是RUNNING状态的。

模板设计模式在Thread中的使用

通过上面的分析,可以知道,一个线程真正的逻辑执行代码是被放到run()方法中,通常情况下run()方法就是我们编写线程执行逻辑的地方,这也就是为什么我们要重写run()方法,用start()启动的线程。Thread类中的run方法如下。

@Override public void run() { if (target != null) { target.run(); } }

默认情况下,这个方法可以看做是一个空方法。方法体中没有任何代码。

不难发现在Thread中的run()方法start()方法就是一个非常典型的模板设计模式,父类编写算法结构代码,子类负责按照对应的模板进行实现。下面来看一个小例子

模板设计模式小例子

首先创建一个消息输出的接口类,这个类就是制定消息输出的规则,代码如下

public interface PrintMessage { abstract void print();}

接下来就是其子类要实现这个消息输出消息了。代码如下

public class Repoter implements PrintMessage { public void printNews() { print(); } @Override public void print() { System.out.println("show news"); }}

编写测试类main方法。

public class TemplateMain { public static void main(String[] args) { Repoter repoter = new Repoter(){ @Override public void print() { System.out.println("这个是一个设计模板方法"); } }; repoter.printNews(); }}

这里会看到整个的模式都是按照Thread的模式来进行。其中的print()方法和printNews()方法就类似于Thread类的run()方法和start()方法。这样一来,父类只需要告诉子类需要做的事情是什么,而不需要知道至于子类如何实现,最终就是子类的事情了。

简单的模拟一个叫号系统

很多人都有过存款或者买票的体验,他们有一个共同的特点就是需要排队叫号。其实这种机制解决了很多的问题。例如可以做到限流,可以合理的分配处理人员的位置,可以很好的解决了由于排队引起的社会矛盾。同时也减轻了业务员、管理人员的压力

我们这里简单的模拟有四个窗口的叫号系统。四个窗口就相当于有四个线程。通过这四个窗口来模拟50人排队的场景。

public class TicketWindow extends Thread { private final String name; private static final int MAX = 50; private int index = 1; public TicketWindow(String name) { this.name = name; } @Override public void run() { while (index<max){ system.out.println(name+"="" 号柜台,出号="" "+index++);="" }="" public="" static="" void="" main(string[]="" args){="" ticketwindow="" ticketwindow1="new" ticketwindow("一");="" ticketwindow1.start();="" ticketwindow2="new" ticketwindow("二");="" ticketwindow2.start();="" ticketwindow3="new" ticketwindow("三");="" ticketwindow3.start();="" ticketwindow4="new" ticketwindow("四");="" ticketwindow4.start();="" }}

运行上面代码之后会看到如下的结果,简单地分析之后,会发现其实这是有问题的。1号票被多个柜台出了。

为什么会出现这样的问题,这就是每个线程执行的逻辑都不一样,新建了四个线程,每个线程的票号都是从0~50。四个线程并没有操作同一个票号排序。所以就会出现上面这个问题。那么我们如何去解决这个问题呢?我们想到了使用static 去修饰 index变量的方式。代码如下。

public class TicketWindow extends Thread { private final String name; private static final int MAX = 50; private static int index = 1; public TicketWindow(String name) { this.name = name; } @Override public void run() { while (index<max){ system.out.println(name+"="" 号柜台,出号="" "+index++);="" }="" public="" static="" void="" main(string[]="" args){="" ticketwindow="" ticketwindow1="new" ticketwindow("一");="" ticketwindow1.start();="" ticketwindow2="new" ticketwindow("二");="" ticketwindow2.start();="" ticketwindow3="new" ticketwindow("三");="" ticketwindow3.start();="" ticketwindow4="new" ticketwindow("四");="" ticketwindow4.start();="" }}

经过修改之后,运行结果如下,这个时候看上去一切都就正常了。

通过对index进行static的修饰,做到了多线程共享索引的操作。看上去是满足了需求,但是实际上还是有很多的问题,如果共享索引的数据太大的时候,也会出现线程安全的问题。这个问题在后续的分享中会提到。所以说static只能解决小问题,需要解决大问题的话还需要大手笔的解决方案。在后续的分享中也会提到。这里就先到这里,大家可以先消化一下Thread 的模板设计模式原理。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注