Java多线程基本概念

1、程序、进程与线程

  • 程序:含有指令和数据的文件,被存储在磁盘或其他的数据存储设备是静态的代码。比如你电脑上的lol.exe。
  • 进程:程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等。
  • 线程:与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

进程==>线程==>协程(纤程)  不断缩减上下文切换带来的资源耗费。加快程序运行速度。目前go语言有对协程的直接支持,java中仅有Quasar类库支持创建并使用协程。

2、线程的生命周期

线程生命周期中的状态:new    runnable   running   blocked   dead 

  • NEW(新建):当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。
  • RUNABLE(可运行):线程被创建之后,执行start方法,线程进入可运行状态。正在运行的方法调用yeild(),进入可运行状态。此时线程处于等待队列中等待CPU分配时间片进行执行的时候。
  • RUNNING(运行):线程竞争到cpu的使用权并且处于run方法的执行阶段,线程即处于运行状态。
  • BLOCKED(阻塞):当在执行状态的线程执行了sleep(),发生了I/O阻塞,等待获取锁,收到通知,调用suspend方法等,会被挂起进入阻塞状态,进入Blocked池中进行等待。
阻塞状态分类
等待阻塞调用Object.wait(),进入等待队列中
同步阻塞尝试获取正在被其他线程占用的同步锁时候
其他阻塞Thread.sleep()、Thread.join()、发出IO请求
  • DEAD(死亡):线程正常执行结束或者异常退出或者调用stop手动退出。相应的线程资源被释放,线程处于死亡状态,结束辉煌的一生。

线程状态切换见下图:

3、java线程的创建方式

3.1 继承Tread

public class MyThread extends Thread {
     public void run() {
     System.out.println("MyThread.run()");
     }
}
MyThread myThread = new MyThread();
myThread.start(); 

3.2 实现Runnable

public class MyThread extends OtherClass implements Runnable {     
public void run() {   
       System.out.println("MyThread.run()");   
      }   
} 

//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target参数给Thread后,Thread的run()方法就会调用,
//其具体逻辑如下,可以看到就是在执行传入实现Runnable接口的实现类的run方法。
public void run() {
     if (target != null) {
     target.run();
     }
} 

3.3 Executor + Callable实现有返回值的线程

      有返回值的必须实现Callable,无返回值的必须实现Runable或者集成Tread

public class MyThread extends F implements Callable<String>{
    private String name;
    public myThread(String name){
        this.name = name;
    }
    public String call(){
        retrun name;
    }
} 

//创建线程池
ExecutorService pool = new FixedThreadPool(5);
//接收返回值
List<Future> list = new List<Future>();
for(int i = 0; i<5; i++){
    MyThread th = new MyThread("jay"+i);
    list.add(pool.submit(th));
}
//获取返回值
for(Future f: list){
     Sout(f.get().touString());
}

3.4 基于线程池

 // 创建线程池 
ExecutorService threadPool = Executors.newFixedThreadPool(10);       
while(true) { 
     threadPool.execute(new Runnable() { // 提交多个线程任务,并执行 
     @Override             
     public void run() { 
         System.out.println(Thread.currentThread().getName() + " is running ..");                  
         try { 
               Thread.sleep(3000); 
         } catch (InterruptedException e) { 
                      e.printStackTrace(); 
                    } 
                } 
            }); 
        } 
} 

4、线程池的工作原理

为什么引入线程池?

频繁的创建和销毁线程是非常消耗系统资源的,使用缓存策略通过线程池创建一定数量的线程,当任务到达后分配相应的线程进行处理。超出所能接收的最大线程的任务会进入等待队列。当线程池中的线程执行完成某个任务之后会重新被置为可用状态,从而达到了线程的复用。其实就是在线程池中的每一个线程的start方法中不断循环传递进来的runnable对象处理其中的run方法中的业务逻辑,待处理的任务放到一个队列中,若线程正在处理,那么此线程不会被分配新的任务。这样保证了系统的线程数是可控制的,也实现了线程资源的复用。

线程池的核心类

顶级接口:Executor

构造线程的核心方法:ThreadPoolExecutor

ThreadPoolExecutor构造方法的具体参数:

