Java多线程
1.线程概述
1.线程和进程
-
在一个系统中,每个独立运行的程序是一个进程;每个程序中有多个顺序流,称为线程;
-
进程的特征:
-
独立性:每个进程都有着自己独立的地址空间,互不影响;
-
动态性;
-
并发性:多个进程可以在单个处理器上并发执行,且互不影响;
- 并发性:同一时刻只能有一条指令执行,但是多个进程指令被快速轮换执行,在宏观上看起来是一起执行的效果;
- 并行性:同一时刻,多条指令在多个处理器上同时执行;
- Windows和Linux都是抢占式的多任务操作策略;
- 线程又被称为是轻量级进程,每个线程是独立运行的;一个进程至少包括一个线程;
2.多线程的优势
- 线程之间共享内存、文件句柄和状态信息;这也是线程执行速度高于进程的原因;
- 多线程的应用:
- javaWeb服务器响应多个用户需求;
- JVM虚拟机的垃圾回收机制;
2.线程的创建和启动
1.继承Thread类创建线程类
- 所有的线程对象都必须是Thread类或其子类的实例;每个线程的任务就是执行一段程序流;
- 创建并启动线程的步骤如下:
/*
1.定义Thread类的子类,重写Thread的run方法,run就是线程的执行体;
2.创建Thread子类的实例,即创建了线程对象;
3.调用线程对象的start()方法来启动线程;
*/
//继承Thread
public class FirstThread extends Thread
{
private int i;
// 重写run
@Override
public void run()
{
for ( ; i < 100; i++)
{
// 直接调用getName方法可以返回当前线程的名字
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
//这是主线程
for (var i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获得当前线程;
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 创建并启用第一个线程
new FirstThread().start();
// 创建并启用第二个线程
new FirstThread().start();
}
}
}
}
- main方法体就是主线程的方法体;
2.实现Runable接口创建线程类
-
步骤如下
/*
使用Runable接口创建线程
1.定义Runable接口的实现类,并重写run方法,run是线程的实现体;
2.创建实现类的实例,并以此实例作为Thread的target来创建Thread对象,该对象为线程对象;
3.调用线程对象的start方法启动线程;
*/
//实现Runable接口
public class SecondThread implements Runnable
{
private int i;
//重写run方法
public void run()
{
for ( ; i < 100; i++)
{
//如果想获得线程名,只能通过Thread.currentThread()方法
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
public static void main(String[] args)
{
for (var i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() //主线程
+ " " + i);
if (i == 20)
{
var st = new SecondThread(); //创建线程实例;
//用实例初始化Thread实例,并启动线程;
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
3.使用Callable和Future创建线程(739)(这个方法有点复杂)
-
Thread将run方法作为线程执行体,但是不能将其他方法作为线程执行体;(C#可以)
-
Callable接口是Runable的增强版,提供了一个较强的call方法代替了run方法;
4.创建线程的三种方式对比
- 由于扩展性原因,一般推荐采用Runable接口实现线程的编写;
3.线程的生命周期
1.新建和就绪状态
- 线程的生命周期中,包括新建New,就绪Ready,运行Running,阻塞Blocked、死亡Dead共5个状态;
- new一个线程对象,就进入了线程的新建状态;
- 线程对象调用start方法,就进入了就绪状态;处于这个状态的线程并没有开始运行,只是表示该线程可以运行了,何时开始运行取决于JVM线程调度器的调度;
- run方法不能直接执行,启动线程只能是start方法;
- 如果希望子线程点击start方法后立即执行,程序可以使用Thread.sleep(1)来让当前运行的线程睡眠1ms,此时CPU就会去执行另一个已经就绪的线程;
2.运行和阻塞状态
- 线程调度平台会定期的让一些运行的线程进入阻塞状态,让其他线程有机会运行;
- Windows和Linux都是抢占式调度方法;而手机系统采用协作式调度方法,即由当前运行线程自动调用sleep或者yield方法进入阻塞状态;
- 当发生如下状态时,系统进入阻塞状态:
- 线程调用sleep方法主动放弃占用线程资源;
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被进程阻塞;
- 线程试图获得一个同步监视器;
- 线程在等待某个通知notify;
- 程序调用了线程的suspend方法将该线程挂起(此方法容易导致死锁)
- 被阻塞的线程会在解除阻塞后重新进入就绪状态;
3.线程死亡
- 线程会以如下三种方式结束,并进入死亡状态:
- run或者call方法执行完成,线程正常结束;
- 线程抛出一个未捕获的Exception或Error;
- 直接调用stop方法停止线程;
- 主线程和子线程是平级关系,主线程死亡不会影响子线程;
- 为了验证线程是否死亡,可以调用isAlive()方法来验证;
- 已经死亡的线程不能被再次start;
4.控制线程
1.join线程
-
join是让一个线程等待另一个线程的方法,当某个程序执行流过程中调用其他线程的join方法时,该线程将被阻塞,直到被join方法加入的join线程执行完为止;
/*
join方法有如下三种重载形式
1.join():等待被join的线程执行完成;
2.join(long millis):等待被join的线程的执行时间最长为millis毫秒;如果在给定时间内被join的线程还没有执行结束,则不再等待;
3.join(long millis, int nanos):等待被join的线程的时间最长为millis+nanos毫微秒;
*/
public class JoinThread extends Thread
{
// 该构造器用于设置线程名;Thread的有参构造器用于给线程命名
public JoinThread(String name)
{
super(name);
}
//重写run方法
public void run()
{
for (var i = 0; i < 100