1. 线程
1.1 概念
线程又可以称为轻量级进程 ,在进程的基础上做出了改进。
一个进程在刚刚启动时,做的第一件事就是申请内存和资源,进程需要把依赖的代码和数据,从磁盘加载到内存中这件事是比较耗费时间的,有的业务场景可能会频繁的创建,销毁进程,也就导致了大量的开销。而线程则省去了分配资源和释放资源带来的开销。
1.2 线程与进程的区别
与进程相同,线程也可以用PCB描述,所拥有的属性也是大致相同的。不同点在于,每个线程都有自己独立的系统资源,而多个线程的系统资源是可以相互共享的。所以这些线程之间需要的重复的系统同资源就只需要申请一次避免了重复的开销
举个例子:1线程需要资源A,B 2线程需要资源B,C 3线程需要资源 A,C。现在1线程执行了,2线程执行时就只需要申请资源 C, 再到3线程执行时则不需要再申请资源了。
也不是所有线程都可以共享资源,系统会把能资源共享的线程分成组,就称为线程组,而线程组也是进程的一部分,也就是一个进程是由多个线程组成的
注意:
- 进程是包含线程的
- 每个线程也是一个独立的执行流,可以执行一些代码并单独参与到cpu的调度中(状态,上下文,优先级,记账信息,每个线程都有自己的一份
- 每个进程有自己的资源,进程中的线程共用这一份资源(内存空间和文件描述符表)
- 进程是资源分配的基本单位,线程是调度执行的基本单位
- 同一个进程中的线程之间,可能会互相干扰引起线程安全问题
- 进程和进程之间不会互相影响,如果同一个进程中的线程,抛出异常,可能会影响到其他线程,把整个进程中的所有线程都终止
2. 多线程编程
写代码的时候,可以使用多进程进行并发编程,也可以使用多线程并发编程。
在Java中是不太推荐多进程编程的,
2.1 创建线程的方法
2.1.1 继承 Thread
1. 创建Thread子类 重写run方法
class MyThread extends Thread {
@Override
public void run() {
System.out.println("调用了MyThread类中的run方法");
}
}
run方法是该线程的入口,不需要手动调用,jvm会在线程创建好时自动调用
2. 创建子类的实例
Thread t = new MyThread();
3. 调用 Thread类中的start方法
t.start();
注意:
- 调用Thread类中的start方法,才会真正调用系统api在内核中创建出线程,直接调用run方法是不会创建出线程的
- 一个Thread对象只能调用一次start方法
上面我们说了,每个线程都是一个独立的执行流,就相当于,我们创建的线程中的run方法和main方法是同时执行的
例如以下代码:
class MyThread2 extends Thread {
@Override
public void run() {
while(true) {
System.out.println("MyThread中的run方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread2();
t.start();
while(true) {
System.out.println("main方法");
Thread.sleep(1000);
}
}
}
输出结果:
如果把
t.start();
改为
t.run();
则不会打印 “main方法” 因为 t并没有创建出线程 ,run方法是在main方法的线程中执行
2.1.2 实现 Runnable接口
实现Runnable 接口重写 run方法
class MyThread3 implements Runnable {
@Override
public void run() {
System.out.println("MyThread3中的run方法");
}
}
创建一个 MyThread3 对象作为参数传入 Thread的构造方法然后调用start
public static void main(String[] args) {
Thread t = new Thread(new MyThread3());
t.start();
}
2.1.3 使用匿名内部类
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("MyThread4中的run方法");
}
};
t.start();
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("MyThread5中的run方法");
}
});
t.start();
}
}
2.1.4 使用lambda表达式
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(()-> {
System.out.println("MyThread6中的run方法");
} );
t.start();
}
}
推荐使用这种写法。
2.2 Thread类的属性和方法
2.2.1 构造方法
- Thread() 无参构造方法,创建一个新的线程对象。
- Thread(Runnable target) 使用指定的Runnable对象作为线程的目标,创建一个新的线程对象。
- Thread(String name) 使用指定的名称创建一个新的线程对象。
- Thread(Runnable target, String name) 使用指定的Runnable对象和名称创建一个新的线程对象。
- Thread(ThreadGroup group, Runnable target) 使用指定的线程组和Runnable对象创建一个新的线程对象。
- Thread(ThreadGroup group, Runnable target, String name) 使用指定的线程组、Runnable对象和名称创建一个新的线程对象。
解释:每个线程都有一个名称,如果没有给它命名则会默认为Thread-0,Thread-1......累加。当一个线程运行时我们可以通过jdk的工具 jconsole 查看线程的状态,名称等。这个工具在jdk目录底下的bin文件夹中
示例代码:
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true) {
System.out.println("ThreadDemo7");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "ThreadDemo7");
t.start();
}
}
运行该代码然后打开jconsole
如图所示:
我们在本地进程即可看到我们运行的ThreadDemo7,点击连接即可查看线程的状态等信息
线程的名字是可以重复的
2.2.2 常见的属性
解释:
-
id:每个线程都有一个唯一的id,用于标识线程。
-
名称:线程可以设置一个可选的名称来标识自己。可以通过setName(String name)方法设置线程的名称。
-
状态:线程在运行过程中会处于不同的状态。(下面会详细讲解)
-
优先级:每个线程都有一个优先级,用于指示线程在竞争CPU资源时的相对重要性。优先级范围从1到10,默认为5。可以通过setPriority(int priority)方法设置线程的优先级。
-
是否后台进程:线程可以设置为后台进程。后台进程不会阻止程序的终止,当所有前台线程结束时,后台线程也会自动结束。可以通过setDaemon(boolean on)方法将线程设置为后台进程
-
是否存活:线程是否仍然存活(即尚未终止)。
-
是否被中断:线程可以通过调用interrupt()方法中断自己或其他线程 以及通过interrupted()静态方法检查当前线程是否被中断,并清除中断状态。
线程中断注意事项:
示例代码:
public class ThreadDemo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//currentThread() 方法是获取当前线程的实例,即这里的t
while(!Thread.currentThread().isInterrupted()) {
System.out.println("ThreadDemo8");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("执行完毕");
});
t.start();
Thread.sleep(5000);
System.out.println("让线程结束");
t.interrupt();
}
}
当我们运行代码结果如下:
我们可以看到,代码并不像我们预期的那样结束,而是在继续运行 ,这是因为 sleep的原因,因为,当我们运行interrupt()时,sleep()可能还未结束,于是sleep就被提前唤醒了
sleep被提前唤醒会做两件事:
1. 抛出InterruptedException异常
2.将Thread对象的isInterrupted标志位设置为false
所以运行完interrupt()后标志位已经被设为true但是sleep又把它改回false了,所以会继续执行。我们要结局这个问题只需在catch中加一个break
2.2.3 join方法
如果我们希望某个线程在另一个线程之前执行完,可以用到join方法
示例代码:
public class ThreadDemo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("ThreadDemo10");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();//让main线程等待t线程执行完
System.out.println("希望t线程执行完再执行这条语句");
}
}
运行结果:
执行join后,如果t线程在运行中,main线程就会阻塞(主动放弃去cpu上执行 )直到t运行结束
除了无参数的join方法还有带参数的join方法
解释:
- join():死等,一定会等到调用该方法的线程执行完
- join(long millis):带超时时间的等待,如果在设定的时间内,调用该方法的线程没有执行完,则不会继续等待
- join(long millis, int nanos):带超时时间的等待,精确到纳秒,如果在设定的时间内,调用该方法的线程没有执行完,则不会继续等待
示例代码:
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("ThreadDemo11");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
//让join只等2秒,如果没等到就不等了
t.join(2000);
System.out.println("main");
}
}
运行结果:
interrupt() 方法可以把阻塞 等待的join提前唤醒
2.3 线程的状态
Java中有以下线程状态:
- NEW:线程被创建但还未开始执行。
- RUNNABLE:线程正在执行或准备开始执行。(就绪状态)
- BLOCKED:锁竞争引起的阻塞。(线程安全会详讲)
- WAITING:线程正在等待另一个线程的特定操作完成。(不带时间的死等,join()或wait()会进入这个状态
- TIMED_WAITING:线程正在等待另一个线程的特定操作完成,但设置了最大等待时间。(使用sleep()方法或者带超时时间的join()方法会进入这个状态)
- TERMINATED:线程已经执行完毕结束。
示例代码:
public class ThreadDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("ThreadDemo12");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//线程被创建但还没开始执行
System.out.println(t.getState());
t.start();
//线程在执行中
System.out.println(t.getState());
t.join();
//线程执行完毕
System.out.println(t.getState());
}
}
执行结果:
我们也可以通过 jdk的jconsole工具查看线程的状态