Java 多线程:基础

[Java]add-open的使用及常见问题解决 add-opens 用于在运行时允许外部模块通过反射访问指定模块的某个包,它更像是一种临时性补救措施,主要用于解决 Java 9+ 模块化带来的兼容性问题,但建议尽量减少使用,优先考虑其他方式来适配模块化设计。 CMAK3.0.0.0启动报错的问题解决 阅读详情

顺序、并行与并发

顺序(sequential)用于表示多个操作『依次』处理。比如把十个操作交给一个人处理时,这个人要一个一个地按顺序来处理。

并行(parallel)用于表示多个操作『同时』处理”。比如十个操作分给两个人处理时,这两个人会并行来处理。

并发(concurrent)相对于顺序和并行来说比较抽象,用于表示『将一个操作分割成多个部分并且允许无序处理』。比如将十个操作分成相对独立的两类,这样便可以开始并发处理了。如果一个人来处理,这个人就是顺序处理分开的并发操作,而如果是两个人。这两个人就可以并行处理同一操作。

如果 CPU 只有一个,那么并发处理就是顺序执行的,而如果有多个 CPU,那么并发处理就可能会并行运行。

什么是程序,进程,线程和协程

程序是计算机的可执行文件;

进程是计算机资源分配的基本单位;

线程是资源调度执行的基本单位,也可以说: 线程 是一个程序里面不同的执行路径,多个线程共享进程中的资源;

协程是一种 用户态的轻量级线程 ,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。协程在子程序内部可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。

协程的特点在于是一个线程执行,那和多线程比,协程有如下优势:

优势一:极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;

优势二:不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注意:协程避免了无意义的调度,由此可以提高性能,但是程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多 CPU 的能力。

一个简单的协程示例, 代码如下:

注:

进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

进程和线程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别在于 是否单独占有内存地址空间及其它系统资源 。

进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即 CPU 分配时间的单位。

进程单独占有一定的内存地址空间,所以进程间存在 内存隔离 ,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。

