java之多线程

程序 进程 线程

  • 1.程序(Program):程序是一组指令的有序集合,用于实现特定任务或完成特定功能。它通常以文件的形式存储在硬盘上,并被操作系统加载到内存中执行。程序本身是静态的,只有在执行时才变为动态状态。

  • 2.进程(Process):进程是计算机中正在运行的程序的实例。它是操作系统进行资源分配和调度的基本单位。每个进程都有自己的地址空间、内存、文件句柄等资源。进程之间相互独立,彼此隔离,各自运行在自己的虚拟环境中。

  • 3.线程(Thread):线程是进程内的一个执行单元,负责执行程序中的代码。一个进程可以包含多个线程,这些线程共享进程的资源,如内存、文件和打开的网络连接等。线程之间可以并发执行,提供了更高效的程序设计方式。多线程编程能够充分利用多核处理器的优势

总结起来,程序是静态的指令集合,进程是程序的动态执行实例,线程是进程内的执行单元。一个程序可以被加载为多个进程,而一个进程可以包含多个线程。进程之间相互独立,而同一进程内的线程共享资源并协同工作。

ps:java程序启动后有 main线程,gc线程,异常处理线程

并行和并发

  • 并行:多核cpu下,多线程同时执行
  • 并发:如果是单核cpu,采用抢占时cpu调度模型,让cpu在多个线程之间切换执行

完成一个任务需要多少个线程

确定一个任务需要多少个线程是一个复杂的问题,没有一个固定的答案。这取决于任务的性质、复杂性以及可用的计算资源等因素。以下是一些常见情况下线程数量的考虑因素:

  • 1.任务类型:不同类型的任务对线程需求量有不同的影响。某些任务可能是串行执行的,即只能在一个线程中执行,而其他任务可能可以并发执行,使用多个线程提高效率。

  • 2.并行性要求:如果任务可以被分解为独立且相互不依赖的子任务,那么可以使用多个线程同时处理这些子任务。并行性需求越高,所需的线程数量就越多。

  • 3.处理器核心数:处理器核心数决定了同时执行线程的能力。通常情况下,为了充分利用多核处理器,可以将线程数量设为处理器核心数的倍数。

  • 4.吞吐量和响应时间要求:如果任务需要高吞吐量,即要求尽快处理大量工作,可以增加线程数以并行处理。但是,如果任务需要快速响应时间,过多的线程可能会导致线程切换开销,反而影响性能。

  • 5.资源限制:线程消耗系统资源,包括内存和处理器时间片。要考虑系统资源的限制,确保线程数量不会超过系统承受的范围。

需要根据具体情况来评估任务的性质和需求,然后进行合理的线程分配。通常可以通过实验和性能测试来确定最佳的线程数量。

常见的线程使用的场景

  • 1.并发编程:线程是实现并发编程的基本单位。当需要同时执行多个任务或处理多个事件时,可以使用线程来实现并发性。例如,在服务器端编程中,可以为每个客户端请求创建一个线程来并行处理请求。

  • 2.I/O操作:当进行I/O操作时,例如文件读写、网络通信等,线程可以异步执行这些操作以避免阻塞主线程。这样可以提高系统的响应性能。例如,可以使用一个线程读取数据,并将数据传递给另一个线程进行处理。

  • 3.图形用户界面(GUI)应用程序:在GUI应用程序中,通常需要在主线程中处理用户界面更新和事件处理,以保持应用程序的响应性。但是,如果有一些长时间运行的任务,可以使用一个或多个额外的线程来执行这些任务,以避免阻塞主线程。

  • 4.计算密集型任务:对于需要大量计算而不涉及阻塞I/O的任务,可以使用多线程来加速计算过程。通过将计算任务分配给多个线程,并行执行这些任务,可以利用多核处理器的优势,提高计算效率。

  • 5.后台任务:在应用程序中执行一些后台任务,例如数据备份、定时任务等,可以使用线程来并行执行这些任务,而不影响主线程的正常工作。

ps:,在使用线程时需要谨慎管理线程的生命周期、资源共享和同步等问题,以避免出现并发安全性和性能问题

多线程实战展示

Thread

  1. 定义子类继承Thread类。
  2. 类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。

启动一个线程,在线程中执行1-10000的偶数打印工作

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            if (i % 2 == 0) {
                //Thread.currentThread().getName():得到线程的名字
                System.out.println(Thread.currentThread().getName() + "\t" + i);

            }
        }
    }
}

