1、创建线程的方式及实现
创建线程有多种方式,本质上只有一种,就是实现Runnable接口
实现Runnable接口
继承Thread类
实现Callable接口,通过FutureTask包装
匿名内部类的方式
lambda表达式的方式
线程池
定时器
2、如何保证线程安全
在Java中线程安全主要体现在三个方面:原子性、可见性及有序性
原子性:是指一个或多个操作,要么全部执行成功,并且在执行过程中不会被任何因素打断,要么全部不执行
可见性:是指多个线程访问同一个变量时,一个线程修改了这个变量,其他线程可以立即看到修改后的值
有序性:是指程序的执行顺序按照代码的先后顺序执行
对于原子性,Java内存模型保证了基本读取和赋值的原子性,对于大范围操作的原子性,可以使用synchronized和lock实现,同时也可以使用atomic包下的几个原子类(通过CAS的方式,调用Unsafe类的compareAndSwap方法,借助CPU指令cmpxchg来保证原子性)
对于可见性,可以使用volatile关键字或者synchronized或者lock
对于有序性,volatile可以保证一定的有序性,当然使用synchronized和lock也能保证有序性,在同一时刻只有一个线程执行同步代码,同时Java内存模型提供了一些先天的有序性,也就是happens-before原则:
程序顺序规则:在一个线程内,书写在前面的操作先行发生于书写在后面的操作
监视器锁规则:对于一个锁的解锁操作,先行发生于对同一个锁的加索操作
volatile变量规则:对于一个volatile变量的写操作,先行发生于对这个volatile变量的读操作
传递规则:A 先行发生于B,B先行发生于C,则A先行发生于C
线程start规则:Thread的start先行发生于对线程的任意后续操作
线程join规则:如果线程A调用ThreadB.join并成功返回,那么线程B中的任意操作先行发生join方法的返回
3、sleep() 、join()、yield()有什么区别
sleep是让当前线程暂停执行指定时间,不会释放对象锁
join方法是指主线程想等子线程结束之后再继续执行,底层调用的就是wait方法,会释放对象锁
yield是指让出CPU资源给其他线程使用,使得当前线程从运行状态变为可运行状态,但是并不能完全保证达到让步的目的,有可能还会被线程调度再次选中
4、volatile关键字原理
volatile关键字底层使用一个“lock;"前缀指令,这个指令相当于一个内存屏障,这个内存屏障会提供几个保证:一是确保内存屏障之前的代码不会被重排序到内存屏障之后,和内存屏障之后的代码不会被重排序到内存屏障之前;二是强制变量刷新回主内存中;三是当对一个volatile变量进行写操作时,会强制其他线程工作内存中的缓存数据失效
5、synchronized关键字原理
synchronized是针对对象进行加锁,在JVM中Java对象由对象头、实例数据和对齐填充三部分组成,对象头中的Mark Word 保存了锁标志位和指向monitor对象的起始地址,当monitor对象被某个线程占用后就处于锁定状态。
底层上,synchronized修饰方式时,会在方法修饰符上添加ACC_SYNCHRONIZED,修饰代码块时,会使用monitorenter和monitorexit指令。针对synchronized获取锁的方式,JVM采用了锁升级的优化方式,先使用偏向锁,优先同一线程获取锁,如果失败,就升级为轻量级锁,如果再失败,就进行短暂的自旋,防止线程被挂起,如果最后都失败,则升级为重量级锁。
6、CAS原理
CAS是基于乐观锁的机制,底层通过UNsafe的compareAndSwap几个方法进行变量的原子更新,源码中是借助CPU指令CMPXCHG指令或者LOCK + CMPXCHG指令来实现,它有三个基本操作变量,内存地址V,旧的预期值A,要修改的新值B,当修改一个变量的时候,只有当内存地址V对应的值和旧的预期值相等时,才会将内存地址V所对应的的值修改为新值,否则返回false。
CAS使用场景:读多写少
CAS缺点:
一个是ABA问题,还有一个就是在高并发情况下,如果一直循环更新不成功,则会导致CPU开销较大
ABA问题可以通过使用版本号或者标记位的方式,也就是使用atomic包下的AtomicStampedReference或者AtomicMarkableReference来解决ABA问题
(ABA问题,CAS操作值时,会检查变量有没有发生变化,如果没有发生变化则更新,如果一个变量值是A,变成了B,又变成了A,当CAS检查变量有没有发生变化时,发现变量没有发生变化则进行了更新,但是实际上变量发生了变化)
7、ThreadLocal原理
ThreadLocal为每一个线程提供一个独立的变量副本,使得每一个线程可以单独改变自己所拥有的变量副本,而不会影响到其他线程所对应的变量副本。
每个线程Thread内部都有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,ThreadLocalMap是ThreadLocal内部实现的一个自定义map类,是用来存储实际的变量副本的,key为当前ThreadLocal对象,value为变量副本。
我们调用ThreadLocal的get方法时,实际上是通过Thread.currentThread获取当前线程对象,然后根据当前ThreadLocal获取当前线程的共享变量,set、remove也是同样的道理。
关于ThreadLocalMap内部类的简单介绍
初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量
ThreadLocal内存泄漏问题
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决内存泄漏:在使用完线程共享变量后,显示调用ThreadLocal的remove方法
ThreadLocal使用场景:解决数据库连接、session管理等
8、线程池实现原理
当有任务提交到线程池时,先去判断当前线程池里的线程数目是否到达corePoolSize,如果没有则创建一个新的线程并执行任务,如果大于,则将任务添加到BlockedQueue任务缓存队列里,如果任务添加缓存队列成功,则该任务会等待空闲线程将其取出并执行,如果失败,则会尝试创建一个线程去执行该任务,如果当前线程池的线程数目大于maximumPoolSize,则会执行拒绝策略。
拒绝策略有四种:
AbortPolicy:丢弃任务并抛出RejectedExecutionException
CallerRunsPolicy:只要线程池未关闭,直接再调用者线程里,执行这个被丢弃的任务
DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的任务,然后尝试再次提交当前任务
DiscardPolicy:直接丢弃任务,不做任何处理
四种长用的线程池:
newFixedThreadPool
固定大小的线程池,corePoolSize与maximumPoolSize相等,使用LinkedBlockingQueue无界阻塞队列。当提交任务比较频繁的时,存在耗尽系统资源的问题。线程池空闲也不会释放空闲线程,还会占用一定系统资源,需要shutdown。
newSingleThreadPool
单个线程线程池,只有一个线程,corePoolSize和maximumPoolSize都为1,阻塞队列使用的是LinkedBlockingQueue,当有多个任务提交时,会被暂存到阻塞队列中,当线程空闲时就会去从队列中按照先入先出获取任务去执行
newCachedThreadPool
缓存线程池,其中corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,缓存线程默认存活时间为60s,阻塞队列使用的是SynchronousQueue,这个队列不会存储任务,总是会创建新的线程去执行任务
newScheduledThreadPool
定时线程池,可以周期性地去执行任务
9、AQS原理(https://javadoop.com/2017/07/20/AbstractQueuedSynchronizer)
AQS是多线程访问共享资源的同步器框架,内部维护着一个volatile int state(共享状态)和一个 FIFO 的CLH双向同步队列,当线程获取同步状态失败后,则会加入到这个 CLH 同步队列的对尾并一直保持着自旋。在 CLH 同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出 CLH 同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。
基于AQS的锁(比如ReentrantLock)原理大体是这样:
有一个state变量,初始值为0,假设当前线程为A,每当A获取一次锁,status++. 释放一次,status--.锁会记录当前持有的线程。
当A线程拥有锁的时候,status>0. B线程尝试获取锁的时候会对这个status有一个CAS(0,1)的操作,尝试几次失败后就挂起线程,进入一个等待队列。
如果A线程恰好释放,--status==0, A线程会去唤醒等待队列中第一个线程,即刚刚进入等待队列的B线程,B线程被唤醒之后回去检查这个status的值,尝试CAS(0,1),而如果这时恰好C线程也尝试去争抢这把锁
非公平锁实现:
C直接尝试对这个status CAS(0,1)操作,并成功改变了status的值,B线程获取锁失败,再次挂起,这就是非公平锁,B在C之前尝试获取锁,而最终是C抢到了锁。
公平锁:
C发现有线程在等待队列,直接将自己进入等待队列并挂起,B获取锁
AQS定义了两种资源共享方式:一个是独占方式Exclusive,一个是共享方式Share。自定义同步器通过实现tryAccquire()、tryRelease()、tryAccquireShare()、tryReleaseShare()来实现不同类型的资源共享方式。
独占型:ReentrantLock
共享型:CountdownLatch、Semphore
组合型(共享+独占):ReentrantReadWriteLock
10、CountdownLatch原理
CountdownLatch是一个并发工具类,它允许一个线程等待其他线程执行结束后继续执行。通过使用AQS中的同步状态state来进行计数。通过构造函数传递计数器的值,该计数器的值也就是要等待的线程数,然后将CountdownLatch实例传递给每一个线程,每个线程执行结束后调用countDown()方法进行计数减1,主调线程通过调用await方法进行阻塞等待,当计数器的值为0时,表明子线程都已经执行完毕,主线程可以恢复继续执行。
使用场景:适用一个任务需要等待其他任务执行完毕,方可执行的场景
11、CyclicBarrier原理
CyclicBarrier字面理解就是可循环的屏障,它让一组线程到达屏障时被阻塞,直到最后一个线程到达屏障后才开门,所有被屏障拦截的线程才会继续执行。通过加计数的方式,调用await方法进行计数加1,也可以通过reset方法进行重置。
使用场景:可以用于多线程进行计算,最后合并数据的场景
12、CountdownLatch与CyclicBarrier区别
CountdownLatch是通过减计数的方式,并且不能重复使用,而CyclicBarrier是通过加计数的方式,可以处理更复杂的业务场景,如可以重复使用,获取阻塞的线程数量,判断阻塞的线程是否被中断等
13、信号量Semaphore原理
信号量Semphore也是一个并发工具类,通过控制一定量的许可证的数量,来达到限制访问资源的目的。计数器的值就是允许同时运行线程的数量,通过acquire方法获取一个许可证,计数器的值就会减1,通过release方法归还一个许可证,计数器的值就会加1,还可使用tryAcquire尝试获取许可证,如果当前没有可用的许可,则线程会一直阻塞等待,直到有可用的许可证。
使用场景:可以用于流量控制,特别是公共资源有限的应用场景,比如数据库链接
14、Exchanger原理(TODO 待后续了解)
用于进行线程间数据交换
15、Lock和Synchronized区别
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
16、什么是守护线程,有什么用
守护线程是在程序运行时,在后台提供的一种通用服务的线程。是用来服务用户线程的,当虚拟机中所有的用户线程都退出后,程序也就终止了,程序终止就会杀死所有的守护线程。
当你希望JVM退出后,线程自动关闭,那么就使用守护线程,守护线程通常用来执行一些后台任务
17、Thread和Runnable的区别是什么
准确的说创建线程的方式只能通过构造Thread类,实现线程执行单元的的方式有两种:一种是继承Thread类,一种是实现Runnable接口。Thread类本身也是实现了Runnable接口。Thread类的run方法和Runnable的run方法最重要的一点不同是,Thread类的run方法不能共享,举个例子就是说线程A不能把线程B的run方法当成自己的执行单元,而使用Runnable则很容易实现,因为一个Runnable可以构造多个不同的实例。Thread主要负责线程本身相关的职责和控制,而Runnable主要负责逻辑执行单元的部分。
18、Thread的run方法合start方法有什么区别
run方法是线程的执行单元,直接调用run方法,相当于只是调用了一个普通的方法,而start方法是启动线程,底层调用的是start0(),是一个JNI方法,调用start方法启动线程时,JVM会去创建一个新线程去调用run方法,换句话说,start0方法调用了run方法。