JAVA系列---多线程

多线程是什么

所谓多线程其实就是开启一个新的线程处理任务而不是在当前线程执行任务。

多线程的目的

多线程的目的就是异步。
做到异步之后就可以进行很多神奇的操作,比如解耦,比如并行。

多线程的实现

Runnable不是多线程

Runnable无法开启多线程:run方法只是一个普通的阻塞方法

  • Runnable本质上就是一个普通的接口,它只是一个实现类而已。它本身没有开启线程的能力。
  • 看源码也能发现,它只是一个功能的实现。就一个run方法,直接执行就是一个阻塞进程。
  • 经常觉得它特殊只是因为它是Thread也实现了该接口。
  • Runnable和Callable只是多线程的功能实现部分,不是功能开启部分

public class A2TestRunnable implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("sleep");
        } catch (Exception e) {
            System.out.println(e);
        }
    }


    public static void main(String[] args) {
        System.out.println("start");
        A2TestRunnable a = new A2TestRunnable();
        // 可以看到 a只有run方法,没有开启线程的能力。开启线程只有一个入口那就是start
        a.run();
        System.out.println("end");
    }
}

/*
以上就是一个普通的单线程,整个线程是阻塞的。输出为:
start
sleep
end
*/

Callable不是多线程

Callable无法开启多线程:它只能执行call方法

  • Callable 的作用和定位与 Runnable 完全相同,唯一的区别就是一个有返回值一个没有返回值
public class A1TestCallable implements Callable {

    @Override
    public Object call() throws Exception {
        Thread.sleep(5000);
        System.out.println("sleep");
        return "sleep";
    }

    public static void main(String[] args) {
        System.out.println("start");
        try {
            A1TestCallable a = new A1TestCallable();
            a.call();
        } catch (Exception e) {
            System.out.println(e);
        }
        System.out.println("end");
    }
}
/*
以上也是一个阻塞线程,跟Runnable在线程功能上完全相同。
start
sleep
end
 */

Thread才是多线程

只有Thread可以开启多线程:且只能运行start方法,使用run方法不是多线程。直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在新线程中执行它,所以这并不是多线程工作。

线程的启动过程:

  1. new一个Thread,线程就会进入了新建状态
  2. 调用start()方法,会启动一个新线程并使线程进入了就绪状态
  3. 就绪状态的新线程当分配到时间片后就可以开始运行了,它自动执行run()方法的内容,此时才开启了一个新线程执行任务。

总述:只有调用start方法才可以启动线程并使线程进入就绪状态。而run方法只是thread 的一个普通方法调用,还是在主线程里执行。

进阶:线程的run()方法是由java虚拟机直接调用的,如果我们没有启动线程(没有调用线程的start()方法) 而是在应用代码中直接调用run()方法,那么这个线程的run()方法其实运行在当前线程(即run()方法的调用方所在的线程)之中,而不是运行在其自身的线程中,从而违背了创建线程的初衷,也就是说并没有创建一个新的线程。

所以多线程的本质取决于虚拟机执行代码时是不是新启动一个线程
新启动线程就是多线程:start
当前线程执行就是单线程: 除了start

public class A3TestThread extends Thread {

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("sleep");
        } catch (Exception e) {
            System.out.println(e);
        }
    }


    public static void main(String[] args) {
        System.out.println("start");
        A3TestThread a = new A3TestThread();
        // 阻塞,单线程,相当于执行一个普通方法
        // a.run();
        // 异步,多线程,这次是真正开启一个线程
        a.start();
        System.out.println("end");
    }
}

/*
输出为:
start
end
sleep
 */

线程池是什么

线程池就是池化思想的一种实现,线程池的优点和其他池化没什么区别,核心优点就是两个:线程功能复用+线程数量控制

使用线程池之后可以不用显式创建线程,直接实现功能即可,线程池接收到Runnable或者Callable之后就会将其中的功能在新线程中执行,这样无论实现的是什么功能都不会阻塞当前线程。

