Java多线程及多线程的使用场景
11706字,阅读需时40分钟

文章导读

多线程可以允许程序并发执行多个子任务,提高程序运行效率,增强用户体验度。那么什么是多线程?线程和进程有什么区别?多线程在什么场景下使用呢?本文将一一给出答案。


文章分成二个小节。第一小节主要介绍多线程的概念,以及进程与线程的区别;第二小节介绍多线程的创建方法及使用场景。

第一小节  认识多线程的概念

要理解线程,首先要理解并发的概念。并发是指在同一时间点,计算机可以同时执行多个任务。当前主流的操作系统,不管是Window系统,还是Linux系统,都是以多任务执行程序的。例如,我们可以在编写Java代码的同时听音乐、发送电子邮件等。

在多任务系统中,每个独立执行的任务(应用程序)称为进程,多个任务可以并发执行。下图是Windows  7系统任务管理器中的进程,从中可以看到当前操作系统中有多个任务同时在执行。

blob.png

图1  Windows任务管理器中的进程

严格来说,多任务系统并不是真正地并发执行多个任务,在单核CPU系统中,操作系统会根据很小的时间间隔交替执行多个任务(应用程序),使得这些应用程序看起来就像是在并行运行一样。多核CPU可以做到真正意义上的并发执行。

1、什么是多线程

前面编写的Java程序都是从main方法开始顺序执行每行代码,代码执行完成之后,结束整个应用程序。这样顺序执行的程序称为单线程程序,单线程程序在同一个时间内只执行一个任务。在实际处理问题的过程中,单线程程序往往不能适应复杂的业务需求。例如,在WEB项目中,多个用户通过浏览器客户端向服务器端发出请求,如果服务器端采用单线程程序,处理用户发送的请求,将会导致用户等待响应时间过长,服务效率低下的问题。要想缩短用户等待时间,提高服务效率,可以采用多线程的程序来同时处理多个请求任务。

多线程程序将主任务按照功能分解成多个子任务来执行,每个子任务称为一个线程,多个线程共同完成主任务的运行过程。例如,前面提到的WEB项目,服务器端主程序将用户的每个请求创建一个线程(子任务)去处理用户的请求,这样就可以提高服务器端的服务性能,缩短用户等待响应时间。

2、进程和线程的区别

进程是每个独立程序在计算机上的一次执行活动。例如,运行中播放器、浏览器等。运行一个程序,操作系统就启动了一个进程,进程加载程序代码、分配程序所需的资源环境,每个进程都有独立的代码和数据空间(进程上下文),进程可以由多条路径并发执行,并发执行的多条路径称为多线程。线程是比进程更小的执行单位,多个线程共享进程的代码、数据空间,但每个线程都有独立的运行栈和程序计数器。

进程是每个独立程序在计算机上的一次执行活动,线程是进程中的一个执行路径,线程依赖于进程而存在。

3、认识线程

先来看一段程序代码:

package com.milihua.threaddemo;
public class ThreadDemo {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ThreadDemo  demo = new ThreadDemo();
        demo.method2("hello");
    }
   
    public void method1(String str)
    {
        System.out.println(str);
    }
 
    public void method2(String str)
    {
        method1(str);
    }
 
}

当编译执行这个程序时,JVM会启动一个线程,将main方法放在这个线程执行空间的最开始处。该线程会从程序入口main方法开始对每行代码逐一调用执行。调用过程如下图所示:

blob.png

图2  Java程序的执行过程

类似的这种程序叫单线程程序。这个运行main方法的线程通常叫做主线程,主线程都是由JVM启动的。

JVM允许在主线程中启动多个子线程,它通过Java.lang.Thread类来实现,下面将介绍子线程的创建和启动。

第二小节  线程的创建方法及使用场景

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

1、线程的创建和启动

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

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

package com.milihua.threaddemo;
public class MyRunner implements Runnable {
    @Override
    public void run() {
        // 在线程中执行的代码
        for( int i = 0; i<100; i++ ) {
            System.out.println("MyRunner:" + i);
        }
    }
}

定义好MyRunner类后,需要把MyRunner类的实例作为参数传入到Thread的构造方法中,来创建一个新线程。代码如下:

package com.milihua.threaddemo;
public class ThreadRunDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunner());
        thread.start();
    }
}

在ThreadRunDemo类的main方法中,实例化Thread对象,并将MyRunner类的实例作为参数传入进去,然后调用Thread对象的start方法启动线程。线程输出结果如下图所示:

blob.png

图3  MyRunner线程输出结果

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

