阅读 550

《面试补习》- 多线程知识梳理

一、基本概念

1.1、进程

进程是系统资源分配的最小单位。由 文本区域数据区域堆栈 组成。

  • 文本区域存储处理器执行的代码
  • 数据区域存储变量和进程执行期间使用的动态分配的内存;
  • 堆栈区域存储着活动过程调用的指令和本地变量。

涉及问题: cpu抢占内存分配(虚拟内存/物理内存),以及进程间通信

1.2、线程

线程是操作系统能够进行运算调度的最小单位。

一个进程可以包括多个线程,线程共用进程所分配到的资源空间

涉及问题: 线程状态并发问题

1.3、协程

子例程: 某个主程序的一部分代码,也就是指某个方法,函数。

维基百科:执行过程类似于 子例程 ,有自己的上下文,但是其切换由自己控制。

1.4、常见问题

  • 1、进程和线程的区别
进程拥有自己的资源空间,而线程需要依赖于进程进行资源的分配,才能执行相应的任务。
进程间通信需要依赖于 管道,共享内存,信号(量)和消息队列等方式。
线程不安全,容易导致进程崩溃等
复制代码
  • 2、什么是多线程
线程是运算调度的最小单位,即每个处理器在某个时间点上只能处理一个线程任务调度。
在多核cpu 上,为了提高我们cpu的使用率,从而引出了多线程的实现。
通过多个线程任务并发调度,实现任务的并发执行。也就是我们所说的多线程任务执行。
复制代码

二、Thread

2.1、使用多线程

2.1.1、继承 Thread 类

class JayThread extends Thread{
    @Override
    public void run(){
        System.out.println("hello world in JayThread!");
    }
}

class Main{
    public static void main(String[] args){
        JayThread t1 = new JayThread();
        t1.start();
    }
}

复制代码

2.1.2、实现 Runnable 接口

class JayRunnable implements Runnable{
    
    @Override
    public void run(){
        System.out.println("hello world in JayRunnable!")
    }
}


class Main{
    public static void main(String[] args){
        JayRunnable runnable = new JayRunnable();
        Thread t1 = new Thread(runnable);
        t1.start();
    }
}

复制代码

2.1.3、实现 Callable 接口

class JayCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("run in JayCallable " + Thread.currentThread().getName());
        return "Jayce";
    }
}


class Main{
    public static void main(String[] args) {
         Thread.currentThread().setName("main thread");
         ThreadPoolExecutor executor =new ThreadPoolExecutor(10,20,60, TimeUnit.SECONDS,new     ArrayBlockingQueue<>(10));
         Future<String> future = executor.submit(new JayCallable());
         try {
                future.get(10, TimeUnit.SECONDS);
         }catch (Exception e){
                System.out.println("任务执行超时");
            }
       }
}
复制代码

2.1.4、常见问题

  • 1、使用多线程有哪些方式

常用的方式主要由上述3种,需要注意的是 使用 ,而不是创建线程,从实现的代码我们可以看到,Java 创建线程只有一种方式, 就是通过 new Thread() 的方式进行创建线程。

  • 2、Thread(),Runnable()Callable()之间的区别

Thread 需要继承,重写run()方法,对拓展不友好,一个类即一个线程任务。

Runnbale 通过接口的方式,可以实现多个接口,继承父类。需要创建一个线程进行装载任务执行。

Callable JDK1.5 后引入, 解决 Runnable 不能返回结果或抛出异常的问题。需要结合 ThreadPoolExecutor 使用。

  • 3、Thread.run()Thread.start() 的区别

Thread.run()

    public static void main(String[] args){
        Thread.currentThread().setName("main thread");
        Thread t1 = new Thread(()->{
            System.out.println("run in "+Thread.currentThread().getName());
        });
        t1.setName("Jayce Thread");
        t1.run();
    }
复制代码

输出结果:

Thread.start()

    public static void main(String[] args){
        Thread.currentThread().setName("main thread");
        Thread t1 = new Thread(()->{
            System.out.println("run in "+Thread.currentThread().getName());
        });
        t1.setName("Jayce Thread");
        t1.start();
    }
复制代码

输出结果:

start() 方法来启动线程,使当前任务进入 cpu 等待队列(进入就绪状态,等待cpu分片),获取分片后执行run方法。

run() 方法执行,会被解析成一个普通方法的调用,直接在当前线程执行。
复制代码

2.2、线程状态

线程状态,也称为线程的生命周期, 主要可以分为: 新建就绪运行死亡堵塞等五个阶段。

图片引用 芋道源码

2.2.1 新建

新建状态比较好理解, 就是我们调用 new Thread() 的时候所创建的线程类。

2.2.2 就绪

就绪状态指得是:

1、当调用 Thread.start 时,线程可以开始执行, 但是需要等待获取 cpu 资源。区别于 Thread.run 方法,run 方法是直接在当前线程进行执行,沿用其 cpu 资源。

2、运行状态下,cpu 资源使用完后,重新进入就绪状态,重新等待获取cpu 资源. 从图中可以看到,可以直接调用Thread.yield 放弃当前的 cpu资源,进入就绪状态。让其他优先级更高的任务优先执行。

2.2.3 运行

步骤2 就绪状态中,获取到 cpu资源 后,进入到运行状态, 执行对应的任务,也就是我们实现的 run() 方法。

2.2.4 结束

