面试官:小伙子,说一下多线程异步编排和线程池吧 您所在的位置:网站首页 线程和异步的区别在哪 面试官:小伙子,说一下多线程异步编排和线程池吧

面试官:小伙子,说一下多线程异步编排和线程池吧

#面试官:小伙子,说一下多线程异步编排和线程池吧| 来源: 网络整理| 查看: 265

1.java内存模型

注 : JAVA中的堆栈和内存模型:,

1.1内存模型:

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性来建立的(三个特性详解见10.) a.Java内存模型将内存分为了主内存和工作内存 b.Java内存模型规定所有的变量都存储在主内存中,每个线程有自己的工作内存 c.主内存主要包括:堆和方法区,主内存是所有线程共享的 d.工作内存主要包括:该线程私有的栈和对主内存部分变量拷贝的寄存器(包括程序计数器和cpu高速缓存区) e.Java内存模型规定了所有变量都存储在主内存中,每个线程有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存中的变量,不同线程之间也无法直接操作对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成,

1.2什么情况下线程栈中的数据会刷新呢主存中的变量?**

①当变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立刻将其刷新到主内存中。

②通过synchronized关键字能够保证可见性,synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存当中。JVM规范定义了线程对内存间交互的八种操作:(待补充)

1.3堆,栈,方法区

1.栈空间(stack),连续的存储空间,遵循后进先出的原则,存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中。); 当在一段代码块定义一个变量时,Java在栈中为这个变量分配内存空间,当该变量退出其作用域Ⅰ后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。 注:Ⅰ:变量的作用域:从变量定义的位置开始,到该变量所在的那对大括号结束 Ⅱ:变量周期性: 从变量定义的位置开始就在内存中活了;到达它所在的作用域的时候就在内存中消失了;

2.堆空间(heap),不连续的空间,用于存放new出的对象,或者说是类的实例;当引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。   实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针!

**3.堆与栈:**堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存 大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态 分配内存,存取速度较慢。

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享(int a = 3再int b = 3此时内存中值存在一个3,a,b两个引用同时指向同一个3)。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。对于栈和常量池中的对象Ⅰ可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定 ,具有很大的灵活性。 注:Ⅰ:用new来生成的对象都是放在堆中的,直接定义的局部变量都是放在栈中的,全局和静态的对象是放在数据段的静态存储区,例如: Class People;People p;//栈上分配内存People* pPeople;pPeople = new People;//堆上分配内存 对于字符串:其对象的引用都是存储在栈中的,如果是 编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才确定的就存储在堆中 。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

4.方法区(method),方法区在堆空间内,用于存放 ①类的代码信息;②静态变量和方法;③常量池(字符串常量和基本类型常量(public static final),具有共享机制);常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名;字段的名称和描述符;方法和名称和描述符。 Java中除了基本数据类型,其他的均是引用类型,包括类、数组等等。

2.Java 线程

2.1.进程和线程的区别是什么? 线程是操作系统能够进行运算调度的最小单位也是进程中的实际运作单位。一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而当前进程下的所有线程共享一片相同的内存空间。 每个线程都拥有单独的栈内存用来存储本地数据 .

2.2线程的几种可用状态。

线程在执行过程中,可以处于下面几种状态: 就绪(Runnable):线程准备运行,不一定立马就能开始执行。 运行中(Running):进程正在执行线程的代码。 等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。 睡眠中(Sleeping):线程被强制睡眠。 I/O 阻塞(Blocked on I/O):等待 I/O 操作完成。 同步阻塞(Blocked on Synchronization):等待获取锁。 死亡(Dead):线程完成了执行。

2.3.创建线程的几种不同的方式

有三种方式可以用来创建线程:

###2.3.1继承 Thread 类 :通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量。

