按照今年的学习计划,并发学习是一个关键内容,之前参考过很多讲Java并发的书籍,感觉国内的相关数据,讲的好的好像并不多,学了一些后来觉得甚是枯燥,没有相关实例就放弃了,但是没有针对这些进行总结,这次打算从头归零,从头开始学习,并做好总结。之前的并发总结文档过于混乱,已将其置为私密。这次的学习几乎参考了所有Java并发编程的书籍
如果说对Java多线程稍微有一点了解的大佬,针对Java中实现多线程的方式有几种这个问题很熟悉,之前网上也讨论过很多,到底有几种实现多线程的方式,有的说1种,有的说2种,有的说4种。百度一下也答案各异
到底有几种实现多线程的方式,这个还是需要好好说道说道
正确答案——2种,这个是基于Oracle官网的结果,既然官网给出的答案是两种,我们就不要再被各种3种,4种,6种这种花里胡哨的结果给误导了,给出一张官网的截图。
有两种方法可以创建新的执行线程,一种是继承Thread类,另一种是实现Runnable接口,这是官网给的答案。
/** * autor:liman * createtime:2021/9/7 * comment:runnable创建线程 */ @Slf4j public class RunnableStyle implements Runnable { public static void main(String[] args) { //将当前实现了Runnable接口的类交给Thread Thread thread = new Thread(new RunnableStyle()); thread.start(); } @Override public void run() { log.info("用runnable的方式实现线程"); } }
/** * autor:liman * createtime:2021/9/7 * comment:继承thread创建线程 */ @Slf4j public class ThreadStyle extends Thread{ public static void main(String[] args) { //使用的是Thread不带任何参数的构造函数 Thread thread = new ThreadStyle(); thread.start(); } @Override public void run() { log.info("继承Thread类的方式实现线程"); } }
一般推荐使用Runnable接口的方式创建线程。主要原因有以下几点:
1、从架构的角度来考虑,Runnable中的逻辑只是具体的实现,而Thread类中其实还做了线程的启动,暂停等操作。具体的实现应该需要独立出来,所以实现Runnable接口从某种层度上来说是解耦了。
2、继承Thread类,每次如果想新建一个任务,其实就是重新新建一个线程,即使很多线程执行的逻辑一样,也都是重新建立一个线程,这样CPU的性能损耗是很大的,而实现Runnable接口则不同,实现了Runnable接口可以利用线程池等工具,一定程度上降低这种损耗。
3、Java不支持多继承,如果一个类继承了Thread类,则无法再继承其他类,这在一定程度上限制了其可扩展性。
综合上述原因考虑,推荐采用实现Runnable接口。
关于Thread类中run方法的源码如下:
/** * If this thread was constructed using a separate * <code>Runnable</code> run object, then that * <code>Runnable</code> object's <code>run</code> method is called; * otherwise, this method does nothing and returns. * <p> * Subclasses of <code>Thread</code> should override this method. * * @see #start() * @see #stop() * @see #Thread(ThreadGroup, Runnable, String) */ @Override public void run() { if (target != null) { target.run(); } }
其实run方法中底层调用的是target的run,而这个target其实就是一个Runnable接口类型的。会在构造函数的时候被传入,当我们实现Runnable接口启动线程的时候,实质是传入了一个Runnable接口的引用。采用实现Runnable接口的方式,Thread最终会执行target.run()方法,而我们集成Thread类,则是覆盖了Thread类中的整个run方法。
可以通过如下实例说明这一问题
/** * autor:liman * createtime:2021/9/7 * comment:同时使用两种方式实现线程 */ @Slf4j public class BothRunnableThread { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println("这是Runnable方式创建的线程"); } }){ //由于本身的run方法被重写过,因此通过runnable对象创建的时候,父类Thread中的run方法已经被重写了,这里是重写run方法 @Override public void run() { System.out.println("这是继承Thread创建的线程");//输出这一行 } }.start(); } }
重写了run方法之后,并不会执行Runnable接口中的逻辑。
到这里,如果后续遇到Java中正常有几种这种问题,我们可以如下回答:
Java中创建线程的方式通常我们可以分为两类,Oracle官网也是这么介绍的。准确的讲,创建线程只有一种方式,就是构造Thread类,而实现线程的执行单元有两种方式:
1、实现Runnable接口的run方法,并把Runnable实例传递给Thread类。
2、重写Thread的run方法(继承Thread类)
至于网上说的其他实现线程的方式,都只不过是在这些方法的基础上,代码的写法不同,或者通过各种各样类的包装而成的,本质与上面两者并没有什么不同。
启动线程其实比较简单并不复杂,正确的就是调用start方法,但是还是深入到原理进行一个小小的总结。
正确的方式,当然是调用Thread中的start方法,即可启动线程,关于start的源码,内容如下:
/** * Causes this thread to begin execution; the Java Virtual Machine * calls the <code>run</code> method of this thread. * <p> * The result is that two threads are running concurrently: the * current thread (which returns from the call to the * <code>start</code> method) and the other thread (which executes its * <code>run</code> method). * <p> * It is never legal to start a thread more than once. * In particular, a thread may not be restarted once it has completed * execution. * * @exception IllegalThreadStateException if the thread was already * started. * @see #run() * @see #stop() */ 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 */ } } }
可以看到,其最先判断的,就是线程状态,之后将其加入到一个线程组中,然后调用一个native的start0方法,只有调用这个方法,才会让线程经历所有的生命状态周期,如果直接调用run方法,线程是不会经历所有的线程生命周期的。
1、不能直接调用Runnable的run方法,如果直接调用run方法,则会当成一个普通方法进行执行
/** * autor:liman * createtime:2021/9/8 * comment:对比start和run两种启动线程的方式 */ @Slf4j public class StartAndRunMethod { public static void main(String[] args) { //初始化一个Runnable Runnable runnable = ()->{ log.info("当前线程的线程名为:{}",Thread.currentThread().getName()); }; //直接调用run方法,会当成一个普通方式执行 runnable.run(); //单独启动一个线程 new Thread(runnable).start(); } }
运行结果:
可以看到,同一行代码,直接调用run和调用start输出的并不一样
2、start方法不能重复调用,因为线程状态已经改变
/** * autor:liman * createtime:2021/10/7 * comment:启动两次的线程 */ @Slf4j public class StartTwiceDemo { public static void main(String[] args) { Thread thread = new Thread(); thread.start(); thread.start(); } }
线程状态已经改变,重复调用start会抛错
线程学习的开篇,只是总结到线程正常的启动方法。