线程初步学习(二)

线程初步学习(二)——多线程

几种线程池

Executors类创建四种常见线程池

前面讲实现线程有四个方法(继承Thread,实现Runable,实现Callable,利用Executors类),第四种是利用Executor创建线程池,这里详细讲解Executor创建的四种常用的线程池。

首先线程池的概念,顾名思义,是可以装载多个线程的池子(容器)。创建一个线程池,需要线程的时候从池中获取。而获取到的线程使用完毕后也不会销毁,而是重新放入线程池中,从而减少了线程的创建和销毁的开销
在工具类Executors中,提供了一些静态方法用于创建线程池,下示四种:

  • newSingleThreadExecutor

创建一个单线程的线程池,这个线程池只有一个线程在工作,也就是单线程以串行的方式去执行所有任务。如果这个单线程因为异常结束,那么线程池会自动生成一个新的线程来代替它,以保证池中有一个单线程存在。

单线程池应用场景通常是需要保证所有任务按顺序执行的时候。

  • newFixedThreadExecutor

创建一个固定线程数的线程池。每次提交一个任务就创建一个线程,直到线程数量达到线程池的最大数。如果有某个线程执行任务时异常死亡,线程池会自动生成新的线程,以保证线程池中的最大线程数。

通常建议根据业务需求创建该类线程池,这样能获得更好的性能(过大的线程数会占用多余的内存,影响性能,可以用缓存线程池处理)。

  • newCachedThreadExecutor

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲线程(60秒不执行任务的线程),当任务数增加是,此线程池又可以智能的添加线程来处理任务。

此线程池不限制大小,线程池的大小完全依赖于操作系统(或者是JVM)能够创建的线程数量。

  • newScheduledThreadExecutor

创建一个无限大小的线程池,此线程池可以定时周期性的执行任务。

线程池的优点

  • 重用线程,降低线程创建和销毁的开销,降低资源损耗
  • 可有效控制并发线程数,提高资源的使用率。在一个任务到达时,可以无需等待线程创建就能执行,提高响应效率
  • 使用线程池对线程进行统一的分配和调优,提高线程的可管理性

线程池的状态

一个线程池有五种状态:

  • Running:这是正常运行状态,接受新的任务,处理等待队列中的任务。(营业)
  • ShutDown:不接受新的任务提交,但是会继续处理等待队列中的任务。(打烊)
  • Stop:不接受新的任务,不再处理队列中的任务,中断正在执行任务的线程。(关门)
  • Tidying:所有任务都销毁了,workCount为0,线程池的状态在转换为Tidying状态时,会执行钩子方法terminated()。
  • Terminated:terminated()方法结束后,线程池的状态就会变成这个。

线程池的execute和submit方法

  • execute只能执行Runable类型的任务,submit可以执行Runable和Callable类型的任务。
  • submit方法可以返回持有计算结果的Future对象(Callable具有返回值),而execute没有。
  • submit方便Exception处理。
  • 注意:这两个方法的区别主要来源于Runable与Callable的区别。Callable有返回值且允许捕获异常进行处理,但是Runable没有返回值,无法捕获异常。
    Callalbe接口的返回值需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

ThreadPoolExecutor创建线程池

百度 《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
禁用Executor肯定是因为有弊端:

  • newFixedThreadExecutor和newSingleThreadExecutor堆积的请求处理队列可能会耗费非常发的内存,严重会导致OOM。
  • newCachedThreadExecutor和newScheduledThreadExecutor线程的最大数是integer.MAX_VALUE,可能会创建特别多的线程,远远超出任务需求量,甚至OOM。

ThreadPoolExecutor创建线程池只有一种方式,就是走它的构造函数,参数自己指定。
ThreadPoolExecutor的构造参数
ThreadPoolExecutor​(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)

  1. int corePoolSize:核心线程数,线程数定义了最小可以运行的线程数量。
  2. int maximumPoolSize:允许线程池创建的最大线程数。
  3. long keepAliveTime:线程池中的线程数大于核心线程数时,如果没有新的任务提交,其余的线程会在等待keepAliveTime后才会被回收销毁。
  4. TimeUnit unit:keepAliveTime的时间单位。
  5. BlockingQueue workQueue:当新任务来的时候,会判断当前运行线程数是否达到核心线程数,如果达到,任务就会被存放在队列中。
  6. ThreadFactory threadFactory:为线程池提供创建新线程的线程工厂。
  7. RejectedExecutionHandler handler:线程池任务队列超过 maxinumPoolSize 之后的拒绝策略。