1、正常任务执行完成,run() 方法执行完毕

2、异常退出,程序抛出异常,没有捕获

2.2.5 阻塞

阻塞主要分为: io等待,锁等待,线程等待 这几种方式。通过上述图片可以直观的看到。

io等待: 等待用户输入,让出cpu资源,等用户操作完成后(io就绪),重新进入就绪状态。

锁等待:同步代码块需要等待获取锁,才能进入就绪状态

线程等待: sleep()join()wait()/notify() 方法都是等待线程状态的阻塞(可以理解成当前线程的状态受别的线程影响)

二、线程池

2.1 池化技术

池化技术,主要是为了减少每次资源的创建,销毁所带来的损耗,通过资源的重复利用提高资源利用率而实现的一种技术方案。常见的例如: 数据库连接池,http连接池以及线程池等。都是通过池同一管理,重复利用,从而提高资源的利用率。

使用线程池的好处:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.2 线程池创建

2.2.1 Executors (不建议)

Executors 可以比较快捷的帮我们创建类似 FixedThreadPool ,CachedThreadPool 等类型的线程池。

// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor();
// 创建固定数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 创建带缓存的线程池
public static ExecutorService newCachedThreadPool();
// 创建定时调度的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 创建流式(fork-join)线程池
public static ExecutorService newWorkStealingPool();
复制代码

存在的弊端:

FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。

CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

复制代码

2.2.2 ThreadPoolExecuotr

构造函数:

       public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
复制代码

几个核心的参数:

  • 1、corePoolSize: 核心线程数
  • 2、maximumPoolSize: 最大线程数
  • 3、keepAliveTime: 线程空闲存活时间
  • 4、unit: 时间单位
  • 5、workQueue: 等待队列
  • 6、threadFactory: 线程工厂
  • 7、handler: 拒绝策略

与上述的 ExecutorService.newSingleThreadExecutor 等多个api进行对比,可以比较容易的区分出底层的实现是依赖于 BlockingQueue 的不同而定义的线程池。

主要由以下几种的阻塞队列:

  • 1、ArrayBlockingQueue,队列是有界的,基于数组实现的阻塞队列
  • 2、LinkedBlockingQueue,队列可以有界,也可以无界。基于链表实现的阻塞队列 对应了: Executors.newFixedThreadPool()的实现。
  • 3、SynchronousQueue,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态。对应了:Executors.newCachedThreadPool()的实现。
  • 4、PriorityBlockingQueue,带优先级的无界阻塞队列

拒绝策略主要有以下4种:

  • 1、CallerRunsPolicy : 在调用者线程执行
  • 2、AbortPolicy : 直接抛出RejectedExecutionException异常
  • 3、DiscardPolicy : 任务直接丢弃,不做任何处理
  • 4、DiscardOldestPolicy : 丢弃队列里最旧的那个任务,再尝试执行当前任务

2.3 线程池提交任务

往线程池中提交任务,主要有两种方法,execute()submit()

1、 execute()

无返回结果,直接执行任务

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.execute(() -> System.out.println("hello"));
}
复制代码

2、submit()

submit() 会返回一个 Future 对象,用于获取返回结果,常用的api 有 get()get(timeout,unit) 两种方式,常用于做限时处理

public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    Future<String> future = executor.submit(() -> {
        System.out.println("hello world! ");
        return "hello world!";
    });
    System.out.println("get result: " + future.get());
}
复制代码

三、线程工具类

3.1 ThreadlLocal

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

3.2 Semaphore

Semaphore ,是一种新的同步类,它是一个计数信号. 使用示例代码:

 // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        // 只能5个线程同时访问
        final Semaphore semp = new Semaphore(5);
        // 模拟20个客户端访问
        for (int index = 0; index < 50; index++) {
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 获取许可
                        semp.acquire();
                        System.out.println("Accessing: " + NO);
                        Thread.sleep((long) (Math.random() * 6000));
                        // 访问完后,释放
                        semp.release();
                        //availablePermits()指的是当前信号灯库中有多少个可以被使用
                        System.out.println("-----------------" + semp.availablePermits()); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();
复制代码

3.3 CountDownLatch

可以理解成是一个栅栏,需要等所有的线程都执行完成后,才能继续往下走。

CountDownLatch 默认的构造方法是 CountDownLatch(int count) ,其参数表示需要减少的计数,主线程调用 #await() 方法告诉 CountDownLatch 阻塞等待指定数量的计数被减少,然后其它线程调用 CountDownLatch#countDown() 方法,减小计数(不会阻塞)。等待计数被减少到零,主线程结束阻塞等待,继续往下执行。

3.4 CyclicBarrier

CyclicBarrierCountDownLatch 有点相似, 都是让线程都到达某个点,才能继续往下走, 有所不同的是 CyclicBarrier 是可以多次使用的。 示例代码:

  
        CyclicBarrier barrier;
        
        public TaskThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }
        
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(getName() + " 到达栅栏 A");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 A");
                
                Thread.sleep(2000);
                System.out.println(getName() + " 到达栅栏 B");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 B");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
复制代码

四、总结

最后贴一个新生的公众号 (Java 补习课),欢迎各位关注,主要会分享一下面试的内容(参考之前博主的文章),阿里的开源技术之类和阿里生活相关。 想要交流面试经验的,可以添加我的个人微信(Jayce-K)进群学习~