单线程,多线程,线程池


1,单线程和多线程

我们通过一个实例来理解单线程和多线程。

假设有一个同学通讯录,通讯录长度为1000,用于记录同学的姓名、电话、地址信息,用户可以并发检索该通讯录,输入通讯录中的姓名,程序从通讯录中查找该姓名,如果存在则输出与该姓名相关的电话、地址信息。任务要求简单模拟1000个用户的并发访问,检索功能分别采用单线程和多线程实现,比较在1000个用户的并发访问下,单线程和多线程的检索效率。

线程的创建和启动

Java提供了两种创建线程的方式。

一种方式是定义实现Java.lang.Runnable接口的类。Runnable接口中只有一个run()方法,用来定义线程运行体。代码如下:

package com.example.demo;

/**
 * Created with IntelliJ IDEA.
 * Author: zhangz
 * Date: 2019/1/8
 * Time: 16:12
 */
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //在线程中执行的代码
        for(int i=0;i<100;i++){
            System.out.println("MyRunner+"+i);
        }
    }
}
View Code

定义好MyRunnable类后,需要把MyRunnable类的实例作为参数传入到Thread的构造方法中,来创建一个新线程。然后在main方法中,实例化Thread对象,并将MyRunnable类的实例作为参数传入进去,然后调用Thread对象的start方法启动线程。代码如下:

public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        /*for MyRunnable.java 实现Java.lang.Runnable接口的类*/
        Thread thread = new Thread(new MyRunnable());
        thread.start();
}
View Code

另外一种方式是将类定义为Thread类的子类,并重写Thread类的run()方法,代码如下:

public class MyThread extends Thread{
    @Override
    public void run(){
        //在线程中执行的代码
        for(int i=0;i<100;i++){
            System.out.println("MyRunner+"+i);
        }
    }
}
View Code

定义好Thread类的子类后,创建一个线程,只需要创建Thread子类的一个实例即可,然后在main方法中,只需实例化Thread对象即可,然后调用Thread对象的start方法启动线程。代码如下:

public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        /*for MyThread.java 将类定义为Thread类的子类,并重写Thread类的run()方法*/
        MyThread myThread =new MyThread();
        myThread.start();
}
View Code

注意:在两种创建线程的方式中,建议使用第一种方式。因为采用实现接口的方式可以避免由于Java的单一继承带来的局限,有利于代码的健壮性。

下面,使用单线程来实现同学通讯录检索任务:

首先建立一个同学通讯录类,代码如下:

@Data
public class PhoneBook {
    String name;
    String phone;
    String address;
}
View Code

加上一个初始化通讯录数据的昂发,用ArrayList集合类存储1000个PhoneBook对象,代码如下:

public void phoneBookInit(ArrayList<PhoneBook> inArray){
        for(int i=0;i<1000;i++){
            PhoneBook temp =new PhoneBook();
            temp.setName("同学"+String.valueOf(i));
            temp.setPhone("号码"+String.valueOf(i));
            temp.setAddress("地址"+String.valueOf(i));
            inArray.add(temp);
        }

    }
View Code

用for循环模拟1000个并发客户检索通讯录,并输出通讯录信息,记录检索全部完成时间,代码如下:

public class PhoneSearch {

    public static void main(String[] args) {
        ArrayList<PhoneBook> array =new ArrayList<PhoneBook>();
        PhoneSearch phoneSearch = new PhoneSearch();
        phoneSearch.phoneBookInit(array);
        long start =System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            phoneSearch.PhoneBookSearch("同学"+String.valueOf(i),array);
        }
        long end = System.currentTimeMillis();
        long total = end-start;
        System.out.println("test spend time:------->"+total+"ms");

    }

    public void PhoneBookSearch(String name,ArrayList<PhoneBook> inArray){
        Iterator iterator = inArray.iterator();
        int num =1;
        while(iterator.hasNext()){
            PhoneBook temp =(PhoneBook) iterator.next();
            if(temp.getName().equals(name)){
                System.out.println("姓名:"+temp.getName()+"电话:"+temp.getPhone()+"地址:"+temp.getAddress());
                return ;
            }


        }

    }

    public void phoneBookInit(ArrayList<PhoneBook> inArray){
        for(int i=0;i<1000;i++){
            PhoneBook temp =new PhoneBook();
            temp.setName("同学"+String.valueOf(i));
            temp.setPhone("号码"+String.valueOf(i));
            temp.setAddress("地址"+String.valueOf(i));
            inArray.add(temp);
        }

    }
}
View Code

