Java 创建线程的三种方式和线程池实现的四种方式

本文主要介绍Java线程的三种创建方式和线程池的四种实现方式。

1.进程和线程

  • A:进程概念

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

  • B:线程的概念

线程:线程是进程中的一个执行单元(执行路径),负责当前进程中程序的执行,
一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

多线程:即就是一个程序中有多个线程在同时执行。
一个核心的CPU在多个线程之间进行着随即切换动作,由于切换时间很短(毫秒甚至是纳秒级别),所以感觉不到

单线程/多线程程序
单线程程序:即若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如去 网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。
多线程程序:即若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

  • C:线程的运行模式

分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

2.线程的状态

3.创建线程的三种方式

继承Thread类创建线程类

Thread的实现步骤: 
1). 定义Thread的子类,重写run()方法,run()方法代表了线程要完成的任务,run()方法称为线程执行体。 
2). 创建Thread子类的实例,子类对象就是线程。 
3). 调用线程对象的start()方法来启动线程。

/*
 *   创建和启动一个线程
 *   创建Thread子类对象
 *   子类对象调用方法start()
 *   让线程程序执行,JVM调用线程中的run
 */
public class ThreadDemo {
  public static void main(String[] args) {
    SubThread st = new SubThread();
    SubThread st1 = new SubThread();
    st.start();
    st1.start();
    for(int i = 0; i < 50;i++){
      System.out.println(Thread.currentThread().getName()+"..."+i);
    }
  }
}
/*
 *  定义子类,继承Thread 
 *  重写方法run 
 */
public class SubThread  extends Thread{
  public void run(){
    for(int i = 0; i < 50;i++){
      System.out.println("run..."+i);
    }
  }
}

输出结果:

main...0
main...1
main...2
main...3
main...4
main...5
main...6
main...7
main...8
main...9
main...10
main...11
main...12
main...13
main...14
main...15
main...16
main...17
main...18
main...19
main...20
main...21
main...22
main...23
main...24
main...25
main...26
main...27
main...28
main...29
main...30
main...31
main...32
main...33
main...34
main...35
main...36
main...37
main...38
main...39
main...40
main...41
main...42
main...43
main...44
main...45
main...46
main...47
main...48
main...49
run...0
run...0
run...1
run...2
run...3
run...4
run...5
run...6
run...7
run...8
run...9
run...10
run...11
run...12
run...13
run...14
run...15
run...16
run...17
run...18
run...19
run...20
run...21
run...22
run...23
run...24
run...25
run...26
run...27
run...28
run...29
run...30
run...31
run...32
run...33
run...34
run...35
run...36
run...37
run...38
run...1
run...2
run...3
run...4
run...5
run...6
run...7
run...8
run...9
run...10
run...11
run...12
run...13
run...14
run...15
run...16
run...17
run...18
run...19
run...20
run...21
run...22
run...23
run...24
run...25
run...26
run...27
run...28
run...29
run...30
run...31
run...32
run...33
run...34
run...35
run...36
run...37
run...38
run...39
run...40
run...41
run...42
run...43
run...44
run...45
run...46
run...47
run...48
run...49
run...39
run...40
run...41
run...42
run...43
run...44
run...45
run...46
run...47
run...48
run...49

Process finished with exit code 0

 实现Runnable接口

1).定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

2).创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

3).调用线程对象的start()方法来启动该线程。

 /*
     *  使用匿名内部类,实现多线程程序
     *  前提: 继承或者接口实现
     *  new 父类或者接口(){
      *     重写抽象方法
     *  }
     */
public class ThreadDemo {
public static void main(String[] args) {
  //继承方式  XXX extends Thread{ public void run(){}}
  new Thread(){
    public void run(){
      System.out.println("!!!");
    }
  }.start();
  
  //实现接口方式  XXX implements Runnable{ public void run(){}}
  
  Runnable r = new Runnable(){
    public void run(){
      System.out.println("###");
    }
  };
  new Thread(r).start();
  
  
  new Thread(new Runnable(){
    public void run(){
      System.out.println("@@@");
    }
  }).start();
  
}
  }

输出结果:

!!!
###
@@@

通过Callable和Future创建线程

Callable的实现步骤: 

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方

法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
 

public class CallableDemo implements Callable<Integer> {

