多线程
一、基础
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程
操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。
二、进程 vs 线程
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
1、创建进程比创建线程开销大,尤其是在Windows系统上;
2、进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃 会直接导致整个进程崩溃。
三、多线程
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
多线程模型是Java程序最基本的并发模型;
网络、数据库、Web开发等都依赖Java多线程模型
创建新线程
线程创建方式一
通过继承Thread来创建线程,并且重写run方法
public class ThreadDemo01 extends Thread{
@Override
public void run() {
System.out.println("创建线程方式一");
}
}
创建线程第二种方式:
实现 Runnable 接口创建线程,并且重写run方法
public class ThreadDemo02 implements Runnable {
@Override
public void run() {
}
第三种创建线程的方法实现Callable接口
Callable接口使用方法和Runnable接口的方法类似不同的一点是Callable接口具有返回值,
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。 Executor线程池的超类:执行已提交的 Runnable 任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。例如,可能会使用以下方法,而不是为一组任务中的每个任务调用 new Thread(new(RunnableTask())).start():
class MyThread implements Callable {
@Override
public Object call() throws Exception {
System.out.println("第三种创建线程的方法,改方式创建的线程具有返回值");
return 12;
}
}
线程的优先级
JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
Thread.setPriority(int n) // 1~10, 默认值5
线程的状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
New:新创建的线程,尚未执行;
Runnable:运行中的线程,正在执行run()方法的Java代码;
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
Waiting:运行中的线程,因为某些操作在等待中;
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
Terminated:线程已终止,因为run()方法执行完毕。
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
public class ThreadDemo04 {
public static void main(String[] args) {
// 创建线程
Thread a = new Thread(new A());
Thread b = new Thread(new B());
// 让a线程休眠,单位毫秒
try {
// 要模拟并发执行的效果,我们可以在线程中调用`Thread.sleep()`,强迫当前线程暂停一段时间:
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 设置线程优先级
a.setPriority(9);
b.setPriority(1);
// 启动线程
a.start();
b.start();
}
}
class A implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("你在哪里");
}
}
}
class B implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("jack:我在这里");
}
}
}
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
获取线程的各种状态的方法
public void run() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 返回此线程的标识符
System.out.println("获取线程ID:" + t.getId());
// 设置当前线程名字
t.setName("DC");
// 获取线程名字
System.out.println("获取当前线程名字:" + t.getName());
// 返回此线程的优先级, 线程默认优先级是5
System.out.println("返回此线程的优先级: " + t.getPriority());
// 设置线程有i先级
t.setPriority(1);
// 返回表示此线程的堆栈转储的堆栈跟踪元素数组。
StackTraceElement[] stackTrace = t.getStackTrace();
for (StackTraceElement element : stackTrace) {
System.out.println(element);
}
// 返回此线程的状态。
System.out.println("返回此线程的状态: " + t.getState());
// 返回此线程所属的线程组。
System.out.println("返回此线程所属的线程组: " + t.getThreadGroup());
// 测试这个线程是否活着
System.out.println("测试这个线程是否活着: " + t.isAlive());
// 设置当前线程为守护线程
// t.setDaemon(true)
// 测试这个线程是否是守护线程
System.out.println("测试这个线程是否是守护线程: " + t.isDaemon());
// 测试这个线程是否被中断
System.out.println("测试这个线程是否被中断: " + t.isInterrupted());
// 等待这个线程死亡。
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断这个线程
t.interrupt();
// 测试这个线程是否被中断
System.out.println("测试这个线程是否被中断: " + t.isInterrupted());
}
判断线程是否中断
public class ThreadDemo08 {
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread t = new Thread(new Td8());
// 启动线程
t.start();
// 主线程休眠2毫秒
Thread.sleep(2);
// 中断子线程
t.interrupt();
// 让主线程等待子线程执行完毕
t.join();
System.out.println("主线程执行完毕");
}
}
class Td8 implements Runnable{
int num = 0;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
// 判断当前线程是否被中断
if (Thread.interrupted()) {
System.out.println("暂停下载");
return;
}
System.out.println("正在下载文件...." + num + "%");
num ++;
}
System.out.println("文件下载完毕");
}
}
守护线程
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
public class ThreadDemo09 {
public static void main(String[] args) {
// 创建线程
Thread aoo = new Thread(new Aoo());
Thread boo = new Thread(new Boo());
// 将boo设为守护线程
boo.setDaemon(true);
// 启动线程
aoo.start();
boo.start();
}
}
class Aoo implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我要飞.....");
}
System.out.println("飞累了,休息一会");
}
}
class Boo implements Runnable{
// 设为守护线程
@Override
public void run() {
while (true){
System.out.println("我陪着你飞,你飞完,我也不飞了");
}
}
}
线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
多线程并发的安全问题:
产生:当多个线程并发操作时,由于线程切换实际的不确定性,会导致操作资源的代码
顺序为按照设计顺序执行,出现操作混乱的情况,严重时可能导致系统瘫痪。
解决:将并发操作同一资源改为同步运行,即:有先后顺序的操作同步与异步:
同步:程序运行有先后顺序
异步:程序运行没有先后顺序
public class ThreadDemo10 {
static int num = 30;
/**
* 模拟购票系统
*/
public static void gp(String name){
/**
* `synchronized`锁
* synchronized(锁名字){
* 需要加锁的代码块
* }
*/
synchronized ("买票"){
if (num > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
System.out.println(name + "购票成功,剩余票数:" + num);
}else {
System.out.println(name + "购票失败");
}
}
}
public static void main(String[] args) {
// 开启两个线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
// 买票
gp("张三-" + Thread.currentThread().getName());
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
// 买票
gp("大锤-" + Thread.currentThread().getName());
}
}
});
// 开启线程
t1.start();
t2.start();
}
}
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
懒汉式加载和饿汉式加载
public class ThreadDemo12 {
private static Student stu;
/**
* 饿汉式加载
*/
// static {
// stu = new Student();
// }
public static void main(String[] args) {
}
/**
* 懒汉式加载
* @return
*/
private synchronized static void getStu(){
if (stu == null) {
// 创建student对象
stu = new Student();
}
}
}
创建锁对象lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
private static Lock lock = new ReentrantLock();
加锁
lock.lock();
释放锁
lock.unlock();
线程池
Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。
那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。
典型用法如下
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
代码实现,线程池的作用,重复利用资源
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo14 {
public static void main(String[] args) {
// 创建线程池, 创建指定大小的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
System.out.println("创建的一个任务");
// 调用线程工作
pool.execute(new Runnable() {
@Override
public void run() {
// 获取当前线程对象
Thread t = Thread.currentThread();
System.out.println(t.getName() + "正在执行任务.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName() + "执行任务完毕");
}
});
}
// 将线程池停止,但是线程池会将当前的任务执行完之后在停止
pool.shutdown();
/*
* 该方法是将线程立刻停止,不会等待线程池完成所有任务
* threadPool.shutdownNow();
*/
// pool.shutdownNow();
System.out.println("main函数执行结束*****************************");
}
}
newCachedThreadPool:
创建一个可缓存的无界线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程。当线程池中的线程空闲时间超过60s,则会自动回收该线程,当任务超过线程池的线程数则创建新的线程,线程池的大小上限为Integer.MAX_VALUE,可看作无限大。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
// 为线程池指派任务
pool.submit(new Runnable()
// 停止线程
pool.shutdown();
newScheduledThreadPool:
创建一个定长的线程池,可以指定线程池核心线程数,支持定时及周期性任务的执行
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
// 创建线程池
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
// 为线程池指派任务
pool.schedule(new Runnable()
// 停止线程
pool.shutdown();
newSingleThreadExecutor:
创建一个单线程化的线程池,它只有一个线程,用仅有的一个线程来执行任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
// 调用线程工作
pool.execute(new Runnable()
// 将线程池停止,但是线程池会将当前的任务执行完之后在停止
pool.shutdown();
————————————————
版权声明:本文为CSDN博主「一个萌新柒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_65696574/article/details/127805457