package com.milihua.threaddemo;
public class MyThread extends Thread{
    //重写Thread的run方法
    public void run()
    {
        // 在线程中执行的代码
        for( int i = 0; i<100; i++ ) {
            System.out.println("MyThread:" + i);
        }
    }
}

定义好Thread类的子类后,创建一个线程,只需要创建Thread子类的一个实例即可。代码如下:

package com.milihua.threaddemo;
public class ThreadDemo2 {
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyThread  thread = new MyThread();
        thread.start();
    }
}

在ThreadDemo2类的main方法中,只需实例化Thread对象即可,然后调用Thread对象的start方法启动线程。

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

2、用单线程完成同学通讯录检索任务

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

package com.milihua.threaddemo;
public class PhoneBook {
    String  name;
    String  phone;
    String  address;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
      
}

(2)初始化通讯录数据,用ArrayList集合类存储1000个PhoneBook对象,代码如下:

    public void InitPhoneBook(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);
        }
    }

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

package com.milihua.threaddemo;
import java.util.ArrayList;
import java.util.Iterator;
public class PhoneBookSearch {
    public static void main(String[] args) {
        ArrayList<PhoneBook>  array = new ArrayList<PhoneBook>();
        PhoneBookSearch  phoneBook = new PhoneBookSearch();
        phoneBook.InitPhoneBook(array);
        long start = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++ )
        {
            phoneBook.searchPhoenBook("同学" + String.valueOf(i),array);
        }
        long end = System.currentTimeMillis(); 
        long total = end - start; 
        System.out.println("byCommonFor------->" + total+" ms");
 
    }
   
    public void searchPhoenBook(String inName,ArrayList<PhoneBook>  array)
    {
        Iterator iterator = array.iterator();
        int num = 1;
        while (iterator.hasNext()) {
            PhoneBook  temp = (PhoneBook) iterator.next();
            if( temp.getName().equals(inName) )
            {
                System.out.println("姓名:" + temp.getName() + "/电话:" + temp.getPhone() + "/地址:" + temp.getAddress());
                return;
            }
        }
    }
   
    public void InitPhoneBook(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);
        }
    }
}

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

blob.png

图4 单线程完成同学通讯录检索任输出结果

从上图可以看出,检索结果顺序输出,耗时46ms。

3、用多线程完成同学通讯录检索任务

(1)改造searchPhoenBook方法为线程

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

package com.milihua.threaddemo;
import java.util.ArrayList;
import java.util.Iterator;
public class SearchPhone implements Runnable {
    ArrayList<PhoneBook>  array;
    String name;
    public SearchPhone(String inName,ArrayList<PhoneBook>  inArray)
    {
        array = inArray;
        name = inName;
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        Iterator<PhoneBook> 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;
            }
        }
    }
}

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

(2)改造PhoneBookSearch类的main方法

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

public static void main(String[] args) {
        ArrayList<PhoneBook>  array = new ArrayList<PhoneBook>();
        PhoneBookSearch  phoneBook = new PhoneBookSearch();
        phoneBook.InitPhoneBook(array);
        long start = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++ )
        {
            //phoneBook.searchPhoenBook("同学" + String.valueOf(i),array);
            SearchPhone  search = new SearchPhone("同学" + String.valueOf(i),array);
            Thread thread = new Thread(search);
            thread.start();
        }
        long end = System.currentTimeMillis(); 
        long total = end - start; 
        System.out.println("byCommonFor------->" + total+" ms");
 
    }

程序输出结果如下图所示:

blob.png

图5  用多线程完成同学通讯录检索任务输出结果

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

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

文章小结

1、多线程可以允许程序并发执行多个子任务,提高程序运行效率,增强用户体验度。例如,我们经常使用的下载工具就采用了多线程技术,一个下载任务启动后,下载工具把文件平分成n份,然后开n个线程分别下载,最后再组装文件。

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

思考与练习

1、线程和进程有什么区别?

2、在程序中创建线程,是用Runnable还是Thread创建?并说明原因。

3、某公司组织年会,会议入场时有两个入口,假设公司有100个员工,利用多线程模拟年会入场过程,并分别统计每个入口入场的人数。

我要评论
全部评论
郎宏林
授课老师
授课老师简介
项目经理,系统分析和架构师,从事多年中文信息处理技术。熟悉项目管理、擅长项目需求分析和设计、精通Java、C#、Python等编程语言。
下载APP

手机、电脑同步学

用微信或手机浏览器扫描二维码,即可下载APP。

  • 备案号:鲁ICP备15001146号
  • @1997-2018 潍坊米粒花网络技术有限公司版权所有