一.认识线程
1.概念
(1)线程是什么?
一个线程就是⼀个"执行流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行
着多份代码.
(2)为什么要有线程
1.单核 CPU 的发展遇到了瓶颈。要想提高算力,就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
2.有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程.
其次,虽然多进程也能实现并发编程,但是线程比进程更轻量.
1.创建线程比创建进程更快.
2.销毁线程比销毁进程更快.
3.调度线程比调度进程更快.
(3)进程和线程的关系
进程是包含线程的。每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间.
2.第一个多线程程序
感受多线程代码与普通代码的区别:
1.每一个线程都是单独的执行流
2.多个线程都是并发执行
这里我们创建两个线程,然后启动两个线程,多次执行,可以发现有时候先打印"Thread1 is running",有时候会先打印"Thread2 is running"
这就说明了CPU在线程调度这一块是随机调度的
java
Thread thread1 = new Thread(() -> {
System.out.println("Thread1 is running!");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread2 is running!");
});
thread1.start();
thread2.start();

同时,我们还可以使用jconsole来观察线程
在这个路径下找到jconsole,然后打开



这个是jdk内置的功能,用来帮助我们查看线程
3.创建线程
start() 方法: 负责请求操作系统分配资源、创建新的执行栈,并最终调用 run()。这是底层的启动逻辑,开发者不应更改。
run() 方法: 这是一个空方法(或者说是一个占位符)。它定义了线程在获取 CPU 后要执行的业务逻辑。
1.创建一个类继承Thread类重写run方法,通过start启动线程
java
class myThread extends Thread{
@Override
public void run(){
System.out.println("Thread1 is running!");
}
}
public class Demo2 {
//通过继承Thread类来创建线程
public static void main(String[] args) {
myThread thread1 = new myThread();
thread1.start();
}
}
还可以根据匿名内部类的方法来实现
java
Thread thread2 = new Thread(){
@Override
public void run(){
System.out.println("Thread2 is running!");
}
};
thread2.start();
2.创建一个类实现Runnable接口,重写run方法,搭配Thread实例,通过start启动线程
java
public class Demo3 {
static class mythread implements Runnable{
@Override
public void run(){
System.out.println("Thread1 is running!");
}
}
//通过实现Runnable接口来创建线程
public static void main(String[] args) {
mythread thread1 = new mythread();
Thread thread2 = new Thread(thread1);
thread2.start();
}
}
或者通过匿名内部类的方式来创建
java
Thread t1 = new Thread(new Runnable(){
@Override
public void run(){
System.out.println("Thread2 is running!");
}
});
t1.start();
3.lambda表达式创建线程
java
public static void main(String[] args) {
//通过lambda表达式来创建线程
Thread t1 = new Thread(() -> {
System.out.println("Thread1 is running!");
});
t1.start();
}
4.多线程的优势----增加运行速度
测试代码:
java
public class ThreadAdvantage {
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_000_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);
}
}

从这里我们可以看出多线程在提高效率的效果
二.Thread类及常见方法
1.Thread()常见的构造方法

我们可以通过代码来分别看一下通过这些方法创建的线程的区别
java
public static void main(String[] args) {
//方法1创建线程
Thread thread1 = new Thread();
//方法2创建线程
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("方法2创建线程");
}
});
//方法3创建线程
Thread thread3 = new Thread("线程3");
//方法4创建线程
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("方法4创建线程");
}
}, "线程4");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
来运行这四个线程,观察结果,可以看到只输出了两句话

也就是说,使用方法1和方法3创建线程,
对于方法1来说,我们只创建了线程对象,没有重写run方法,也就是说这个线程里面没有任何内容执行
对于方法3来说,我们只在创建线程对象的基础上给这个线程对象命名了,但是仍然没有重写run 方法,也就是说也不会去执行任何操作.
对于方法2和方法4,方法2是只重写了run方法,方法4是重写了run方法,也给这个线程对象进行命名
2.Thread的几个常见属性