进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。

  1. 需要引入 quasar-core 依赖包。

  2. 如果在 Java SE 16 以及更高版本上运行,需要增加如下参数

    --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
    package git.snippets.juc;
    
    import co.paralleluniverse.fibers.Fiber;
    import co.paralleluniverse.fibers.SuspendExecution;
    import co.paralleluniverse.strands.channels.Channel;
    import co.paralleluniverse.strands.channels.Channels;
    
    import java.util.concurrent.ExecutionException;
    
    /**
     * Java协程示例
     * JDK 11 ~ JDK 15 没问题,
     *
     * JDK 16 开始,需要增加如下参数
     *
     * --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
     *
     * @since jdk11
     * 需要引入:quasar-core依赖包
     */
    public class FiberSample {
        private static void printer(Channel<Integer> in) throws SuspendExecution, InterruptedException {
            Integer v;
            while ((v = in.receive()) != null) {
                System.out.println(v);
            }
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution {
            //定义两个Channel
            try (Channel<Integer> naturals = Channels.newChannel(-1); Channel<Integer> squares = Channels.newChannel(-1)) {
    
                //运行两个Fiber实现.
                new Fiber(() -> {
                    for (int i = 0; i < 10; i++) {
                        naturals.send(i);
                    }
                    naturals.close();
                }).start();
    
                new Fiber(() -> {
                    Integer v;
                    while ((v = naturals.receive()) != null) {
                        squares.send(v * v);
                    }
                    squares.close();
                }).start();
    
                printer(squares);
            }
    
        }
    }

    线程和进程的关系

    线程就是轻量级进程,是程序执行的最小单位。

    多进程的方式也可以实现并发,为什么我们要使用多线程?主要是基于以下两方面的原因:

  3. 共享资源在线程间的通信比较容易。

  4. 线程开销更小。

多线程访问成员变量与局部变量

类变量(类里面 static 修饰的变量)保存在“方法区”

实例变量(类里面的普通变量)保存在“堆”

局部变量(方法里声明的变量)“虚拟机栈”

“方法区”和“堆”都属于线程共享数据区,“虚拟机栈”属于线程私有数据区。

因此,局部变量是不能多个线程共享的,而类变量和实例变量是可以多个线程共享的。事实上,在 Java 中,多线程间进行通信的唯一途径就是通过类变量和实例变量。也就是说,如果一段多线程程序中如果没有类变量和实例变量,那么这段多线程程序就一定是线程安全的。

开发过程中,为了解决线程安全问题,有如下角度可以考虑:

第一种方案:尽量使用局部变量,代替实例变量和静态变量。

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了( 1 个线程对应 1 个对象,100 个对象对应 100 个对象,对象不共享,就没有数据安全问题了)

第三种方案:如果不使用局部变量。对象也不能创建多个。这个时候,就只能选择 syncharonized 了。

线程的共享资源和独有资源

其中共享资源包括:

进程代码段

进程的公有数据

独有资源包括:

  1. 线程ID:每个线程都有自己的线程 ID,这个 ID 在本进程中是唯一的。进程用此来标识线程。

  2. 寄存器组的值:由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

  3. 线程的堆栈:堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。

  4. 错误返回码:由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了 err no 值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。

  5. 线程的信号屏蔽码:由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。

  6. 线程的优先级:由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

什么是线程切换?

从底层角度上看,CPU 主要由如下三部分组成,分别是:

  • ALU: 计算单元

  • Registers: 寄存器组

  • PC:存储到底执行到哪条指令

T1 线程在执行的时候,将 T1 线程的指令放在 PC,数据放在 Registers,假设此时要切换成 T2 线 程,T1 线程的指令和数据放 cache,然后把 T2 线程的指令放 PC,数据放 Registers,执行 T2 线程即可。

以上的整个过程是通过操作系统来调度的,且线程的调度是要消耗资源的,所以,线程不是设置越多越好。

示例:

单线程和多线程来累加 1 亿个数。 示例代码如下

 

package git.snippets.juc;

import java.text.DecimalFormat;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * 多线程求1亿个Double类型的数据
 *
 * @author <a href="mailto:410486047@qq.com">Grey</a>
 * @date 2021/7/7
 * @since
 */
public class CountSum {
    private static final double[] NUMS = new double[1_0000_0000];
    private static final Random R = new Random();
    private static final DecimalFormat FORMAT = new DecimalFormat("0.00");
    static {
        for (int i = 0; i < NUMS.length; i++) {
            NUMS[i] = R.nextDouble();
        }
    }
    static double result1 = 0.0, result2 = 0.0, result = 0.0;
    public static void rand() {
        for (int i = 0; i < NUMS.length; i++) {
            NUMS[i] = R.nextDouble();
        }
    }

    /**
     * 单线程计算一亿个Double类型的数据之和
     *
     * @return
     */
    public static String m1() {
        long start = System.currentTimeMillis();
        double result = 0.0;
        for (double num : NUMS) {
            result += num;
        }
        long end = System.currentTimeMillis();
        System.out.println("计算1亿个随机Double类型数据之和[单线程], 结果是:result = " + FORMAT.format(result) + " 耗时 : " + (end - start) + "ms");
        return String.valueOf(FORMAT.format(result));
    }

    /**
     * 两个线程计算一亿个Double类型的数据之和
     *
     * @return
     */
    private static String m2() throws Exception {
        long start = System.currentTimeMillis();
        result1 = 0.0;
        result2 = 0.0;
        int len = (NUMS.length >> 1);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < len; i++) {
                result1 += NUMS[i];
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = len; i < NUMS.length; i++) {
                result2 += NUMS[i];
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        result = result1 + result2;
        long end = System.currentTimeMillis();
        System.out.println("计算1亿个随机Double类型数据之和[2个线程], 结果是:result = " + FORMAT.format(result) + " 耗时 : " + (end - start) + "ms");
        return String.valueOf(FORMAT.format(result));
    }

    /**
     * 10个线程计算一亿个Double类型的数据之和
     *
     * @return
     */
    private static String m3() throws Exception {
        long start = System.currentTimeMillis();
        final int threadCount = 10;
        Thread[] threads = new Thread[threadCount];
        double[] results = new double[threadCount];

        final int segmentCount = NUMS.length / threadCount;
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            int m = i;
            threads[i] = new Thread(() -> {
                for (int j = m * segmentCount; j < (m + 1) * segmentCount && j < NUMS.length; j++) {
                    results[m] += NUMS[j];
                }
                latch.countDown();
            });

        }
        double resultM3 = 0.0;

        for (Thread t : threads) {
            t.start();
        }
        latch.await();
        for (double v : results) {
            resultM3 += v;
        }

        long end = System.currentTimeMillis();
        System.out.println("计算1亿个随机Double类型数据之和[10个线程], 结果是:result = " + FORMAT.format(resultM3) + " 耗时 : " + (end - start) + "ms");
        return String.valueOf(FORMAT.format(resultM3));
    }

    public static void main(String[] args) throws Exception {
        int testCount = 10;
        boolean correct = true;
        for (int i = 0; i < testCount; i++) {
            rand();
            String s = m1();
            String s1 = m2();
            String s2 = m3();
            if (!s1.equals(s2) || !s1.equals(s)) {
                System.out.println("oops!");
                System.out.println(s1);
                System.out.println(s2);
                System.out.println(s);
                correct = false;
                break;
            }
        }
        if (correct) {
            System.out.println("test finished");
        }
    }
}