public class MyRunable implements Runnable{
    private String command;

    public MyRunable(String command) {
        this.command = command;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Start at "+ new Date().toString());
        //睡眠模拟任务时间
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" finish at "+ new Date().toString());
    }

    @Override
    public String toString() {
        return this.command;
    }

}

public class ThreadPoolExecutorDemo {
    private static final int CORE_POOL_SIZE=5;
    private static final int MAX_POOL_SIZE=10;
    private static final int QUEUE_CAPACITY=100;
    private static final Long KEEP_ALICE_TIME=1L;


    public static void main(String[] args) {
        /**
         *  CORE_POOL_SIZE:核心线程数5
         *  MAX_POOL_SIZE:最大线程数10
         *  KEEP_ALICE_TIME:等待时间1L
         *  TimeUnit.SECONDS:时间单位秒
         *  ArrayBlockingQueue<>(QUEUE_CAPACITY):最大队列容量100
         *  ThreadPoolExecutor.CallerRunsPolicy():饱和策略CallerRunsPolicy
         */
        ThreadPoolExecutor threadPoolExecutor=
                new ThreadPoolExecutor(
                            CORE_POOL_SIZE,
                            MAX_POOL_SIZE,
                            KEEP_ALICE_TIME,
                            TimeUnit.SECONDS,
                            new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                            new ThreadPoolExecutor.CallerRunsPolicy());

             System.out.println("demo start at "+new Date().toString());

            //模拟任务请求
            for(int i=0;i<10;i++){

                Runnable work = new MyRunable("-" + i);
                threadPoolExecutor.execute(work);
            }

            //终止线程池
            threadPoolExecutor.shutdown();

            while (!threadPoolExecutor.isTerminated()){
                //线程池不是Terminated状态就无法结束.
            }

        System.out.println("demo finish at "+new Date().toString());
    }

}

运行结果

demo start at Wed Nov 04 11:51:43 CST 2020
pool-1-thread-1 Start at Wed Nov 04 11:51:43 CST 2020
pool-1-thread-2 Start at Wed Nov 04 11:51:43 CST 2020
pool-1-thread-3 Start at Wed Nov 04 11:51:43 CST 2020
pool-1-thread-4 Start at Wed Nov 04 11:51:43 CST 2020
pool-1-thread-5 Start at Wed Nov 04 11:51:44 CST 2020
pool-1-thread-1 finish at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-1 Start at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-2 finish at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-2 Start at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-4 finish at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-4 Start at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-3 finish at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-3 Start at Wed Nov 04 11:51:46 CST 2020
pool-1-thread-5 finish at Wed Nov 04 11:51:47 CST 2020
pool-1-thread-5 Start at Wed Nov 04 11:51:47 CST 2020
pool-1-thread-1 finish at Wed Nov 04 11:51:49 CST 2020
pool-1-thread-2 finish at Wed Nov 04 11:51:49 CST 2020
pool-1-thread-4 finish at Wed Nov 04 11:51:49 CST 2020
pool-1-thread-3 finish at Wed Nov 04 11:51:49 CST 2020
pool-1-thread-5 finish at Wed Nov 04 11:51:50 CST 2020
demo finish at Wed Nov 04 11:51:50 CST 2020

可以看到,任务被start10次,同一时刻处于工作状态的线程数保持在5,每个工作时长被设定为三秒,单线程串行工作时长应该为30秒,而多线程工作总时长为7秒。

线程的调度

线程调度这部分应该写在第一部分线程学习中,但是考虑到线程调度的基础是多线程,便写在这里对照学习,方便理清思路。
前面说到多线程的运行规则是等待分配或者是抢占cpu时间片,那么是谁线程分配cpu时间片,先给谁分配,分配多少。

线程调度器(Thread Scheduler)

线程调度器是一个操作系统服务,他负责给Runnable就绪状态的线程分配cpu时间。一旦我们创建一个线程并启动它,它的执行便依赖线程调度器实现。

分配cpu时间可以基于线程的优先级或者是线程等待时间。另外,线程调度并不受java虚拟机控制,所以最好不要让你的程序依赖于线程的优先级,应该由应用程序来控制它。

线程调度相关方法

wait():使当前线程处于等待(阻塞)状态,并释放所持有的对象锁。
sleep():使当前线程处于睡眠状态,并不会释放当前线程所持有的锁。sleep是Thread的一个静态方法。
notify():唤醒一个正在处于等待状态的线程。调用此方法的时候并不能确切的唤醒某个等待线程,而是由jvm选择去唤醒谁,而且与优先级无关。
notifyAll():唤醒当前等待的所有线程,让他们去竞争获取锁对象。

  • 注意:上述的几种方法除了sleep以外,都是Object的方法,并且需要结合关键字synchronized关键字使用。

JVM模型和CAS

相信很多刚开始学习的同学都跟我一样对大佬口中的jvm总是越看越混乱,之前的学习都是自己在网上查询,解释各不相同,后来深入学习到多线程和内存缓存问题,才稍微明白一点jvm的构造。所以这里将jvm的简单组成和CAS对比记录,方便理解。

jvm主要组成部分

来自网络
这里简略说明一下运行时数据区域的作用,jvm其他涉及的知识另外学习。
当我们敲出一串代码,并运行时,jvm会将java文件编译成class文件,经过类加载器加载进jvm的数据区域

  • 方法区(Method Area):用于存储已被虚拟机加在的类信息,常量,静态变量,及时编译后的代码等数据。
  • java 堆(Heap):又叫堆内存,是jvm中内存最大的一块,堆被所有线程共享,几乎所有的对象实例都在这里分配内存。
  • java虚拟机栈(Stacks):用户存储局部变量表,操作数栈,动态链接,方法出口等信息。Stacks中有很多栈帧,每个栈帧都是一个线程的线程栈。每个线程都会有自己的线程栈,自己的寄存器,自己的本地存储。(这里寄存器和本地存储不做概念理解)
  • 程序计数器(Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能,都需要依赖这个计数器来完成。
  • 本地方法栈暂时不做学习。
 public static void main(String[] args) {
        Object o = new Object();
    }

这里举个例子,当main方法开始执行时,被创建的object实例会被存放在堆内存Heap中,作为形参的o被装载进栈Stacks。
如果此时有一个线程1去修改o的值,会先进入cpu缓存修改 ==》java栈 ==》最后修改堆内存的值。
而此时如果在这个过程中有另外一个线程2去读取o的值(读取堆内存),就可能发生读取到数据的同时cpu缓存中有另外一个新值,从而读取完后内存值被修改,造成不一致,这就是我们一直讨论的线程安全问题

什么是CAS

cas是 compare and swap 的缩写,顾名思义,比较交换。

cas是基于锁的一种操作,而且是乐观锁。

乐观锁和悲观锁:悲观锁是认为总在发生线程安全问题,所以会将资源锁住,等一个之前获取锁的线程释放锁之后,下一个线程才可以访问。而乐观锁则是相反,通过某种方式不加锁来处理资源,比如比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

cas的操作有三个重要参数,内存位置V,预期原值A,新值B
之前我们讲jvm模型提到过,当一个线程在数值被修改的同时去读取数据,就会造成线程安全问题。这里CAS很好的解决了这个问题。

CAS会去判断当前内存地址V中存储的值跟A的值是否相等,如果是相等的,那就说明之前没有线程去修改过,便可以将内存地址的值修改成B。

CAS是通过无限循环来获取数据的。例如线程s想去获取的内存地址的值被线程H修改了,那么compare之后不通过的线程a就会自旋,通过循环去重新获取这个内存地址V的值并重新计算新值B,再完成修改。

CAS的缺点

cas虽然高效率的解决了线程原子操作问题,但是也存在很明显的弊端。

  • 自旋时间过长的话cpu开销很大
  • 只能保证一个变量的原子操作,当线程想操作多个变量时,循环CAS就无法保证原子性操作。(用锁解决)
  • ABA问题。一个线程将值A修改为B又修改回A,下一个线程无法判断变量是否被修改过。(增加版本号)

线程并发——synchronized

在java中,synchronized使用来控制线程同步的,就是在多线程的环境下,控制synchronized代码块不被多个线程同时执行。
synchronized可以修饰类,方法,变量。
synchronized属于悲观锁。

synchronized的主要使用方法

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获取对象实例的锁。
  • 修饰静态方法:给当前类加锁。static是该类的静态资源,不管new了多少个对象,被static修的只有一份。所以如果一个线程A调用一个实例对象的非静态synchronized方法,而同时另一个线程B调用该实例对象所属类的静态synchronized方法,是允许的。因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块:指定加锁对象,进步代码块之前必需持有指定对象锁。

下面贴一个经典单例模式;

双重校验锁实现对象单例(线程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
       
        if (uniqueInstance == null) {//1
            //类对象加锁
            synchronized (Singleton.class) {//2
                if (uniqueInstance == null) {//3
                    uniqueInstance = new Singleton();//4
                }
            }
        }
        return uniqueInstance;
    }
}

