首页 > 技术文章 > java多线程学习 基础篇(二) Thread类和Runnable接口

r1-12king 2021-12-09 19:59 原文

一个线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。下图显示了一个线程完整的生命周期。

 

 

如果我们需要有一个“线程”类,JDK提供了Thread类和Runnalble接口来让我们实现自己的“线程”类。

  • 继承Thread类,并重写run方法(注意:Thread类实现了Runnable接口)
  • 实现Runnable接口的run方法

 

继承Thread类

1.首先创建一个任务类extends Thread类,因为Thread类实现了Runnable接口,所以自定义的任务类也实现了Runnable接口,重新run()方法,其中定义具体的任务代码或处理逻辑。

2.创建一个任务类对象,可以用Thread或者Runnable作为自定义的变量类型。

3.调用自定义对象的start()方法,启动一个线程。

 1 public class ThreadDemo extends Thread{
 2 
 3     @Override
 4     public void run() {
 5         System.out.println("my Thread");
 6     }
 7 
 8     public static void main(String[] args) {
 9         Thread t1 = new ThreadDemo();
10         t1.start();
11         System.out.println("结束");
12     }
13 }

使用start()方法启动一个线程,注意不能多次调用start()方法,否则会抛出异常。

>>
结束
my Thread

 

实现runnable接口

1.定义一个任务类实现Runnable接口,实现Runnable接口中的run()方法(run()方法告知系统线程该如何运行),run()方法中定义具体的任务代码或处理逻辑。

2.定义了任务类后,为任务类创建一个任务对象。

3.任务必须在线程中执行,创建一个Thread类的对象,将前面创建的实现了Runnable接口的任务类对象作为参数传递给Tread类的构造方法。

4.调用Thread类对象的start()方法,启动一个线程。它会导致任务的run()方法被执行,当run()方法执行完毕,则线程就终止。

如:

 1 public class RunnableDemo implements Runnable{
 2     @Override
 3     public void run() {
 4         System.out.println("my Thread by implements Runnable");
 5     }
 6 
 7     public static void main(String[] args) {
 8         RunnableDemo mt = new RunnableDemo();
 9         Thread t = new Thread(mt);
10         t.start();
11         System.out.println("end");
12     }
13 }
调用start方法会启动一个线程,导致任务中的run方法被调用,run方法执行完毕则线程终止
>>
end
my Thread by implements Runnable

 

Thread类的currentThread()方法

 

currentThread():静态方法,返回对当前正在执行的线程对象的引用

 

该方法可以可以返回代码段正在被哪个线程调用的信息。实例代码如下:
1     public static void main(String[] args) {
2          System.out.println(Thread.currentThread().getName());
3     }

 

结果会在控制台打印main,证明main方法正在被名字叫main的线程调用。 修改代码如下:
 
 1 public class ThreadDemo2 extends Thread{
 2     public ThreadDemo2() {
 3         System.out.println("构造方法打印:" + Thread.currentThread().getName());
 4     }
 5 
 6     @Override
 7     public void run() {
 8         System.out.println("run方法打印:" + Thread.currentThread().getName());
 9     }
10 
11     public static void main(String[] args) {
12         Thread t1 = new ThreadDemo2();
13         t1.start();
14     }
15 }

输出结果如下,证明构造函数是被main线程调用的,而run()方法是被名叫“Thread-0”调用的。

>>
构造方法打印:main
run方法打印:Thread-0

 

再次修改代码如下:
 1 public class ThreadDemo2 extends Thread{
 2     public ThreadDemo2() {
 3         System.out.println("构造方法打印:" + Thread.currentThread().getName());
 4     }
 5 
 6     @Override
 7     public void run() {
 8         System.out.println("run方法打印:" + Thread.currentThread().getName());
 9     }
10 
11     public static void main(String[] args) {
12         Thread t1 = new ThreadDemo2();
13         t1.run();
14     }
15 }

 

输出结果如下,证明两个线程都是被main调用的。
>>
构造方法打印:main
run方法打印:main

解释:

对于start()方法,Thread用 start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

对于run()方法,Runnable run() 方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待 run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一 个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

总结

Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参数方法。设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread 类实现了Runnable。激活的意思是说某个线程已启动并且尚未停止。此外,Runnable 为非 Thread 子类的类提供了一种激活方式。通过实例化某个Thread 实例并将自身作为运行目标,就可以运行实现Runnable 的类。大多数情况下,如果只想重写run() 方法,而不重写其他 Thread 方法,那么应使用Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。(推荐使用创建任务类,并实现Runnable接口,而不是继承Thread类)。

(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

 

守护线程

User Thread(用户线程)和Daemon Thread(守护线程)从本质上来说并没有什么区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
 
 

线程的优先级

在现代操作系统中基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下一次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
 
在java线程中,通过一个整型的成员变量 Priority来控制线程优先级 ,每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。
 
首先看下JDK对线程优先级的设置有哪些:
    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

 

 
那么JDK是通过什么方法设置线程的优先级呢?答案是通过setPriority(int newPriority)这个方法设置优先级,参数newPriority越大,优先级越高。但是需要注意的是优先级虽然高,占得CPU资源较多,但是也不能保证优先级高的线程全部执行完,因为优先级具有随机性
 
尽量不要依赖优先级,如果确实要用,应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态,低优先级线程可能永远也不能执行。
每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程可能永远不会被执行到。
因此我们在设置优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高的优先级,而偏重计算(需要较多CPU时间或者运算)的线程则设置较低的优先级,这样才能确保处理器不会被长久独占。
当然还有要注意就是在不同的JVM以及操作系统上线程的规划存在差异,有些操作系统甚至会忽略对线程优先级的设定

Thread类与Runnable接口的比较

实现一个自定义的线程类,可以有继承Thread类或者实现Runnable接口这两种方式,它们之间有什么优劣呢?
  •  由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
  •  Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
  •  Runnable接口出现,降低了线程对象和线程任务的耦合性。
  •  如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。
所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。

 

推荐阅读