线程(Thread)
1.1 概念
1) 线程是什么
一个线程就是一个 "执行流",即每个线程之间都有自己的执行逻辑. 多个线程之间 "同时" 执行着多份代码.
举个例子:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
2) 为啥要有线程
首先, "并发编程" 成为 "刚需".
单核 CPU 的发展遇到了瓶颈(太小进入了量子力学,会出现不确定性). 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.线程是为了解决并发编程引入的机制.
其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量. 创建线程比创建进程更快. 销毁线程比销毁进程更快. 调度线程比调度进程更快.
进程包含线程,同一个进程里的若干线程之间共享着内存资源和文件描述符表。每个线程被独立调度执行,每个进程都有自己的状态、优先级、上下文、记账信息
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)
3) 进程和线程的区别
进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别
人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是
不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多
进程的最大区别。
一个线程对应一个 PCB , PCB 里的状态,上下文、优先级、记账信息这些都是每个线程自己的,即各自记录各自的。但是同一个进程里面的 PCB 之间的 pid 是一样的,内存指针和描述符表也是一样的
线程安全问题:在一个房间里有十个人吃十只鸡,其中 1 和 2 看上了同一块鸡腿,这时候谁也不让谁,就可能打起来(这两个线程崩溃了);又或者 1 先把鸡腿抢吃了,但 2 就不乐意了,于是把桌都掀了(把其他线程也搞崩溃了)。在多进程里面就不会有这样的问题,因为是分是个房间十个人各自吃各自的(但是这样资源消耗大)。线程安全问题的罪魁祸首就是抢占式执行,随即调度的原因。
进程是操作系统分配资源的最小单位,线程是操作系统调度的最小单位。
4) Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
1.2 第一个多线程程序
感受多线程程序和普通程序的区别:
每个线程都是一个独立的执行流
多个线程之间是 "并发" 执行的.
package Thread;
import java.util.Random;
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("Hello world");
// 为了方便看,每次执行完休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while (true){
System.out.println("World Hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
使用 jconsole 命令观察线程
1.3 创建线程
方法1 继承 Thread 类
1) 继承 Thread 来创建一个线程类.
2) 创建 MyThread 类的实例
3) 调用 start 方法启动线程
就是上面第一个多线程的代码
方法2 实现 Runnable 接口
1) 实现 Runnable 接口
package Thread;
// Runnable作用是描述一个“要执行的任务” ,run方法就是任务的执行细节
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello Thread");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//这里只是描述了这个任务
Runnable runnable = new MyRunnable();
//把任务交给线程来执行
Thread t = new Thread(runnable);
t.start();
}
}
方法3 匿名内部类创建 Thread 子类对象
package Thread;
//使用匿名内部类来创建线程
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread(){
//1.创建了一个Thread的子类(子类没有名字)所以叫做“匿名”
//2.创建了子类的实例,并让t引用只想该实例
@Override
public void run() {
System.out.println("Hello");
}
};
t.start();
}
}
方法4 匿名内部类创建 Runnable 子类对象
package Thread;
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
//这里的写法和2本质相同,只不过是把实现Runnable任务交给匿名内部类
//此处是创建了一个类,实现Runnable,同时创建了类的实例,并且传给了Thread的构造方法
@Override
public void run() {
System.out.println("hello");
}
});
t.start();
}
}
方法5 lambda 表达式创建 Runnable 子类对象
package Thread;
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
//把服务用lambda表达式来描述
//直接把lambda传给Thread构造方法
System.out.println("hello");
});
t.start();
}
}
1.4 多线程的优势-增加运行速度
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
使用 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++) {方法
说明
Thread()
创建线程对象
Thread(Runnable target)
使用 Runnable 对象创建线程对象
Thread(String name)
创建线程对象,并命名
Thread(Runnable target, String name)
使用 Runnable 对象创建线程对象,并命名