运行结果

……
计算1亿个随机Double类型数据之和[单线程], 结果是:result = 49998124.71 耗时 : 114ms
计算1亿个随机Double类型数据之和[2个线程], 结果是:result = 49998124.71 耗时 : 53ms
计算1亿个随机Double类型数据之和[10个线程], 结果是:result = 49998124.71 耗时 : 54ms

计算1亿个随机Double类型数据之和[单线程], 结果是:result = 50000309.80 耗时 : 102ms
计算1亿个随机Double类型数据之和[2个线程], 结果是:result = 50000309.80 耗时 : 53ms
计算1亿个随机Double类型数据之和[10个线程], 结果是:result = 50000309.80 耗时 : 35ms

计算1亿个随机Double类型数据之和[单线程], 结果是:result = 50001943.57 耗时 : 108ms
计算1亿个随机Double类型数据之和[2个线程], 结果是:result = 50001943.57 耗时 : 58ms
计算1亿个随机Double类型数据之和[10个线程], 结果是:result = 50001943.57 耗时 : 41ms

计算1亿个随机Double类型数据之和[单线程], 结果是:result = 49997176.44 耗时 : 102ms
计算1亿个随机Double类型数据之和[2个线程], 结果是:result = 49997176.44 耗时 : 53ms
计算1亿个随机Double类型数据之和[10个线程], 结果是:result = 49997176.44 耗时 : 29ms
……

可以看到结果中,创建 10 个线程 不一定会比创建 2 个线程要执行更快。

单核 CPU 设定多线程是否有意义

有意义,因为线程的操作中可能有不消耗 CPU 的操作,比如:等待网络的传输,或者线程 sleep,此时就可以让出 CPU 去执行其他线程。可以充分利用 CPU 资源。

工作线程数(线程池中线程数量)设多少合适

  • 和 CPU 的核数有关

  • 最好是通过压测来评估。通过 profiler 性能分析工具 JProfiler ,或者 Arthas

  • 公式

  • N = Ncpu * Ucpu * (1 + W/C)

    其中:

  • Ncpu 是处理器的核的数目,可以通过 Runtime.getRuntime().availableProcessors() 得到

  • Ucpu 是期望的 CPU 利用率(该值应该介于 0 和 1 之间)

  • W/C 是等待时间和计算时间的比率。

  • 一个 Hello World 程序运行的时候启动了几个线程

    使用如下代码:

public class HowManyThreadHelloWorld {
    
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println("\n线程:" + t.getName() + "\n");
        System.out.println("hello world!");

        for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
            Thread thread = entry.getKey();

            StackTraceElement[] stackTraceElements = entry.getValue();

            if (thread.equals(Thread.currentThread())) {
                continue;
            }

            System.out.println("\n线程: " + thread.getName() + "\n");
            for (StackTraceElement element : stackTraceElements) {
                System.out.println("\t" + element + "\n");
            }
        }
    }
}

 在 Java SE 11 下执行,可以看到,有如下线程信息

线程:main
线程: Reference Handler
线程: Signal Dispatcher
线程: Finalizer
线程: Common-Cleaner
线程: Attach Listener

在 Java SE 8 下执行,有如下线程信息

线程:main
线程: Finalizer
线程: Attach Listener
线程: Signal Dispatcher
线程: Reference Handler

其中

Reference Handler:处理引用对象本身的垃圾回收

Finalizer:处理用户的 Finalizer 方法

Signal Dispatcher:外部 jvm 命令的转发器

Attach Listener: jvm 提供一种 jvm 进程间通信的能力,能让一个进程传命令给另外一个进程