假设有线程A,B去访问getUniqueInstance方法,A在1处判断通过到达2获取到Singleton的类锁(Singleton.class),而A此时没来得及到达3和4。
此时B来到1处,判断通过,在2处等待A完成3,4操作释放Singleton.class锁。
这里注意,双重验证在于3处,如果没有3的验证,此时获取到锁的B会直接到4,new出第二个“单例”。

lock体系和AQS

java下整个Lock接口下实现的锁机制是通过AQS和condition实现的 。

Lock是一个接口。整体来说,Lock可以看做synchronized的扩展版,他提供了更具扩展性的锁操作。
Lock的优势

  1. 可以使锁更公平
  2. 可以让线程在等待锁的时候响应中断
  3. 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  4. 可以在不同的范围,以不同的顺序获取锁

AQS简介

AQS(AbstactQueuedSynchronizer),在java.util.concurrent.locks包下。
AQS的设计基于方法模式的,也就是说,使用者需要继承同步器并重写指定方法,随后将同步器组合在自定义同步组件的实现中。

上面这段话其实我也不是很理解,应该很多初学者也都是云里雾里,我们直接跳过,继续看下面。

AQS的核心思想

一个线程去请求资源,如果资源是空闲状态,则将请求当前资源的线程设置为有效线程,并将被请求的共享资源锁定。
如果资源不是空闲状态(被占用),那么线程就会阻塞。此时就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制就是AQS用CLH队列锁实现的。——即将暂时获取不到锁的线程加入队列中,封装为CLH队列中的一个节点。

AQS使用一个int成员变量来表示同步状态,用过内置的FIFO队列来完成获取资源线程的排队工作。
AQS使用CAS对该同步状态进行原子操作实现对其值的修改

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
来自网络

以上是线程请求锁分配机制介绍;将等待状态的线程封装为CLH的一个节点,被唤醒之前,会经常使用自旋(while)的方式不停的尝试去获取锁,直到被其他线程唤醒成功获取到锁为止。——由此可见,AQS是自旋锁

AQS的资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁
    • 公平锁:按照线程在队列中的排序顺序,先到者先拿锁。
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁。
  • share(共享):多个线程可以同时执行,如Semaphore(用于控制对某组资源的访问权限)、CountDownLatch(线程A等一组线程全部执行完后执行),Cyclicbarrier(一组线程等待直某一状态一起执行)等。

有关lock体系尚未学习完全,后续完善。

ReentrantLock(可重入锁)

一个线程可以获取到自己已占用的所资源。

ThreadLocal(线程副本变量)

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。
应用场景:比如数据库连接(connection),每个线程都需要,但又不相互影响,就用ThreadLocal实现。

有关ThreadLocal就不做过多介绍了,直接po代码作用。

public class ThreadLocalDemo {

    //模拟银行存取款,创建银行对象
    static class Blank{
        //每个账户的初始金额10
       private ThreadLocal<Integer> threadLocal=  new ThreadLocal<Integer>(){
           @Override
           protected Integer initialValue() {
               return 10;
           }
       };

       public Integer get(){
           return threadLocal.get();
       }

        public void set(Integer i){
            threadLocal.set(threadLocal.get()+i);
        }
    }

//创建转账对象
    static class Tranfer implements Runnable{
        private Blank blank;

        public Tranfer(Blank blank) {
            this.blank = blank;
        }

        @Override
        public void run() {
            for(int i = 0;i<10;i++) {
                blank.set(10);
                System.out.println(Thread.currentThread().getName()+"账户余额:"+blank.get());
            }
        }
    }

    public static void main(String[] args) {
        Blank blank = new Blank();
        Tranfer tranfer = new Tranfer(blank);

        new Thread(tranfer,"A客户").start();
        new Thread(tranfer,"B客户").start();

    }


}

A客户账户余额:20
A客户账户余额:30
A客户账户余额:40
A客户账户余额:50
A客户账户余额:60
A客户账户余额:70
A客户账户余额:80
A客户账户余额:90
A客户账户余额:100
A客户账户余额:110
B客户账户余额:20
B客户账户余额:30
B客户账户余额:40
B客户账户余额:50
B客户账户余额:60
B客户账户余额:70
B客户账户余额:80
B客户账户余额:90
B客户账户余额:100
B客户账户余额:110

上面模拟银行转账,两个用户转账之间相互不影响,各自初始金额10,最终余额110.由此可见,使用ThreadLocal维护每个线程副本变量成功。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值