public class A4TestThreadPoll {
    private static int syncCorePoolSize = 20;
    private static int syncMaximumPoolSize = 20;
    private static int syncKeepAliveTime = 2;
    private static int syncQueueSize = 100;



    public static ExecutorService init() {
        return  new ThreadPoolExecutor(syncCorePoolSize, syncMaximumPoolSize, syncKeepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(syncQueueSize),
                new ThreadFactory() {
                    AtomicInteger count = new AtomicInteger(0);
                    public Thread newThread(Runnable r) {
                        Thread th = new Thread(r, "syncUserDetail-" + count.getAndIncrement());
                        return th;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy());
    }



    /**
    *
    * /*
    * sumFuture.get() 注释时输出为:
    * start
    * end
    * hello
     *
    * sumFuture.get() 不注释时输出为:
    * start
    * hello
    * 390927294
    * end
    **/
    // ExecutorService异步处理,具体操作可以写业务类继承Runnable接口,并在构造方法传入具体业务参数等等
    public void syncExecutor() throws Exception {
        ExecutorService service = init();
        Future<Integer> sumFuture = service.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Thread.sleep(2000);
                System.out.println("hello");
                return RandomUtil.randomInt();
            }
        });
        // 此处主线程可以先处理其他事情,在来调用.get()方法,处理异步结果的内容。
        System.out.println("start");
        System.out.println(sumFuture.get());
        service.shutdown();
        System.out.println("end");
    }

    public static void main(String[] args) {
        AtomicInteger start = new AtomicInteger(0);
        ExecutorService service = init();
        while(start.get() < 1) {
            int num = start.incrementAndGet();
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    System.out.println(num);
                    try {
                        Thread.sleep(4000);
                    } catch (Exception e) {
                        System.out.println(e);
                    }
                    System.out.println("我会被阻塞");

                }
            });
            System.out.println("我不会阻塞的");
        }
        // shutdown方法不会阻塞,它的作用是关闭线程池,线程池与已经开启的线程无关。果树被伐不会影响去年结的果子呀
        service.shutdown();
        System.out.println("我也不会被阻塞");

    }
}
/**
 * main方法输出为:
 * 我不会阻塞的
 * 我也不会被阻塞
 * syncUserDetail-0
 * 1
 * 我会被阻塞
 */
  • Runnable:开启一个新的线程的时候,需要传递一个Runnable接口作为执行对象,这个接口里面只包含一个run方法,无参数,无返回值。
  • Callable:和Runnable类似,但是有返回值。Callable里面只有一个方法:V call() throws Exception;
  • execute:ExecutorService类的execute方法,该方法来自于父类Executor的方法,入参为Runnable,无返回值。
    所以该方法适用于开启线程做异步操作,无需等待返回结果及超时时间。
  • submit:ExecutorService类的方法,有两个构造法方法,一个用来提交Runnable对象,一个用来提交Callable对象。
    这些方法都是返回一个Future的对象。所以该方法适用于开启线程做同步操作

总述:

  1. Runnable和Callable本身不具备多线程的能力,只能借助线程池进行实现,准确来说二者都是线程池的入参。
  2. 向线程池中提交任务的submit方法不是阻塞方法,而Future.get方法是一个阻塞方法,当submit提交多个任务时,只有所有任务都完成后,才能使用get按照任务的提交顺序得到返回结果,所以一般需要使用future.isDone先判断任务是否全部执行完成,完成后再使用future.get得到结果。(也可以用get (long timeout, TimeUnit unit)方法可以设置超时时间,防止无限时间的等待)
  3. 三段式的编程:1.启动多线程任务2.处理其他事3.收集多线程任务结果,Future虽然可以实现获取异步执行结果的需求,但是它没有提供通知的机制,要么使用阻塞,在future.get()的地方等待future返回的结果,这时又变成同步操作;要么使用isDone()轮询地判断Future是否完成,这样会耗费CPU的资源。
  4. 其中Future为同步阻塞、CompletionService为同步非阻塞、CompletableFuture为异步非阻塞
  5. Future总结(同步、异步使用)
    同步可以用submit方法:
    原理:主线程阻塞等待线程池执行结果,从共享变量获取结果
    使用场景:需要程序提高执行速度,并且需要获取执行结果;需要有超时等待任务执行完毕,超时获取结果
    异步可以用execute方法:
    只需要提交一个任务(不需要超时等待),且主线程(main)提交任务后无操作
  6. Future类的不足之处
    无法被动接收异步任务的计算结果:虽然我们可以主动将异步任务提交给线程池中的线程来执行,但是待异步任务执行结束之后,主线程无法得到任务完成与否的通知,它需要通过get方法主动获取任务执行的结果。
    Future件彼此孤立:有时某一个耗时很长的异步任务执行结束之后,你想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future都是彼此之间都是孤立的,所以才有了后面的CompletableFuture,CompletableFuture就可以将多个Future串联起来形成任务流。
    Futrue没有很好的错误处理机制:截止目前,如果某个异步任务在执行发的过程中发生了异常,调用者无法被动感知,必须通过捕获get方法的异常才知晓异步任务执行是否出现了错误,从而在做进一步的判断处理。

