『Java练习生的自我修养』java-se进阶¹ • 初识多线程

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第1张图片

☕☕ Java进阶攻坚克难,持续更新,一网打尽IO、注解、多线程…等java-se进阶内容。


线程的基本概念

程序、进程、与线程?

想要搞明白什么是线程,首先要对程序与进程有一个清晰的概念,大学课堂上的《操作系统》这门课开篇就是这部分内容,学过这门课的同学对程序、进程和线程应该已经不陌生了。

程序(program): 为完成特定任务,用某种语言编写的一组指令集合,程序是静态的,这是它与进程的区别。

进程(process): 程序的一次执行过程,或是正在运行的一个程序,进程强调的是动态的过程。

在不引入线程这个概念之前,进程是系统运行时资源分配和调度的基本单位。为了充分利用系统资源,提高程序的运行效率,将进程进一步细化为线程,取代进程成为了调度和执行的基本单位。

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第2张图片

✨线程(Thread)的特点:

  1. 线程是程序内部的一条执行路径;
  2. 每个线程拥有独立的运行栈和程序计数器(PC),线程之间切换的开销小;
  3. 一个进程中的多个线程共享相同的内存单元,可以访问相同的变量和对象;
  4. 线程之间的通信简便、高效,但是多个线程访问同一资源时也会带来安全隐患。
  5. 如果只有一个CPU时,多线程是模拟出来的,只能称作并发执行,当有多个CPU即多核的状态下,多个线程才是真正的同时执行,可以称作并行。

并行与并发的区别

并行: 多个CPU同时执行多个任务。

并发: 一个CPU同时执行多个任务。这里的“同时”并不是真的有多个任务在同时执行,CPU在一次还是只能执行一个任务,每个任务执行一段时间后就切换其他任务,只不过CPU的速度很快,看起来像是多个任务在同时执行。(操作系统讲到的CPU时间片轮转调度算法就是典型的并行执行例子)

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第3张图片

线程的几种状态

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第4张图片

何时需要多线程?

  1. 程序需要同时执行两个或多个任务;
  2. 程序需要实现一些需要等待的任务,比如用户输入,文件读写,搜索等等;
  3. 需要后台运行的程序…

线程的创建与使用

JVM启动我们的Java程序以后,进程中会有默认的线程,比如Main主方法线程与GC垃圾回收线程。通常我们编写的代码都是在Main主方法线程中执行的,又称作用户线程,而GC垃圾回收线程称作守护线程

✨Java中想要自己创建线程,有以下三种方式:✨

  1. Thread class —▶ 继承Thread类
  2. Runnable接口 —▶ 实现Runnable接口
  3. Callable接口 —▶ 实现Callable接口

1.继承Thread类

继承Thread类,并重写其中的run()方法,通过调用start()方法开启线程。

//    方式一:继承Thread类
//    1.创建子类
class MyThread extends Thread{
//    2.重写run()方法
    @Override
    public void run() {
        System.out.println("创建一个线程");
    }
}

public class Test {
    public static void main(String[] args) {
//        3.创建对象
        MyThread myThread = new MyThread();
//        4.调用start()
        myThread.start();
    }
}

写一个小Demo,看一下我们自己创建的线程与主方法线程在运行时的顺序:

(为了避免CPU运行速度过快看不出多线程的效果,可以将循坏次数设置的大一些)

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.println("***这是自己创建的线程***MyThread---");
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        new MyThread().start();
        for (int i = 0; i < 300; i++)
            System.out.println("***这是主方法默认线程***Main---");
    }
}

运行,可以看到即便代码中是先运行MyThread线程,但是两个线程中的输出语句是交替出现的。

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第5张图片

2.实现Runnable接口

声明实现类Runnable接口,并实现其中的run()方法,然后分配类的实例,在创建Thread时作为参数传递(这里用了静态代理设计模式),调用Thread下的start()方法启动。

//  方式二:实现Runnable接口
class MyThread implements Runnable {
//    实现run()方法线程体
    @Override
    public void run() {
        System.out.println("创建一个线程");
    }
}

public class Test {
    public static void main(String[] args) {
//        2.创建Runnable接口的实现类对象
        MyThread myThread = new MyThread();
//        3.创建线程对象并传递参数,通过线程对象开启我们的线程
        new Thread(myThread).start();
    }
}

使用匿名内部类的方式传递Runnable接口的实现类,让代码更简洁灵活:

public class Test {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("创建一个线程");
            }
        }).start();
    }
}

JDK1.8新特性,通过Lambda表达式进一步简化代码:

public class Test {

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("创建一个线程");
        }).start();
    }
}

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第6张图片

3.实现Callable接口

Callable接口创建线程的流程:

实现Callable接口→重写call方法→创建目标对象→ 创建执行服务→提交执行→获取结果→关闭服务

import java.util.concurrent.*;

//  方式三:实现Callable接口
//     实现Callable接口
class TestCallable implements Callable<Boolean> {

    //    实现call()方法,执行结束后返回一个值
    @Override
    public Boolean call() throws Exception {
        System.out.println("创建一个线程");
        return true;
    }
}

public class Test {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
//        创建目标对象
        TestCallable t = new TestCallable();
//        创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(1);
//        提交执行
        Future<Boolean> r = ser.submit(t);
//        获取结果
        boolean res = r.get();
//        关闭服务
        ser.shutdown();
    }
}

线程方法

方法 说明
setPriority(int newPriority) 更改线程优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,不推荐使用
boolean isAlive() 测试线程是否处于活动状态

调用线程方法可以控制线程的运行状态:

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第7张图片
停止线程:

  • JDK提供的stop()destroy()方法已经废弃,不推荐使用。
  • 推荐让线程自己停下来。
  • 建议使用一个标志位,当flag=false时终止线程运行。

