1. Thread类的常见方法
- void start() 使该线程开始执行
- void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
- getPriority() 返回线程的优先级
2. 线程和进程区别
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1-n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
3. 多线程读脏数据
两个线程同时对一个数据进行访问,此时一个线程修改了该数据的值,而另一个进程读取了该数据。但是因为某种突发的情况,导致刚刚的数据操作失败,进行了回滚。而刚刚读取数据的进程读取的数据就是一个错误的数据,在这里就叫做脏数据
解决的方法:在进行数据修改前加上锁
4. 什么是进程?进程和程序有区别吗?
- 进程是系统进行资源分配和调度的一个独立单位
- 程序是指令的有序集合,是一个静态概念;进程是程序在处理机上的一次处理过程,是一个动态的概念
- 没有一一对应的关系,不同的进程可以包含同一程序,同一程序在执行中也可以产生多个进程
5. synchronized锁升级
在jdk1.6后,Java对synchronize锁进行了升级过程,主要包含偏向锁、轻量级锁和重量级锁,主要是针对对象头MarkWord的变化
5.1 偏向锁
为什么要引入偏向锁:大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁
偏向锁的升级:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致
- 如果一致,则无需使用CAS来加锁、解锁
- 如果不一致,那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁。如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程
5.2 轻量级锁
为什么要引入轻量级锁:考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放
自旋:如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁
轻量级锁什么时候升级为重量级锁:
- 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间,然后使用CAS(下面会进行解释)把对象头中的内容替换为线程1存储的锁记录的地址
- 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2 CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁
6. 什么是线程安全,如何实现线程安全
6.1 线程安全
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的
6.2 如何实现线程安全
6.2.1 互斥同步
指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方式。Java中实现互斥同步的手段主要有synchronized关键字或ReentrantLock等
ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区
Synchronized:
- 加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活
- 需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
- 可以实现公平锁,公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权
- 可响应中断,当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法
6.2.2 非阻塞同步
类似是一种乐观并发的策略,比如CAS
CAS:compare and swap - 比较并交换
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作
6.2.3 无同步方案
比如使用ThreadLocal
是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题
ThreadLocal提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
7. synsynchronized和ReentLock的区别
相同点:
- 都是可重入锁(某个线程已经获得某个锁,可以再次获取锁而不会出现死锁)
- 保证了可见性和互斥性
- 可以用于控制多线程对共享对象的访问
不同点:
- ReentrantLock等待可中断
- synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但是可以通过修改参数来实现公平锁
- ReentrantLock绑定多个条件
- synchronized是Java中的关键字是JVM级别的锁,而ReentrantLock是一个Lock接口下的实现类,是API层面的锁
- synchronized隐式获取锁和释放锁,ReentrantLock显示获取和释放锁,在使用时要避免程序异常无法释放锁,需要在finally控制块中进行解锁操作
8. synchronized和volatile的区别
- volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块
- volatile关键字能保证数据的可见性(一个线程对共享变量的修改,能及时的被其他线程看到) ,但不能保证数据的原子性(即不可再分了,不能分为多步操作) 。synchronized 关键字两者都能保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
9. synchronize锁的作用范围
- synchronize作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象
- synchronize作用于静态方法时,锁住的是Class实例
- synchronize作用于一个代码块时,锁住的是所有代码块中配置的对象
10. Java中线程间通信方式
- 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制
- 信号量(Semphares):允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
- 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作
11. Java中线程的状态有哪些
12. AQS
AQS是一个抽象队列同步器,通过维护一个状态标志位state和一个先进先出的(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架
AQS的原理:给每个共享资源都设置一个共享锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果没有获取到共享锁,该线程被放入到等待队列中,等待下一次资源调度
资源共享方式:
- 独占式:只有一个线程能执行,具体的Java实现有ReentrantLock
- 共享式 :多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch
13. CAS带来的问题是什么?如何解决的?
问题:ABA问题、循环时间长开销很大、只能保证一个共享变量的原子操作
解决:乐观锁每次在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加1操作,否则执行失败
14. 什么是乐观锁,什么是悲观锁?
悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题
乐观锁:获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新
15. 创建线程的方式
- 继承Thread类,重写run方法
- 重写Runable接口,重写run方法
- 重写Callable接口,重写call方法
- 使用线程池
16. 线程池
16.1 好处
- 降低资源消耗:反复创建线程是一件很消耗资源的事,利用已创建的线程降低线程创建和销毁造成的消耗
- 提供处理速度:当任务到达时,可以直接使用已有线程,不必等到线程创建完成才去执行
- 线程资源可管理性
- 通过控制系统的最大并发数,以保证系统高效且安全的运行
16.2 核心参数
- corePoolSize:核心线程数
- maximumPoolSize:线程池中最大线程数
- keepAliveTime:多余空闲线程数的存活时间,当前线程数大于corePoolSize,并且等待时间大于keepAliveTime,多于线程或被销毁直到剩下corePoolSize为止
- TimeUnit unit: keepAliveTime的单位
- workQueue:阻塞队列,被提交但未必执行的任务
- threadFactory:用于创建线程池中工作线程的线程工厂,一般用默认的;
- handler:拒绝策略,当堵塞队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)
16.3 执行流程
- 当线程数小于核心线程数的时候,使用核心线程数
- 如果核心线程数小于线程数,就将多余的线程放入任务队列(阻塞队列)中
- 当任务队列(阻塞队列)满的时候,就启动最大线程数
- 当最大线程数也达到后,就将启动拒绝策略
16.4 拒绝策略
- ThreadPoolExecutor.AbortPolicy:线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常(即后面提交的请求不会放入队列也不会直接消费并抛出异常)
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃(也不会抛出任何异常,任务直接就丢弃了)
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务(丢弃掉了队列最前的任务,并不抛出异常,直接丢弃了)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务(不会丢弃任务,最后所有的任务都执行了,并不会抛出异常)
17. ThreadLocal
是Java中所提供的线程本地存储机制,可以利用该机制将数据存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
底层是通过ThreadLocalmap来实现的,Map的key为ThreadLocal对象,Map的value为需要缓存的值
17.1 用它可能会带来什么问题
会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalmap,ThreadLocalmap也是通过强引用指向Entry键值对对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏
解决方法:在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry键值对对象
17.2 什么是强/软/弱/虚引用
- 强引用:是使用最普遍的引用。只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象
- 软引用:是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。只有在内存不足的时候JVM才会回收该对象
- 弱引用:具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中
- 虚引用:也称为幻影引用,虚引用的存在不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用