程序用for循环模拟1000个并发客户检索通讯录,在模拟检索任务开始之前调用System的currentTimeMillis方法获取系统当前时间,模拟检索任务执行结束后,再获取任务执行完成后的时间,然后计算两个时间的差值,该差值就是检索任务运行的时间。程序输出结果如下图所示:

耗时:178ms

接下来用多线程完成同学通讯录检索任务:

上面代码PhoneSearch 类PhoenBookSearch方法完成通讯录的检索及信息输出。下面的代码把该方法改造为线程,这样就可以实现当多用户检索通讯录时,程序针对每个用户的检索请求,都会启动一个线程去执行检索任务,由顺序执行改为并发执行。改造代码如下:

package com.example.demo;

import com.example.demo.entity.PhoneBook;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Created with IntelliJ IDEA.
 * Author: zhangz
 * Date: 2019/1/8
 * Time: 17:32
 */
public class MutiThreadPhoneSearch implements Runnable{
    ArrayList<PhoneBook> array;
    String name;
    public MutiThreadPhoneSearch(String inName,ArrayList<PhoneBook> inArray){
        array = inArray;
        name = inName;
    }
    @Override
    public void run(){
        Iterator iterator = array.iterator();
        int num =1;
        while(iterator.hasNext()){
            PhoneBook temp =(PhoneBook) iterator.next();
            if(temp.getName().equals(name)){
                System.out.println("姓名:"+temp.getName()+"电话:"+temp.getPhone()+"地址:"+temp.getAddress());
                return ;
            }


        }

    }


}
View Code

代码定义一个MutiThreadPhoneSearch,该类实现Runnable接口,并重写Runnable接口的run()方法,在run方法中,完成通讯录的检索及输出功能。

 在main方法中,不再调用PhoenBookSearch方法,而是实例化Thread对象,并将MutiThreadPhoneSearch类的实例作为参数传入进去,然后调用Thread对象的start方法启动线程,代码如下:

public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        /*for MutiThreadPhoneSearch.java 多线程实现*/
        ArrayList<PhoneBook> array =new ArrayList<PhoneBook>();
        PhoneSearch phoneSearch = new PhoneSearch();
        phoneSearch.phoneBookInit(array);
        long start =System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            MutiThreadPhoneSearch mutiThreadPhoneSearch = new MutiThreadPhoneSearch("同学"+String.valueOf(i),array);
            Thread thread = new Thread(mutiThreadPhoneSearch);
            thread.start();
        }
        long end = System.currentTimeMillis();
        long total = end-start;
        System.out.println("test spend time:------->"+total+"ms");
    }
    public void phoneBookInit(ArrayList<PhoneBook> inArray){
        for(int i=0;i<1000;i++){
            PhoneBook temp =new PhoneBook();
            temp.setName("同学"+String.valueOf(i));
            temp.setPhone("号码"+String.valueOf(i));
            temp.setAddress("地址"+String.valueOf(i));
            inArray.add(temp);
        }

    }

}
View Code

执行结果如下:

耗时278ms,还没执行完。

从输出结果看,检索结果并没有按照顺序输出,整个检索耗时278ms。用多线程技术实现通讯录的并发检索,并没有提高检索效率,反而不如单线程的运行速度快。主要原因是系统每启动一个线程,都要耗费一定的系统资源,导致运行效率降低,多线程在这个例子程序中,并没有体现出多线程的性能优势。

