Java多线程详解(一)

一、线程的创建

一、继承Thread类

引入Java.lang.Thread类,然后编写一个类继承于Thread,然后重写好其核心方法run()

public class Progress extend Thread{
   @Override
   public void run(){
      //业务逻辑
    }
   public static void main(String[] args){
       Progress progress = new Progress();
       progress.start();
   }
}

二、实现Runnable接口创建线程

通过实现Runnable接口的方法来创建的线程,重写核心方法run(),能够解决Java单继承的约束,本身不包含线程启动的start()方法,需要通过Thread线程对象来启动

public class Progress implements Runnable{
   @Override
   public void run(){
      //业务逻辑
    }
   public static void main(String[] args){
       Progress progress = new Progress();
       Thread thread = new Thread(progress);
       thread.start();
   }
}

三、实现Callable接口创建线程

通过实现Callable接口及FutureTask类,重写好其带有返回值的核心方法call(),FutureTask类能通过get()方法获取call()方法执行完的返回值,FutureTask对象会一直等待该类线程的完成和返回值的返回,直至超时或用户取消。

public class Progress implements Callable{
   @Override
   public String call() throws Exception{
      //业务逻辑
    }
   public static void main(String[] args){
       Progress progress = new Progress();
       FutureTask<String> futureTask = new FutureTask<String>(progress);
       //Thread 启动
       Thread thread = new Thread(futureTask);
       thread.start();
       
       //线程池启动
       ExecutorService threadPool = Executors.newFixedThreadPool(10);
       threadPool.submit(futureTask);
   }
}

调用一个线程的start()方法,是真正的启动线程的方法,即真正在原来主线程或某线程中以多线程的形式开辟一个线程来运行该线程的run()方法的逻辑,而原线程无须理会新线程的run()方法的执行,放手让新线程管理。

二、线程的生命周期

一、生命周期

新建(New)、可运行(Runnable)、阻塞(Block)、等待(Waiting)、调教时间的等待(Time_Waiting)、终止(Terminated)。
生命周期图谱

一、新建(New)

线程创建成功,但是还没调用start()方法启动

二、可运行(Runnable)

线程处于可执行的状态,线程已在java虚拟机中执行,但可能它还在等待操作系统其他资源。

三、阻塞(Block)

线程处于阻塞。其实是指该线程正在等待一个监控锁,特别是在多线程下的场景等待另一个线程同步块的释放

四、等待(Waiting)

线程处于等待状态,指的是该线程正在等待另一个线程执行某些特定的操作。

五、调教时间的等待(Time_Waiting)

是与时间的等待,一般是调用了某些特定等待时长的方法。例如sleep(100),wait(200);

六、终止(Terminated)

线程执行完毕的状态

二、线程状态的获取

使用线程的getState()方法获取该线程在目前这一刻的状态

三、线程的活动情况

isActive()方法可以获取线程的活动情况,判断一个线程是否还在活动(存活)

四、线程的优先级

Java线程的优先级使用int型的数值来表示。范围1-10,默认是5,如果数据类型不对,或者不在范围1-10会报错,设置优先级,Thread.setPriority()。

1为最小优先级,10为最大优先级。

三、多线程的调度

一、抢占式调度

指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

二、协同式调度

指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

三、上下文切换

CPU切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行。任务的状态保存及再加载, 这段过程就叫做上下文切换。

线程切换:同一进程中的两个线程之间的切换;
进程切换:两个进程之间的切换 模式切换;
在给定线程中:用户模式和内核模式的切换;
地址空间切换:将虚拟内存切换到物理内存;

四、线程的睡眠、等待、让步

线程的睡眠会占用监控锁,等待不占用监控锁等资源

一、sleep()

让一个运行的线程睡眠或者休息一段时间(runnable–>time_waiting)继续执行,线程自带的方法,带一个long型的参数,代表睡眠的毫秒数。sleep()方法被调用后不会释放监控锁,所以当前线程是在监控锁内进行 sleep()方法操作。

Thread.sleep(3000) == TimeUnit.SECONDS.sleep(3)

二、wait()

wait()方法让一个线程休息一段时间(runnable–>time_waiting/waiting)不是线程自带的方法,wait()方法需要使用notify()方法或notifyAll()方法来重新唤醒。wait()方法一旦被调用,该对象就会先释放监控锁,使其他使用同一个监控锁的同步块或同步方法能够进行线程的同步处理。

  1. 在使用wait()方法时,需要加 上synchronized 关键字,让 wait()方法 处于 synchronized
    所管辖的同步块中,否则程序会在使用该方法的地方抛出 java. lang llegalMonitorStateException
    异常,这是因为程序无法获取监控锁的状态。
  2. 可以在 wait()方法中加入一个整数,以代表等待毫秒数,如 wait(2000),如果 wait( )方法中 加入了时间参数,线程就会进入 TIMED_WAITING 状态,当时间到达时,该线程就会被自动唤醒。