1.ID 是线程的唯一标识,不同线程不会重复
2.名称是各种调试工具用
3.状态表示线程当前所处的一个情况,下面我们会进一步说明
4.优先级高的线程理论上来说更容易被调度到
5.关于后台线程,需要记住一点:JVM 会在一个进程的所有非后台线程结束后,才会结束运行。
6.是否存活,即简单的理解,为 run 方法是否运行结束了
我们来创建一个新的线程,用上述方法得到线程的各种属性
java
Thread t1 = new Thread(() ->{
for(int i = 0 ; i < 10 ;i++){
System.out.println("线程1正在执行"+i);
}
System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程ID"+Thread.currentThread().getId());
System.out.println("当前线程名称"+Thread.currentThread().getName());
System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程状态"+Thread.currentThread().getState());
System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程优先级"+Thread.currentThread().getPriority());
System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程是否为后台进程"+Thread.currentThread().isDaemon());
System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程是否存活"+Thread.currentThread().isAlive());
System.out.println("当前线程名称"+Thread.currentThread().getName()+"当前线程是否中断"+Thread.currentThread().isInterrupted());
}, "线程1");
t1.start();
我们可以通过调用这些方法在需要观察状态的时候使用

3.启动一个线程
之前我们可以知道线程执行的是run方法里面的代码内容,
但是只有使用.start()方法之后,一个线程才算是被创建出来.
1.调用start()方法是非常快的,几乎没有阻塞
所以观察下列代码,在start完之后打印main线程正在执行
重复执行这个代码,大部分时候都是先打印"main线程正在执行",然后再打印"线程1正在执行"
虽然操作系统对于线程的调度是随机的.但是线程的创建是有开销的.
也就是说在start之后,操作系统对于t1线程和main线程是"并行关系"

所以说大部分时候,都是会先执行到打印"main线程正在执行"这句话

2.同一个线程只能start一次
我们可以试着对同一个线程start两次,看看会有什么情况,可以看到给我们报错了
显示IllegalThreadStateException非法线程状态异常

这是因为当一个线程执行完start之后就是处于就绪或者堵塞状态了
start里面对线程状态进行了判断,处于就绪或者堵塞状态的线程不能再次start.
4.中断一个线程
正常情况下,我们需要等一个线程执行完run方法里面的内容才能结束线程
但是有时候我们需要提前结束线程,尤其是在sleep的时候
这时候我们就有两种方法来提前结束线程了
1.通过变量
就如同下面的代码,通过设置一个flag变量,改变flag来控制线程是否结束
java
public class Demo9 {
public static boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
while(flag){
System.out.println("线程1正在执行");
}
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}
}
2 .使用isInterruptted()方法
在Thread对象中,包含了一个内置boolean变量,也就是相当标志位,
如果为false,就说明没有人去尝试结束这个线程,如果为true就说明有人尝试结束这个线程
当我们直接尝试去调用这个方法的时候会显示异常

这是因为针对lambda表达式的定义,是在new Thread()之前的,也就是说,这个时候还不存在t1这个变量
这里我们就需要使用Thread.currentThread()这个方法了,哪个线程调用这个方法,就返回哪个线程的引用
然后需要结束的时候在调用interrupt()这个方法,就是把标志位从false改为true.
java
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程1正在执行");
}
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
System.out.println("线程1是否被中断: " + t1.isInterrupted());
}
这个就是上述代码执行的效果.

注意:在程序终止这里有一个奇怪的设定,如果说线程正在sleep的时候,调用interrupt()方法会把线程提前唤醒.同时在唤醒之后,会把这个标志位重置为false,这个时候就需要手动决定是否结束线程了
还是刚刚那个代码,这次在线程中增加了一个2秒的休眠,然后再main线程中让线程启动,然后调用interrupt()方法
这个时候,线程并不会停止,而是会持续打印,
这是因为在调用interrupt方法的时候,t1线程在休眠中
这时候interrupt方法将线程1唤醒,
然后线程里面的标志位又重新回到false,
但是之后的代码又没有人把这个标志位改为true,所以线程就不会中断
java
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程1正在执行");
}
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
System.out.println("线程1是否被中断: " + t1.isInterrupted());
}
我们看try,catch这一块,其实当sleep被唤醒的时候,就会触发InterruptedException,但是下面我们只进行了打印这个异常的操作,并没有其他操作,所以说在捕获异常之后,还是会继续执行

