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方法。

收藏
评论加载中...