把Java 多线程说个透

        今天我们来聊聊Java多线程的问题,多线程在并发编程中尤其重要,从jdk1.0引入的Thread 类和Runable接口,以及到后来的jdk1.5版本引入的Callable 接口,已经让多线程编程变的越来重要,功能越来越强大,从之前一个线程执行完后没有返回结果,到后期的一个线程执行完后可以返回执行结果,都在直接影响我们的程序的设计思路。本文章会把通过不同的多线程实现用详细的例子给大家展示出来,其中还涉及到部分线程池和其他相关的东西,希望通过本章的讲解能够让大家对多线程有一个深入的了解。

     本文的内容还是通过先理论,后实践,在衍生、最后总结的方式一条线给大家顺下来,网上有一些关于多线程相关的技术文章,但是大部分都支离破碎,碎片化的知识加上跳跃式的章节让一些初学者没有头绪。如果大家耐心的读完本文章,并且根据本文章的示例运行一遍相关的程序,你基本就掌握了Java多线程技术,后期结合自己在工作种的一些经验,我相信大家的技术会更上一层。在这里用一句话和大家共勉:天行健,君子以自强不息。地势坤,君子以厚德载物。

下面我们就言归正传。

1.    多线程原理

                第一个问题,就是线程和进程的关系,可能好多人初学者搞不清楚这个概念,什么是进程什么线程从不同的理解角度去看,可能有不同种说法,网上有人从CPU执行时间角度去说的,有人从程序调度角度去说的,各有各的说法,但是只要你理解得当,都可以说没问题了。就比如形容“幸福”,每个人理解的角度不同,在你饿的时候吃一顿饭,是幸福,渴的时候喝一瓶水喝,是幸福,累的时候有觉睡,是幸福,无聊的时候打个游戏都是幸福,不同的角度阐述的问题不同,今天我们就从程序员理解的角度去说,什么是进程,什么是线程?

            进程就是操作系统对一个应用程序分配资源(比如:CPU,内存,磁盘、GPU,上下文环境等)的一个单位,而线程是在这个单元内进一步颗粒化的一个资源利用,比如说:我们启动QQ程序,那么系统就会给QQ程序分配各种资源,这个QQ程序我们就可以统称为一个进程。在QQ程序中,我们可以聊天,可以一对一聊天,也可以群聊,也可以下载文件等,但是我们这些操作都是在QQ这个程序(我们称之为进程)中执行了,在这里面的每一步操作,都是基于QQ进程的环境和上下文为依据的,这里每一个操作都被称为一个线程,那么他们是如何执行呢?比如说我同时和10个人聊天,那么这10个线程首先会放到一个线程池里,然后根据CPU执行的情况,去调用每一个线程。由于CPU执行的速度非常快,给我们感觉是一次行执行完毕,其实他内部是挨个去执行的。下面我们看个进程的图例:


        TIM这个进程,在widow情况下,理论可以申请到4G的内存空间,CPU可以分配的情况不同环境下不同,假如TIM现在CPU可以分配到40%,内存可以分配到2G,GPU最大可以分配20%的情况下,我们如果TIM同时启动1000个线程进行文件下载操作,那么这部分线程的资源环境都是基于TIM进程分配的最大资源环境下进行的,假如我们这1000个线程是按照编号从1到1000.CPU执行完编号1的线程后,如果编号3准备好,那么他可能就线执行3号线程,然后在执行2号线程,由于CPU执行的速度很快,给我们感觉是在一起执行,其实CPU内部的调度也是挨个去执行的。