我们将循环里面的打印操作删除,这样方便让我们看异常提示,这里就捕获到异常并打印了,但是没有进行其他操作,所以不会中断线程

如果我们在里面加入break,也可以起到中断线程的效果,同时还可以再break前面加一些善后逻辑

5.等待一个线程
由于操作系统是随机调度线程的,所以说当两个线程同时启动的时候,我们并不能确定哪一个线程先启动.
但是有时候我们需要将线程启动的顺序确定下来
这时候就可以使用.join()方法了

对于这个代码在上面就已经被说过了,大部分时候都是会先打印main线程正在执行
但是如果我们就是要先打印"线程1正在执行"呢?

这时候可以在main线程里面调用t1.join()
这时候main线程就会等待t1线程结束之后再继续t1.join这句代码后面的逻辑
同时在使用join的时候是需要抛出一个异常的

这时候我们无论执行多少次都是会先打印"线程1 正在执行"
同时我们还可以通过增加参数来限制等待的时长,如果不加参数的话,就是死等.
6.获取当前线程引用

使用这个方法就可以获取调用这个方法的线程的引用,在上面已经演示过很多次了
7.休眠当前线程

通过sleep()方法进行休眠操作,第二种方法可以更高精度的填写休眠时间.也已经使用过很多次了在上面
三.线程的状态
下面是线程的各种状态

1.NEW
Thread对象创建了,但是还没开始start
java
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
for(int i = 0 ; i < 10 ;i++){
System.out.println("线程1正在执行"+i);
}
});
System.out.println("线程1当前状态:"+t1.getState());
}

2.RUNNABLE
就绪状态,随时可以去cpu上运行,代码中不触发阻塞状态的时候都是RUNNABLE状态
java
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
for(int i = 0 ; i < 3 ;i++){
System.out.println("线程1正在执行"+i);
}
System.out.println("线程1当前状态:"+Thread.currentThread().getState());
});
t1.start();
}

3.TERMINATED
线程执行完了,但是Thread对象还在

剩下的这三个都是属于阻塞状态,只不过是不同情况下的阻塞状态
4.BLOCKED
这个是由于加锁时产生的阻塞状态,后面会重点讨论,先演示一下
对于这个代码,我们给线程1和线程2都加了同一把锁,
这时候线程1没解锁的时候,线程2是拿不到锁的,此时线程2就是在等待线程1解锁,这个状态就是BLOCKED
java
public class Demo13 {
static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
synchronized (object) {
while(true){
}
}
});
Thread t2 = new Thread(() ->{
synchronized (object) {
while(true){
}
}
});
t1.start();
t2.start();
System.out.println("线程1当前状态:"+t1.getState());
System.out.println("线程2当前状态:"+t2.getState());
}
}

5.WAITING
这个是无时间的等待,通常出现在无时间版本的join中
通过在t2线程中加入t1.join(),等待t1线程执行结束再执行t2
此时t1线程的状态是RUNNABLE,t2的状态就是WATING
java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
while(true){
}
});
Thread t2 = new Thread(() ->{
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}

6.TIMED_WATING
这个是有时间的等待,通常是出现在有时间版本的join中
将上述代码的join增加等待时间,t2线程的状态就会变成TIME_WATING
java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
while(true){
}
});
Thread t2 = new Thread(() ->{
try {
t1.join(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
while(true){
}
});
t1.start();
t2.start();
}

多线程初阶(一)到这里就结束了,后面会为大家带来更多关于线程问题的知识讲解.