1.1 线程相关概念

  1. 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。可以理解为正在操作系统中运行的一个程序。
  2. 线程(thread)是进程的一个执行单元。
    1. 一个线程就是进行中一个单一顺序的控制流,进程的一个执行分支;
    2. 进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程;
    3. 在操作系统中是以进程为单位分配资源,如虚拟存储空间、文件描述符,每个线程都有各自的线程栈、自己的寄存器环境、自己的线程本地存储。
  3. 主线程与子线程
    1. JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程;
    2. Java中的线程不孤立,线程之间存在一些联系,如果在A线程中创建了B线程,称B线程为A线程的子线程,相应的A线程就是B线程的父线程。
  4. 串行、并发与并行

    1. 串行(Sequential):所有任务逐个完成;
    2. 并发(Concurrent):交替执行,减少CPU空闲时间,可以提高处理事物的效率,即一段时间内可以处理或者完成更多的事情;
    3. 并行(Parallel):同时执行,是一种理想的并发。
    4. 从硬件角度来说,如果单核CPU,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让CPU快速地在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时执行,如果是多核心CPU,可以为不同的线程分配不同的CPU内核。

      1.2 线程的创建与启动

      在Java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)。Thread类有两个常用的构造方法:Thread()与Thread(Runnable),对应创建线程的两种方式(这两种创建线程的方式没有本质的区别):

      定义Thread类的子类; 定义一个Runnable接口的实现类。

  5. 定义Thread类的子类 ```java /**

    • 1) 定义类继承Thread */ public class MyThread extends Thread{

      /**

      • 2) 重写Thread父类中的run()
      • run()方法体中的代码是子线程要执行的任务 */ @Override public void run() { System.out.println(“这是子线程打印的内容”); } }

public class Test { public static void main(String[] args) { System.out.println(“JVM启动main线程,main线程执行main方法”); // 3) 创建子线程对象 MyThread thread = new MyThread(); // 4) 启动线程 thread.start(); /* 调用线程的start()方法来启动线程,启动线程的实质是请求JVM运行 相应的线程,这个线程具体在什么时候运行由线程高度器(Scheduler)决定。 注意:1. start()方法调用结束并不意味着子线程开始运行;

  1. 2. 新开启的线程会执行run()方法;
  2. 3. 如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序;
  3. 4. 多线程运行结果与代码执行或者调用顺序无关。
  4. */
  5. System.out.println("main线程后面其他的代码……");
  6. }

}

  1. 2. 定义一个Runnable接口的实现类。
  2. ```java
  3. /**
  4. * 当线程类已经有父类,就不能用继承Thread类的形式创建线程,可以使用实现Runnable接口的形式
  5. * 1) 定义类实现Runnable接口
  6. */
  7. public class MyRunnable implements Runnable{
  8. /**
  9. * 2) 重写Runnable接口中的抽象方法run(),run()方法就是子线程要执行的代码
  10. */
  11. @Override
  12. public void run() {
  13. for (int i = 1; i <= 100; i++){
  14. System.out.println("sub thread --> " + i);
  15. }
  16. }
  17. }
  18. public class Test {
  19. public static void main(String[] args) {
  20. // 3) 创建Runnable接口的实现类对象
  21. MyRunnable runnable = new MyRunnable();
  22. // 4) 创建线程对象,也可以传递匿名内部类对象
  23. Thread thread = new Thread(runnable);
  24. // 5) 开启线程
  25. thread.start();
  26. // 当前是main线程
  27. for (int i = 1; i <= 100; i++){
  28. System.out.println("main -> " + i);
  29. }
  30. }
  31. }

1.3 线程的常用方法

1.3.1 currentThread()方法

Thread.currentThread()方法可以获得当前线程,Java中的任何一段代码都是执行在某个线程中的,执行当前代码的线程就是当前线程。同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是代码实际运行时候的线程对象。

  1. /**
  2. * 定义一个线程类
  3. */
  4. public class SubThread1 extends Thread{
  5. public SubThread1(){
  6. System.out.println("构造方法打印当前线程名称:" + Thread.currentThread().getName());
  7. }
  8. @Override
  9. public void run() {
  10. System.out.println("run方法打印当前线程名称:" + Thread.currentThread().getName());
  11. }
  12. }
  13. /**
  14. * 测试当前线程
  15. */
  16. public class Test01CurrentThread {
  17. public static void main(String[] args) {
  18. System.out.println("mian方法中打印当前线程:" + Thread.currentThread().getName());
  19. // 创建子线程,在main线程中调用构造方法,所以构造方法的当前线程是main线程
  20. SubThread1 t1 = new SubThread1();
  21. // 启动子线程,子线程调用run()方法,所以run()方法的当前线程是Thread-0子线程
  22. t1.start();
  23. // 在main线程中直接调用run()方法,此时run()方法的当前线程是main线程
  24. t1.run();
  25. }
  26. }

1.3.2 setName()/getName()

thread.setName(线程名称):计线程名称 thread.getName():返回线程名称