    public static void main(String args[]) {

        FutureTask<Integer> futureTask = new FutureTask<Integer>(new CallableDemo());
        new Thread(futureTask).start();

        try {
            System.out.println("子线程返回值:" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        if (futureTask.isDone()) {
            System.out.println("线程结束");
        }
    }

    @Override
    public Integer call() throws Exception {
        System.out.println("线程开始");
        int ss = 0;
        for (int i = 0; i < 20; i++) {
            ss += i;
        }
        return ss;
    }

}

输出结果:

线程开始
子线程返回值:190
线程结束

 

4.线程相关问题总结

A、采用实现Runnable、Callable接口的方式创建多线程的优缺点

      优势:

       线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

       在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

       劣势:

     编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

B、使用继承Thread类的方式创建多线程的优缺点

      优势:

      编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

      劣势:

      线程类已经继承了Thread类,所以不能再继承其他父类。

C、Runnable和Callable的区别

     (1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。

     (2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

     (3) Callable的call方法可以抛出异常,Runnable的run方法不可以。

     (4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的

完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
 

5.线程池的使用

使用线程池必要性

1. 重用存在的线程,减少对象创建、消亡的开销,性能佳

记创建线程消耗时间 T1,执行任务消耗时间 T2,销毁线程消耗时间 T3

如果 T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!

正好,线程池缓存线程,可用已有的闲置线程来执行新任务,避免了 T1+T3 带来的系统开销

 

2. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞

线程可以共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况

运用线程池能有效的控制线程最大并发数,避免以上的问题

 

3.提供定时执行、定期执行、单线程、并发数控制等功能

 

线程池的原理


1).在java中,如果每个请求到达就创建一个新线程,开销是相当大的。
2).在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。
3).除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。
如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。
为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

6.常见的几种线程池实现

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的

线程池接口是ExecutorService。

ThreadPoolExecutor是线程池中最核心的一个类,后面提到的四种线程池都是基于ThreadPoolExecutor实现的。

ThreadPoolExecutor提供了四个构造方法,其中最重要的一个构造函数

public class ThreadPoolExecutor extends AbstractExecutorService {
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);
}

 

函数的参数含义如下:
corePoolSize: 线程池维护线程的最少数量
maximumPoolSize:线程池维护线程的最大数量
keepAliveTime: 线程池维护线程所允许的空闲时间
unit: 线程池维护线程所允许的空闲时间的单位
workQueue: 线程池所使用的缓冲队列
handler: 线程池对拒绝任务的处理策略

threadFactory - 执行程序创建新线程时使用的工厂

核心线程:

线程池新建线程的时候,如果当前线程总数小于 corePoolSize,则新建的是核心线程,如果超过 corePoolSize,则新建的是非核心线程。

核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。

如果指定 ThreadPoolExecutor 的 allowCoreThreadTimeOut 这个属性为 true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。

线程池执行的过程:

线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
​ a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
​ b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
​ c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
​ d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

Java通过Executors提供四种线程池:

CachedThreadPool:(推荐使用)可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。

FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。

  • newCachedThreadPool

这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪.

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   try {  
    Thread.sleep(index * 1000);  
   } catch (InterruptedException e) {  
    e.printStackTrace();  
   }  
   cachedThreadPool.execute(new Runnable() {  
    public void run() {  
     System.out.println(index);  
    }  
   });  
  }  
 }  
}  
  •  newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   fixedThreadPool.execute(new Runnable() {  
    public void run() {  
     try {  
      System.out.println(index);  
      Thread.sleep(2000);  
     } catch (InterruptedException e) {  
      e.printStackTrace();  
     }  
    }  
   });  
  }  
 }  
}  
  • newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

结果依次输出,相当于顺序执行各个任务。

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   singleThreadExecutor.execute(new Runnable() {  
    public void run() {  
     try {  
      System.out.println(index);  
      Thread.sleep(2000);  
     } catch (InterruptedException e) {  
      e.printStackTrace();  
     }  
    }  
   });  
  }  
 }  
}
  • newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

例如下述代码  表示延迟1秒后每3秒执行一次。

import java.util.concurrent.Executors;  
import java.util.concurrent.ScheduledExecutorService;  
import java.util.concurrent.TimeUnit;  
public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
  scheduledThreadPool.scheduleAtFixedRate(new Runnable() {  
   public void run() {  
    System.out.println("delay 1 seconds, and excute every 3 seconds");  
   }  
  }, 1, 3, TimeUnit.SECONDS);  
 }  
} 

比较重要的几个类:

ExecutorService: 真正的线程池接口。

ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor: ExecutorService的默认实现。

ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此

在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

 

 

 

部分片段参考文章:

https://blog.csdn.net/qq_35275233/article/details/87893337

https://blog.csdn.net/L359389556/article/details/84062149

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值