[02] 线程的创建和常用方法


1、线程的创建

线程的创建可以通过两种方式,第一种是 Thread类,第二种是 Runnable接口:
  • 继承 Thread 类,覆盖 run()
  • 实现 Runnable 接口,实现 run()

然后线程的启用是通过 start() 方法,它会自动调用 run() 方法,如下例:
//继承Thread
public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 100; i++) {
            System.out.println("MyThread:" + i);
        }
    }
}

//实现Runnable
public class MyRunnableImpl implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("MyRunnableImpl:" + i);
        }
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        Thread thread1 = new MyThread();
        Thread thread2 = new Thread(new MyRunnableImpl());
        thread1.start();
        thread2.start();
    }
}

//输出结果示例
...
MyThread:23
MyThread:24
MyThread:25
MyRunnableImpl:0
MyThread:26
MyRunnableImpl:1
MyThread:27
MyRunnableImpl:2
...

可以看到,线程的运行是并行的,而不是先执行完整个 thread1 的 start() 再执行 thread2 的 start()

另外,Runnable 接口的存在主要是为了解决 Java 中不允许多继承的问题,所以我们往往通过实现 Runnable 接口,然后再将其封装到一个 Thread 类中使用(如上例 Thread thread2 = new Thread(new MyRunnableImpl()); )

事实上,我们也可以通过匿名内部类的方式,快速实现线程,如上可以修改为:
public class Test {
    public static void main(String[] args) {
        //Thread
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("anonymous thread:" + i);
                }
            }
        };
        //Runnable
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("anonymous runnable:" + i);
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}


2、线程的常用方法

2.1 线程的生命周期

要说到线程的常用方法,首先就要先了解一个线程的生命周期,如下图:
  • 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程

  • 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中(排队状态),要等待JVM里线程调度器的调度

  • 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态

  • 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。阻塞又可以分为三种:
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态

  • 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态

2.2 线程的常用方法

2.2.1 start、run

基于线程的生命周期,那么 start() 和 run() 方法想必也就不用多说了:
  • start()  启动线程,进入就绪状态(万事俱备,只欠CPU)
  • run()    运行状态,执行操作

2.2.2 sleep

sleep() 用于使线程进入阻塞状态,强制当前正在执行的线程休眠,计时到后返回到就绪状态。它是Thread类的静态方法:
  • Thread.sleep(long millis)    休眠 ${millis} 毫秒
  • Thread.sleep(long millis, int nanos)    休眠 ${millis} 毫秒 + ${nanos} 纳秒

注意:休眠完成之后进入的是就绪状态,等待进入运行状态,这意味着 "休眠 --> 执行" 的过程会大于 sleep 方法设置的值,因为实际上还要算上就绪状态的排队时间,即真正过程是 "休眠 --> 就绪 --> 执行"

另外,sleep() 是 Thread 的静态方法,调用的是当前运行的线程,所以要保证某个线程的休眠,应将 sleep() 的调用放在线程的 run() 之中

2.2.3 yield

Thread.yield() 的作用是,暂停当前正在执行的线程,并让其回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。但是,实际中无法保证 yield() 达到让步的目的,因为让步的线程在就绪状态仍可能被再次选中执行。

注意:yield() 只是将线程转到就绪状态,而不会进入阻塞状态。

2.2.4 join

join(long millis) 方法的作用是让某个线程 "霸占" 资源一段时间,如两个线程交互运行,如果 thread2.join(5000),那么 thread2 将强制霸占执行 5s,之后其他线程才有使用的机会。

2.2.5 setPriority

setPriority(int newPriority) 方法用于设置线程的优先级别,数值越高表示优先级越高。优先级的范围在 1 - 10 之间,默认为5

每个线程具有各自的优先级,线程的优先级表示该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定优先使哪个线程进入运行状态。但这并不意味着低优先级的线程得不到运行,只是运行的几率比较小,如垃圾回收机制线程的优先级就比较低。另外:
  • 线程优先级具有继承性,如A线程启动B线程,则B线程的优先级和A一样
  • 线程优先级具有随机性,即线程优先级高的不一定每次都先执行

2.3 守护线程

多线程分类
  • 用户线程:运行在前台,执行具体的任务,如程序的主线程,用户自己创建的线程
  • 守护线程:运行在后台,为其他前台线程服务(守护)

它们的区别在于 "前台线程保证完成,守护线程不保证完成":
  • 前台线程完成了之后,虚拟机执行的进程才会结束
  • 进程结束之后(此时前台线程已经结束),也会结束后台线程,但是不论后台线程是否已经完成都会将其结束

守护线程的应用如:数据库连接池中的检测线程、JVM启动后的检测线程、垃圾回收线程等

setDaemon(true) 可以将线程设置为守护线程,必须在线程进入就绪状态之前,即必须在 start() 方法调用之前使用。

3、参考链接:



posted @ 2018-08-11 23:36  Dulk  阅读(283)  评论(0编辑  收藏  举报