前言
按照今年的学习计划,并发学习是一个关键内容,之前参考过很多讲Java并发的书籍,感觉国内的相关数据,讲的好的好像并不多,学了一些后来觉得甚是枯燥,没有相关实例就放弃了,但是没有针对这些进行总结,这次打算从头归零,从头开始学习,并做好总结。之前的并发总结文档过于混乱,已将其置为私密。这次的学习几乎参考了所有Java并发编程的书籍
实现多线程的方法
如果说对Java多线程稍微有一点了解的大佬,针对Java中实现多线程的方式有几种这个问题很熟悉,之前网上也讨论过很多,到底有几种实现多线程的方式,有的说1种,有的说2种,有的说4种。百度一下也答案各异
到底有几种实现多线程的方式,这个还是需要好好说道说道
正确答案——2种,这个是基于Oracle官网的结果,既然官网给出的答案是两种,我们就不要再被各种3种,4种,6种这种花里胡哨的结果给误导了,给出一张官网的截图。
有两种方法可以创建新的执行线程,一种是继承Thread类,另一种是实现Runnable接口,这是官网给的答案。
实现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的方式实现线程");
}
}
继承Thread类
/**
* 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会抛错
总结
线程学习的开篇,只是总结到线程正常的启动方法。