栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > Java

多线程的基础知识

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程的基础知识

目录

1 线程

1.1 线程是什么?

1.2 线程的创建和使用

1.2.1 继承Thread类重写run()

1.2.2 实现Runnable接口实现run()

1.2.3 实现Callable接口实现call()

1.2.4 使用线程池创建

1.3 线程的生命周期

2 锁机制

2.1 Java三种锁机制

2.1.1 同步方法

2.1.2 同步代码块

2.1.3 同步锁

2.2 三种锁机制的区别

2.3 synchronized锁机制的原理

 2.3.1 同步方法

2.3.2 同步代码块

2.4 synchronized和同步锁

3 线程池

3.1 什么是线程池以及线程池的作用?

3.2 线程池的创建

3.2.1 通过普通传参方式

3.2.2 通过Executors工具类创建


1 线程

1.1 线程是什么?

        说到线程,我们首先需要先了解进程,什么是进程?通俗的说我们一个程序运行起来就是一个进程,它是操作系统分配系统资源的最小单位;而一个进程由一个或多个线程组成,线程是CPU进行任务分配和调度的最小单位。

        创建进程需要系统分配一块完成的内存区域,同一进程的多个线程之间堆和方法区式共享的,虚拟机栈、本地方法栈和程序计数器是私有的。

1.2 线程的创建和使用

1.2.1 继承Thread类重写run()

        继承Thread类重写run(),这种方法有一个弊端,继承父类不强制需要重写父类的方法,而且Java是单继承多实现的,如果继承Thread类表示将不能再继承别的类。创建方法如下图所示。

1.2.2 实现Runnable接口实现run()

        这种方法是除了线程池最常用的方法,他需要实现Runnable接口,实现接口必须强制实现里面的方法,然后他后续还能继续继承和实现其他类,而且Runnable接口里面只有一个run(),我们平时可以使用lambda表达式创建,很方便。创建方法如下图所示。

1.2.3 实现Callable接口实现call()

        这种方法是针对需要有返回值的,它里面也只有一个call(),但是它不能直接使用,需要使用其他类比如FutureTask来创建一个中间类,再将中间类传入Thread中创建线程,这种方法一般也很少使用。创建方法如下图所示。

1.2.4 使用线程池创建

        使用线程池创建是我们最常使用的方法,它可以使用new对象的形式创建,也可以使用Excutor工具类创建,但是后者阿里巴巴不推荐,具体的后面我会继续说明。创建方式在下面我会进行叙述。

1.3 线程的生命周期

        线程在被创建以后会进入新建状态,调用它的start()会进入就绪状态,由于线程之间的CPU资源是抢占式的,当线程抢占到CPU资源后会进入运行状态,在它运行过程中也可能被其他线程抢走CPU资源,当它的CPU资源被抢走以后会重新进入就绪状态,在它的程序计数器中会记录它当前运行的行号,等它下次抢占到CPU资源的时候会读取程序计数器中的行号继续执行,在运行过程中也可能因为锁的问题进入阻塞状态,比如sleep()、wait()、挂起线程、IO流的操作等都会让线程进入一个阻塞状态,需要特定的条件才能唤醒线程,让线程重新进入就绪状态,比如sleep()时间到了以后会自动恢复、notify()/notifyAll()唤醒wait()、恢复线程、执行完IO流操作等,当线程运行完以后会进入死亡状态,等待gc回收。

2 锁机制

2.1 Java三种锁机制

2.1.1 同步方法

        顾名思义,同步方法是作用在方法上的, 使用synchronized关键字进行修饰,如果修饰的是静态方法,它的锁对象为当前类.class,如果修饰的是普通方法,那么它的锁对象为this,通过锁对象的wait()进入阻塞状态,通过锁对象的notify()/notifyAll()唤醒线程,它在进入wait()方法时会自动释放锁。

2.1.2 同步代码块

        它的使用类似于同步方法,但它时使用在方法内部的,使用synchronized关键字修饰代码块,它的锁对象可以为任意对象,通过对象的wait()进入阻塞状态,对象的notify()/notifyAll()唤醒线程,它在进入wait()方法时也会自动释放锁。

2.1.3 同步锁

        同步锁的实现时通过Lock的lock()和unlock()实现的,而且两个方法是成双成对的出现的,如果加锁以后不进行解锁会出现死锁的情况,通常我们会将unlock()放在finally中来保证它的执行。

2.2 三种锁机制的区别

        1、粒度不同,同步方法>同步代码块>同步锁

        2、效率不同,同步锁>同步代码块>同步方法

        3、方便性不同,同步方法>同步代码块/同步锁

2.3 synchronized锁机制的原理

 2.3.1 同步方法

        在JVM常量池里面的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法,方法被调用时会先检查ACC_SYNCHRONIZED是否被设置了,如果设置了线程会先持有monitor,当方法执行完时会将释放monitor。

2.3.2 同步代码块

        执行同步代码块时会生成一个monitor监视器,它里面有一个计数器,当获取到锁对象时,会将计数器的count设为1,执行完代码块或执行wait()会将count减1,count为0时会释放锁;

        将class文件通过javap -c反编译成汇编语言后,里面有一个monitorEntry和monitorExit,执行monitorEntry时会尝试获取锁对象,count+1,执行monitorExit时会将count-1,count为0时释放锁。

