1. 认识线程(Thread)
1.1 概念
1.1.1 线程是什么
一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间"同时" 执行着多份代码.
举个栗子:
我们设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找 来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队, 自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别 排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
1.1.2 为啥需要线程
首先, "并发编程" 成为 "刚需"。
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)
1.1.3 进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
多进程就是例如同时打开QQ音乐和微信,他们连各自为一个进程。
多线程就是例如打开微信的聊天功能和朋友圈,他们各自为一个线程。
进程是资源分配的基本单位!
线程共享同一份资源!
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
1.1.4 Java的线程和操作系统线程的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库)
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
注:也就是说Java的线程和操作系统的线程是两回事,能思想线程的功能是操作系统内核
1.2 第一个多线程程序
感受多线程程序和普通程序的区别:
- 每个线程都是一个独立的执行流
- 多个线程之间是 "并发" 执行的
class MyThread extends Thread{ @Override public void run(){ while(true){ System.out.println("hello t"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Test { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); while(true){ System.out.println("hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
使用jconsole命令观察线程
这是jdk提供的工具,能够让我们查看java进程里面线程的详情!
jconsole只能分析java进程,不能识别非java写的进程~~
除了main和Thread-0两个线程之外,剩下都是JVM自己创建的!
1.3 创建线程的方式(5种)
1.3.1 继承Thread类
class MyThread extends Thread{ @Override public void run(){ while(true){ System.out.println("hello t"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Test { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); while(true){ System.out.println("hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
1.3.2 实现Runnable接口
public static void main(String[] args) { Runnable run = new Thread2(); Thread thread = new Thread(run); thread.start(); while(true){ System.out.println("hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Thread2 implements Runnable{ @Override public void run() { while(true){ System.out.println("hello run"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
注:以上两种创建线程的区别在于,实现Runnable接口的方法使线程和线程要做的事情分开了,达到了解耦合的作用
1.3.3 继承Thread类,使用匿名内部类
public static void main(String[] args) { Thread t1 = new Thread(){ @Override public void run(){ System.out.println("hello"); } }; }
1.3.4 实现Runnable接口,使用匿名内部类
public static void main(String[] args) { Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("hello"); } }); }
1.3.5 使用lambda表达式创建线程(推荐)
public static void main(String[] args) { Thread t3 = new Thread(() -> System.out.println("hello") ); }
1.4 多线程的优势—增加运行速度
可以观察多线程在一些场合是可以提高程序整体运行的效率
- 使用 System.currentTimeMillis()可以记录当前系统的 纳秒 级时间戳.
(1)串行
public static void main(String[] args) { long start = System.currentTimeMillis(); for(int a = 0;a < 1000;a++){ try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } for(int b = 0;b < 1000;b++){ try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } long end = System.currentTimeMillis(); System.out.println(end - start); }
(2)并发
public static void main(String[] args) { Thread t = new Thread(()->{ for(int a = 0;a < 1000;a++){ try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }); long start = System.currentTimeMillis(); t.start(); for(int b = 0;b < 1000;b++){ try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } long end = System.currentTimeMillis(); System.out.println(end - start); }
2. Thread类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象,就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
2.1 Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建下线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
2.2 Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台进程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
特别说明:isDaemon()方法
true表示是后台线程
false表示是前台进程
(1)后台进程不阻止Java进程结束。哪怕后台线程还没有执行完,Java进程该结束就结束了。
(2)前台线程会阻止Java进程结束,必须得java进程中所有的前台线程都执行完,Java进程才会结束。
(3)创建的线程默认都是前台的,可以通过setDaemon()方法设置为后台。