Demo案例:

class MyThread implements Runnable {

//    1.设置一个标志位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("Thread is running..." + i++);
        }
    }

//    2.设置一个公开的方法停止线程,转换标志位
    public void stop() {
        this.flag = false;
    }
}

public class TestStop {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
//        运行myThread线程
        new Thread(myThread).start();
//        当主线程循环到180次时结束myThread
        for (int i = 0; i < 300; i++) {
            System.out.println("***main***" + i);
            if (i == 180) {
                myThread.stop();
                System.out.println("myThread线程已经停止");
            }
        }
    }
}

运行结果:

线程休眠:

  • sleep(long millis)指定当前线程阻塞的毫秒数。
  • sleep(long millis)方法存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态。
  • sleep可以模拟网络延时、倒计时等。
  • 多线程环境下,每个对象都有一个锁,sleep不会释放锁。

Demo案例:

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestSleep {

//    打印当前系统时间
    public static void main(String[] args) throws InterruptedException {
//        获取当前时间
        Date time = new Date(System.currentTimeMillis());
        while (true) {
//            每隔1s打印一次时间
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(time));
//            更新时间
            time = new Date(System.currentTimeMillis());
        }
    }
}

运行结果:

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第8张图片

线程礼让:

  • 调用yield()方法即可让当前正在执行的线程暂停,但不阻塞。
  • 将线程从运行态转为就绪态。
  • CPU会重新调度,但线程礼让不一定成功,取决于系统的调度。

强制执行:

  • 调用join()方法合并线程,其他线程阻塞,待此线程执行完成后,再执行其他线程。

查看线程状态:

调用getState()方法查看线程状态:

  • NEW — 线程尚未启动。
  • RUNNABLE — 线程正在执行。
  • BLOCKED — 线程被阻塞。
  • WATTIGN — 线程正在等待另一个线程执行特定动作。
  • TIMED_WATTING — 正在等待另一个线程执行动作到达指定等待时间。
  • TERMINATED — 线程已经退出。

示例代码:

public class TestState {

    public static void main(String[] args) {
        Thread myThread = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("这是一个线程");
            }
        });
//        查看线程状态
        Thread.State state = myThread.getState();
        System.out.println(state);
//        观察启动后
        myThread.start();
        System.out.println(myThread.getState());
    }
}

输出结果:

NEW
RUNNABLE
这是一个线程
这是一个线程
这是一个线程

Process finished with exit code 0

线程优先级:

  • Java提供线程调度器来监控程序中进入就绪状态的线程,线程调度器按照优先级进行调度,但是需要注意优先级不是一定生效,实际的调度顺序取决与CPU调度,只不过优先级高的被先调度的概率会高一些。
  • 线程优先级用数字表示,1~10:
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10
    • Thread.NORM_PRIORITY = 5
  • 获取或改变优先级:
    • getPriority()
    • setPriority(int x)
  • 只能先设置优先级再启动线程,反过来不行。

守护线程

线程分为用户线程与守护线程,操作日志、内存监控、垃圾回收等都属于守护线程。虚拟机只需要确保用户线程执行完毕,而不用等待守护线程也执行完。

通过setDaemon(boolean x)方法设置守护线程,默认值是false,代表用户线程,正常的线程都是用户线程,setDaemon(true)将线程切换为守护线程。

代码示例:

public class TestDaemon {

    public static void main(String[] args) {
//        设置用户线程执行20次
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                System.out.println("***我是一个用户线程***" + i);
            }
        });
//        设置守护线程执行100次
        Thread daemonThread = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("---我是一个守护线程---" + i);
            }
        });
//        将线程设定为守护线程
        daemonThread.setDaemon(true);
        userThread.start();
        daemonThread.start();
    }
}

运行结果:

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第9张图片

可以发现即便代码部分守护线程循环了100次,但是用户线程结束,整个程序都会结束,JVM不会再等待守护线程也执行完。


线程池技术

如果用过数据库连接池的同学对线程池也有一定概念了,它们都属于池化技术,目的就是提高程序的性能。

为什么需要线程池?

频繁创建和调用线程尤其是并发情况下的线程,对性能的影响很大。如果提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回线程池中,可以有效避免线程的频繁创建销毁,实现重复利用。

引入线程池技术的好处:

  • 降低资源消耗。
  • 便于线程管理。
  • 提高响应速度,减少创建新线程的时间。

使用线程池:

JDK5.0版本提供的线程池工具类:ExecutorService 和 Executors,其中Executors是线程池的工厂类,用于创建并返回不同类型的线程池,ExecutorService是真正的线程池接口。

  • Executors.newFixedThreadPool(int parm)方法返回一个线程池池,参数执行线程池的大小。
  • ExecutorService类下的execute(Runnable command)方法用于将线程丢进线程池并执行线程。
  • 线程执行完毕后可以使用shutdown()方法关闭线程池链接。

线程池编程3步:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyThread implements Runnable {

    @Override
    public void run() {
//        循环输出线程名称+循环次数
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " :" + i);
        }
    }
}

public class Demo {

    public static void main(String[] args) {
//        1.创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
//        2.将线程丢进线程池并启动
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
//        3.关闭线程池链接
        service.shutdown();
    }
}

执行结果:

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第10张图片

下篇预告:并发与多线程


创作不易,如果觉得本文对你有所帮助,欢迎点赞关注收藏。‍♀️

@作者:Mymel_晗,计算机专业练习时长两年半的Java练习生~‍♂️

文末已至,咱们下篇再见

┊一片春愁待酒浇。江上舟摇,楼上帘招。┊
一剪梅·舟过吴江-蒋捷

『Java练习生的自我修养』java-se进阶¹ • 初识多线程_第11张图片

你可能感兴趣的:(Java进阶指北,java,多线程)