2.        线程的五种状态

        对线程有了一个初步认识后,我们在大脑建立一个抽象的模型,可以构思一下,线程也是有生命周期的。比如我们人类,从出生到死亡,在整个生命轴上,我们可以吃饭睡觉,生病健康。线程也类似,他被创建和死亡,有各种状态,下面我们通过图例说说线程的六种状态:

        第一种:新建状态(new):创建一个线程,但线程并未启动。线程没有执行run()、start()或者execute()方法。

        第二种:可运行状态(runable):线程执行run()、start()或者execute()方法,进入线程池等待被线程调度选中。

        第三种:阻塞状态(blocked):当前线程在等待另一个线程的执行,比如等待一个synchronized修饰的方法或者块。

        第四种:等待状态(waitting):处于这种状态的线程不会被CPU分配时间片。如果没有其他线程唤醒,将无限期等待下去

        第五种:有时间等待状态(timed_waitting):处于这种状态的线程不会被CPU分配时间片,在指定的时间段内线程将被自动换醒。

        第六种:死亡状态(terminated):线程结束后的一种状态。

        这六种状态都来自Enum Thread.State枚举类中,我们通过程序先来看第一种状态(new):


这种状态就是线程通过new的方式刚被创建,没有调用run()方法,可以称之为一个简单的线程壳。

第二状态是在线程执行run()或者start()方法后,在java 虚拟机上的一种执行,他可能在等待来自操作系统的资源分配,比如等待获CPU时间片。下面我通过示例来看:

package util;

import java.util.concurrent.TimeUnit;

public class TestThread  {

public static void main(String[] args) {

WaitingThread waitingThread =new WaitingThread();

        waitingThread.start();

        System.out.println(waitingThread.getName());

        System.out.println(Thread.currentThread().getName());

    }

//Waiting 线程

    static class WaitingThread  extends Thread{

       public  void run(){

        while(true){

        synchronized (WaitingThread.class){

        try {

        WaitingThread.class.wait();

                    }catch (InterruptedException e) {

        e.printStackTrace();

                        }

                }

            }

        }

    }

}

运行后,我们获取俩个线程名称,一个main函数的主线程,一个WaitingThread 类的线程,我们在WaitingThread类线程中,让run()方法执行一个wait()方法,这边main方法就会一直等待WaitingThread类执行完毕后,在执行主线程,此时我们通过Java mission control 控制台可以看到当前俩个线程的状态,main线程是一个runable状态,一直在等带分配资源,而WaitingThread线程是一个waiting状态,此时如果不手动终止程序执行,这俩个线程将一直保持该状态直到天长地久。


首先我们通过idea 控制台获取这里俩个线程的名字,主线程名称为main,WaitingThread类线程名称为 Thread-0。然后我们通过Java mission control 查看这俩个线程的状态:


Thread-0线程状态:


那么大家通过上面的示例,我们可以明白,通过调用wait()方法,可以使线程进入waiting状态,其实线程进入waiting状态有三种方式:

        1. 调用wait()方法,没有设置时间,。

        2. 调用join()方法,在等待其他线程终止时。

        3. 调用lockSupport.park()方法。

下面我们通过示例演示调用join()方法,使线程进行等待状态,第三种方式,不在这里演示,大家自行测试。

package util;

import java.util.concurrent.TimeUnit;

public class TestThread  {

public static void main(String[] args)throws InterruptedException {

WaitingThread waitingThread =new WaitingThread();

        CallWaitingTHread callWaitingTHread =new CallWaitingTHread(waitingThread);

        waitingThread.start();

        callWaitingTHread.start();

    }

//Waiting 线程

    static class WaitingThread    extends Thread{

public  void run(){

try {

for(int i =0;i<10;i++){

System.out.println(i);

                            TimeUnit.SECONDS.sleep(2);

                        }

}catch (InterruptedException e) {

e.printStackTrace();

                    }

}

}

static class CallWaitingTHread    extends Thread{

private WaitingThreadwaitingThread;

        public CallWaitingTHread(WaitingThread waitingThread ){

this.waitingThread = waitingThread;

        }

public void run(){

try {

waitingThread.join();

                for (int i =10;i<20;i++){

System.out.println(i);

                }

}catch (InterruptedException e) {

e.printStackTrace();

            }

}

}

}

