快速认识线程
本文参考自java高并发编程详解
1、创建并启动一个线程
下面是不添加线程的程序代码。
package concurrent.chapter01; import java.util.concurrent.timeunit; public class tryconcurrency { public static void main(string[] args) { browsenews(); enjoymusic(); } private static void browsenews() { while(true) { system.out.println("uh-huh,the good news."); sleep(1); } } private static void enjoymusic() { while(true) { system.out.println("uh-huh,the nice music"); sleep(1); } } private static void sleep(int i) { try { timeunit.seconds.sleep(i); }catch (exception e) { } } }
运行结果如下:
程序永远不会执行第二个方法。因此我们需要使用线程。
这里通过匿名内部类的方式创建线程,并且重写其中的run方法,使程序交互运行。
package concurrent.chapter01; import java.util.concurrent.timeunit; public class tryconcurrency { public static void main(string[] args) { new thread() { @override public void run() { enjoymusic(); } }.start(); browsenews(); } private static void browsenews() { while(true) { system.out.println("uh-huh,the good news."); sleep(1); } } private static void enjoymusic() { while(true) { system.out.println("uh-huh,the nice music"); sleep(1); } } private static void sleep(int i) { try { timeunit.seconds.sleep(i); }catch (exception e) { } } }
运行结果如下:
注意:
1、创建一个线程,需要重写thread中的run方法,override的注解是重写的标识,然后将enjoymusic交给他执行。
2、启动新的线程需要重写thread的start方法,才代表派生了一个新的线程,否则thread和其他普通的java对象并无区别,start放法是一个立即返回方法,并不会让程序陷入阻塞。
如果使用lambda表达式改造上面的代码,那么代码会变得更简洁。
public static void main(string[] args) { new thread(tryconcurrency::enjoymusic).start(); browsenews(); }
2、线程的创建与结束生命周期。
1、线程的new状态。
当我们用关键字new创建一个thread对象时,此时他并不处于执行状态,因为没用start启动该线程,那么线程的状态为new状态,准确的说,它只是thread对象的状态,因为在没用start之前,该线程根本不存在,与你用new创建一个普通的java对象没什么区别。
2、线程的runnable状态
线程对象进入runnable状态必须调用start方法,那么此时才是真正地在jvm中创建了一个线程,线程一经启动就可以立即执行吗?答案是否定的,线程的运行与否和进程一样都要听令于cpu的调度,那么我们把这个中间状态成为可执行状态,也就是说它具备执行的资格,但是并没有真正地执行起来,而是等待cpu的调度。
3、线程的running状态
一旦cpu通过轮询或者其他方式从任务可执行队列中选中了线程,那么此时它才能真正的执行自己的逻辑代码,需要说明一点是一个正在running状态的线程事实上也是runnable的,但是反过来则不成立。
在该状态中,线程的状态可以发生如下的状态转换。
- 直接进入terminated状态,比如调用jdk已经不推荐使用的stop方法或者判断某个逻辑标识。
- 进入blocked状态,比如调用了sleep,或者wait方法而加入了waitset中。
- 进行某个阻塞的io操作,比如因网络数据的读写而进入了blocked状态。
- 获取某个锁资源,从而加入到该锁的阻塞队列中而进入了blocked状态。
- 由于cpu的调度器轮询使该线程放弃执行,进入runnable状态。
- 线程主动调用yield方法,放弃cpu执行权,进入runnable状态。
4、线程的blocked状态
blocked为线程阻塞时的状态,它能进入以下几个状态:
- 直接进入terminated状态,比如调用jdk已经不推荐使用的stop方法或者jvmcrash
- 线程阻塞的操作结束,比如读取了想要的数据字节进入到runnable状态。
- 线程完成了指定时间的休眠,进入到了runnable状态
- wait中的线程被其他线程notify/notifyall唤醒,进入runnable状态。
- 线程获取到了某个锁资源,进入runnable状态。
- 线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入runnable状态。
5、线程的terminated状态
terminated是一个线程的最终状态,在该状态中,线程将不会切换到其他任何状态,线程进入terminated状态意味着整个线程的生命周期结束了,下列情况将会使线程进入terminated状态。
- 线程运行正常结束,结束生命周期。
- 线程运行出错意外结束
- jvm崩溃,导致所有的线程都结束。
3、线程的start方法是什么?
首先:thread start源码如下:
public synchronized void start() { /** * this method is not invoked for the main method thread or "system" * group threads created/set up by the vm. any new functionality added * to this method in the future may have to also be added to the vm. * * a zero status value corresponds to state "new". */ if (threadstatus != 0) throw new illegalthreadstateexception(); /* notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadstartfailed(this); } } catch (throwable ignore) { /* do nothing. if start0 threw a throwable then it will be passed up the call stack */ } } } private native void start0();
start方法的源码足够简单,其实最核心的部分是start0这个本地方法,也就是jni方法;
也就是说在start方法中会调用start0方法,那么重写的那个run方法何时被调用了呢?
实际上在开始执行这个线程的时候,jvm将会调用该线程的run方法,换言之,run方法是被jni方法start0调用的,仔细阅读start的源码将会总结出如下几个知识要点。
- thread被构造后的new状态,实际上threadstatus这个内部属性为0.
- 不能俩次启动thread,否则就会出现illegalthreadstateexception异常。
- 线程启动后会被加入到一个threadgroup中。
- 一个线程生命周期的结束也就是到了terminated再次调用start方法是不允许的,也就是说terminated状态是没有办法回到runnable状态的。
如执行以下代码:
import java.util.concurrent.timeunit; public class a { public static void main(string[] args) { thread thread = new thread() { @override public void run() { try { timeunit.seconds.sleep(10); }catch (exception e) { e.printstacktrace(); } } }; thread.start(); thread.start(); } }
此时程序就会抛出
当我们改下代码,也就是生命周期结束后,再重新调用时。
import java.util.concurrent.timeunit; public class a { public static void main(string[] args) throws interruptedexception { thread thread = new thread() { @override public void run() { try { timeunit.seconds.sleep(1); }catch (exception e) { e.printstacktrace(); } } }; thread.start(); timeunit.seconds.sleep(5); thread.start(); } }
我们会发现程序同样会抛出illegalthread异常。
注意:程序虽然同样会抛出异常,但是这俩个异常是有本质区别的。
- 第一个是因为重复启动,只是第二次启动时不允许的,但是此时线程是处于运行状态的。
- 第二次企图重新激活也抛出了非法状态的异常,但是此时没有线程,因为该线程的生命周期已经被终结。
通过以上分析我们不难看出,线程真正的执行逻辑是在run方法中,通常我们会把run方法成为线程的执行单元。
如果我们没有重写run,那run就是个空方法。
thread的run和start是一个比较经典的模板设计模式,父类编写算法结构代码,子类实现逻辑细节,下面是一个简单的模板设计模式。
package concurrent.chapter01; public class templatemethod { public final void print(string message) { system.out.println("###"); wrapprint(message); system.out.println("###"); } protected void wrapprint(string message) { } public static void main(string[] args) { templatemethod t1 = new templatemethod() { @override protected void wrapprint(string message) { system.out.println("*"+message+"*"); } }; t1.print("hello thread"); templatemethod t2 = new templatemethod() { @override protected void wrapprint(string message) { system.out.println("+"+message+"+"); } }; t2.print("hello thread"); } }
运行结果如下:
4、下面是一个模拟营业大厅叫号机的程序
假设共有4台出号机,这就意味着有4个线程在工作,下面我们用程序模拟一下叫号的过程,约定当天最多受理50笔业务,也就是说号码最多可以出到50
代码如下:
package concurrent.chapter01; 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 t1 = new ticketwindow("一号初号机"); t1.start(); ticketwindow t2 = new ticketwindow("二号初号机"); t2.start(); ticketwindow t3 = new ticketwindow("三号初号机"); t3.start(); ticketwindow t4 = new ticketwindow("四号初号机"); t4.start(); } }
运行结果如下:
显然这不是我们想看到的。如何改进呢?
这里我将index设置为staic变量
貌似有了改善。但是会出现线程安全问题。
所以java提供了一个接口:runnable专门用于解决该问题,将线程和业务逻辑的运行彻底分离开。
5、runnable接口的引入及策略模式
runnalbe接口非常简单,只是定义了一个无参数无返回值的run方法,具体代码如下:
public interface runnable{ void run(); }
在很多书中,都会说,创建线程有俩种方式,第一种是构造一个thread,第二种是实现runnable接口,这种说法是错误的,最起码是不严谨的,在jdk中,代表线程的就只有thread这个类,我们在前面分析过,线程的执行单元就是run方法,你可以通过继承thread然后重写run方法实现自己的业务逻辑,也可以实现runnable接口实现自己的业务逻辑,代码如下:
@override public void run(){ if(target!=null){ target.run(); } }
上面的代码段是thread run方法的源码,我们从中可以去理解,创建线程只有一种方式,那就是构造thread类,而实现线程的执行单元则有俩种方式,第一种是重写thread的run方法,第二种是实现runnable接口的run方法,并将runnable实例用作构造thread的参数。
策略模式
其实无论是runnable的run方法还是,thread本身的run方法都说想将线程的控制本身和业务逻辑的运行分离开,达到职责分明,功能单一的原则,这一点与gof设计模式中的策略设计模式很相近。
以jdbc来举例子:
package concurrent.chapter01; import java.sql.resultset; public interface rowhandler <t>{ t handle(resultset set); }
rowhandler接口只负责对从数据库中查询出来的结果集进行操作,至于最终返回成什么样的数据结构,需要自己去实现,类似于runnable接口。
package concurrent.chapter01; import java.sql.connection; import java.sql.preparedstatement; import java.sql.resultset; import java.sql.sqlexception; public class recordquery { private final connection connection; public recordquery(connection connection) { this.connection = connection; } public<t> t query(rowhandler<t> handler,string sql,object... params) throws sqlexception{ try(preparedstatement stmt = connection.preparestatement(sql)){ int index = 1; for(object param:params) { stmt.setobject(index++,param); } resultset resultset = stmt.executequery(); return handler.handle(resultset); } } }
上面的代码的好处就是可以用query方法应对任何数据库的查询,返回结果的不同只会因为你传入rowhandler的不同而不同,同样recodequery只负责数据的获取,而rowhanlder则只负责数据的加工,职责分明,每个类均功能单一。
重写thread类的run方法和实现runnable接口的run方法是不能共享的,也就是说a线程不能把b线程的run方法当作自己的执行单元,而使用runnable接口则很容易就能实现这一点即使用同一个runnable的实例构造不同的实例。
如果不明白的话,看下面的代码。
package concurrent.chapter01; public class ticketwindowrunnable implements runnable{ private int index = -1; private final static int max = 50; @override public void run() { while(index<=max) { system.out.println(thread.currentthread()+" 的号码是:"+(index++)); try { thread.sleep(100); }catch (exception e) { e.printstacktrace(); } } } public static void main(string[] args) { final ticketwindowrunnable task = new ticketwindowrunnable(); thread windowthread1 = new thread(task,"一号窗口"); thread windowthread2 = new thread(task,"二号窗口"); thread windowthread3 = new thread(task,"三号窗口"); thread windowthread4 = new thread(task,"四号窗口"); windowthread1.start(); windowthread2.start(); windowthread3.start(); windowthread4.start(); } }
运行结果如下:
惊不惊喜?
上面并没有对index进行static进行修饰,但是和上面被static修饰的是一个效果。原因是我们每次操作的都是同一个对象即task。