Java多线程(架构师之路 )

JAVA架构师之路

本系列文章均是博主原创,意在记录学习上的知识,同时一起分享学习心得。
第一章
第一节 Java集合总结(架构师之路 )
第二节 Java多线程(架构师之路 )



前言

本章主要介绍JAVA多线程,主要分3个方面介绍,多线程是什么、有什么用、如何用。


提示:以下是本篇文章正文内容,下面案例可供参考

一、基础简介

  1. 进程
    进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
    在这里插入图片描述

  2. 线程
    线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
    在这里插入图片描述

  3. 多线程
    多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。

  4. 为何使用多线程
    开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

二、Java多线程

使用Java多线程的3种常用方式:

1.继承Thread

代码如下(示例):

public class MyThread extends Thread{

    @Override
    public void run() {
        super.run();
        System.out.println("这是线程名称:" + Thread.currentThread().getName());
    }
}

public class ThreadTest {

    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        MyThread m3 = new MyThread();
        m1.start();
        m2.start();
        m3.start();
    }
}

在这里插入图片描述

2.实现Runnable接口

代码如下(示例):

public class MyRunable implements Runnable {
    @Override
    public void run() {
        System.out.println("这是线程名称:" + Thread.currentThread().getName());
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunable());
        Thread t2 = new Thread(new MyRunable());
        Thread t3 = new Thread(new MyRunable());
        t1.start();
        t2.start();
        t3.start();
    }
}

在这里插入图片描述

3.使用线程池

Java线程池是对多线程的集合管理,避免不必要的线程创建销毁动作,因为在面向对象编程中,对象的创建和销毁是非常消耗时间和资源的。

比较重要的几个类:

类名描述
ExecutorsJava线程池主要使用到Executors 类来创建线程池,使用 Executors 工具类来得到 ExecutorService 接口的具体对象
ExecutorService真正的线程池接口。
ScheduledExecutorService能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutorExecutorService的默认实现。
ScheduledThreadPoolExecutor继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

3.1 Executors 类

Java线程池主要使用到Executors 类来创建线程池,使用 Executors 工具类来得到 Executor 接口的具体对象,需要注意的是 Executors 是一个类,不是 Executor 的复数形式。
Executors 提供了以下一些 static 的方法:

  • callable(Runnable task):将 Runnable 的任务转化成 Callable 的任务。
  • newFixedThreadPool(int poolSize) :产生一个 ExecutorService 对象,这个对象带有一个大小为
    poolSize 的线程池,若任务数量大于 poolSize ,任务会被放在一个 queue 里顺序执行。
  • newCachedThreadPool( ):产生一个 ExecutorService对象,这个对象带有一个线程池,线程池的大小会根据需要自动调整,线程执行完任务后将会返回线程池,供执行下一次任务使用。
  • newSingleThreadExecutor( ):产生一个 ExecutorService对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
  • newSingleThreadScheduledExecutor :产生一个 ScheduledExecutorService对象,这个对象的线程池大小为 1 ,若任务多于一个,任务将按照先后顺序执行。
  • newScheduledThreadPool(int poolSize):产生一个 ScheduledExecutorService对象,这个对象的线程池大小为 poolSize ,若任务数量大于 poolSize ,任务会在一个 queue 里等待执行。

3.1.1 newFixedThreadPool

创建一个可重用的固定线程数量的线程池,以共享的无界队列方式来运行这些线程。

ExecutorService threadPool = Executors.newFixedThreadPool(3);    // 创建可以容纳3个线程的线程池

注意:创建一个固定大小的线程池。每次提交一个任务就会创建一个线程,直到创建的线程数量达到线程池的最大值n。线程池的大小一旦达到最大值就会保持不变。如果在所有线程都处于活动状态时,这时再有其他任务提交,他们将进入等待队列中直到有空闲的线程可用。如果任何线程由于执行过程中的故障而终止,将会有一个新线程将取代这个线程执行后续任务。

代码如下(示例):

    private static void newFixedThreadPool() {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            final int col = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程名称:" 
                    + Thread.currentThread().getName() + ", 执行任务序号:" + col);
                }
            });
        }
        executorService.shutdown();
    }

在这里插入图片描述

3.1.2 newCachedThreadPool

创建一个可缓存的线程池,可以根据需要自动创建新线程的线程池,但是,在以前创建好的线程可用时 将重用它们。
注意:在JAVA文档中是这样介绍可回收缓存线程池的,创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可以提高程序性能。如果线程池中有线程可用,那么调用 execute 将重用以前构造的线程;如果线程池中没有可用的线程,则创建一个新线程并添加到线程池中。此线程池不会对线程池的大小做限制,线程池大小完全依赖操作系统或者说JVM所能够创建的最大线程数量大小。此线程池会终止并从缓存中移除那些已有 60 秒钟(默认)未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。(可以通过java.util.concurrent.ThreadPoolExecutor 类的构造方法构造更加具体的类,例如指定时间参数等)。创建线程本身需要很多资源,包括内存,记录线程状态,以及控制阻塞等等。因此,相比另外两种线程池,在需要频繁创建短期异步线程的场景下,newCachedThreadPool能够复用已完成而未关闭的线程来提高程序性能。通俗讲就是:CachedThreadPool会创建一个缓存区,将初始化的线程缓存起来,如果线程有可用的,就使用之前创建好的线程,如果没有可用的,就会新创建线程。另外,终止并且从缓存中移除已有60秒未被使用的线程。