通过控制台输出我们可以看到。CallWaitingTHread线程 一直在等待WaitingThread 线程执行完毕后才输出:


通过Java mission control可以看到:

通过join()方法的源码,我们可以看到,在join()方法内部其实也是调用 的wait()方法:


接下来我们time_waiting 状态,他是线程在指定的时间内等待,通过javaAPI我们可以看到,他有五种方法,可以让线程进行等待状态:

1.     调用sleep(time),指定时间。

2.    调用wait(time),指定时间。

3.    调用join(time),指定时间。

4.     调用lockSupport.parkNanos方法

5.     调用LockSupport.parkUntil方法

三:多线程三种实现方式

多线程的实现有三种方式,从jdk1.0的THread 类Runable 接口,到jdk1.5的Callable接口。下面我们通过详细的示例讲解三种实现方式。

    1.    继承THread类

通过继承Thread类实现,该类位于java.lang包下,他实现了Run able接口,有8个构造方法和若干个实现的方法,下面我们通过例子在展示:

package util;

import java.util.concurrent.TimeUnit;

public class TestThread  {

public static void main(String[] args)throws InterruptedException {

WaitingThread waitingThread =new WaitingThread();

        waitingThread.start();

    }

//Waiting 线程

    static class WaitingThreadextends Thread{

public  void run(){

try {

for(int i =0;i<10;i++){

System.out.println(i);

                            TimeUnit.SECONDS.sleep(2);

                        }

}catch (InterruptedException e) {

e.printStackTrace();

                    }

}

}

}

这是一个非常简单的通过继承Thread类实现的一个线程,在他的run()方法里面主要干了一件事,就是每隔2秒循环一个数字,从0开始直到9结束。在这里我们是用的是用的java.util.concurrent.TimeUnit枚举类,该类下面有7个枚举常量,分别是

通过该枚举常量然后调用sleep()方法,可以给我们非常直观的时间概念,比如说我想让想让当前线程睡眠1秒,那么我们就可以TimeUnit.SECONDS.sleep(1),是不是比Thread.sleep(1000) 看起来更加符合人类的语言呢??

    2.    实现Runable接口

第二种放方式是通过实现Runable接口创建线程,对于通过实现接口比继承的好处有很多,我就步一一在这里阐述, 但是在多线程中,通过实现接口实现多线程一个重要的优势是,线程池只接受接口。我们看一下jdkAPI的几个线程池提交方法:

ListshutdownNow()

Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.

Futuresubmit(Callable task)

Submits a value-returning task for execution and returns a Future representing the pending results of the task.

Futuresubmit(Runnabletask)

Submits a Runnable task for execution and returns a Future representing that task.

Futuresubmit(Runnabletask, T result)

Submits a Runnable task for execution and returns a Future representing that task.

他都是只接受接口,其中就是Runable 和Callable接口。当然了,类也不是说一无是处,他有自己的优点,比如说类中start()方法,就是一个异步启动执行线程的,后面我们在区别和联系中会详细讲解到。

下面我们看通过Runbale运行的后示例:

package util;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class TestRunable  {

public static void main(String[] args) {

RunableThread1 runableThread1 =new RunableThread1();

        RunableThread2 runableThread2 =new RunableThread2();

        runableThread1.run();

        runableThread2.run();

    }

static class RunableThread1implements Runnable{

@Override

        public void run() {

for(int i =0;i<10;i++){

System.out.println("我是线程RunableThread1的线程第: "+i+"个");

            }

}

}

static class RunableThread2implements Runnable{

@Override

        public void run() {

for(int i =10;i<20;i++){

System.out.println("我是线程RunableThread1的线程第: "+i+"个");

            }

}

}

}