我们换个场景,假如把通讯录的检索放到服务器端,1000个用户在同一时间并发检索通讯录,如果服务端是单线程服务,虽然1000个用户是并发访问,但要在服务器端随机排队等候服务器响应,如果1个用户的响应时间为1秒,那么依次类推,最后1个用户的响应时间为1000秒。如果是多线程服务,平均每个用户的响应时间为2到3秒左右,显然能够满足大多数用户的响应需求。在这个场景下,多线程就体现出了性能优势。

在正常情况下,让程序来完成多个任务,只使用单个线程来完成比用多个线程完成所用的时间会更短。因为JVM在调度管理每个线程上肯定要花费一定资源和时间的。那么,在什么场景下使用多线程呢?一是对用户响应要求比较高,又允许用户并发访问的场景;二是程序存在耗费时间的计算,整个系统都会等待这个操作,为了提高程序的响应,将耗费时间的计算通过线程来完成。

 

线程池及其特点,场景

线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

如果用生活中的列子来说明,我们可以把线程池当做一个客服团队,如果同时有1000个人打电话进行咨询,按照正常的逻辑那就是需要1000个客服接听电话,服务客户。现实往往需要考虑到很多层面的东西,比如:资源够不够,招这么多人需要费用比较多。正常的做法就是招100个人成立一个客服中心,当有电话进来后分配没有接听的客服进行服务,如果超出了100个人同时咨询的话,提示客户等待,稍后处理,等有客服空出来就可以继续服务下一个客户,这样才能达到一个资源的合理利用,实现效益的最大化。

 

Java中的线程池种类

1. newSingleThreadExecutor

创建方式:

ExecutorService pool = Executors.newSingleThreadExecutor();

一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

使用方式:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
   public static void main(String[] args) {
       ExecutorService pool = Executors.newSingleThreadExecutor();
       for (int i = 0; i < 10; i++) {
           pool.execute(() -> {
               System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
           });
       }
   }
}

输出结果如下:

pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....
pool-1-thread-1    开始发车啦....

从输出的结果我们可以看出,一直只有一个线程在运行。

2.newFixedThreadPool

创建方式:

ExecutorService pool = Executors.newFixedThreadPool(10);

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

使用方式:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
   public static void main(String[] args) {
       ExecutorService pool = Executors.newFixedThreadPool(10);
       for (int i = 0; i < 10; i++) {
           pool.execute(() -> {
               System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
           });
       }
   }
}

输出结果如下:

pool-1-thread-1    开始发车啦....
pool-1-thread-4    开始发车啦....
pool-1-thread-3    开始发车啦....
pool-1-thread-2    开始发车啦....
pool-1-thread-6    开始发车啦....
pool-1-thread-7    开始发车啦....
pool-1-thread-5    开始发车啦....
pool-1-thread-8    开始发车啦....
pool-1-thread-9    开始发车啦....
pool-1-thread-10 开始发车啦....

3. newCachedThreadPool

创建方式:

ExecutorService pool = Executors.newCachedThreadPool();

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程,当任务数增加时,此线程池又添加新线程来处理任务。

使用方式如上2所示。

4.newScheduledThreadPool

创建方式:

ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);

此线程池支持定时以及周期性执行任务的需求。

使用方式:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPool {
   public static void main(String[] args) {
       ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
       for (int i = 0; i < 10; i++) {
           pool.schedule(() -> {
               System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
           }, 10, TimeUnit.SECONDS);
       }
   }
}

上面演示的是延迟10秒执行任务,如果想要执行周期性的任务可以用下面的方式,每秒执行一次

//pool.scheduleWithFixedDelay也可以
pool.scheduleAtFixedRate(() -> {
               System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
}, 1, 1, TimeUnit.SECONDS);

5.newWorkStealingPool

newWorkStealingPool是jdk1.8才有的,会根据所需的并行层次来动态创建和关闭线程,通过使用多个队列减少竞争,底层用的ForkJoinPool来实现的。ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM