一、认识线程
1.1线程的概念
一个线程就是一个 "执行流"。每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时"执行着多份代码。
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
1.2为啥要有线程
1.2.1首先, "并发编程" 成为 "刚需".
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
1.2.2其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
1.2.3最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"(Coroutine)
1.3进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
1.4 Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
1.5面试题
二、创建线程
InterruptedExceptipn表示终断异常,意思是sleep睡眠过程中还没到点就被唤醒了。
2.1方法1 继承 Thread 类
class MyThread extends Thread {
//继承Thread创建一个线程
@Override
public void run() {
while (true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
//创建MyThread的实例
Thread t = new MyThread();
// start 会创建新的线程
t.start();
// run 不会创建新的线程. run 是在 main 线程中执行的~~
// t.run();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上述代码涉及到两个线程:
1.main方法所对应的线程(一个进程里至少得有一个线程),也可以称之为主线程
2.通过t.start创建的新的线程
(每个线程是一个独立的“执行流”)
注意:此处的交替不是严格意义的交替,具体谁先打印是不确定的。(多个线程在CPU上执行调度的顺序是不确定的,随机的!即使存在优先级,也只是建议)
1.run叫做入口方法,这种特殊的方法可以被系统自动的调用
2.可以把run堪称别的线程的main就和正常写程序一样,想用的放在run里面让run自动调用这些逻辑。
3.run是重写了父类的方法(多态)
4.sleep是Thread的静态方法,参数单位为ms
2.2实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
对比上面两种方法:
继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
2.3第三方软件查看多线程
1.在本地jdk目录的bin目录中找到jconsole(jdk提供的工具)
2.选择本地进程(前提是程序在运行中)
(若进程列表全空,一般是权限问题。通过管理员身份运行就可以解决)
堆栈线程描述了当前的代码执行到了哪里,便于后期我们找错
2.4匿名内部类创建 Thread 子类对象
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread =new Thread(){
@Override
public void run(){
while(true){
System.out.println("Hello Thread");
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
}
};
thread.start();
while(true){
System.out.println("Hello Main");
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
2.5匿名内部类创建 Runnable 子类对象
public class ThreadDemo4 {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("Hello Thread");
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
}
});
thread.start();
while(true){
System.out.println("Hello Main");
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
2.6lambda 表达式创建 Runnable 子类对象
Lambda表达式本质是一个匿名函数
public class ThreadDemo5 {
public static void main(String[] args) {
Thread thread =new Thread(()->{
while(true){
System.out.println("Hello Thread");
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
});
thread.start();
while(true){
System.out.println("Hello Main");
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
三、多线程的优势-增加运行速度
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
- 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
- serial 串行的完成一系列运算. concurrency 使用两个线程并行的完成同样的运算.
public class ThreadAdvantage {
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使用并发方式
concurrency();
// 使用串行方式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利用一个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运行结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串行: %f 毫秒%n", ms);
}
}
并发: 399.651856 毫秒
串行: 720.616911 毫秒
四、Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
4.1Thread 的常见构造方法
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
name参数,是给线程起了一个名字。这里的名字,不影响程序执行,只是方便咱们在调试的时候,快速找到咱们关心的线程。
4.2Thread 的几个常见属性
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还
活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
}
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello t");
});
t.start();
try {
// 上述 t 线程没有进行任何循环和 sleep, 意味着里面的代码会迅速执行完毕.
// main 线程如果 sleep 结束, 此时 t 基本上就是已经执行完了的状态. 此时 t 对象还在
// 但是在 系统中 对应的线程已经结束了.
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.isAlive());
}
}
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
}
});
// 默认是前台线程, 也就是设为 false
// 此时这个线程会阻止进程结束
// 改成 true 变成后台线程. 不影响进程的结束.
t.setDaemon(true);
t.start();
}
}