通过控制台,我们可以看到输出的详细信息,如果大家仔细查看的话,发现是按顺序先执行线程1,然后在执行线程2,这个就是run()执行的步骤:


    3.     实现Callable接口

        Callable接口的出现是为了满足线程执行完毕后返回结果而出现的,我们都知道,通过实现Runable接口和继承Thread类运行run()方法后, 该类是没有返回值的,假设我对一个班级的学生成绩统计后返回一个list,那么我就没法通过run()方法去实现,我只能通过一个类的全局变量,然后在run()方式里面复制去实现,如果一个线程没有问题,线程多的话,就涉及到一个变量共享问题,导致我存的数据可能不是我想要的。那么Callable就能解决该问题,下面我们通过示例展示callable接口的实现:

package util;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.List;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutorService;

public class TestCallable {

public static void main(String[] args)throws Exception {

List list = Arrays.asList("张三","李四","王五");

        Callabletest callabletest =new Callabletest(list);

        List list1 = callabletest.call();

        for(String  str :list1 ){

                System.out.println(str);

        }

}

static class Callabletest  implements Callable>{

private Listlist;

        public Callabletest(List list){

            this.list = list;

        }

@Override

        public List call()throws Exception {

        List lists =new ArrayList();

            list.forEach(e ->{

                lists.add(e.toString().toCharArray()[0]+"");

            });

            return lists;

        }

}

}

通过上述例子我们可以看到,通过实现Callable 接口,调用他对于的call方法可以返回具体执行的对象,在本例程序中,我们处理一个人员名称对象列表,然后获取姓氏,可以通过统计该列表中每个姓氏人员有多少个,通过call()方法实现后,我们将处理的lists返回,通过控制台打印后,我们可以看到返回的结果:


其中,在call()方法中,我们使用了jdk1.8最新的lambda表达式,在以后的文章中,我会详细简绍lambda表达式。

    4.     三种方式区别和内在联系

        通过上面的示例,我们基本了解了三种线程的实现方式,也对三种实现方式有了最基本认识,那么下面我们就一起来看看他们的区别和联系:

        1.    Thread类是一个实现了Runable接口的类,他有三个静态变量可以设置线程的优先级,MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY,如果我们有这个设置线程优先级的需求,我们就的需要通过继承Thread类。

        2.    Thread类有个一个start()方法,他是可以做的多线程异步执行,通过上面的示例,我们可以很明确的了解到,start()方式执行后,线程并不是按照顺序执行的。

        3.    Runable是一个接口,最后的好处是接口可以多实现,而类是个单继承,其次,如果我们在实际业务中用到线程池及时,那么我们就的使用Runable或则Callable接口了,上面我们也讲到线程池只接受接口参数。

        4.  Callable也是一个接口,他可以接受一个泛型,是为了返回值做准备的,通过上面的示例我们也可以看到,Callable最大最好用的是call()方法可以返回你想要的任何数据。

      5.    关于线程优先级这块,在这里不做详细讲解,在实际业务的涉及到线程优先级的很少,而且如果控制不当,很容易造成严重的后果,在后面我们会重点介绍一下线程锁的详细使用。       

    5.    在看线程的状态转换

上面我们详细了介绍了线程的几种状态,并且通过各种示例对线程的状态进行详细的展示,下面我们总结一下线程之间这几种状态的转换。大家可以结合上面示例和本图进行一个详细的理解,这样有助于对线程状态的进一步深入了解。

    6.  从源码总结run()、start()和Call()方法区别

首先我们看start方法源码:


在start()方法源码中,调用了一个start0()方法,我们可以看到start0()方法是通过private native void start0() native进行修饰的,该方法是一个原生态的方法,方法的实现不在当前文件中,下面我们通过示例展示run()方法和start()方法的的区别:

package util;

public class RunAndStartTest {

public static void main(String[] args) {

MuThread muThread =new MuThread();

        muThread.run();

        System.out.println(muThread.getName());

    /*    muThread.start();

System.out.println(muThread.getName());*/

    }

static class MuThreadextends  Thread{

public  void run(){

System.out.println("我是MuThread,我的线程名称是: "+Thread.currentThread().getName());

        }

}

}

