Catalog
多线程基础
1. 程序、进程&线程
- 程序:为完成某个特定任务,用某种语言编写的一组指令集合,即一段静态的代码段。
- 进程:即运行的程序就是进程,进程可以理解为动态的程序,是操作系统分配和调用资源的最小单位。
- 线程:进程进一步细化则是线程,线程可以理解为进程中的一条工作路径,比如点开某个软件(相当于开启了一个进程),同时开启该软件中的多个功能,相当于开启了多个线程,如百度网盘中,同时开启多个下载任务,即相当于开启了多个线程。
- 进程和线程之间的关系:只有当一个进程中的所有线程都结束了,这个进程才会结束。
2. 并行和并发
- 并行:同一时刻,多个任务交替执行。单个cpu执行多项任务,即cpu在多个任务间快速切换执行,形成貌似在同时执行多个任务的样子。
- 并发:同一时刻,多个任务同时实行。多个cpu在同一时刻各自执行自己的任务。
- 说明:并行中也可能包含并发,如开启了后台很多任务时,多个并行的cpu也需要交替执行多个任务(并发)。
3. 开启线程的底层方法
Thread类中的run方法只是一个普通的方法,真正开启线程的方法是Thread类中的start()方法中的本地方法start0(),该方法由虚拟机调用,涉及到操作的系统的IO资源等的调用。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
4. 创建线程的两种方式
-
继承Thread类,实现run()方法,调用start()方法开启线程:
public class Thread01 { public static void main(String[] args) { Dog dog = new Dog(); dog.start(); } } class Dog extends Thread { int time = 0; @Override public void run() { while (true) { try { Thread.sleep(1000); System.out.println("旺旺旺" + ++time); } catch (InterruptedException e) { e.printStackTrace(); } } } }
-
实现Runnable接口,实现run()方法,通过Thread类静态代理,开启线程:
public class Thread02 { public static void main(String[] args) { Computer computer = new Computer(); //通过Thread类静态代理,开启线程 Thread thread = new Thread(computer); thread.start(); System.out.println("main .... "); } } class Computer implements Runnable{ @Override public void run() { try { Thread.sleep(3000); System.out.println(Thread.currentThread().getName() + " " +"begin to running"); } catch (InterruptedException e) { e.printStackTrace(); } } }
5. 线程终止
让线程终止一般有两种方式:
-
线程执行完任务(代码语句)后,会自动结束线程;
-
通知线程结束,如通过控制一个变量结束线程中的run方法,即可终止线程,demo如下:
public class ThreadExit { public static void main(String[] args) throws InterruptedException { Thread01 thread01 = new Thread01(); //开启线程 thread01.start(); //让主线程沉睡,给线程多一些运行的时间 Thread.sleep(20 * 1000); //通过修改变量,控制线程退出--通知方式 thread01.setLoop(false); } } class Thread01 extends Thread { int time = 0; //设置变量,控制线程退出 private boolean loop = true; @Override public void run() { while (loop) { try { Thread.sleep(50); System.out.println("run..." + ++time); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ": 线程终止"); } public void setLoop(boolean loop) { this.loop = loop; } }
6. Thread类中常用的API
方法 | 描述 |
---|---|
start() | 用于启动线程并调用run()方法,使线程进入就绪状态。 |
run() | 定义了线程的操作,会在新线程中执行。 |
sleep(long millis) | 使当前线程暂停执行一段时间,让其他线程有机会继续执行。 |
join() | 允许一个线程等待另一个线程完成其执行。 |
interrupt() | 用于中断正在执行的线程,向线程发送一个中断信号,一般用于中断线程的休眠。 |
isAlive() | 用于检查线程是否还活着。 |
setPriority(int priority) | 用于设置线程的优先级。 |
yield() | 使当前线程放弃CPU并允许其他线程运行,当系统资源较充足的时候,可能会放弃失败。 |
Demo: 中断线程休眠
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException{
//创建一个线程对象
Eat eat = new Eat();
//设置线程名字
eat.setName("Amin");
//设置线程优先级
eat.setPriority(Thread.MIN_PRIORITY);
//开启线程
eat.start();
//主线程通知Eat线程中断睡眠
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Hi " + eat.getName());
}
//中断线程
eat.interrupt();
}
}
class Eat extends Thread{
int times = 0;
@Override
public void run() {
while (true){
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "吃饺子..." + i);
}
//线程沉睡
try {
Thread.sleep(20 * 1000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " is interrupted ...");
}
}
}
}
demo2: 协调线程
public class ThreadMethod02 {
public static void main(String[] args) throws InterruptedException {
//创建线程对象
ThreadHighLevel threadHighLevel = new ThreadHighLevel();
threadHighLevel.start();
for (int i = 0; i < 20; i++) {
Thread.sleep(1000);
System.out.println("main:" + "hi" + " -> " + i);
if (i == 4){
System.out.println("开始暂停...");
//直接让线程插队
threadHighLevel.join();
//根据系统资源是否充足,礼让别的线程(让出cpu)
//Thread.yield();
}
}
}
}
class ThreadHighLevel extends Thread{
int times = 0;
@Override
public void run() {
while (times < 20){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":hello" + times);
times++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7. 多线程可能出现的问题
同步互斥问题
线程同步,即多个线程操作同一内存(数据)时,同一时刻,只允许一个线程对数据进行修改,该线程退出后,才能由下一个线程进行修改。
- 多线程同步导致数据异常,即多线程操作同一内存,导致数据发生异常。
public class BookingOffice {
public static void main(String[] args) {
//模拟互斥同步现象
//创建3个线程,即同时售卖票
SellingTicket01 sellingTicket01 = new SellingTicket01();
SellingTicket01 sellingTicket02 = new SellingTicket01();
SellingTicket01 sellingTicket03 = new SellingTicket01();
//开启线程
sellingTicket01.start();
sellingTicket02.start();
sellingTicket03.start();
}
}
class SellingTicket01 extends Thread{
//创建静态变量,让多个线程共享,--票剩余数量
public static Integer ticketNumber = 100;
@Override
public void run() {
//模拟买票的场景
try {
while (true){
//1. 判断票数是否为0
if (ticketNumber == null || ticketNumber <= 0){
System.out.println("售票结束...");
break;
}
Thread.sleep(50);
//2. 开始售出票
System.out.println(Thread.currentThread().getName() + ":售出一张票, " + "剩余票数为:" + (--ticketNumber));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 超卖现象
- 原因分析
比如票数只剩余1张或者两张的这一时刻,三个线程同时通过了售空退出循环的判断,导致三个线程都能够对票数–,出现负数。
-
解决方案:
可以在需要同步的代码块或者方法上添加sychronized关键字,即给代码块或者方法添加了一个对象互斥锁(通过改变同一对象的某个位实现加锁)。- 代码块:可以是静态代码块和普通代码块,前者需要绑定当前类,后者需要绑定同一对象(Object类对象都可以),绑定代码块的使用场景更广泛,因为代码块限制的范围更小,同步代码越少,程序执行效率一般会更高。
- 方法:可以是静态方法或者是实例方法,前者需要绑定当前类,后者需要绑定同一对象(Object类对象都可以)。
抽离出买票代码,封装成实例方法,添加synchronized关键字,设置为同步方法
public class BookingOffice { public static void main(String[] args) { //模拟互斥同步现象 /* //创建3个线程,即同时售卖票 SellingTicket01 sellingTicket01 = new SellingTicket01(); SellingTicket01 sellingTicket02 = new SellingTicket01(); SellingTicket01 sellingTicket03 = new SellingTicket01(); //开启线程 sellingTicket01.start(); sellingTicket02.start(); sellingTicket03.start();*/ //实现Runnable接口开启线程,目的为让三个线程调用同一个run方法 SellingTicket02 sellingTicket021 = new SellingTicket02(); new Thread(sellingTicket021).start(); new Thread(sellingTicket021).start(); new Thread(sellingTicket021).start(); } } class SellingTicket01 extends Thread{ //创建静态变量,让多个线程共享,--票剩余数量 public static Integer ticketNumber = 100; @Override public void run() { //模拟买票的场景 try { while (true){ //1. 判断票数是否为0 if (ticketNumber == null || ticketNumber <= 0){ System.out.println("售票结束..."); break; } Thread.sleep(50); //2. 开始售出票 System.out.println(Thread.currentThread().getName() + ":售出一张票, " + "剩余票数为:" + (--ticketNumber)); } } catch (InterruptedException e) { e.printStackTrace(); } } } class SellingTicket02 implements Runnable{ //使用的为同一个对象,不需要创建静态变量,让多个线程共享,--票剩余数量 public Integer ticketNumber = 100; public boolean loop = true; public synchronized void sell(){ //模拟买票的场景 try { //1. 判断票数是否为0 if (ticketNumber == null || ticketNumber <= 0){ System.out.println("售票结束..."); loop = false; return; } Thread.sleep(50); //2. 开始售出票 System.out.println(Thread.currentThread().getName() + ":售出一张票, " + "剩余票数为:" + (--ticketNumber)); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 不能直接在run方法上面添加synchronize关键字,因为run方法中while中的代码才是执行逻辑,直接添加会导致只有一个线程能够进行买票。 */ @Override public void run() { while (loop){ sell(); } } }
Demo
//1. 同步静态代码块 static{ synchronized (SellingTicket02.class){ System.out.println("同步静态代码块"); } } public void m1(){ //2. 同步代码块,注意在方法中才可以使用,这里使用的this关键字,也可以替换成其它对象 synchronized (this){ System.out.println("同步代码块"); } } //3. 静态同步方法 public synchronized static void m2(){ System.out.println("静态同步方法"); } //4. 同步实例方法 public synchronized void m3(){ System.out.println("同步实例方法"); }
死锁
在多线程中,可能会出现两个线程互相等待获取对方资源的场景,即一个线程优先拿到了一个锁A,另一个线程拿到了锁B,而第一个线程需要再获取一个资源(这个资源需要锁B)才能进行下一步,第二个线程也需要再获取一个资源(这个资源需要锁A)才能进行下一步,从而出现两个线程相互等待,导致整个程序无法执行下一步。
- 出现场景:一般出现在某些线程需要获取多把锁的情况。
Demo(老韩)
public class DeadBlock01 { public static void main(String[] args) { //模拟死锁现象 DeadLockDemo A = new DeadLockDemo(true); A.setName("A 线程"); DeadLockDemo B = new DeadLockDemo(false); B.setName("B 线程"); A.start(); B.start(); } } //线程 class DeadLockDemo extends Thread { static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用 static static Object o2 = new Object(); boolean flag; public DeadLockDemo(boolean flag) {//构造器 this.flag = flag; } @Override public void run() { //下面业务逻辑的分析 //1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁 //2. 如果线程 A 得不到 o2 对象锁,就会 Blocked //3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁 //4. 如果线程 B 得不到 o1 对象锁,就会 Blocked if (flag) { synchronized (o1) {//对象互斥锁, 下面就是同步代码 System.out.println(Thread.currentThread().getName() + " 进入 1"); synchronized (o2) { // 这里获得 li 对象的监视权 System.out.println(Thread.currentThread().getName() + " 进入 2"); } } } else { synchronized (o2) { System.out.println(Thread.currentThread().getName() + " 进入 3"); synchronized (o1) { // 这里获得 li 对象的监视权 System.out.println(Thread.currentThread().getName() + " 进入 4"); } } } } }
8. 用户线程&守护线程
-
用户线程:即工作线程,上述通过Thread类创建的普通线程就是工作线程;
-
守护线程:守护线程是为工作线程服务的,守护线程的特点就是,当所有用户进程结束之后,守护线程会自动退出。
-
常见的守护线程应用场景:监控线程、垃圾回收机制…
-
使用方法:调用daemon()方法设置为true,即可将调用方的线程设置为守护线程。
//创建线程对象 ThreadHighLevel threadHighLevel = new ThreadHighLevel(); //注意设置守护线程,需要在开启线程之前(即调用start方法之前) threadHighLevel.setDaemon(true); threadHighLevel.start();
-
9. 线程的生命周期(虚拟机)
在Java虚拟机中,线程的生命周期可以分为七个(注意是在虚拟机中,并非是操作系统中关于线程的所有状态)。
- NEW:即新建状态,对应到代码阶段,就是创建了一个线程对象(Thread类)。
- RUNNABLE:即可运行状态,这个状态可以细分为两个状态,这两个状态之间的切换设计到底层的操作系统,即内核态关于线程的调度。
- Ready:即就绪态,这时线程并没有真正开始运行。
- running:即运行态,这时候,线程开始真正地运行。
- TIMED_WAITING:指定一个时间段,一般为等待另一个线程完成一些特定的操作,然后重新进入RUNNABLE状态。
- WAITING:线程无限期地等待。
- BLOCK:即线程的阻塞状态,这里涉及到加锁等方面的内容。
- TERMINATED:即线程处于终止状态。
状态图(韩老师)
Demo:查看线程的一些状态
public class ThreadState {
public static void main(String[] args) throws InterruptedException{
ThreadStateExample threadStateExample = new ThreadStateExample();
Thread thread = new Thread(threadStateExample);
//查看线程状态
System.out.println(thread.getName() + "'s state: " + thread.getState());
//开启线程
thread.start();
System.out.println(thread.getName() + "'s state: " + thread.getState());
while (thread.getState() != Thread.State.TERMINATED){
//main线程沉睡
Thread.sleep(500);
System.out.println(thread.getName() + "'s state: " + thread.getState());
}
}
}
class ThreadStateExample implements Runnable{
@Override
public void run() {
int times = 20;
while (times > 0){
try {
System.out.println(Thread.currentThread().getName() + ": 吃饭、睡觉、打豆豆");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
times--;
}
}
}
10. 释放锁
释放锁,一般是指线程拿到锁,使用完资源之后,归还锁,让该资源能够让别的线程使用。
场景
- 线程使用完资源:同步代码块执行完(或者遇到break、return关键字、出现error和异常)。
- 执行了线程对象中的wait()方法。
细节
- 线程睡眠不会释放锁