线程池实现“线程复用”的原理

平时对线程的理解是,通过 Thread 创建线程,执行任务,任务完成的同时线程自动销毁。这种理解局限了对线程复用的理解。

线程复用的核心:把线程和任务进行解耦,线程归线程,任务归任务。将任务放到任务队列,线程从任务队列中获取任务,执行完一个任务再执行下一个任务,而非执行完一个任务就销毁。

在线程池中,同一个线程可以从 BlockingQueue 中不断提取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法,把 run 方法当作和普通方法一样的地位去调用,相当于把每个任务的 run() 方法串联了起来,所以线程数量并不增加。

线程池创建新线程的时机和规则:
在这里插入图片描述
如流程图所示,当提交任务后,线程池首先会检查当前线程数,如果此时线程数小于核心线程数,比如最开始线程数量为 0,则新建线程并执行任务,随着任务的不断增加,线程数会逐渐增加并达到核心线程数,此时如果仍有任务被不断提交,就会被放入 workQueue 任务队列中,等待核心线程执行完当前任务后重新从 workQueue 中提取正在等待被执行的任务。此时,假设我们的任务特别的多,已经达到了 workQueue 的容量上限,这时线程池就会启动后备力量,也就是 maxPoolSize 最大线程数,线程池会在 corePoolSize 核心线程数的基础上继续创建线程来执行任务,假设任务被不断提交,线程池会持续创建线程直到线程数达到 maxPoolSize 最大线程数,如果依然有任务被提交,这就超过了线程池的最大处理能力,这个时候线程池就会拒绝这些任务,我们可以看到实际上任务进来之后,线程池会逐一判断 corePoolSize 、workQueue 、maxPoolSize ,如果依然不能满足需求,则会拒绝任务。

我们接下来具体看看代码是如何实现的,我们从 execute 方法开始分析,源码如下所示。

public void execute(Runnable command) { 
    if (command == null) 
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) { 
        if (addWorker(command, true)) 
            return;
        c = ctl.get();
    } 
    if (isRunning(c) && workQueue.offer(command)) { 
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command)) 
            reject(command);
        else if (workerCountOf(recheck) == 0) 
            addWorker(null, false);
    } 
    else if (!addWorker(command, false)) 
        reject(command);
}

线程复用源码解析

//如果传入的Runnable的空,就抛出异常
if (command == null) 
    throw new NullPointerException();

execute 方法中通过 if 语句判断 command ,也就是 Runnable 任务是否等于 null,如果为 null 就抛出异常。

接下来判断当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker() 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程

if (workerCountOf(c) < corePoolSize) { 
    if (addWorker(command, true)) 
        return;
        c = ctl.get();
}

那 addWorker 方法又是做什么用的呢?addWorker 方法的主要作用是在线程池中创建一个线程并执行第一个参数传入的任务,它的第二个参数是个布尔值,如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程,大于等于则不增加;同理,如果传入 false 代表增加线程时判断当前线程是否少于 maxPoolSize,小于则增加新线程,大于等于则不增加,所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增线程的判断。addWorker() 方法如果返回 true 代表添加成功,如果返回 false 代表添加失败。