通过输出控制台我们可以看到,run()方法是直接运行在main函数中的主线程中执行的(准确的说是被创建新线程的当前线程所执行,如果我们在Thread2中执行Thread1的run()方法,你会发出他输出的是Thead2的名称)。:


而我们通过start()方法执行后,通过控制台发现他是在当前线程下执行的:


通过上述我们发现,run()方法其实他是当作一个普通的方法执行的,他必须等run()方法体内程序逻辑执行完毕后,才会执行后续的代码,而start()方法是真是意义上的多线程,他的执行不会去等待run()方法体内的逻辑,下面我们在通过一个打印序列进一步了解这俩个方法执行的逻辑。

package util;

public class RunAndStartTest {

public static void main(String[] args) {

MuThread muThread =new MuThread();

        MuThread1 muThread1 =new MuThread1();

        muThread.start();

        muThread1.start();

    }

static class MuThreadextends  Thread{

public  void run(){

for(int i =0;i<10;i++){

System.out.println(Thread.currentThread().getName() +"的序号是 "+ i);

          }

}

}

static class MuThread1extends  Thread{

public  void run(){

for(int i =10;i<20;i++){

System.out.println(Thread.currentThread().getName()+"的序号是 " +i);

            }

}

}

}

虽然我们执行过程中计划是先循环1到10,然后在循环10到20,可是实际上通过start()方法后,他马上把该线程状态设置为可运行状态,放入等待队列,然后执行下个操作,他不会等run()方法体的具体执行情况,通过控制台我们可以看到,他的输出不是有序的,start()方法可以说才是真正的多线程模式。

然后我们在看run()方法执行过程,run()方法是按顺序执行的,只有第一个线程执行完毕后,才会执行第二个线程,不管你执行多少次都是有序的输出,和start()方法不同,start()方法我们可以多次尝试发现,每次执行输出的结果都会不一样。:


接下来我们看call()方法,该方法是Callable接口中唯一的一个方法,返回一个Object对象,如果我们通过实现Callable接口然后调用call()方法,发现他出和run()方法有点类似,除了他能返回结果外。其中都是在main主线程中执行的,并且执行的过程中都是有顺序的,下面我们看看具体是示例:

package util;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Callable;

public class RunAndStartTest {

public static void main(String[] args)throws Exception {

MuCallable muCallable =new MuCallable();

        MuCallable1 muCallable1 =new MuCallable1();

        List list = (List) muCallable.call();

        List list1 = (List) muCallable1.call();

    }

static  class MuCallableimplements Callable{

@Override

        public Objectcall()throws Exception {

List list  =new ArrayList();

            for(int i =10;i<20;i++){

System.out.println(Thread.currentThread().getName()+"的序号是 " +i);

                list.add(i);

            }

return list;

        }

}

static  class MuCallable1implements Callable{

@Override

        public Objectcall()throws Exception {

List list =new ArrayList();

            for(int i=0;i<10;i++){

System.out.println(Thread.currentThread().getName()+"的序号是 " +i);

                list.add(list);

            }

return list;

        }

}

}

控制台输出结果:

在实际应用中,Callable会和Future以及ExecutorService线程池结合使用,具体详细的讲解我们在下面线程池技术中一一列出。

