目录
- 并发和并行的区别
- 线程和进程的区别?
- 线程有哪些状态?
- 创建线程的方式有哪些?有什么特点?
- Runnable 和 Callable 的区别?
- 如何启动一个新线程、调用 start 和 run 方法的区别?
- Java中常见的锁
- Synchronized锁的升级过程
- 什么是CAS?
- 线程相关的基本方法?
- 什么是线程死锁?死锁如何产生?
- 如何避免线程死锁?
- wait和sleep有哪些区别?
- JUC包提供了哪些原子类?
- JUC包常用的辅助类
- Lock和synchronized的区别
- Lock 接口实现类有哪些?有什么作用?
- synchronized的作用有哪些?
- volatile关键字有什么用?
- 什么是ThreadLocal?它的原理是什么?
- volatile和synchronized的区别是什么?
- ConcurrentHashMap原理
并发和并行的区别
-
并发:两个及两个以上的作业看起来像是同时进行的,实际上它们是在交替执行。
-
并行:并行则强调真正的同时性,两个及两个以上的作业在物理上同时执行。
线程和进程的区别?
-
进程是程序运行和资源分配的基本单位,一个进程可以包含多个线程,而且最少拥有一个线程。
-
线程是是cpu调度和分配的基本单位
线程有哪些状态?
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕
创建线程的方式有哪些?有什么特点?
-
继承
Thread
类并重写run
方法创建线程:实现简单但不可以继承其他类。 -
实现
Runnable
接口并重写run
方法:避免了单继承局限性,编程更加灵活,实现解耦。 -
实现
Callable
接口并重写call
方法:可以获取线程执行结果的返回值,并且可以抛出异常。 -
使用线程池创建(使用
java.util.concurrent.Executor
接口)扫描二维码关注公众号,回复: 17521042 查看本文章
Runnable 和 Callable 的区别?
-
Runnable
接口run
方法无返回值,异常且无法捕获处理; -
Callable
接口call
方法有返回值,支持泛型, 可以获取异常信息
如何启动一个新线程、调用 start 和 run 方法的区别?
run
方法只是thread
的一个普通方法,线程对象调用run
方法不开启线程- 调用
start
方法可以启动线程,使得线程进入就绪状态,并让 jvm 调用run
方法在开启的线程中执行
Java中常见的锁
分类标准 | 分类 |
---|---|
根据线程是否需要对资源加锁 | 悲观锁/乐观锁 |
根据多个线程是否能获取同一把锁 | 共享锁/独享(独占、排他)锁 |
根据锁是否能够重复获取 | 可重入锁/不可重入锁 |
根据锁的公平性进行区分 | 公平锁/非公平锁 |
当多个线程并发访问资源时,当使用synchronized时 | 锁升级( 偏向锁Q /轻量级锁/重量级锁) |
根据资源被锁定后,线程是否阻塞 | 自旋锁/适应性自旋锁 |
公平锁与非公平锁
按照线程访问顺序获取对象锁。
synchronized
是非公平锁,Lock
默认是非公平锁,可以设置为公平锁,公平锁会影响性能
共享式与独占式锁
共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。
例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
悲观锁与乐观锁
-
悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,
synchronized
和ReentrantLock
属于悲观锁。 -
乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。
乐观锁最常见的实现就是
CAS
。 -
适用场景:
- 悲观锁适合写操作多的场景。
- 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能
Synchronized锁的升级过程
-
一开始是无锁状态
-
当一个线程首次获得对象锁时,JVM会设置为 偏向锁。
-
当第二个线程尝试获取偏向锁失败时,偏向锁会升级为 轻量级锁
-
此时,JVM会使用CAS自旋操作来尝试获取锁,如果成功则进入临界区域,否则升级为 重量级锁。
什么是CAS?
CAS全称Compare And Swap
,比较与交换,是乐观锁的主要实现方式。
CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock
内部的AQS和原子类内部都使用了CAS
线程相关的基本方法?
线程相关的基本方法有 wait
,notify
,notifyAll
,sleep
,join
,yield
等
-
线程等待(wait)
调用wait
方法的线程进入WAITING
状态,只有等待另外线程的通知或被中
断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。 -
线程睡眠(sleep)
sleep 导致当前线程休眠,进入TIMED-WATING
状态,与 wait 方法不同的是sleep
不会释放当前占有的锁, -
线程让步(yield)
yield 会使当前线程 让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。 -
线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。线程在合适的时候中断. -
Join 等待其他线程终止
join() 方法,等待其他线程终止,当前线程再由阻塞状态变为就绪状态Runable
. -
线程唤醒
- notify:唤醒在等待的 单个线程,被唤醒的线程会继续与其他线程进行竞争
- notifyAll :唤醒等待的 所有线程。
什么是线程死锁?死锁如何产生?
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
-
死锁产生条件:
-
互斥:一个资源一次只能被一个线程持有。
-
请求与保持:一个进程因请求资源而阻塞时,不释放获得的资源。
-
不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺。
-
循环等待:进程之间循环等待着资源
-
当以上条件同时满足时,就可能会出现死锁的情况。
如何避免线程死锁?
要避免线程死锁,可以采取以下几种方法:
-
尽量避免使用多个锁,尽量使用一个锁或者使用更加高级的锁,例如读写锁或者
ReentrantLock
。 -
减少锁的粒度, 确保同步代码块的执行时间尽可能短,这样可以减少线程等待时间,从而避免死锁的产生。
-
使用尝试锁,通过
ReentrantLock.tryLock()
方法可以尝试获取锁,如果在规定时间内获取不到锁,则放弃锁。 -
避免嵌套锁,如果需要使用多个锁,确保它们的获取顺序是一致的
wait和sleep有哪些区别?
相同点:
- 它们都可以使当前线程暂停运行,把机会交给其他线程
- 任何线程在调用wait()和sleep()之后,在等待期间被中断都会抛出
InterruptedException
不同点:
- wait()是Object超类中的方法;而sleep()是线程Thread类中的方法
- 对锁的持有不同,wait()会释放锁,而sleep()并不释放锁
- 唤醒方法不完全相同,wait()依靠
notify
或者notifyAll
、中断、达到指定时间来唤醒;而sleep()到达指定时间被唤醒 - 调用wait()需要先获取对象的锁,而Thread.sleep()不用
JUC包提供了哪些原子类?
基本类型原子类
使用原子的方式更新基本类型
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
数组类型原子类
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :引用类型数组原子类
引用类型原子类
- AtomicReference:引用类型原子类
- AtomicStampedReference:带有版本号的引用类型原子类。
- AtomicMarkableReference :原子更新带有标记的引用类型。
JUC包常用的辅助类
Semaphore(信号量)
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量
Semaphore 有两种模式:
- 公平模式: 调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO(先进先出);
- 非公平模式: 默认,抢占式的
CountDownLatch (倒计时器)
CountDownLatch
用于某个线程等待其他线程执行完任务再执行,CountDownLatch
是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch
使用完毕后,它不能再次被使用
CyclicBarrier(循环栅栏)
CyclicBarrier
用于一组线程互相等待到某个状态,然后这组线程再同时执行,CyclicBarrier
的计数器可以使用reset()方法重置,可用于处理更为复杂的业务场景
Lock和synchronized的区别
Lock
是接口,synchronized
是关键字synchronized
是非公平锁,Lock
接口支持公平锁和非公平锁- 当线程离开
synchronized
块或者方法时,锁会自动释放,使用Lock
接口时,必须显式地调用lock()
方法获取锁,并且在完成任务后显式地调用unlock()
方法释放锁 synchronized
锁不可被中断等待,除非锁被释放,Lock
接口可以通过调用lock.tryLock()
方法尝试获取锁,如果失败则可以选择放弃等待
Lock 接口实现类有哪些?有什么作用?
- ReentrantLock:支持重入(即可以由持有锁的同一个线程多次获取)。可以指定公平策略。如果不指定,默认是非公平的。
- ReentrantReadWriteLock:这个类实现了读写锁,允许多个读取者同时访问,但是一次只能有一个写入者。读取锁通常用于读多写少的情况,以提高并发性能。
synchronized的作用有哪些?
-
原子性:确保线程互斥的访问同步代码;
-
可见性:保证共享变量的修改能够及时可见;
-
有序性:有效解决重排序问题。
volatile关键字有什么用?
volatile
是轻量级的同步机制,volatile
保证变量对所有线程的可见性,不保证原子性。- 禁止进行指令重排序。
什么是ThreadLocal?它的原理是什么?
ThreadLocal
是一个线程本地变量,它可以为每一个线程都创建一个私有的变量,每个线程只能获取到自己的变量,从而避免了线程安全问题。
ThreadLocal
的核心是 ThreadLocalMap
类,它是一个线程级别的哈希表,用于存储每个线程的变量。
ThreadLocalMap
以 ThreadLocal
对象作为 key,并且key是软引用
以变量值作为 value,value中的对象是强引用,每个线程都可以通过 ThreadLocalMap
获取自己的变量。
volatile和synchronized的区别是什么?
volatile
只能使用在变量上;而synchronized
可以在类,变量,方法和代码块上。volatile
至保证可见性;synchronized
保证原子性与可见性。volatile
禁用指令重排序;synchronized
不会。volatile
不会造成阻塞;synchronized
会
ConcurrentHashMap原理
-
Java7 中
ConcurrentHashMap
使用的分段锁每一个
Segment
上同时只有一个线程可以操作,每一个Segment
都是一个类似HashMap
数组的结构,它可以扩容,它的冲突会转化为链表。但是Segment
的个数一但初始化就不能改变。
-
Java8 中的
ConcurrentHashMap
使用的Synchronized
锁加 CAS 的机制。结构由 Java7 中的
Segment
数组 +HashEntry
数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个HashEntry
的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表