代码如下(示例):

    private static void newCachedThreadPool() {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int col = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程名称:" + Thread.currentThread().getName() 
                    + ", 执行任务序号:" + col);
                }
            });
        }
        executorService.shutdown();
    }

在这里插入图片描述

3.1.3 newSingleThreadExecutor

创建一个使用单个 工作线程的 Executor,以无界队列方式来运行该线程。
注意:创建一个单线程的线程池。这个线程池中只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么就会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

代码如下(示例):

private static void newSingleThreadExecutor() {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 7; i++) {
        final int col = i;
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程名称:" + Thread.currentThread().getName() 
                + ", 执行任务序号:" + col);
            }
        });
    }
    executorService.shutdown();
}

在这里插入图片描述

3.1.4 newScheduledThreadPool

创建一个可安排在给定延迟时间后 运行的或者可定期地执行的线程池。
注意:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
代码如下(示例):

private static void newScheduledThreadPool() {
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
    // 5秒后执行任务
    scheduledThreadPool.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",执行。");
        }
    }, 5, TimeUnit.SECONDS);
    // 5秒后执行任务,以后每2秒执行一次
    scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println("当前线程名称:" + Thread.currentThread().getName() + ",执行。");
        }
    }, 8, 2, TimeUnit.SECONDS);
}

在这里插入图片描述

3.2 ThreadPoolExecutor类

(1)无论创建哪种线程池,必须要调用 ThreadPoolExecutor类。线程池类是 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit
, BlockingQueue workQueue, RejectedExecutionHandler handler)
  • corePoolSize: 线程池维护核心线程的最少数量。
  • maximumPoolSize:线程池维护线程的最大数量。
  • keepAliveTime: 线程池维护线程所允许的空闲时间。
  • unit: 线程池维护线程所允许的空闲时间的单位。
  • workQueue: 线程池所使用的缓冲队列。
  • handler: 线程池对拒绝任务的处理策略。

(2)一个任务通过 execute(Runnable r ) 方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。

(3)当一个任务通过execute(Runnable)方法 欲添加到线程池时:

  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  • 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。

  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过
    handler所指定的策略来处理此任务。

处理任务的优先级为:

  • 核心线程池corePoolSize、任务队列workQueue、最大线程maximumPoolSize,若三者都满了,使用handler处理被拒绝的任务。

  • 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

(4)unit 可选的参数为java.util.concurrent.TimeUnit中的几个静态属性: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。

(5)workQueue 常用的可以是:java.util.concurrent.ArrayBlockingQueue

(6)拒绝任务的处理策略,handler有四个常用选择和自定义策略:

  • ThreadPoolExecutor.AbortPolicy():抛出java.util.concurrent.RejectedExecutionException异常。
  • ThreadPoolExecutor.CallerRunsPolicy():重试添加当前的任务,他会自动重复调用execute()方法。
  • ThreadPoolExecutor.DiscardOldestPolicy():抛弃旧的任务 。
  • ThreadPoolExecutor.DiscardPolicy():抛弃当前的任务 。
  • 根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

三、共享内存的问题

3.1 竞态条件

所谓竞态条件是指,当多个线程访问和操作同一个对象时,最终执行结果和执行时序有关,可能正确也可能不正确。

代码实例:

public class CounterThread extends Thread{

    private static int counter = 0;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num = 1000;
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            threads[i] = new CounterThread();
            threads[i].start();
        }
        for (int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter);
    }
}

在这里插入图片描述
期望的结果是1000000,但实际执行结果每次都不一样。因为counter++这个操作不是原子操作,分为三个步骤:
1)取counter的当前值;
2)在当前值基础上加1;
3)将新值重新赋值给counter。
两个线程可能同时执行第一步,取到了相同的counter值,最终结果就可能与预期不一致。
如何解决这个问题?

  • 使用synchronize关键字;
  • 使用显示锁;
  • 使用原子变量;

3.2 内存可见性

多个内存可以共享和访问操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上能看到,甚至永远也看不到。

代码示例:

public class VisibilityDemo {
    private static boolean shutdown = false;
    static class HelloThread extends Thread {
        @Override
        public void run() {
            while (!shutdown) {

            }
            System.out.println("exit hello");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new HelloThread().start();
        Thread.sleep(1000L);
        shutdown = true;
        System.out.println("exit main");
    }
}

在这里插入图片描述
期望结果是两个线程都退出,但实际执行时,HelloThread永远都不会退出,也就是说shutdown永远为false,即使main线程已经更改为true。
这就是内存可见性问题。在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,稍后才会同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是两一个线程根本没有从内存读。
如何解决这个问题?

  • 使用volatile关键字。
  • 使用synchronize关键字或显示锁同步。

总结

本次学习笔记仅仅简单介绍了多线程是什么、为什么用、如何用,但是在实际场景中不单单只是使用,还需要了解同步和异步,多线程之间的协作通信等,接下来会继续学习多线程之间的同步协助等内容。


版权声明:本文为qq_23986927原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。