Common-Cleaner: 该线程是 Java SE 9 之后新增的守护线程,用来更高效的处理垃圾回收

Java 中创建线程的方式

  1. 继承 Thread 类,重写 run 方法。

  2. 实现 Runnable 接口,实现 run 方法,这比方式 1 更好,因为一个类实现了 Runnable 以后,还可以继承其他类

  3. 通过线程池创建。

  4. 在需要返回值的时候,可以通过 Callable 、 Future 与 FutureTask 来创建。

示例代码如下

package git.snippets.juc;

import java.util.concurrent.*;

/**
 * 创建线程的方式
 *
 * @author <a href="mailto:410486047@qq.com">Grey</a>
 * @date 2021/7/7
 * @since 1.8
 */
public class HelloThread {
    public static void main(String[] args) throws Exception {
        MyFirstThread t1 = new MyFirstThread();
        Thread t2 = new Thread(new MySecondThread());
        Thread t3 = new Thread(new FutureTask<>(new CallableThreadTest()));
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> System.out.println("方式3:使用线程池来创建线程。"));
        t1.start();
        t2.start();
        t3.start();
        executor.shutdown();
        boolean b = executor.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(b ? "停止成功" : "停止失败");
    }

    static class MyFirstThread extends Thread {
        @Override
        public void run() {
            System.out.println("方式1:继承Thread类并重写run方法来创建线程");
        }
    }

    /**
     * 方式二, 实现Runnable接口来创建线程
     */
    static class MySecondThread implements Runnable {

        @Override
        public void run() {
            System.out.println("方式2:实现Runnable方式来创建线程");
        }
    }

    static class CallableThreadTest implements Callable<Integer> {
        @Override
        public Integer call() {
            int i;
            for (i = 0; i < 10; i++) {
                i++;
            }
            System.out.println("方式4,实现Callable接口方式来创建有返回值的线程,返回值是:" + i);
            return i;
        }
    }
}

 

线程状态和切换

NEW:线程刚刚创建,还没有启动,New Thread 的时候,还没有调用 start 方法时候,就是这个状态

RUNNABLE:可运行状态,由线程调度器可以安排执行,包括以下两种情况:

  • READY

  • RUNNING

READY 和 RUNNING 通过 yield 方法来切换

WAITING:等待被唤醒

TIMED_WAITING:隔一段时间后自动唤醒

BLOCKED:被阻塞,正在等待锁,只有在 synchronized 的时候在会进入 BLOCKED 状态

TERMINATED:线程执行完毕后,是这个状态

各个线程状态切换如下

线程基本操作

sleep:当前线程睡一段时间

yield:这是一个静态方法,一旦执行,它会使当前线程让出一下 CPU。但要注意,让出 CPU 并不表示当前线程不执行了。当前线程在让出 CPU 后,还会进行 CPU 资源的争夺,但是是否能够再次被分配到就不一定了。

join:等待另外一个线程的结束,当前线程才会运行,示例代码如下:

public class ThreadBasicOperation {
    static volatile int sum = 0;

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
        });
        t.start();
        // join 方法表示主线程愿意等待子线程执行完毕后才继续执行
        // 如果不使用join方法,那么sum输出的可能是一个很小的值,因为还没等子线程
        // 执行完毕后,主线程就已经执行了打印sum的操作
        t.join();
        System.out.println(sum);
    }
}

interrupt:打断线程执行,有三个方法。

// 打断某个线程(设置标志位)
interrupt()
// 查询某线程是否被打断过(查询标志位)
isInterrupted()
// 查询当前线程是否被打断过,并重置打断标志位
Thread.interrupted()

示例代码如下

package git.snippets.juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * interrupt示例
 *
 * @author <a href="mailto:410486047@qq.com">Grey</a>
 * @since 1.8
 */