三、yileld()

yield()方法让一个线程退出运行状态一段时,然后再次运行。但yield()方法又有些特别,由线程生命周期图谱可以知道,yield(方法能把一个运行中的线程转成就绪状态,但实际上这些状态都还在线程的 RUNNABLE 状态当中。yleld( )方法的让步,只是把 CPU 的资源让出,让操作系统去调度,但对于 Java 内部机制来说Java 虛拟机还是把这样的状态当作 RUNNABLE 状态来看待,这样的处理能简化许多烦琐的逻辑。因为 Java对于毫秒级别的 CPU 分片去区分一个线程到底是 READY 还是RUNNING 的意义其实己经不大了,所以Java 虚拟机把这样的状态统一为PUNNABLE 状态,告诉操作系统该线程是可运行的就足够了。

四、notify()、notifyAll()

wait()方法配对出现的 notiy()方法,其能让进入 WAITING 状态的线桯唤醒,但这一过程是随机的,即如果有多条线程处于 WATING 状态,则一般会有其中的一条被唤醒,然后继续之前的工作。
notityAll()方法中包含 All一词就是指调用该方法,会唤醒所有处在 WAITING 状态的线程。但是在多线程的情况下,若是处理不好,可能会出现死锁。

五、Join()

线程的插队分为主动插队和被动插队,其中主动插队用Join(),而被动插队一般是调大线程的优先级。

四、多线程的线程组和线程池

一、线程组

线程组(Thread Group),即线程的分组,它表示一组线程的集合。线程组中包含一到多个线程,甚全可以包到多个其他线程组,即它的组织形式是树状的。
ThreadGroup(String name) 方法和 ThreadGroup(ThreadGroup parent, String name) 它们都包含设置线程组组名的功能。另外,其也可以设置所表属的线程组,即将其指向一个父线程组,线程组及线程可以组成树状结构。
使用:

public class CreateThreadGroup{
   public static void main (string] args){
         ThreadGroup threadGroup = new ThreadGroup ("ThreadGroup-01") ;
         Thread thread = new Thread(xxxxx);
         thread.setName("Thread-01");
         thread.start();
     }
}
 Thread.currentThread().getThreadGroup().getName()  //获取所在线程组的名称 
 Thread.currentThread().getThreadGroup().getParent().getName()//获取所在线程组的父级线程组

二、线程池

线程池是一个包含了能提供相同功能的多个线程的集合,能为调用者提供线程服务。

线程池中的线程一般称为工作线程。线程池能够提前先创建一部分线程待用,因为线程池中己经提前进行了线程的初始化,所以向线程池提交执行新任务时,可以做到线程拿来即用,减少了 重复创建线程的等待时间,以及注销线程的时间消耗。有了线程池,就可以帮助我们快速地响应临时的实时性要求高的请求。同时,线程池可以规定一定的线程容量,包括基本核心线程数和最大线程数,可以做到一定的运算扩容能力,并旦控制了并发线程的数量,对于系统稳定和健壮运行起到了一定的帮助作用,

一、线程池的实现

首先是要设置一个能装载线程的池,可以为它定义初始的核心线程数量、所占允许的最大线程数量、核心线程超时时间、预备线程最大元许空闲时间、指定阻塞队列的类型、线程的工程类等。然后线程池会按照给定的配置和初始的核心线程数量,创建特定功能的线程。

二、构造函数