2.4 synchronized和同步锁

        1、synchronized是Java关键字,而Lock是一个接口;

        2、synchronized会自动释放锁,Lock必须使用unlock()来释放锁,否则会出现死锁的情况;

        3、synchronized的性能小于Lock;

        4、synchronized无法获取锁,Lock可以通过tryLock()来尝试获取锁;

3 线程池

3.1 什么是线程池以及线程池的作用?

        关于线程池,提到池我们应该不陌生,最先想到的就是容器,简单点说,它就是管理线程的一个容器,主要进行线程的创建、调度和回收;

        作用:

                1、降低资源消耗;重复使用已创建的线程可以降低创建和销毁线程造成的资源消耗;

                2、提高响应速度;能够节约创建线程的等待时间;

                3、提高线程的可管理性;线程是稀缺资源,如果无限次的创建,不仅浪费系统资源,                         还会降低系统稳定性。

3.2 线程池的创建

3.2.1 通过普通传参方式
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

        上面代码是ThreadPoolExcutor类的构造方法之一,它有七大参数,也是创建线程池的七大参数,接下来我来一一解释。

        corePoolSize:核心线程的数量,也是线程池的初始线程数,核心线程默认是不会被线程池回收的,当然也可以设置allowCoreThreadTimeOut来允许核心线程被回收;

        maximumPoolSize:最大线程数,包括核心线程和非核心线程;

        keepAliveTime:保持存活时间,用来设置非核心线程的过期时间,如果非核心线程在此时间段没有接收到任务就会被线程池回收;

        unit:时间单位;

        workQueue:工作队列,这个会在后面列举几个常用的工作队列;

        threadFactory:线程工厂,用于创建线程;

        handler:拒绝策略,当任务数超过最大线程数时拒绝执行任务的方法。

3.2.2 通过Executors工具类创建

3.2.2.1、newCachedThreadPool 可缓存型线程池

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

        这是创建可缓存型线程池的源代码,根据线程池的七大参数,我们不难发现它的核心线程数为0;最大线程数为Integer.MAX_VALUE,也就是2^31,我们也可以直接说成无限制的,因为我们日常中不可能使用到2^31个线程,所以它的所有线程都是普通线程;而且它的存活时间为60s,也就是当线程60s内没有任务就会被线程池回收;它的工作队列为SynchronousQueue,它是一种不存储元素的阻塞队列,默认创建的时非公平的。

3.2.2.2、newScheduledThreadPool 可定时的线程池

 public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory, handler);
    }

        这是它底层的一部分代码,他需要我们传递一个核心线程数进去,最大线程数也是2^31,只不过它既有核心线程也有非核心线程;它的过期时间为0,一旦线程执行完任务就会被回收;使用的工作队列为DelayedWorkQueue优先队列;

 public ScheduledFuture scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (period <= 0)
            throw new IllegalArgumentException();
        ScheduledFutureTask sft =
            new ScheduledFutureTask(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(period));
        RunnableScheduledFuture t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

        这是newScheduledThreadPool的方法之一,initialDelay为第一次执行时需要等待的时间,period为每次执行需要等待的时间,如果前面有sleep(),会与sleep()的时间叠加。

public ScheduledFuture scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        if (command == null || unit == null)
            throw new NullPointerException();
        if (delay <= 0)
            throw new IllegalArgumentException();
        ScheduledFutureTask sft =
            new ScheduledFutureTask(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));
        RunnableScheduledFuture t = decorateTask(command, sft);
        sft.outerTask = t;
        delayedExecute(t);
        return t;
    }

        这是newScheduledThreadPool的另一个方法,他与上面的主要不同就是它的delay不会与sleep()叠加。

3.2.2.3、newSingleThreadExcutor 单线程线程池

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

        上面代码是其底层源码的一部分,我们可以看出它的底层也是基于ThreadPoolExcutor的,它的核心线程数为1,总的线程数也为1,所以他只有一个核心线程,它的工作队列为LinkedBlockingQueue,是一种基于链表的双向有界阻塞队列,最大为Integer.MAx_Value,按先进先出的顺序对元素进行排序。

3.2.2.4、 newFixedThreadPool 定长型线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

        他也是基于ThreadPoolExecutor的,它的最大线程数等于它的核心线程数等于传入的参数,工作队列也是LinkedBlockingQueue。

        其实Exectors的四种创建线程池的方式都是基于ThreadPoolExecutor的,只是它帮我们封装了一些代码,但它的灵活性不够高,更建议使用ThreadPoolExecutor来创建线程池。


3.3 线程的执行(源码)
3.3.1 执行execute
public void execute(Runnable command) {

        //如果传入的command为null;抛出异常
        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();

            //当前线程池不处于RUNNING状态,移除任务队列workQueue中的任务对象并执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);

            //当线程池的线程数为0时,添加一个非核心线程
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }

        //如果添加非核心线程失败执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

 

本文对线程、三种做机制以及线程池的概念做了一些梳理,如有错误的地方还请帮忙指出!

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1041210.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号