多线程部分
1.创建一个线程有哪些方式?
- 继承Thread类,重写run()方法。
- 实现Runnable接口,重写run()方法。
- 实现callable接口,重写call方法(可以获取线程的返回值、参数一些信息)。
2.拒绝策略有哪些?
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务但并不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。
3.线程的生命周期
启动一个线程的start()方法、新建、就绪、运行、阻塞、销毁
4.创建线程池的方式
- newSingleThreadExecutor():创建一个单一线程池(始终只有一个线程,实际情况下很少这么用)
- newCachedThreadPool():创建一个可缓存线程池(无限制线程的大小。弊端是init的最大值,会无限制创建线程,浪费资源,有系统崩溃的风险)
- newFixedThreadPool():创建一个定长线程池(自定义设置线程池的大小,在实际使用中稍微用的多一点)
- newSchedulThreadPool():创建一个定时线程池(可以指定时间跑任务,基本用不到)
5.线程池的底层原理
创建线程池的时候,开始一个线程都没有。随着任务的增加创建线程,当前线程数小于核心线程数的时候创建新线程,否则放入阻塞队列,阻塞队列未满继续放入。阻塞队列满了,判断最大线程,如果当前线程数小于最大线程,继续创建线程。如果当前线程数大于最大线程数,使用拒绝策略。
6.线程池参数有哪些?
- corePoolSize:线程池核心线程大小。
- maximumPoolSize:线程池最大线程数量。
- keepAliveTime:空闲线程存活时间。
- unit:空闲线程存活的时间单位。
- workQuene:工作队列。
- threadFactory:线程工厂。
- handler:拒绝策略。
7.如何设置线程池参数?
- CPU密集度:2*CPU核数(例如四核,核心线程数设置为8)
- IO密集度:3*CPU核数(线程池的大小设置的稍微大点,最大线程数必须大于核心线程数)
线程安全部分
1.解决线程安全的方式
- Synchronized(互斥锁。它是通过监视器jvm帮我们完成。在锁的代码片段加入monitorenter(进入)、monitorexit(退出)会把并发型线程改变为序列型线程,就是说拿到锁的会执行线程任务,其他线程只能等待,锁释放以后,其他线程挨个执行任务,性能比较低,本质上是一个悲观锁。锁的的对象,改变的是对象的对象头)
- Reentratlock(锁、AQS)
- atomic(通过CAS实现,是CPU的指令去完成,一般用不到)
- ThreadLocal(通过一个Map保存着各个线程的副本信息,每个共享的变量都在自己独立的工作空间运行)
- java还提供了一些线程安全的类。比如ConcurrentHashMap。
- volatile(1.保证各个线程的可见性 2.防止指令重排序,不能保证原子性,不能解决线程安全)
2.Synchronized的作用
- 原子性
- 可见性
- 可重入性
- 有序性
3.Synchronized的使用
- 修饰实例方法,对当前实例对象加锁
- 修饰静态方法,对当前的Class对象加锁
- 修饰代码块,对synchronized括号内的对象加锁
4.Synchronized底层原理
互斥锁。它是通过监视器jvm帮我们完成。在锁的代码片段加入monitorenter(进入)、monitorexit(退出)指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定或者当前线程已经拥有了那个对象锁,就会把锁的计算器加1。相应的,在执行monitorexit指令时,会将锁的计算器减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一线程释放为止。
5.Lock底层原理(AQS:AbstractQuenedSynchronizer)
- lock的存储结构:一个int类型状态值(用于锁的状态的变更),一个双向链表(用于存储等待中的线程)
- lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
- lock释放锁的过程:修改状态值,调整等待链表。
6.ThreadLocal是什么?
ThreadLocal是线程局部变量。线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程之间共享,是java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。(任何线程局部变量一旦在工作完成后没有释放,java应用就存在内存泄漏的风险。)
7.ThreadLocal底层原理
ThreadLocal类对ThreadLocalMap进行了封装,通过set()、get()、remove()【remove()方法可以处理value值强引用而引起的内存溢出问题(清除ThreadLocalMap中的Entry对象)】方法来实现对ThreadLocalMap操作。ThreadLocalMap中存放的键值对(key为Thread对象,value存放的Object[具体的数据]),这样当前的线程只能获取当前线程在ThreadLocalMap中对应key的value值,实现了多线程之间的数据隔离,从而实现了多线程安全。
8.Atomic底层原理
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程对单个(包括基本类型及引用类型)变量进行操作时,具有排他性。即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。
9.Synchronized和volatile的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要在主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别。
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程阻塞。
10.synchronized和lock的区别
- synchronized是java内置关键字;在jvm层面,lock是个java类。
- synchronized无法判断获取锁的状态;lock可以判断是否获取到锁。
- synchrnized会自动释放锁(线程执行完同步代码或执行过程中发生议程都会释放锁);lock需要在finally中手工释放锁(unlock()方法),否则容易造成线程死锁。
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2等待。如果线程1阻塞,线程2则会一直等待下去;lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
- synchronized的锁可重入、不可中断、非公平;lock锁可重入、可判断、可公平。
- synchronized锁适合代码少量的同步问题;lock锁适合大量同步的代码的同步问题。
11.AQS
AQS(AbstractQueneSynchronizer)抽象的队列式同步器。除了java自带的synchronized关键字之外的锁机制。
AQS基于CLH队列,用volatile修饰共享变量(state),线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
12.CAS的含义
CAS是compare and swap的缩写,即我们说的比较交换。
CAS是一种基于锁的操作,而且是乐观锁。(在java中分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问,而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。)
CAS包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存地址里面的值更新成B。
CAS是通过无限循环来获取数据。如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能有机会执行。
13.CAS的问题
- 容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过。解决方法是使用版本号标识,每次操作version加1。在java5中,已经提供了AtomicStamapedReference来解决问题。
- CAS造成CPU利用率增增加。CAS里面是一个循环判断的过程,如果一直没有获取到状态,CPU资源就会被一直占用。