JavaSE-多线程


线程和进程

进程(Process):当一个程序进入内存运行时,即变成一个进程。进程是出于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己独有的地址空间。在没有经过进程自身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
  • 动态性:进程和程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响

补充:并发性(concurrency)和并发性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果

线程(Thread):多线程则扩展了多进程的概念,线程也被称作轻量级进程。一个进程可以拥有多个线程,一个线程必须拥有一个父进程。线程在程序中是独立的、并发的执行流

  • 线程拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源
  • 线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行

总结:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程

线程比进程具有更高的性能:多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的共有数据等。利用这些共享的数据,线程很容易实现相互之间的通信(当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多进程实现并发的性能要高得很多)

线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表这段程序流

在默认情况下,主线程的名字为main,用户启动的多个线程的名字依次为 Thread-0、Thread-1、Thread-2、Thread-n等

继承Thread类

通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法;该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体
  2. 创建Thread子类的实例,即创建线程对象
  3. 调用线程对象的start()方法启动线程
代码案例:继承Thread类创建线程类
public class Main {
    public static void main(String[] args) {
        // 开启一个线程(Thread-0)
        Thread thread = new FirstThread();
        thread.start();
        // 主线程(main)
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class FirstThread extends Thread {
    private int i;
    @Override
    public void run() {
        for (; i < 100; i++) {
            System.out.println(this.getName() + " " + i);
        }
    }
}

实现Runnable接口

通过实现Runnable接口来创建并启动多线程的步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
  2. 创建Runnable实现类的实例(任务对象)
  3. 把Runnable任务对象交给Thread处理
  4. 调用线程对象的start()方法启动线程
代码案例:实现Runnable接口创建线程类
public class Main {
    public static void main(String[] args) {
        Runnable target = new SecondThread();
        Thread thread = new Thread(target, "新线程");
        thread.start();
        // 主线程(main)
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class SecondThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
代码案例:实现Runnable接口(匿名内部类形式)
public class Main {
    public static void main(String[] args) {
        // 匿名内部类方式
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        };
        Thread thread = new Thread(target, "新线程1");
        thread.start();
        // 简化写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        }, "新线程2").start();
        // 简化写法
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }, "新线程3").start();
        // 主线程(main)
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

实现Callable和Future接口

通过实现Callable接口和FutureTask接口来创建并启动多线程的步骤如下:

  1. 定义类实现Callable接口,重写call方法,封装要做的事情
  2. 用FutureTask把Callable对象封装成线程任务对象
  3. 把线程任务对象交给Thread处理
  4. 调用Thread的start方法启动线程,执行任务
  5. 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果
代码案例:使用Callable接口和FutureTask实现类创建线程类
public class Main {
    public static void main(String[] args) {
        Callable call = new ThirdThread(100);
        FutureTask task = new FutureTask<>(call);
        Thread thread = new Thread(task);
        thread.start();
        try {
            String result = task.get();
            System.out.println("计算结果为:" + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class ThirdThread implements Callable {
    private int n;
    public ThirdThread(int n) {
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

创建线程的三种方式对比

方式 优点 缺点
继承Thread类 编码比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继续继承其他的类;不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类 不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类,可以得到线程执行的结果 编程相对复杂

线程的生命周期

当线程被创建并启动后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(new)、就绪(runnable)、运行(running)、阻塞(blocked)和死亡(dead)五种状态。尤其是当线程启动以后,它不可能一直霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换

新建和就绪状态

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体
当线程对象调用了start()方法之后,该线程就处于就绪状态——就绪状态相当于“等待执行”。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了,至于该线程何时开始运行,取决于JVM里线程调度器的调度

运行和阻塞状态

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然在一个多处理器的机器上,将会有多个线程并行执行(parallel);当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象
当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级

当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
  • 线程在等待某个通知(notify)
  • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度他

当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:

  • 调用sleep()方法的线程经过了指定时间
  • 线程调用的阻塞式IO方法已经返回
  • 线程成功地获得了试图取得的同步监视器
  • 线程正在等待某个通知时,其他线程发出了一个通知
  • 处于挂起状态的线程被调用了resume()恢复方法

线程死亡

线程会以如下三种方式结束,结束后就处于死亡状态

  • run()或call()方法执行完成,线程正常结束
  • 线程抛出一个未捕获的Exception或Error
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

当主线程结束时,其他线程不受任何影响,并不会随意结束,一但子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响

为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法。当线程处于就绪、运行、阻塞三种状态时,该方法将返回true,当程序处于新建和死亡时,该方法将返回false

Thread常用API

Thread常用方法
常用方法 返回值类型 说明
start() void 导致此线程开始执行; Java虚拟机调用此线程的run方法
run() void 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回
toString() String 返回此线程的字符串表示,包括线程的名称,优先级和线程组
currentThread() static Thread 返回对当前正在执行的线程对象的引用
getId() long 返回此线程的标识符
getName() String 返回此线程的名称
getPriority() int 返回此线程的优先级
getState() Thread.State 返回此线程的状态
getThreadGroup() ThreadGroup 返回此线程所属的线程组
getContextClassLoader() ClassLoader 返回此Thread的上下文ClassLoader
getStackTrace() StackTraceElement[] 返回表示此线程的堆栈转储的堆栈跟踪元素数组
getAllStackTraces() static Map 返回所有活动线程的堆栈跟踪图
setName(String name) void 将此线程的名称更改为等于参数 name
setPriority(int newPriority) void 更改此线程的优先级
setDaemon(boolean on) void 将此线程标记为 daemon线程或用户线程
setContextClassLoader(ClassLoader cl) void 设置此线程的上下文ClassLoader
isAlive() boolean 测试这个线程是否活着
isDaemon() boolean 测试这个线程是否是守护线程
isInterrupted() boolean 测试这个线程是否被中断
checkAccess() void 确定当前正在运行的线程是否有权限修改此线程
interrupt() void 中断这个线程
interrupted() static boolean 测试当前线程是否中断
join() void 等待被join的线程执行完毕
join(long millis) void 等待被join的线程的时间最长为millis毫秒;如果在millis毫秒内被join的线程还没执行结束,则不再等待
join(long millis, int nanos) void 等待被join的线程的时间最长为millis毫秒加nanos毫微秒
sleep(long millis) static void 让当前正在执行的线程暂停millis毫秒并进入阻塞状态
sleep(long millis, int nanos) static void 让当前正在执行的线程暂停millis毫秒加nanos毫微秒并进入阻塞状态
yield() static void 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器
holdsLock(Object obj) static boolean 当且仅当当前线程在指定的对象上保持监视器锁,返回true
activeCount() static int 返回当前线程的thread group及其子组中活动线程数的估计
enumerate(Thread[] tarray) static int 将当前线程的线程组及其子组中的每个活动线程复制到指定的数组中
dumpStack() static void 将当前线程的堆栈跟踪打印到标准错误流
clone() protected Object 复制
  • Thread类提供了让一个线程等待另一个线程完成的方法——join()方法
  • 【线程睡眠】如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态方法sleep()来实现
    • 当当前线程调用sleep()进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会
  • 【线程让步】yield()方法也是Thread类提供的静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态
    • yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行
    • 实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会
  • 【后台线程】有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程
    • 后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡
    • 主线程默认是前台线程;前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程
    • 前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon()必须在start()方法之前调用,否则会引发异常
  • 【改变线程的优先级】每个线程执行时都具有一定的优先级,优先级高的线程获得较多地执行机会,而优先级低的线程则获得较少的机会
    • 优先级范围:1-10,默认优先级:5
sleep yield
sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级 yield()方法只会给优先级相同,或优先级更高的线程执行机会
sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态 yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态
sleep()方法声明抛出了异常,所以调用sleep方法要么捕捉该异常,要么显式声明该异常 yield()方法则没有声明抛出任何异常

sleep()比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行

线程同步

线程同步就是为了解决线程安全的问题

线程通信

线程池

定时器