测试

public class Test1 {

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.start();

        // MyThread myThread2 = new MyThread();
        // myThread2.start();

        System.out.println(Thread.currentThread().getName() + "  main 线程 over");
        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        System.out.println("main over");
    }

}

这里的 JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞这个是用于在 Java Swing 应用程序中显示一个带有消息文本的对话框。这个对话框通常用于提示用户或获取用户的确认

JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞该代码位于主线程的执行流程中,当执行到这一行时,主线程会被阻塞,直到用户在对话框上做出选择(例如点击确认按钮)。只有在用户做出选择后,主线程才会继续执行后续的代码。

如果子线程执行中,进程就不会停止

Runable

  • 定义子类,实现Runnable接口。
  • 类中重写Runnable接口中的run方法。
  • 通过Thread类含参构造器创建线程对象。
  • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  • 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
public class ThreadDemo {

    public static void main(String[] args) {
        //方式1:Thread子类,启动
        MyThread thread1 = new MyThread();
        thread1.start();

        //方式2:Runable方式(推荐的方式)
        //优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
        MyTask task = new MyTask();
        Thread thread2 = new Thread(task);
        thread2.start();

        //方式3:匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
            }
        }).start();
    }

}

//方式1
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "\t" + i);
        }
    }
}

//方式2
//优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
class MyTask implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "\t" + i);
        }
    }
}

在线程中你调用start()方法是会执行对象的run()方法的

Thread常见方法

  • Thread.currentThread().getName()得到线程的名字

  • start(): 启动线程,并调用线程的run()方法。

  • run(): 线程执行的具体逻辑,通常重写该方法来定义线程的任务。

  • sleep(long millis): 使当前线程进入休眠状态,暂停执行指定毫秒数的时间。

  • join(): 等待线程终止,在当前线程中调用某个线程对象的join()方法会阻塞当前线程,直到该线程执行完毕。

  • interrupt(): 中断线程,给线程发送中断信号,但并不能直接终止线程的执行。

  • isInterrupted(): 判断线程是否被中断。

  • yield(): 暂停当前正在执行的线程,让其他线程有机会继续执行。

  • getName(): 获取线程的名称。

  • setName(String name): 设置线程的名称。

  • isAlive(): 判断线程是否还活着(即是否处于可运行或已完成的状态)。

  • currentThread(): 获取当前正在执行的线程对象。

  • setPriority(int priority): 设置线程的优先级,优先级范围为1(最低)到10(最高)。

线程的cpu分片

在多线程环境中,CPU时间被分成一小段一小段的时间片(time slice),每个时间片用于执行一个线程的指令。这就是常说的线程CPU分片。

操作系统使用调度算法来决定每个线程应该获得的时间片,并根据优先级、线程状态和其他因素进行调度。通常情况下,操作系统会采用抢占式调度(preemptive scheduling)策略,即在一个时间片结束时,当前运行的线程可能被挂起,而另一个就绪的线程被调度执行。

具体的调度算法因操作系统而异,常见的调度算法包括轮转调度(Round Robin Scheduling)、优先级调度(Priority Scheduling)、多级反馈队列调度(Multilevel Feedback Queue Scheduling)等。这些算法都旨在公平地分配CPU时间,使得不同线程都能够执行并完成任务。

每个时间片的长度可以根据系统配置和需求进行调整,常见的时间片长度在几毫秒到几十毫秒之间。

需要注意的是,线程的CPU分片并非线程直接控制,而是由操作系统负责管理和调度的。线程只能通过设置优先级和合理设计进程间的同步与互斥来影响其调度行为,以满足应用程序的需求。

sleep

指定线程休眠时间,单位毫秒 ,让出当前cpu时间片,其他线程可以抢占cpu时间片

yield

yield() 方法是 Java 中 Thread 类的一个方法,用于提示当前线程暂时放弃执行,让出CPU资源给其他线程。调用 yield() 方法会使当前线程从运行状态变为可运行状态,然后让出CPU资源,允许具有相同优先级的其他线程执行。

public class MyThread extends Thread {
    @Override
    public void run() {
        // 在此定义线程执行的操作
        System.out.println("线程开始执行");
      
        // 线程执行到某个点时调用yield()
        System.out.println("线程调用yield()");
        Thread.yield();
      
        // 继续执行剩余的操作
        System.out.println("线程继续执行");
    }
  
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 启动新线程并执行 run 方法
    }
}