public class ThreadInterrupt {
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (; ; ) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("t thread interrupted");
                    System.out.println(Thread.currentThread().isInterrupted());
                    break;
                }
            }
        });
        t.start();
        TimeUnit.SECONDS.sleep(3);
        t.interrupt();

        Thread t2 = new Thread(() -> {
            for (; ; ) {
                if (Thread.interrupted()) {
                    System.out.println("t2 thread interrupted");
                    // Thread.interrupted()会将线程中断状态置为false
                    System.out.println(Thread.currentThread().isInterrupted());
                    break;
                }
            }
        });
        t2.start();
        TimeUnit.SECONDS.sleep(3);
        t2.interrupt();

        Thread t3 = new Thread(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("t3 interrupted");
                // 如果不加上这一句,那么Thread.currentThread().isInterrupted()将会都是false,因为在捕捉到InterruptedException异常的时候就会自动的中断标志置为了false
                Thread.currentThread().interrupt();
                System.out.println(Thread.currentThread().isInterrupted());
            }
        });

        t3.start();
        TimeUnit.SECONDS.sleep(3);
        t3.interrupt();

        final Object o = new Object();
        Thread t4 = new Thread(() -> {
            synchronized (o) {
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    System.out.println("t4 interrupted!");
                    Thread.currentThread().interrupt();
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });
        t4.start();
        TimeUnit.SECONDS.sleep(10);
        t4.interrupt();

        Thread t5 = new Thread(() -> {
            synchronized (o) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t5.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t6 = new Thread(() -> {
            synchronized (o) {

            }
            System.out.println("t6 finished");
        });
        t6.start();
        t6.interrupt();


        Thread t7 = new Thread(() -> {
            LOCK.lock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
            System.out.println("t7 end");
        });
        t7.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t8 = new Thread(() -> {
            LOCK.lock();
            try {
            } finally {
                LOCK.unlock();
            }
            System.out.println("t8 end");
        });
        t8.start();
        TimeUnit.SECONDS.sleep(1);
        t8.interrupt();

        Thread t9 = new Thread(() -> {
            LOCK.lock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
            System.out.println("t7 end");
        });
        t9.start();
        TimeUnit.SECONDS.sleep(1);
        Thread t10 = new Thread(() -> {
            System.out.println("t10 start");
            try {
                LOCK.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("t10 interrupted");
            } finally {
                LOCK.unlock();
            }
            System.out.println("t8 end");
        });
        t10.start();
        TimeUnit.SECONDS.sleep(1);
        t10.interrupt();

    }
}

关于线程的 start 方法

问题1:反复调用同一个线程的 start() 方法是否可行?

问题2:假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start() 方法是否可行?

两个问题的答案都是不可行,在调用一次 start() 之后, threadStatus 的值会改变( threadStatus !=0 ),此时再次调用 start() 方法会抛出 IllegalThreadStateException 异常。

如何结束一个线程

不推荐的方式

  • stop 方法

  • suspend 结合 resume 方法

以上两种方式都 不建议使用 , 因为会释放所有的锁, 所以容易产生数据不一致的问题。

优雅的方式

  • 如果不依赖循环的具体次数或者中间状态, 可以通过设置标志位的方式来控制。

  • 如果要依赖循环的具体次数或者中间状态, 则可以用 interrupt 方法。

上述四种方式的示例代码如下:

package git.snippets.juc;

import java.util.concurrent.TimeUnit;

/**
 * 如何结束一个线程
 *
 * @author <a href="mailto:410486047@qq.com">Grey</a>
 * @since 1.8
 */
public class ThreadFinished {
    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        // 推荐方式:设置标志位
        useVolatile();
        // 推荐方式:使用interrupt
        useInterrupt();
        // 使用stop方法来结束线程,不推荐
        useStop();
        // 使用suspend/resume方法来结束线程,不推荐
        useResumeAndSuspend();
    }

    private static void useResumeAndSuspend() throws InterruptedException {
        Thread t2 = new Thread(() -> {
            System.out.println("t2 start");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                // e.printStackTrace();
            }
            System.out.println("t2 finished");
        });
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        t2.suspend();
        TimeUnit.SECONDS.sleep(1);
        t2.resume();
    }

    private static void useStop() throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("t start");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                // e.printStackTrace();
            }
            System.out.println("t finished");
        });
        t.start();
        TimeUnit.SECONDS.sleep(1);
        t.stop();
    }

    private static void useInterrupt() throws InterruptedException {
        Thread t4 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {

            }
            System.out.println("t4 end");
        });
        t4.start();
        TimeUnit.SECONDS.sleep(1);
        t4.interrupt();
    }

    private static void useVolatile() throws InterruptedException {
        Thread t3 = new Thread(() -> {
            long i = 0L;
            while (flag) {
                i++;
            }
            System.out.println("count sum i = " + i);
        });
        t3.start();
        TimeUnit.SECONDS.sleep(1);
        flag = false;
    }
}

 

 

 

 

