Java面试-多线程&并发篇

1. 说说Java中实现多线程有几种方法

创建线程的常用三种方式:

1. 继承Thread类
2. 实现Runnable接口
3. 实现Callable接口
4. 线程池方式创建

通过继承Thread类或者实现Runnable接口,Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,知识Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下。

  • 采用实现Runnable,Callable接口的方式创建线程的优缺点
    • 优点:线程类只是实现了Runnable或者Callable接口,还可以继承其他类。这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好地体现了面型对象的思想
    • 缺点:编程稍微复杂一点,如果需要访问当前线程,则必须使用Thread.currentThread()方法
  • 采用继承Thread类的方式创建线程的优缺点
    • 优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获取当前线程
    • 缺点:因为线程类已经继承了Thread类,Java语言是单线程的,所以就不能再继承其他父类了

2. 如何停止一个正在运行的线程

1. 使用退出标志,使线程正常退出,也就是当run方法完成后进程终止
2. 使用stop方法强制终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法
3. 使用interrupt方法中断线程

3. notify()和notifyAll()有什么区别?

notify可能会导致死锁,而notifyAll则不会

  • 任何时候只有一个线程可以获得锁,也就是说只有一个线程可以执行synchronized中的代码
  • 使用notifyall可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个
  • wait()应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如不满足,必须调用notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行

4. sleep()和wait()有什么区别?

  • sleep()方法属于Thread类中。而wait()方法属于Object类中
  • sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁
  • 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。

5. volatile是什么?可以保证有序性吗?

一旦一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
  • 禁止进行指令重排序
    • volatile不是原子性操作
    • 什么叫保证部分有序性?
      当程序执行到volatile变量的读操作或写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
      使用volatile一般用于标记状态量和单例模式的双检锁

6、Thread类中的start()和run()方法有什么区别

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

7、为什么wait,notify,notifyAll这些方法不在thread类里面?

明显的原因是Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的那个锁就不明显了,简单地说,由于wait,nitify和nitifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象

8. 为什么wait和notify方法要在同步块中调用?

  1. 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法
  2. 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常
  3. 还有一个原因就是为了避免wait和notify之间产生竞态条件
  • wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法
  • 在调用对象的notify()和notifyAll()方法之前,调用线程必须得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify或notifyAll()方法
  • 调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:“特殊状态已经被设置”。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)

9. Java中interrupted和isInterruptted方法的区别?

interrupted()和isInterrupted()的主要区别就是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其他线程的中断状态且不会改变中断状态标识。简单地说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有可能被其他线程调用中断来改变。

10.Java中synchronized和ReentrantLock有什么不同?

  1. 相似点:
    这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。
  2. 区别:
    这两种方式最大的区别就是对于synchronized来说,它是Java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
    synchronized进行编译,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,响应的,在执行monitorexit指令时会将锁计算器减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
    由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
    • 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相对于Synchronized来说可以避免出现死锁的情况
    • 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数时创建的非公平锁,但公平锁表现得性能不是很好
    • 锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象

11. 有三个线程T1,T2,T3,如何保证顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
实际上先启动三个线程中哪一个都行,因为在每个线程的run方法中用join方法限定了3个线程的执行顺序

12. SynchronizedMap和ConcurrentHashMap有什么区别?

SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException

13. 什么是线程安全

个人认为最好的解释就是:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的.
这个问题有值得一提的地方,就是线程安全也是有几个级别的:

  1. 不可变
    像String,Integer,Long这些,都是final类型的类,任何一个线程都改变不了他们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
  2. 绝对线程安全
    不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList,CopyOnWriteArraySet
  3. 相对线程安全
    相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add,remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制
  4. 线程非安全
    ArrayList,LinkedList,HashMap等都是线程非安全的锁

14. Thread类中的yield方法有什么作用?(礼让)

yield方法可以暂停当前正在执行的线程对象,让其他有相同优先级的线程执行。它是一个静态方法,而且只保证当前线程放弃CPU占用而不能保证使其他线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

15. Java线程池中submit()和execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中,而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其他线程池像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

16. 说一说自己对于synchronized关键字的了解

synchronized关键字解决的时多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰方法或者代码块在任一时刻只能有一个线程执行。另外,在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,Java的线程是映射到操作系统的原上层线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态切换到内核态,这个状态之间的转换需要相对比较长的事件,时间成本比较高,这也是为什么早期的synchronized效率低的原因。清醒的时Java6之后Java官方对JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化的很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁操作的开销。