public ThreadPoolExecutor (int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • int corePoolsize:核心线程池的大小,也是一开始初始化后的线程池基本核心线程教。
  • int maximumPoolSize:线程池的最大允许线程池数,也是线程池承载峰值时的最大值一般用于突发大量调用的情况。实际上该值并非设置多大,线程池中的线程数就能达到该数值,该 值与机器性能及系统有关。
  • long keepAliveTime:线程池中线程的允许空闲的时间。如果超过这个数值,线程一般有可能会被终止。特别是在目前线程池中的线程数量已经超过了 corepoolsize 规定的数值时,会有部分线程被拿出来与 KeepAliveTime 进行空闲时间对比,如果超时了,则终止该线程。
  • TimeUnit unit: Timeunit枚举类中的时回常量,有 SECONDS、 MINUTES、HOURS。
  • BlockingQueue workaueue:排队的策路,是线程池中的等待工作队列。一般用于线程阻塞的情况时存储等待执行的任务。
  • ThreadFactory threadFactory:用于创建线程池中线程的工厂类。其内部通过addworker()方法来新增线程。所有调用方都必须为调用adaWorker()方法时做好失败的准备,因为调用该方法时,可能受线程池的策略及线程数限制。
  • RejectedExecutionHandler handler:拒绝的策略,拒绝任务时的把控处理类。
  • execute (),执行一个任务,没有返回值。
  • submit (),提交一个线程任务,有返回值。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,500, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
//核心线程数为3,允许的最大线程数为5,线程池中每一个线程的keepAlive时间为0.5秒,排队的任务队列为5

当向这个线程池中放入 10 个简单的可执行的任务时,该线程池的变化:线程池中的工作线程数由 0 逐步上升到该线程池规定的核心线程数3,然后上升到规定的最大线程数 5。其中,当线程池中达到核心线程数时,新加入的任务会先放入 BlockingQueue任务排队队列中排队,当线程池中的 BlockingQueue 任务排队队列也排满时,才会继续增加线程池中的工作线程。

三、线程池的创建类

由于 ThreadPoolExecutor 构造方法的参数及参数的选型众多,因此合理的配置可以创造出良好的符合业务需求的线程池。但如果一时间不清楚到底怎样去配置这些参数才对时,不妨使用下面这些 Java 多线程包中提供的线程池创建类。

  • newCachedThreadPool:
    是带缓存功能的线程池。如果任务不多,则法线程池会自动缩小线程数量,可灵活回收空闲线程,若无可回收,可新建线程。当线程发现下一个任务与前一个任务相同,且前一个任务已经完成时,该线程池会复用前一个任务的工作线程来服务新的任务。
  • newFixedThreadPool:
    能没置最大工作线程数的线程池。一开始每当提交一个任务时,都会新建一个工作线程来运行任务,同时该线程会仍然作为工作线程在线程池中待命,以便服务其他任务。当后来任务加入导致工作线程数量达到我们设定的最大工作线程数时,该线程池会将新加入的任务放入BlockingQueve 任务排队队列中。当一段时间没有了任务时,该线程池中的工作线程仍然不会减少,会一直等待新的任务。
  • newSingleThreadExecutor:
    只有一个线程的线程池,即其内部只有唯一的一个工作线程来完成任务,这样做的好处是能够保证所有任务按照指定顺序来执行。
  • new ScheduleThreadPool:
    能设置最大的工作线程数的线程池。其与newFixedThreadPool不同,它的主要任务是定时调度任务,如 TimerTask 定时任务。

以创建newFixedThreadPool为例:

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

public class CreateThreadPool {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
        for (int i = 1 ;i <= 10;i++){
            fixedThreadPool.execute(new DoWork(i));
        }
    }
}

class DoWork implements Runnable{
    private int num;
    public DoWork(int num){
       this.num = num;
    }
    @Override
    public void run() {
        System.out.println("我是" + num + "任务");
        try {
            Thread.currentThread().sleep(100);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

四、线程池执行流程

线程池执行流程

五、多线程定时任务TimerTask

一、Timer类

构造函数

public Timer()//默认按顺序生成线程名
public Timer(boolean isDaemon)//是否设置为守护线程。
public Timer(String name) //设置线程名
public Timer(String name, boolean isDaemon)

Timer 的核心方法 schedule()及 scheduleAtFixedRate()方法。

二、TimerTask抽象类

如果说 Timer是一个定时器,那么TimerTask就是该定时器所要触发的真实任务。实际上TimerTask 是一个抽象类。
下面使用Timer TimerTask每5秒输出当前时间:

public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("当前的时间为:"+simpleDateFormat.format(new Date()));
            }
        },2000,5000);
    }
}

输出为

当前的时间为:2022-09-23 21:48:36
当前的时间为:2022-09-23 21:48:41
当前的时间为:2022-09-23 21:48:46
当前的时间为:2022-09-23 21:48:51
当前的时间为:2022-09-23 21:48:56
当前的时间为:2022-09-23 21:49:01
当前的时间为:2022-09-23 21:49:06

 public void schedule(TimerTask task, long delay, long period)

第一个参数是需要重写 run()方法的 Timerlask类,第二个参数代表延时的毫秒数时间,第三个参数代表每隔多久再次运行一次的毫秒数时间。该示例要求程序每隔 5秒打印一次当前的时间,并且延迟 2秒(当程序成功运行后开始计算),才进行首次输出。

Timer及TimerTask 一般只适合简单的定时任务或简单的任务调度,而如果需要更为稳健和准确的任务调度,甚至满足大数据处理情况下的任务调度,则需要使用其他的类或框架辅助。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值