jdk17 --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add 在使用Java 17(JDK 17)时,您可能需要使用选项来打开某些模块的包,以便让非模块化代码或其他模块能够访问这些包中的受保护成员(如protected修饰符的方法或字段)。这是因为Java 9引入了模块系统(),它默认情况下限制了对模块包的访问。 阅读详情

相关推荐

Flink运行时报错: --add-exports=java.base/sun.net.util=ALL-UNNAMED 大数据

Flink运行时报错: --add-exports=java.base/sun.net.util=ALL-UNNAMED 大数据当在处理大数据时使用Apache Flink时,有时候可能会遇到一些运行时报错。其中一个可能的错误提示是"–add-exports=java.base/sun.net.util=ALL-UNNAMED"。本文将详细解释这个错误的背景和可能的解决方法。

2301_79367634的博客 970

Spring Boot 应用启动时 java.lang.reflect.InaccessibleObjectException 问题的解决

Spring Boot 的应用启动的时候遇到下面的错误 java.lang.reflect.InaccessibleObjectException: Unable to make private native ... accessible

千里之行 1万+

【Java实战⑭】从0到1:Java多线程基础实战秘籍

本文围绕 Java 多线程基础实战展开,先对比进程与线程的资源占用、调度效率差异,点明多线程在提升并发能力、优化资源利用率上的优势及文件下载、网络请求处理场景。接着介绍继承 Thread 类、实现 Runnable 和 Callable 接口(配 FutureTask 获返回值)三种线程创建方式并对比优劣。最后解析线程五状态生命周期,讲解 start、sleep 等方法,结合案例示状态转换,还提及线程优先级与守护线程操作,为入门提供指引。

邓邓子的博客 937

解决Java --add-opens java.base/java.lang=ALL-UNNAMED的具体操作步骤

选项是Java 9引入的一个命令行选项,用于打开模块之间的包,以便其他模块可以访问这些包中的类和成员。通过使用选项,我们可以解决由于模块的隔离性而导致的访问限制问题。但是,应该注意的是,选项可能会降低代码的安全性,因为它打开了。

qq_51081700的博客 1万+

基于OpenMV与双舵机云台的激光追踪系统实战解析

本文详细解析了基于OpenMV摄像头与双舵机云台的激光追踪系统设计与实现。从硬件选型、图像识别算法到PID控制逻辑,全面介绍了如何构建一个高效稳定的激光追踪系统,并分享了电子设计竞赛中的实战经验与调试技巧。该系统在智能玩具、安防监控及教学演示等领域具有广泛应用前景。

prometheus9mon的博客 262

深入浅出 Java 多线程:从基础到实战

掌握 Java 多线程技术是进阶高级 Java 开发的必经之路。多线程编程不仅能提升程序性能,还能拓展程序的功能边界,但同时也伴随着线程安全、死锁等复杂问题。建议读者通过实际项目练习,将理论知识应用于实践,加深对多线程概念和技术的理解。此外,结合 JUC(java.util.concurrent)工具包进行深入学习和实践,能够进一步提升多线程编程能力,编写出高效、稳定的多线程程序。​上述内容从多方面丰富了文章,使 Java 多线程知识讲解更全面深入。

2202_75601845的博客 955

SpringCloud2025+SpringBoot3.5.0动态路由503问题排查与负载均衡依赖解析

本文详细排查了SpringCloud 2025与SpringBoot 3.5.0环境下,Spring Cloud Gateway动态路由返回503错误的根本原因。核心问题在于新版网关未默认集成负载均衡器,导致无法解析lb://服务名。通过对比静态路由与动态路由,定位到需显式引入spring-cloud-starter-loadbalancer依赖,并提供了完整的解决方案与最佳实践配置。

s3t4u的博客 504

理解 Java 多线程:从基础到应用

多线程是指在软件或硬件上实现多个线程并发执行的技术。具有多线程能力的计算机可以同时执行多个线程,显著提升性能。

Rockivy的博客 1125

纯JS实现SM3加密算法(兼容IE浏览器)

纯JS实现SM3加密,IE、谷歌、火狐等浏览器可以直接使用。

qianpou的博客 7356

PID详解