我们接下来看下一部分代码:

if (isRunning(c) && workQueue.offer(command)) { 
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command)) 
        reject(command);
    else if (workerCountOf(recheck) == 0) 
        addWorker(null, false);
}

如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过if (isRunning© && workQueue.offer(command))检查线程池状态是否为 Running,如果线程池状态是 Running 就把任务放入任务队列中,也就是 workQueue.offer(command)。如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下所示:

if (! isRunning(recheck) && remove(command)) 
    reject(command);

下面我们再来看后一个 else 分支:

else if (workerCountOf(recheck) == 0) 
    addWorker(null, false);

能进入这个 else 说明前面判断到线程池状态为 Running,那么当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了),所以此时如果检查当前线程数为 0,也就是 workerCountOf(recheck) == 0,那就执行 addWorker() 方法新建线程。

我们再来看最后一部分代码:

else if (!addWorker(command, false)) 
    reject(command);

执行到这里,说明线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了,根据规则,此时需要添加新线程,直到线程数达到“最大线程数”,所以此时就会再次调用 addWorker 方法并将第二个参数传入 false,传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize,小于则增加新线程,大于等于则不增加,也就是以 maxPoolSize 为上限创建新的 worker;addWorker 方法如果返回 true 代表添加成功,如果返回 false 代表任务添加失败,说明当前线程数已经达到 maxPoolSize,然后执行拒绝策略 reject 方法。如果执行到这里线程池的状态不是 Running,那么 addWorker 会失败并返回 false,所以也会执行拒绝策略 reject 方法。

可以看出,在 execute 方法中,多次调用 addWorker 方法把任务传入,addWorker 方法会添加并启动一个 Worker,这里的 Worker 可以理解为是对 Thread 的包装,Worker 内部有一个 Thread 对象,它正是最终真正执行任务的线程,所以一个 Worker 就对应线程池中的一个线程,addWorker 就代表增加线程。线程复用的逻辑实现主要在 Worker 类中的 run 方法里执行的 runWorker 方法中,简化后的 runWorker 方法代码如下所示。

runWorker(Worker w) {
    Runnable task = w.firstTask;
    while (task != null || (task = getTask()) != null) {
        try {
            task.run();
        } finally {
            task = null;
        }
    }
}

以看出,实现线程复用的逻辑主要在一个不停循环的 while 循环体中。

  • 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。
  • 直接调用 task 的 run 方法来执行具体的任务(而不是新建线程)。

在这里,我们找到了最终的实现,通过取 Worker 的 firstTask 或者 getTask方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。

多线程使用示例

示例一:使用java初始线程池:ExecutorService

public class A4TestThreadPoll {
    private static int syncCorePoolSize = 20;
    private static int syncMaximumPoolSize = 20;
    private static int syncKeepAliveTime = 2;
    private static int syncQueueSize = 100;

    public static ExecutorService init() {
        return  new ThreadPoolExecutor(syncCorePoolSize, syncMaximumPoolSize, syncKeepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(syncQueueSize),
                new ThreadFactory() {
                    AtomicInteger count = new AtomicInteger(0);
                    public Thread newThread(Runnable r) {
                        Thread th = new Thread(r, "syncUserDetail-" + count.getAndIncrement());
                        return th;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy());
    }
    
    public static void main(String[] args) {
        AtomicInteger start = new AtomicInteger(0);
        ExecutorService service = init();
        while(start.get() < 1) {
            int num = start.incrementAndGet();
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    System.out.println(num);
                    try {
                        Thread.sleep(4000);
                    } catch (Exception e) {
                        System.out.println(e);
                    }
                    System.out.println("我会被阻塞");

                }
            });
            System.out.println("我不会阻塞的");
        }
        // shutdown方法不会阻塞,它的作用是关闭线程池,线程池与已经开启的线程无关。果树被伐不会影响去年结的果子呀
        service.shutdown();
        System.out.println("我也不会被阻塞");
    }
}
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lipviolet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值