1corePoolSize线程池中核心线程的数量
2maximumPoolSize线程池中支持的最大线程的数量
3keepAliveTime当前线程数量超过corePoolSize时,空闲线程的存活时间
4unitkeepAliveTime的时间单位
5woekQueue任务队列,被提交但尚未执行的任务存在的地方
6threadFactory指定线程工厂,如何生产线程。可以使用默认和自定义
7handler任务拒绝策略

线程池的工作流程

        线程池在刚被创建出来的时候,只是向系统申请相应的资源,如执行线程的队列和管理线程池的辅助线程的资源。当执行execute()方法添加一个任务的时候,线程池会进行如下操作:

  • 如果正在运行的线程数量少于corePoolSize,线程池就会立刻创建线程并执行该线程任务。
  • 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程务。
  • 在阻塞队列已满且正在运行的线程数量大子等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException 异常。
  • 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。 
  • 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过了corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。

线程池的拒绝策略

我已经最大负荷了,不要再给我新的任务了!jvm听取意见并且比大多数的老板都人性化。

  • AbortPlicy:直接抛出异常,阻止线程正常运行
  • CallerRunsPolicy:如果被丢弃的线程任务未被关闭,则执行。未真正拒绝。
  • DiscardOldestPolicy:移除线程队列中最早的一个线程任务。
  • DiscardPolicy:直接丢弃,啥也不干。
  • 自定义拒绝策略:实现RejectedExecutionHandler接口,重写rejectedExecution方法。

5种常用的线程池

  • newCachedThreadPool

        优先使用线程池中的空闲可重用线程。适用大量任务但是每个任务的执行时间较短。

  • newFixedThreadPool

        创建固定线程数量的线程池

  • newScheduledThreadPool

        创建一个可定时调度的线程池,可以在给定的延迟时间后执行或者定期执行某个线程任务。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() { System.out.println("延迟三秒"); }
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
  • newSingleThreadExecutor

        创建只有一个线程可用的线程池,且保证当前线程停止或出现异常的时候新建一个线程代替

  • newWorkStealingPool

        jvm向系统申请尽可能多的线程到线程池中,主要用于进行快速运算的场景

5、java操作线程的基本方法

  • wait():Object.wait(),调用wait会进入等待状态。会释放锁,一般用于同步代码中。
  • sleep():Thread.sleep(),调入会进入timed-waiting状态。不会释放锁
  • yield():线程让步,让出当前的执行权,进入就绪等待队列重新竞争cpu时间片,runnable状态。
  • interrupt():线程中断。其实是设置线程中一个中断标志位的值。这时候调用isInterrupted方法返回为true。调用该方法并不会影响线程的状态,线程的具体的状态的变化是在调用了该方法之后,后续方法识别到标志位的值之后的处理决定的。当当前线程调用了sleep方法而处于超时等待状态的时候,调用此方法会抛出异常,并且使当前线程提前结束超时等待状态。在抛出异常的时候会清除标志中断位,使得调用isInterrupted方法返回为false。实用场景:当业务执行代码执行过程中产生异常的时候,在异常捕获后的处理中加入interrupt(),后续判断isInterrupted,如果为true,执行相应的锁的释放资源的释放等。
  • join():当前执行的线程中加入子线程,当前线程进入阻塞状态,子线程执行结束后父线程重新进入就绪状态等待系统资源的分配。childrenThread.join()。
  • notify():和wait()配套使用。若有一个处于等待状态则唤醒,若有多个则随机唤醒一个线程。notifyAll()唤醒处于等待状态的所有线程。
  • setDaemon():设置当前线程为守护线程。setDaemon(true)。等待用户线程全部都不执行之后才会主动停止。

6、线程终止

方式一:正常执行结束

方式二:使用stop。由于此方法在执行的时候是暴力的终止线程并且会释放线程所持有的锁对象,可能会造成数据不一致的情况产生,所以现在一般不用。

方式三:设置标志位

public class ThreadSafe extends Thread {
    public volatile boolean exit = false;
    public void run() { 
            while (!exit){
                //do something
            }
    }
}

方式四:使用interrupt

1、线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。

public class ThreadSafe extends Thread {
    public void run() {
        while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
            try{
                Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
            }catch(InterruptedException e){
                e.printStackTrace();
                break;//捕获到异常之后,执行 break 跳出循环
            }
        }
    }
}

2、线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

部分内容摘自:Offer来了,java面试核心知识点 王磊著 如有违权 请联系删除。

本文仅供个人学习记录。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值