建议为每个线程设计名称,体现线程功能。

1.3.3 isAlive()

thread.isAlive()判断当前线程是否处于活动状态,即已启动并且尚未终止。

  1. public class SubThread extends Thread{
  2. @Override
  3. public void run() {
  4. System.out.println("run方法,isAlive = " + this.isAlive());
  5. }
  6. }
  7. public class Test {
  8. public static void main(String[] args) {
  9. SubThread t1 = new SubThread();
  10. System.out.println("begin==" + t1.isAlive());
  11. t1.start();
  12. System.out.println("end==" + t1.isAlive());
  13. }
  14. }

1.3.4 sleep()

Thread.sleep(mills):让当前线程休眠指定的毫秒数。

1.3.5 getId()

thread.getId():可以获得线程的唯一标识。

注意:某个编号的线程运行结束后,该编号可能被后续创建的线程使用;重启JVM后,同一个线程的编号可能不一样。

1.3.6 yield()

Thread.yield():放弃当前的CPU资源。

  1. public class SubThread extends Thread{
  2. @Override
  3. public void run() {
  4. long begin = System.currentTimeMillis();
  5. long sum = 0;
  6. for (int i = 1; i <= 1e+6; i++){
  7. sum += i;
  8. Thread.yield(); //线程让步,放弃CPU资源,等待资源重新分配
  9. }
  10. long end = System.currentTimeMillis();
  11. System.out.println("子线程用时:" + (end - begin));
  12. }
  13. }
  14. public class Test {
  15. public static void main(String[] args) {
  16. // 子线程计算累加
  17. SubThread t1 = new SubThread();
  18. t1.start();
  19. // main线程中计算累加
  20. long begin = System.currentTimeMillis();
  21. long sum = 0;
  22. for (int i = 1; i <= 1e+6; i++){
  23. sum += i;
  24. }
  25. long end = System.currentTimeMillis();
  26. System.out.println("main线程用时:" + (end - begin));
  27. }
  28. }

运行结果:

main线程用时:4 子线程用时:764

1.3.7 setPrority()

thread.setPrority(num):设置线程的优先级,取值为1-10,超出范围会抛出IllegalArgumentException。

注意:1. 线程优先级本质只是给线程调度器一个提示信息,不能保证优先级高的线程先运行;

  1. 设置不当或者滥用可能导致某些线程永远无法得到运行,即产生线程饥饿;
  2. 优先级具有继承性,在某线程中创建的子线程的优先级与该线程一致。

1.3.8 interrupt()

interrupt():中断线程。

注意:调用这个方法仅仅是在当前线程打一个 停止标志 ,并不是真正停止线程,需要配合isInterrupted()方法实现子线程的中断。

1.3.9 setDaemon()

Java中线程分为用户线程与守护线程。守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。

注意:守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出。

1.4 线程的生命周期

1 线程概述 - 图1
线程的生命周期可以通过getState()方法获得,线程的姿态是Thread.State枚举类型定义的,有以下几种:

  1. NEW:新建状态。创建了线程对象,在调用start()启动之前的状态;
  2. RUNNABLE: 可运行状态。它是一个复合状态,包含REDAY和RUNNING两个状态
    1. REDAY状态线程可以被线程调度器进行调度使它处于RUNNING状态;
    2. RUNNING状态表示该线程正在执行,Thread.yield()方法可以把线程由RUNNING状态转换为REDAY状态。
  3. BLOCKED:阻塞状态。线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态,处于阻塞状态的线程不会占用CPU资源。当阻塞I/O操作执行完成,或者线程获得了其申请的资源,线程可以转换为RUNNING状态。
  4. WAITING:等待状态。线程执行了object.wait(),thread.join()方法会把线程转换为WAITING状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态。
  5. TIMED_WAITING:与WAITING状态类似,都是等待状态,区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为RUNNABLE。
  6. TERMINATED:终止状态。

    1.5 多线程编程的优势与存储的风险

    优势:

  7. 提高系统的呑吐率(Throughout)。多线程编程可以使一个进程有多个并发(concurrent),即同时进行的)的操作。

  8. 提高响应性(Responsiveness)。Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。
  9. 充分利用多核(Multicore)处理器资源。通过多线程可以充分的利用CPU资源。

多线程编程存在的问题与风险:

  1. 线程安全(Thread safe)问题:多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读脏数据,丢失数据更新。
  2. 线程活性(thread liveness)问题:由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种:
    1. 死锁(Deadlock):线程相互锁定;
    2. 锁死(Lockout):解锁条件无法成立,类似睡美人故事王子死了;
    3. 活锁(Livelock):解锁条件一直无法达到,类似小猫咬自己尾巴;
    4. 饥饿(Starvation):无法获得资源而导致线程死亡,类似健壮的雏鸟从母鸟嘴中抢到食物。
  3. 上下文切换(Context Switch):处理器从执行一个线程切换到执行另外一个线程。
  4. 可靠性:可能会由一个线程导致JVM意外终止,其他的线程也无法执行。