1、什么是PID及其作用 设定一个输出目标,反馈系统传回输出值,如与目标不一致,则存在一个误差,PID 根据此误差调整输入值,直至输出达到设定值. 为什么需要PID呢,比如控制温度,我们需要设置一个要达到的目标温度值,当将温度稳定在目标温度值 例如,我们希望水的目标温度值为70°,其希望无非就是可以快速而且无抖动的达到70°,并稳定在70° 如果我们使用普通的方式加热,当温度到达70°后就以及停止加热,岂不是简单快速,当然用在要求不高的场合也可以,但是如果要求更高的场合呢,会出现温度到达70°后......

weixin_48435215的博客 5万+

JavaSE基础二十:Java 多线程(线程基础知识、Java 多线程、Java 实现多线程(继承 Thread 类、实现 Runnable 接口、实现 Callable 接口))

一、线程 线程是依赖于进程存在的,在了解线程之前我们需要先了解进程 1.进程概述 进程就是正在运行的程序,是系统进行资源分配和调用的独立单位 我们可以通过任务管理器来看到计算机运行了哪些进程 2.多进程的意义 意义在于:我们可以操作计算机同时做多件事 例如:我们可以一边浏览新闻,一边听歌 3.线程概述 在一个进程内部还可能执行多个任务,在进程内部执行的任务我们就称为线程 4.多线程的意义 提高了...

她长眠于月色 533

CKAD-CN 考试知识点分享(12) Secret - 1

步骤操作完成1提取 Deployment 中的硬编码 Secret 值✅2创建名为postgres的 Secret✅3修改 Deployment 使用引用 Secret✅4验证新 Pod 正常运行✅❌未删除原 Deployment✅(遵守要求)✅任务完成。敏感信息已从环境变量移出,安全地存储在 Kubernetes Secret 中。

轻的博客 634

Java基础系列:多线程基础

来小伙伴们,我们认识一下。 俗世游子:专注技术研究的程序猿 这节我们来聊一下Java中多线程的东西 本人掐指一算:面试必问的点,???? 好的,下面在聊之前,我们先了解一下多线程的基本概念 基本概念 进程 那我们先来聊一聊什么是程序: 程序是一个指令的集合,和编程语言无关 在CPU层面,通过编程语言所写的程序最终会编译成对应的指令集执行 通俗一点来说,我们在使用的任意一种软件都可以称之为程度,比如: QQ,微信,迅雷等等 而操作系统用来分配系统资源的基本单元叫做进程,相同程序可以存在多个进程

谢先生 201

《嵌入式-STM32开发指南》第三部分 外设篇 - 第2章 温度传感器DS18B20

2.1 理论分析 2.1.1概述 DS18B20 是 DALLAS 最新单线数字温度传感器,新的"一线器件"体积更小、适用电压更宽、更经济。Dallas 半导体公司的数字化温度传感器 DS1820 是世界上第一片支持 "一线总线"接口的温度传感器。 DS18B20采用的单总线协议,也就是只需占用主机一个I/O口,无需其他外围电路,直接将环境温度转换为数字信号,使用户可轻松地组建传感器网络,为测量系统的构建引入全新概念。另外,以"一线总线"的数字方式传输,大大提高了系统的抗干扰性。适合于恶劣环境的现场温度测量

不问归期的博客 8305

在 SAP BTP 上为应用开启多租户能力:基于 SaaS Provisioning 和 XSUAA 的完整实践指南

本文介绍了在SAP BTP上配置多租户应用的实战指南。主要内容包括: 多租户架构概述:解释provider子账户、consumer子账户和tenant的概念,以及数据隔离机制。 安全配置:在xs-security.json中设置tenant-mode为shared,并为SaaS Provisioning服务添加回调scope授权。 路由配置:通过manifest.yml设置TENANT_HOST_PATTERN来定义多租户访问模式,确保不同租户通过不同子域名访问同一应用。 服务绑定:将XSUAA和SaaS

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 149

Java基础:多线程基础之【线程概念】

概念说明进程(Process)一个正在运行的Java程序实例(如java MyApp启动的JVM),拥有独立的内存空间(堆、方法区、栈等),是操作系统资源分配的基本单位。线程(Thread)进程内的一个执行流,是CPU调度的基本单位。一个JVM进程可包含多个线程,它们共享堆和方法区,但各自拥有独立的虚拟机栈和程序计数器。进程是“程序的运行实例”,线程是“进程内的执行单元”;Java中多线程共享堆内存,但栈私有。Java线程有6种状态,理解状态转换是排查死锁、活锁、性能瓶颈的基础。