四:线程池技术

        我们在讨论多线程的时候,不得不提线程池,在实际生产环境应用中,有好多大名鼎鼎的相关插件在帮我们管理者线程池,不需要我们自己去写程序实现,实际上,线程方法面的技术比较复制,有时候我们多线程运用不当,会造成非常严重的问题,而这些问题有时候又是无法即使讲解和定位的。比如说我们在本地模拟一个聊天程序,一个服务器处理1万个用户简单的聊天请求,如果我们在本地气1万个线程去响应用户请求,我们估计电脑马上就会奔溃,这时候我们就需要用到线程池。

        我相信大家看到这里的时候,多前面线程有一个基本的了解和应用,那么在这里讲解线程池技术我会穿插一些新的知识点,帮助大家进一步加深理解,比如:线程安全和共享资源问题、线程避免死锁问题、阻塞队列和同步器问题、以及讲解一下阿姆达尔定律。那么下面我们就言归正传,先看一下jdk API中和线程池相关的几个类和接口。在jdk1.5之后Java通过突出推出一套executor框架来专门用与处理线程池相关问题,他能够分解任务、任务调度和执行相关线程,下面我们通过我们通过一个

在JDK 1.5之后通过了一套Executor框架能够解决这些问题,能够分解任务的创建和执行过程。该框架包括Executor,ExecutorService,Callable等基础接口和Executors,ThreadPoolExecutor等实现类。


上面是用powerdesigner 简单的画了一个类图,在这里变常用到并且设计到的类和接口大致这么多,有一部分由于篇幅有限,我在这里没有一一罗列,具体的一些继承和实现类的方法大家可以查看API。从上面我们可以看到,创建线程池是通过Executors类的五个方法,也就是五种创建线程池方式。

    1.    newSingleThreadExecutor()

        创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程会代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newScheduledThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

package util;

import java.util.concurrent.Executors;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.ScheduledFuture;

import java.util.concurrent.TimeUnit;

public class NewSingleThreadScheduleExecutorTest {

public static void main(String[] args) {

NewSingleThreadScheduleExecutorTest  newSingleThreadScheduleExecutorTest =new NewSingleThreadScheduleExecutorTest();

        newSingleThreadScheduleExecutorTest.beepForAnHour();

    }

private final ScheduledExecutorServicescheduler = Executors.newScheduledThreadPool(1);

    public void beepForAnHour() {

final Runnable beeper =new Runnable() {

public void run() {

System.out.println("beep");

            }

};

        final ScheduledFuture beeperHandle =scheduler.scheduleAtFixedRate(beeper, 10, 10, TimeUnit.SECONDS);

        scheduler.schedule(new Runnable() {

public void run() {

beeperHandle.cancel(true);

            }

}, 60 *60, TimeUnit.SECONDS);

    }

}

    2.    newCachedThreadPool()

        创建一个可以缓存线程池,根据需要创建新的线程,但是当它们可用时,将重用以前构建的线程。他有一个重载方法newCachedThreadPool(ThreadFactory threadFactory),可以根据需要通过ThreadFactory 创建新的线程。他适用于一些生命周期非常短的任务,因为没有线程池上线,如果操作IO时间长,处理业务复制的问题,将会导致创建大量线程,最终会导致内存溢出问题。

package util;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.TimeUnit;

public class newCachedThreadPoolTest {

public static void main(String[] args)throws InterruptedException {

ExecutorService executorService = Executors.newCachedThreadPool();

        for(int i =0;i<100;i++){

        final int val = i;

            TimeUnit.MILLISECONDS.sleep(5);

            executorService.submit(new Callable() {

                @Override

                public Integercall()throws Exception {

                    System.out.println(val);

                    return val ;

                }

            });

        }

        executorService.shutdown();

    }

}

    3.    newFixedThreadPool(int n)

        创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。API官方文档是这么描述的:

Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue. At any point, at most nThreads threads will be active processing tasks. If additional tasks are submitted when all threads are active, they will wait in the queue until a thread is available. If any thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks. The threads in the pool will exist until it is explicitly shutdown.

核心点:创建固定数量的线程,

    4.    newWorkStealingPool()

节后跟新

    5.    newSingleThreadScheduledExecutor()

节后跟新

五:多线程技术衍生


节后跟新

六:源码分析


节后跟新

七:多线程详细示例


节后跟新

你可能感兴趣的:(把Java 多线程说个透)