1. 概念
进程:在操作系统中运行的程序就是进程,例如QQ,微信等软件。
线程:线程是程序执行的基本单位,也可以说是执行程序的一次执行过程,它是一个动态的概念。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
通常线程有以下特点:
1)一个线程只能属于一个进程,但是一个进程可以有多个线程。
2)通常main()为主线程,为系统的入口,用于执行整个程序。
3)在同一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器与操作系统紧密相关,先后顺序不能人为干预。
4)同一份资源在操作时,会存在资源抢夺问题,需要加入并发控制。
5)线程会带来额外开销,如CPU调度时间,并发控制开销。
6)每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
2. 线程的生命周期
线程的生命周期通常有以下5种状态:
- 新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
一个完整的线程生命周期如下图:
3. 创建一个线程
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口(推荐使用);
- 通过继承 Thread 类本身;
- 通过 和 Future 创建线程。
2.1 实现Runnable 接口
实现Runnable 接口启动线程逻辑:实现Runnable 接口,重写run()方法,执行线程需要传入Runnable 接口实现类,然后调用start()方法启动线程。
来看代码例子:
public class Demo implements Runnable{
//重写run方法
@Override
public void run(){
for(int num=1;num<=6;num++){
System.out.println("第"+num+"次Running Test");
}
}
public static void main(String[] args){
// Demo t=new Demo(); //创建Runnable接口的实现类对象
// Thread a=new Thread(t); //创建线程对象,通过线程对象开启线程
// a.start();
new Thread(new Demo()).start(); //传入接口实现类Demo类的对象,然后调用Thread的start()方法启动线程
for(int num=0;num<2;num++){
System.out.println("main方法线程测试");
}
}
}
结果:
main方法线程测试
main方法线程测试
第1次Running Test
第2次Running Test
第3次Running Test
第4次Running Test
第5次Running Test
第6次Running Test
需要注意的是,调用run方法并不会启动线程,只是单纯的调用run方法,而调用start方法会调用start0方法,start0底层C++代码逻辑也会调用run方法同时启动线程,因此,多线程场景通常调用start()方法来启动线程,这样子线程和主线程由CPU调度并行交替执行。调用start方法并不意味着线程立即执行,具体什么时候执行由CPU调度决定。
2.2 继承Thread类
继承Thread类启动线程逻辑:继承Thread类,重写run()方法,调用start()方法启动线程。
来看代码例子:
public class Demo extends Thread{
@Override
public void run(){
for(int num=1;num<=6;num++){
System.out.println("第"+num+"次Running Test");
}
}
public static void main(String[] args){
Demo t=new Demo(); //实例化Demo对象
t.start(); //调用start方法启动线程
for(int num=0;num<2;num++){
System.out.println("main方法线程测试");
}
}
}
结果:
main方法线程测试
main方法线程测试
第1次Running Test
第2次Running Test
第3次Running Test
第4次Running Test
第5次Running Test
第6次Running Test
对比以上2种方式,推荐使用实现Runnable接口的方式来创建线程,这主要是因为Java中类只能单继承,而接口可以多继承,这样可以避免单继承的局限性,方便同一个对象被多个线程使用。
例如:
Students s=new Students(); //一份资源
//多个代理
new Thread(s, name:"xiaozhang").start
new Thread(s, name:"xiaowang").start
new Thread(s, name:"xiaoli").start
2.3 通过 Callable 和 Future 创建线程
-
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
来看代码例子:
public class Demo implements Callable<Integer> { //Callable接口有返回值
//重写call方法
@Override
public Integer call() throws Exception {
int num;
for (num = 1; num <= 6; num++) {
System.out.println("第" + num + "次Running Test");
}
return num;
}
public static void main(String[] args){
Demo ctt = new Demo();
FutureTask<Integer> ft = new FutureTask<>(ctt); //将Demo对象作为参数传入futureTask类
new Thread(ft).start(); //创建线程对象,通过线程对象开启线程
for(int num=0;num<2;num++){
System.out.println("main方法线程测试");
}
}
}
callable与runnable区别:
1)最大的区别,runnable没有返回值,而实现callable接口的任务线程能返回执行结果。
2)callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是runnable接口实现类中run方法的异常必须在内部处理,不能对外抛出。
多线程练习:龟兔赛跑
需求分析:
1)同一赛道龟兔从同一起点出发
2)兔子速度快,乌龟速度慢,兔子中途睡觉,乌龟不休息
3)先跑到终点者赢得比赛
public class Demo implements Runnable{
private int Step1=0;
private int Step2=0;
public static boolean flag = true;
@Override
public void run() {
while (flag){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(Thread.currentThread().getName().equals("turtle")){
System.out.println("比赛开始,"+Thread.currentThread().getName()+"跑了"+Step1++ +"步");
if(Step1>100){
flag=false;
System.out.println("比赛结束,恭喜"+Thread.currentThread().getName()+"赢得比赛");
break;
}
}
if(Thread.currentThread().getName().equals("rabbit")){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Step2+=2;
System.out.println("比赛开始,"+Thread.currentThread().getName()+"跑了"+Step2 +"步");
if(Step2>100){
flag=false;
System.out.println("比赛结束,恭喜"+Thread.currentThread().getName()+"赢得比赛");
break;
}
}
}
}
public static void main(String[] args) {
Demo race=new Demo();
Thread t1=new Thread(race,"turtle");
Thread t2=new Thread(race,"rabbit");
t1.start();
t2.start();
}
}
结果:
比赛开始,turtle跑了0步
比赛开始,rabbit跑了0步
比赛开始,turtle跑了1步
比赛开始,rabbit跑了2步
...
比赛开始,turtle跑了98步
比赛开始,rabbit跑了64步
比赛开始,turtle跑了99步
比赛开始,turtle跑了100步
比赛开始,rabbit跑了66步
比赛结束,恭喜turtle赢得比赛
4. 线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,大概率比低优先级的线程优先获得CPU资源。但是,线程优先级不能保证线程执行的顺序,非常依赖于操作系统调度。
Java中可以通过 setPriority(int newPriority)
来设置新的优先级,通过 getPriority()
来获取线程的优先级。
来看代码例子:
public class TestPriority {
static AtomicLong minTimes = new AtomicLong(0);
static AtomicLong normTimes = new AtomicLong(0);
static AtomicLong maxTimes = new AtomicLong(0);
public static void main(String[] args) {
List<MyThread> minThreadList = new ArrayList<>();
List<MyThread> normThreadList = new ArrayList<>();
List<MyThread> maxThreadList = new ArrayList<>();
int count = 1000;
for (int i = 0; i < count; i++) {
MyThread myThread = new MyThread("min----" + i);
myThread.setPriority(Thread.MIN_PRIORITY); //设置线程优先级为1
minThreadList.add(myThread);
}
for (int i = 0; i < count; i++) {
MyThread myThread = new MyThread("norm---" + i);
myThread.setPriority(Thread.NORM_PRIORITY); //设置线程优先级为5
normThreadList.add(myThread);
}
for (int i = 0; i < count; i++) {
MyThread myThread = new MyThread("max----" + i);
myThread.setPriority(Thread.MAX_PRIORITY); //设置线程优先级为10
maxThreadList.add(myThread);
}
for (int i = 0; i < count; i++) {
maxThreadList.get(i).start();
normThreadList.get(i).start();
minThreadList.get(i).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("maxPriority 统计:" + maxTimes.get());
System.out.println("normPriority 统计:" + normTimes.get());
System.out.println("minPriority 统计:" + minTimes.get());
System.out.println("普通优先级与最高优先级相差时间:" + (normTimes.get() - maxTimes.get()) + "ms");
System.out.println("最低优先级与普通优先级相差时间:" + (minTimes.get() - normTimes.get()) + "ms");
}
static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " priority: " + this.getPriority());
switch (this.getPriority()) {
case Thread.MAX_PRIORITY :
maxTimes.getAndAdd(System.currentTimeMillis());
break;
case Thread.NORM_PRIORITY :
normTimes.getAndAdd(System.currentTimeMillis());
break;
case Thread.MIN_PRIORITY :
minTimes.getAndAdd(System.currentTimeMillis());
break;
default:
break;
}
}
}
}
结果:
min----13 priority: 1
norm---15 priority: 5
min----4 priority: 1
min----9 priority: 1
min----5 priority: 1
min----10 priority: 1
max----8 priority: 10
min----1 priority: 1
...
...
...
max----994 priority: 10
norm---996 priority: 5
min----999 priority: 1
maxPriority 统计:1699858043845831
normPriority 统计:1699858043846434
minPriority 统计:1699858043846707
普通优先级与最高优先级相差时间:603ms
最低优先级与普通优先级相差时间:273ms
从结果上看,优先级高的线程和优先级低的线程是交替执行的,这说明了:优先级高的线程不代表一定比优先级低的线程优先执行。也可以换另一种说法:代码执行顺序跟线程的优先级无关。看看第二部分的结果,我们可以发现最高优先级的 1000 个线程执行时间戳之和最小,而最低优先级的 1000 个线程执行时间戳之和最大,因此可以得知:一批高优先级的线程会比一批低优先级的线程优先执行,即高优先级的线程大概率比低优先的线程优先获得 CPU 资源。