lzq617789的博客 970

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Java基础:多线程

1. 多线程概述人们在日常生活中,很多事情都是可以同时进行的。例如,一个人可以一边听音乐,一边打扫房间,可以一边吃饭,一边看电视。在使用计算机时,很多任务也是可以同时进行的。例如,可以一边浏览网页,一边打印文档,还可以一边聊天,一边复制文件等。计算机这种能够同时完成多项任务的技术,就是多线程技术。Java是支持多线程的语言之一,它内置了对多线程技术的支持,可以使程序同时执行多个执行片段。1.1 多线

JackChan 4380

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Java基础知识:多线程

黑马程序员---------Java基础知识:多线程 ------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------                  一:线程的概念 进程是一个正在执行的程序。每一个进程都有一个执行顺序,该顺序叫一个执行路径,或者叫一个控制单元。而线程就是进程中一个独立的控制单元,也就是说线程在控制着进程的执行,一个进程当中起码有一

yuminyan的专栏 362

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Java多线程终极指南:从基础到高级应用

/ 自定义线程池2, // 核心线程数4, // 最大线程数60, // 空闲时间new ArrayBlockingQueue<>(2), // 任务队列容量2new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略// 提交10个任务i <= 10;i++) {System.out.println("执行任务 " + taskId + ",线程: " + Thread.currentThread().getName());try {

本博客聚焦 YOLOv11 全流程落地,涵盖架构优化、数据集处理、训练技巧与多场景部署,兼及国产数据库、Java 开发与 AI 模型应用,内容兼顾理论与工程实践,为开发者提供系统干货,助力高效提升技术能力。 1774

java多线程:基础原理

java多线程:基础原理 java支持多线程编程,为了能够深入理解java多线程机制,以及解决多线程的安全问题,本文介绍多线程的基础知识和原理分析。 Part1 概念总结 线程的概念 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任

XINGKONG_04的博客 2682

Java多线程:基础与实践

多线程是Java编程中的一个重要概念,它允许程序同时执行多个任务。在Java中,每个线程都是一个独立的执行路径,可以共享进程的资源,如内存空间和文件句柄。多线程编程可以提高程序的执行效率,特别是在处理大量并发任务时。

qq_40014433的博客 526

《Java中的多线程编程:从基础到实战》

线程是程序执行的最小单位,一个进程可以包含多个线程。多线程允许程序同时执行多个任务,从而提高效率。当多个线程同时访问共享资源时,可能会导致数据不一致的问题。线程安全是指通过同步机制确保多线程环境下程序的正确性。多线程编程是Java开发中的重要技能,但也需要谨慎处理线程安全和性能问题。通过本文的学习,你应该掌握了多线程的基础知识、线程同步、线程池的使用以及一个简单的多线程下载文件的实战案例。如果你对多线程编程还有疑问,欢迎在评论区留言讨论!

2303_78307248的博客 458

Java 多线程编程:从基础到实战

多线程是指在一个程序中同时运行多个线程,每个线程执行不同的任务。这样可以充分利用 CPU 资源,提高程序的执行效率。@OverrideSystem.out.println("线程 " + Thread.currentThread().getName() + " 正在运行");通过本文的介绍,我们深入探讨了 Java 的多线程机制,从线程的创建和生命周期,到线程同步和并发工具类的使用。多线程编程可以显著提升程序的性能和响应能力,但同时也需要谨慎处理线程安全问题。

C_V_Better的博客 508

Java基础:多线程基础之【线程同步】

本文系统讲解了Java多线程编程中的线程同步机制。首先通过计数器案例展示了多线程竞争共享资源导致的数据不一致问题,然后详细介绍了三种核心同步机制:synchronized关键字(同步方法/代码块)、volatile变量和ReentrantLock显式锁,对比了它们的特性和适用场景。文章还介绍了java.util.concurrent包中的高级同步工具如原子类、CountDownLatch等,并给出了线程同步的最佳实践建议,包括缩小锁范围、避免死锁等。最后总结了不同同步机制在原子性和可见性方面的差异,帮助开发

lzq617789的博客 936
上一篇: ChaosBlade Java 场景性能优化,那些你不知道的事
下一篇: 面试突击:什么是跨域问题?如何解决?
java_lujj
博客等级 码龄4年 18粉丝 152原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值