在上述示例中,我们创建了一个继承自 Thread 的 MyThread 类,并在 run 方法中使用了 yield() 方法。当线程执行到 yield() 方法时,它会暂时让出CPU资源,让其他具有相同优先级的线程有机会执行。之后,线程会继续执行剩余的操作。

需要注意的是,yield() 方法只是一个提示,不能保证实际上会让出CPU资源,具体是否让出由操作系统决定。在某些情况下,即使调用了 yield() 方法,当前线程仍然可能会继续执行。

通常情况下,不建议频繁使用 yield() 方法,因为过多的线程切换可能会影响性能。只有在特定情况下,当你希望让其他线程有更多的机会执行时,才会使用它。

线程优先级

  • 线程的优先级等级
    • MAX_PRIORITY: 10
    • MIN _PRIORITY: 1
    • NORM_PRIORITY: 5
public static void main(String[] args) {
    MyTask myTask = new MyTask();
    Thread thread1 = new Thread(myTask, "t1");
    thread1.setPriority(Thread.MIN_PRIORITY);
    thread1.start();

    Thread thread2 = new Thread(myTask, "t2");
    thread1.setPriority(Thread.MAX_PRIORITY);
    thread2.start();
}

对于线程的优先级,确实较低的优先级线程在资源竞争时可能会被较高优先级线程抢占资源。但这并不意味着较低优先级的线程一定无法执行或一直被阻塞。

在多线程环境中,线程的优先级仅仅是给操作系统一个提示,帮助操作系统在调度时进行决策。较高优先级的线程倾向于获得较多的CPU时间片以及更频繁地被调度执行,但并不是绝对的。

操作系统的调度算法和具体实现可能因平台而异,而且不同的操作系统也有不同的调度策略。因此,并不能保证较低优先级的线程一定无法执行。

此外,需要注意的是,在 Java 中,线程的优先级设置仅作为一个指导,具体的行为可能因操作系统和虚拟机的差异而有所不同。并且,线程的优先级并不是唯一影响线程调度的因素,其他因素如线程状态、锁等也会影响线程的执行。

因此,尽管较低优先级的线程在资源竞争时可能被较高优先级的线程预占,但并不意味着它们一直无法执行。操作系统会尽量公平地分配CPU资源,并尽可能满足所有线程的执行需求。

守护线程

  • 其他线程都执行结束,守护线程自动结束
  • 守护线程启动子线程,其子线程也是守护线程
  • 守护线程的语法thread.(setDaemon(true)设置守护线程

关于守护线程无效问题

守护线程(Daemon Thread)是在 Java 中一种特殊类型的线程,其生命周期与普通线程(用户线程)不同。与用户线程不同,当所有的用户线程结束时,守护线程会自动退出。

在 Java 中,创建守护线程的方法是通过 setDaemon(true) 方法将线程设置为守护线程。如果一个线程被设置为守护线程,在所有用户线程结束时,JVM 将自动终止守护线程,而无需等待它们完成。

然而,需要注意以下几点原因导致守护线程可能看起来无效:

  • 1.守护线程必须在启动前设置为守护线程。也就是说,setDaemon(true) 方法必须在调用 start() 方法之前调用,否则守护属性将无效。一旦线程开始执行,就不能再更改其守护状态。

  • 2.守护线程依赖于用户线程的存在。当所有的用户线程结束时,JVM 可能会立即退出,从而导致守护线程提前终止。所以,如果你的程序中只剩下守护线程,那么它们可能被强制终止。

  • 3.守护线程不应该持有任何需要正常关闭的资源,例如打开的文件、数据库连接等。因为守护线程可能突然终止,无法保证资源的正常释放和关闭。

  • 4.当所有的用户线程结束并且只剩下守护线程运行时,JVM 可能会立即退出,从而导致守护线程停止。在这种情况下,守护线程不会继续执行。

  • 5.守护线程的生命周期与用户线程有所不同。当只有守护线程运行时,JVM 认为它们没有继续运行的必要,因此会直接退出。这意味着守护线程不能保证一定会执行完毕,而可能会在任意时间被强制停止。

守护线程的停止与操作系统的线程调度机制有关

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值