17. 说说自己是怎么使用synchronized关键字?

修饰实例方法:作用域当前对象实例加锁,进入同步代码之前要获得对象实例的锁。
修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表名这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象锁。
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块之前要获得给定对象的锁。
总结:synchronized关键字加到static静态方法和synchronized(class)代码块上都是给Class类上锁。synchronized关键字加到实例方法上是给对象上锁。尽量不要使用synchronized(String a)因为JVM中,字符串常量池具有缓存功能

18. 什么是线程安全?Vector是一个线程安全类吗?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现失误。很显然你可以将集合类分为两组,线程安全和非线程安全的。Vector是使用同步方法来实现线程安全的,而和它相似的ArrayList不是线程安全的。

19. volatile关键字的作用?

一旦一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
- 禁止进行指令重排序
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞
- volatile仅能使用在变量级别;synchronized则可以使用在变量,方法和类级别的
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

20. 常用的线程池有哪些?

- newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照人物的提交顺序执行
- newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小
- newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小
- newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求

21. 简述以下你对线程池的理解

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

22. Java程序是如何执行的

我们日常的工作中都使用开发工具可以很方便的调试程序,或者是通过打包工具把项目打成jar包或者war包,放入Tomcat等web容器中就可以正常运行了,但有没有想过Java程序内部是如何运行的的?
其实不论是在开发工具中运行还是在Tomcat中运行,Java程序的执行流程基本都是相同的,它的执行流程如下:

  1. 先把Java代码编译成字节码,也就是把.java类型的文件编译成.class类型的文件。这个过程的大致执行流程:Java源代码->词法分析器->语法分析器->语义分析器->字符码生成器->最终生成字节码,其中任何一个节点执行失败就会造成编译失败
  2. 然后把class文件放置到Java虚拟机,这个虚拟机通常指的是Oracle官方自带的Hotspot JVM;
  3. Java虚拟机使用类加载器(Class Loader)装载class文件
  4. 类加载完成之后,会进行字节码校验,字节码校验通过之后JVM解释器会把字节码翻译成机器码交由操作系统执行。但并不是所有代码都是解释执行的,JVM对此做了优化。
    在这里插入图片描述

23. 锁的优化机制了解吗

  • 从JDK1.6版本以后,synchronized本身也在不断优化锁的机制,有些情况下他并不是一个很重量级的锁了。优化机制包括自适应锁,自旋锁,锁消除,锁粗化,轻量级锁和偏向锁
  • 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件下也是可能发生的。
  1. 自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所以没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转为内核态,自旋锁可以通过设置-XX:+UseSponong来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置
  2. 自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定的时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定
  3. 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除
  4. 锁粗化:锁粗化指的是有很多操作都是对同一个对象及逆行枷锁,就会把锁的同步范围扩展到整个操作序列之外。
  5. 偏向锁:当线程访问同步代码块获取锁时,会在对象头和战阵中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以通过设置-XX:+UseBiasedLocking开启偏向锁
  6. 轻量级锁:JVM的对象的对象头中包含一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
    偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程,其他线程全部阻塞

24、 说说进程和线程的区别?

  1. 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
  2. 线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其他资源(所以通信和同步等操作比进程更加容易)
  3. 线程上下文的切换比进程上下文切换要快很多
- 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置
- 线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作

25. 产生死锁的四个必要条件?

- 互斥条件:一个资源每次只能被一个线程使用
- 请求和保持条件:一个线程因请求资源而阻塞时,对以获得的资源保持不放
- 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

26. 如何避免死锁?

指定获得锁的顺序,举例如下:

1. 比如某个线程只有获得A锁和B锁才能对某资源进行操作,在多线程条件下,如何避免死锁?
	获得锁的顺序是一定的,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。

27. 线程池核心线程数怎么设置呢?

分为CPU密集型和IO密集型

  1. CPU
    这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下,多出来的一个线程就可以充分利用CPU的空闲时间。
  2. IO密集型
    这种任务应用起来,系统会用大部分的时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是:核心线程数=CPU核心数量*2

28. Java线程池中队列常用类型有哪些?

- ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
- LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue.
- PriorityBlockingQueue是一个具有优先级的无线阻塞队列。也是基于最小二叉堆实现
- DelayQueue
		- 只有当其指定的延迟时间到了,才能够从队列中获取到该元素
		- DelayQueue是一个没有大小限制的队列
		- 因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞

