前言
这是新的系列开始,进入了JavaEE的范围了,虽然已经学了挺久,但是今天才开始写博客。那么下面就来学习一下我们多线程的学习吧。
什么叫多线程
用通俗的话来说就是进程的在分,一个进程里可以有很多的线程,每个线程可以完成执行代码,完成一些部分功能。进程就是cpu层面的程序。进程完成了我们应用分布的任务的任务,而我们的线程就是进程的小弟,多个线程完成一个进程的工作。线程是一个轻量级进程。
多线程的作用
多线程的作用很简单就是提升效率,多线程可以理解为多个人共同完成一个任务。
举个例子:假设:我们有一个任务,这个任务就是坐在桌子面前触碰一张桌子10000次,那么如果我们不使用多线程的情况,就是一个人在桌子面前进行触碰桌子10000次。如果我们使用多线程就是多个人一起触碰桌子,每个人的触碰次数加起来够10000次即可。但是需要注意的是:有人可认为线程越多越好,效率就可以嘎嘎提高,实则不然,cpu提供给一个进程的资源有限,但资源分配过多时,效率反而会降低。回到例子:就是桌子只能有10个位置,可是来了20个人挤进来,会导致每个人都很难触碰桌子效率降低,因为太拥挤了。甚至我们可以说有人会掀桌呢!!!哈哈哈
并发成为了必不可少的东西:
- 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU 资源.
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
虽然多进程也能实现 并发编程, 但是:
- 线程⽐进程更轻量. 创建线程⽐创建进程更快.
- 销毁线程⽐销毁进程更快.
- 调度线程⽐调度进程更快.
认识Thread类
首先线程是操作系统的概念,,操作系统内核实现了线程这样的机制,并为用户层提供了API,给用户使用。
而Thread则是Java对操作系统提供的API进行的在封装和抽象,提供给我们Java程序员使用。
上面讲了那么多都没用代码展现给大家如何使用代码实现多线程程序,那么下面我们一一展开。
如何创建线程
首先我们利用Thread对象进行创建的。主要有三种方式。
第一种:继承Thread类,并重写run方法。
public class Main {public static void main(String[] args) {MyThread thread1=new MyThread();thread1.start();}
}class MyThread extends Thread{public void run(){System.out.println("第一个线程");}
}
第二种:将一个实现了Runable接口并重写了run方法的Runable子对象传入构造方法中
public class Main {public static void main(String[] args) {Thread thread2=new Thread(new MyRubable());thread2.start();}
}class MyRubable implements Runnable{@Overridepublic void run() {System.out.println("第二个线程");}
}
第三种:采用匿名内部类的方式,传入构造方法
public class Main {public static void main(String[] args) {Thread thread3=new Thread(new Thread(){public void run(){System.out.println("第三个线程");}});thread3.start();}
}
结合我们上一篇文章的知识,我们这里可以使用lambda表达式,简化第三种方法的代码:
public class Main {public static void main(String[] args) {Thread thread3=new Thread(()->{System.out.println("第三个线程");});thread3.start();}
}
从上面的三种方法来看我们可以很容易看出,创建线程时,我们都涉及到了run方法。
这个run方法就是我们这个线程要执行的代码,这里面的代码就是线程要执行的任务。
如果我们没有重写run方法,那么就是执行默认的润方法里的代码。也就是空的程序。
调用start的时候就会在调用run方法。
注意:线程真正创建的时候是start();方法执行的时候。不能直接调用run方法。不然是无法创建出线程的,就跟普通的串行执行没区别了。且一个thread对象只能start一次
Thread的构造方法:
如果我们传入的参数有字符串那么这个字符串就是这个线程的名字。
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
线程的随机调度
首先我们要知道cpu对线程的调度是随机进行的,也就是说线程的执行的时间先后是随机的,并不是代码中谁先调用run方法的代码在前(run方法中的代码较为复杂时),就是谁先执行的。下面我们看一个例子:
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {public static void main(String[] args) {MyThread thread1=new MyThread();Thread thread2=new Thread(new MyRubable());Thread thread3=new Thread(()->{System.out.println("第三个线程");for (int i = 30; i <40 ; i++) {System.out.println(i);}});thread1.start();thread2.start();thread3.start();}
}class MyRubable implements Runnable{@Overridepublic void run() {System.out.println("第二个线程");for (int i = 10; i <20; i++) {System.out.println(i);}}
}class MyThread extends Thread{public void run(){System.out.println("第一个线程");for (int i = 1; i <10 ; i++) {System.out.println(i);}}
}
可以看到我们的控制台打印出来的时候,不是按照我们上面的顺序的。
线程的中断
在Java中,中断一个线程是通过设置线程的中断标志来实现的。线程必须自己检查这个标志并决定如何响应。
那么我们具体如何中断一个线程呢?注意Thread中内置了一个中断标志。
1. 使用 interrupt() 方法
要中断一个线程,可以调用该线程的 interrupt() 方法。这会设置线程的中断标志。如果线程正在执行 wait()、join() 或 sleep()等方法,它会抛出 InterruptedException 并清除中断标志。
Thread thread = new Thread(() -> {try {while (!Thread.currentThread().isInterrupted()) {// 执行任务System.out.println("Thread is running");Thread.sleep(1000); // 模拟耗时操作}} catch (InterruptedException e) {System.out.println("Thread was interrupted");}
});thread.start();
// 在适当的时候中断线程
thread.interrupt();
2. 在循环中检查中断标志
如果线程没有调用会抛出 InterruptedException 的方法,可以在循环中定期检查中断标志,并在标志被设置时退出循环。
Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {// 执行任务System.out.println("Thread is running");// 检查中断标志}System.out.println("Thread was interrupted");
});thread.start();
// 在适当的时候中断线程
thread.interrupt();
3. 使用 volatile 变量
这个volatile这个关键字在后面会讲到,可暂时不理会。
虽然这不是中断机制的一部分,但另一种控制线程执行的方式是使用 volatile 变量。线程可以定期检查这个变量的值,并根据其值决定是否继续执行。
public class MyRunnable implements Runnable {private volatile boolean running = true;@Overridepublic void run() {while (running) {// 执行任务System.out.println("Thread is running");}System.out.println("Thread stopped");}public void stopRunning() {running = false;}
}MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();// 在适当的时候停止线程
myRunnable.stopRunning();
总结
在Java中,线程中断是一种协作机制,用于请求一个线程停止其正在执行的任务。中断本身不会立即停止线程,而是通过设置一个中断标志来通知线程应该停止。线程必须检查这个标志并决定如何响应。以下是关于线程中断的两种主要方式的解析:
1. 通过 InterruptedException 通知
当线程因为调用 wait()、join() 或 sleep() 等方法而阻塞时,如果该线程被中断,它会抛出一个 InterruptedException。这种异常的抛出会清除线程的中断标志。处理InterruptedException 的方式决定了线程的后续行为:
- 结束线程:在 catch 块中,可以选择结束线程的执行。例如,通过返回或抛出一个新的异常来终止线程。
- 忽略异常:也可以选择忽略这个异常,继续执行线程的其他任务。但这种做法通常不推荐,因为它违背了中断的初衷。
示例代码:
try {Thread.sleep(1000);
} catch (InterruptedException e) {// 选择结束线程return;// 或者处理异常,但通常不推荐忽略
}
2. 通过 isInterrupted() 方法通知
如果线程没有因为调用 wait()、join() 或 sleep() 而阻塞,中断请求只会设置线程的中断标志。线程可以通过调用 Thread.currentThread().isInterrupted() 来检查这个标志,而不会清除它。这种方式允许线程在任何时刻检查中断状态,并作出相应的响应。
示例代码:
while (!Thread.currentThread().isInterrupted()) {// 执行任务// 检查中断标志if (someCondition) {// 执行一些操作}
}
// 线程可以在这里处理中断,例如清理资源或退出
总结
- InterruptedException:当线程在阻塞状态被中断时抛出,自动清除中断标志。处理方式决定了线程是否继续执行。
- isInterrupted():允许线程在任何时刻检查中断标志,而不清除它。这种方式更灵活,允许线程在检测到中断后立即作出反应。
这两种方式都强调了中断的协作性质:线程需要主动检查中断标志并决定如何响应。
join()------等待一个线程
join是thread里的一个等待方法,可以理解为:哪个线程调用join方法,那么主线程就会先等待这个调用join方法的线程执行完代码,在往下执行代码。这样我们就可以使上面所说的随机调度,导致打印顺序错乱的问题。
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {public static void main(String[] args) throws InterruptedException {MyThread thread1=new MyThread();Thread thread2=new Thread(new MyRubable());Thread thread3=new Thread(()->{System.out.println("第三个线程");for (int i = 30; i <40 ; i++) {System.out.println(i);}});thread1.start();thread1.join();thread2.start();thread2.join();thread3.start();thread3.join();}
}class MyRubable implements Runnable{@Overridepublic void run() {System.out.println("第二个线程");for (int i = 10; i <20; i++) {System.out.println(i);}}
}class MyThread extends Thread{public void run(){System.out.println("第一个线程");for (int i = 1; i <10 ; i++) {System.out.println(i);}}
}
sleep方法
public static void sleep(long millis) throws InterruptedException
millis 表示暂停的毫秒数
public static void sleep(long millis, int nanos) throws InterruptedException
millis 表示暂停的毫秒数,nanos 表示额外的纳秒数。这两个参数共同决定了线程暂停的总时间。
InterruptedException:如果当前线程在睡眠期间被中断,则抛出此异常。当这个异常被抛出时,线程的中断标志会被清除。
注意:暂停的实际时间都是比定的时间要长的,因为本身调用这个方法也是需要时间的,而且线程的随机调度的问题也会导致这个时间变长一点。