public class ThreadTest extends Thread { private int ticket = 10; @Override public void run() { for (int i = 0; i < 10; i++) { synchronized (this) { if (this.ticket > 0) { try { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + "卖票---->" + (this.ticket--)); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public static void main(String[] arg) { ThreadTest t1 = new ThreadTest(); new Thread(t1, "线程1").start(); new Thread(t1, "线程2").start(); //也达到了资源共享的目的然而事实却不尽如此。 } } 2.3.2实现 Runnable 接口 : 这种方式更受欢迎,因为这不需要继承 Thread 类。在应用设计中已经继 承了别的对象的情况下,这需要多继承(而 Java 不支持多继承),只能实现接口。 public class RunnableTest implements Runnable { private int ticket = 10; @Override public void run() { for (int i = 0; i < 10; i++) { //添加同步快 synchronized (this) { if (this.ticket > 0) { try { //通过睡眠线程来模拟出最后一张票的抢票场景 Thread.sleep(100); System.out.println(Thread.currentThread().getName() + "卖票---->" + (this.ticket--)); } catch (Exception e) { e.printStackTrace(); } } } } } public static void main(String[] arg) { RunnableTest t1 = new RunnableTest(); new Thread(t1, "线程1").start(); new Thread(t1, "线程2").start(); } } 2.3.3 实现Callable接口 : 是Runnable接口的增强版,使用call()方法作为线程的执行体增强了之前的run()方法,因为call()方法有返回值,也可以声明抛出异常;

MyTask.java类

FutureTask使用方法:

Callable接口与Runnable接口对比:

1.Callable规定的方法是call(),而Runnable规定的方法是run(). 2.Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。 3.call() 方法可抛出异常,而run() 方法是不能抛出异常的。 运行Callable任务可拿到一个FutureTask对象, FutureTask表示异步计算的结果

3.线程池 3.1线程池介绍 :

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

#####3.2线程池的工作机制 : 在线程池的工作模式下,任务是整个提交给线程池的,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3.3使用线程池的原因 :

降低资源的消耗 通过重复利用已经创建好的线程降低线程创建和销毁带来的损耗 提高响应速度 线程池中的线程没有超过上限时,有线程处于等待分配任务的状态,当任务来时无需创建线程这一步骤就能直接执行。 提高线程的可管理性 线程池里提供了操作线程的方法,这就为对管理线程提供了可能性。

#####3.4四种常见的线程池详解 : 线程池的返回值ExecutorService: 是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程 我们在实际业务中,以上三种线程启动的方式都不用。 将所有的多线程异步任务都交给线程池

3.4.1 原生线程池

创建 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor();

原生线程池的七大参数 corePoolSize : 核心线程数(一致存在除非设置了allowThreadTimeOut),线程池创建好之后就等待来接受异步任务去执行。 maximumPoolSize : 最大线程数,控制资源并发 keepAliveTime : 存活时间,如果当前线程数量大于核心线程数,且线程空闲的时间大于指定的keepAliveTime就会释放线程(不会释放核心线程) unit : 指定存活时间的时间单位 BlockingQueue workQueue : 阻塞队列的最大数量(该值的大小有压力测试后的峰值决定),如果任务数大于maximumPoolSize,就会将任务放在队列里,只要有线程空闲就会去队列里去除新的任务执行。 ThreadFactory threadFactory : 线程的创建工厂。 RejectedExecutionHandler handler : 如果workQueue满了按照指定的拒绝策略拒绝执行任务

运行流程 1.线程池创建,准备好core数量的核心线程,准备接受任务。 2.新的任务进来用core准备好的空闲线程执行。 (1)如果core满了,就将再进来的任务放入阻塞队列中,空闲的core就会自己去阻塞队列获取任务执行 (2)如果阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数量 (3)max任务都执行好了。Max减去core的数量的空闲线程会在keepAliveTime 指定的时间后自动销毁。最终保持到core大小 (4)如果线程数开到max的数量还不够用就是用RejectedExecutionHandler 指定的拒绝策略进行处理。 3.所有的线程都是由指定的factory创建 面试提

3.4.2种常用的线程池(返回值都是ExecutorService)

3.4.2.1 Executors.newCacheThreadPool(): 可缓存线程池,core的数量为0,所有都可以回收, 先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务(运行结果见下汇总图).

线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程

3.4.2.2 Executors.newFixedThreadPool(int n):

创建一个可重用固定个数的线程池,core的数量为max,都不可以回收,以共享的无界队列方式来运行这些线程。(运行结果见下汇总图).

3.4.2.3 Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行(运行结果见下汇总图).

3.4.2.4 Executors.newSingleThreadExecutor():

创建一个单线程化的线程池,从阻塞队列里挨个获取任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO , LIFO,优先级)执行(运行结果见下汇总图).

以上的所有execute都可以使用submit代替,并且submit可以有返回值

4.CompletableFuture异步编排

4.1创建异步对象

CompletableFuture提供了四个静态方法来创建一个异步操作。 Supplier supplier : 参数为一个方法 Executor executor可以传入自定义线程池,否则使用自己默认的线程池

public static CompletableFuture runAsync(Runnable runnable) public static CompletableFuture runAsync(Runnable runnable, Executor executor) public static CompletableFuture supplyAsync(Supplier supplier) public static CompletableFuture supplyAsync(Supplier supplier, Executor executor)

给出一个例子

public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService future= Executors.newFixedThreadPool(10); CompletableFuture integerCompletableFuture = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程------------------" + Thread.currentThread().getName()); int i = 10 / 2; return i; }, future); //获取异步执行的结果在线程执任务行完之后返回 Integer integer = integerCompletableFuture.get(); System.out.println("结果为"+integer);//结果为5 } 4.2whenComplete(计算完成时回调方法)

CompletableFuture提供了四个方法计算完成时回调方法

public CompletableFuture whenComplete(BiConsumer other,Runnable action) public CompletableFuture runAfterBothAsync(CompletionStage other,Runnable action) public CompletableFuture runAfterBothAsync(CompletionStage other,Runnable action,Executor executor) public CompletableFuture thenAcceptBoth(CompletionStage other,Runnable action) public CompletableFuture runAfterEitherAsync(CompletionStage other,Runnable action) public CompletableFuture runAfterEitherAsync(CompletionStage other,Runnable action,Executor executor) public CompletableFuture acceptEither(CompletionStage... cfs) public static CompletableFuture anyOf(CompletableFuture... cfs)

allOf : 阻塞线程,等待所有任务完成,才继续往下进行否则阻塞 anyOf : 阻塞线程,只要有一个任务完成,就继续往下进行

给出一个例子

5.synchronized关键字与Lock 5.1 synchronized关键字:

Java 语言中,每个对象有一把锁。线程可以使用synchronized关键字获取对象上的锁。 同步代码块: synchronized(锁对象){ 需要同步的代码 } 此处的所对象必须是存在堆中(多个线程的共享资源)的对象 同步方法: 权限关键字 synchronized 返回值 方法名(){ 需要被同步的代码块 } 同步方法的锁对象是this 静态方法及锁对象问题: 锁对象是类的字节码文件对象(类的class文件)

5.2锁的释放时机:

① 当前线程的同步方法、代码块执行结束的时候释放 ② 当前线程在同步方法、同步代码块中遇到break、return 终于该代码块或者方法的时候释放。 ③ 当前线程出现未处理的error或者exception导致异常结束的时候释放。 ④ 程序执行了同步对象wait方法,当前线程暂停,释放锁。

5.3 lock和ReadWriteLock:

两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。

5.3.1Lock相较synchronized的优点:

①synchronized实现同步线程(IO读文件时)阻塞不释放锁时,其他线程需一直等待,Lock可以通过只等待一定的时间 (tryLock(long time, TimeUnit unit)) 或者能够响应中断(lockInterruptibly())解决。

②多个线程读写文件时,读1操作与读2操作不会起冲突synchronized实现的同步的话也只有一个线程在执行读1操作.读2操作需等待,Lock可以解决这种情况 (ReentrantReadWriteLock)。

③可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

5.3.2lock中的方法: Lock lock = new ReentranLock;

**lock();**用来获取锁。如果锁已被其他线程获取,则进行等待;必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,

**tryLock()😗*尝试获取锁,获取成功返回true;获取失败(锁已被其他线程获取),返回false,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)

**tryLock(long time, TimeUnit unit)😗*拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。拿到锁,则返回true。

**lockInterruptibly()😗*当通过这个方法去获取锁时,如果其他线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

**interrupt()**方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。

**unlock:**释放锁在finally语句块中执行。

5.3.3ReadWriteLock中的方法:ReadWriteLock rl= new ReentrantReadWriteLock();**

维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的。 rl.readLock();//返回Lock接口可通过Lock接口内方法获取锁 rl.writeLock();//返回Lock接口可通过Lock接口内方法获取锁

6.volatile 6.1volatile简介

volatile用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。

6.2volatile特性 volatile具有可见性、有序性,不具备原子性。

注意,volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异,这一点在面试中也是非常容易问到的点。 原子性:原子性通常指多个操作不存在只执行一部分的情况,要么全部执行要么全部失败 可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2…线程n能够立即读取到线程1修改后的值。 有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

那么可能的一个执行顺序是:语句2 -> 语句1 -> 语句3 -> 语句4 那么可不可能是这个执行顺序: 语句2 -> 语句1 -> 语句4 -> 语句3。 不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。重排序不会影响单个线程内程序执行的结果,但在多线程处理器不能保证

看完有什么不懂的欢迎在下方留言评论,都看到这了点个赞再走呗!


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有