29. 线程安全需要保证几个基本特征?

  1. 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现
  2. 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为讲线程本地状态反映到内存上,volatile就是负责保证可见性的
  3. 有序性,是保证线程内串行语义,避免指令重排等。

30. 说一下线程之间是如何通信的?

线程之间的通信有两种方式:共享内存和消息传递

  1. 共享内存
    在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信
    例如上图线程A与线程B之间如果要通信的化,那么就必须经历下面两个步骤:
- 线程A把本地内存A更新过的共享变量刷新到住内存中去
- 线程B到主内存中去读取线程A之前更新过的共享变量
  1. 消息传递
    在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在Java中典型的消息传递方式,就是wait()和notify()或者BlockingQueue

31. CAS的原理?

CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:

  1. 变量内存地址,V表示
  2. 旧的预期值,A表示
  3. 准备设置的新值,B表示
    当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作

32. CAS有什么缺点嘛?

CAS的缺点主要有3点:

  1. ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。知识ABA的问题大部分场景下都不影响并发的最终效果
    Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于于其标志,全部相等的话才会更新
  2. 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销
  3. 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过Atomic Reference来处理或者使用锁synchronized实现

33. 引用类型有哪些?有什么区别?

引用类型主要分为强软弱虚四种:

1. 强引用指的就是代码中普遍存在的赋值方式。强引用关联的对象,永远不会被GC回收
2. 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
3. 弱引用可以用Weak Reference来描述,它的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
4. 虚引用也被称为幻影引用,是最弱的引用关系,可以用Phantom Reference来描述,他必须和Reference Queue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存

34. 说说ThreadLocal原理

  • ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间
  • ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力
  • 弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收
  • 但是这样还是会存在内存泄漏的问题,假如key和ThreadLocal对象被回收之后,entry终究存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行
  • 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的

35. 线程池原理知道吗?以及核心参数

首先线程池有几个核心的参数概念:

  1. 最大线程数maximumPoolSize
  2. 核心线程数corePoolSize
  3. 活跃时间keepAliveTime
  4. 阻塞队列workQueue
  5. 拒绝策略RejectedExecutionHandler
    当提交一个新任务到线程池时,具体的执行流程如下:
- 当我们提交任务,线程会根据corePoolSize大小创建若干任务数量线程执行任务
- 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
- 当阻塞队列也满了之后,那么会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
- 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理

36. 线程池的拒绝策略有哪些?

主要有4种拒绝策略:

1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2. CallerRunsPolicy:只用调用者所在的线程来处理任务
3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
4. DiscardPolicy:之间丢弃任务,也不抛出异常

37. 说说你对JMM内存模型的理解?为什么需要JMM?

随着CPU的内存的发展速度的差异问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1,L2,L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题没所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正式对多线程操作下的一系列规范约束,因为不可能让代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。
原子性:Java内存模型通过read,load,assign,use,store,write来保证原子性操作,此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令
可见性:可见性的问题在上面的回答已经说过,Java保证可见性可以认为通过volatile,synchronized,final来实现
有序性:由于处理器和编译器的重排序导致的有序性问题,Java通过volatile,synchronized来保证
happen-before规则
虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排作出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点:

1. 单线程每个操作,happen-before于该线程中任意后续操作
2. volatile写happen-before与后续对这个变量的读
3. synchronized解锁happen-before后续对这个锁的加锁
4. final变量的写happen-before于final域对象的读,happen-before后续对final变量的读
5. 传递性规则,A先于B,B先于C,那么A一定先于C发生

说了半天,到底工作内存和主内存是什么?
主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的

38. 多线程有什么用?

  1. 发挥多核CPU的优势
    单核CPU上所谓的多线程那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换的比较快,看着像多个线程“同时”运行罢了。多核CPU上的多线程才是真正的多线程,他能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
  2. 防止阻塞
    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回有没有设置超时时间,那么你的整个程序在数据返回来之前就停止进行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其他任务的执行。
  3. 便于建模
    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B,任务C,任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那么就简单多了

39. 说说CyclicBarrier和CountDownLatch的区别?

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,两者的区别在于:

  1. CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
  2. CyclicBarrier只能唤起一个任务,CountDowmLatch可以唤起多个任务
  3. CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不会再用了

40. 什么是AQS

简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了
ReentrantLock,CountDownLatch,Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上从第一个Entry开始运行
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能

41. 了解Semaphore吗?

42. 什么是Callable和Future?

  • Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable
  • Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

43. 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值