SYN关键字辨析,各种锁优缺点分析和面试题讲解
文章目录
- synchronized的实现原理
- 了解各种锁
- 自旋锁
- 自旋锁的优缺点
- 自旋锁时间阈值
- 锁的状态
- 偏向锁
- 轻量级锁
- 不同锁的比较
- 相关面试题分析
- Synchronized修饰普通方法和静态方法的区别?什么是可见性?
- 锁分哪几类
- 线程要不要锁住资源
- 锁住同步资源失败,线程要不要阻塞?
- 多个线程竞争同步资源的流程细节有没有区别?
- 多个线程竞争锁时要不要排队?
- 一个线程中多个流程能不能获取同一把锁?
- 多个线程能不能共享一把锁
- CAS无锁编程的原理
- ReentrantLock的实现原理
- AQS原理
- Synchronized的原理以及与ReentrantLock的区别
- Synchronized做了哪些优化(京东)
- 锁消除
- 锁粗化
- 偏向锁
- 轻量级锁
- 重量级锁
- 自旋锁+适应性自旋
- Synchronized static与非static锁的区别和范围(小米)
- Volatile能否保证线程安全?在DCL上的作用是什么?
- volatile和synchronized有什么区别?
- 什么是守护线程?你是如何退出一个线程的?
- sleep、wait、yield的区别,wait的线程如何唤醒它?
- sleep是可中断的么
- 线程的生命周期
- ThreadLocal是什么
- 线程池基本原理
- 有三个线程T1,T2,T3,怎么确保它们按顺序执行?
synchronized的实现原理
Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
对同步块,MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。
对同步方法,从同步方法反编译成.class文件的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
synchronized使用的锁是存放在Java对象头里面
具体位置是对象头里面的MarkWord,MarkWord里默认数据是存储对象的HashCode等信息
但是会随着对象的运行改变而发生变化,不同的锁状态对应不同的记录存储方式
了解各种锁
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要
JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
JDK1.6中-XX:+UseSpinning开启自旋锁; JDK1.7后,去掉此参数,由jvm控制;
锁的状态
一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。
偏向锁
引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
偏向锁获取过程:
步骤1、 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
步骤2、 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
步骤3、 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
步骤4、 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
步骤5、 执行同步代码。
偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁的适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。
jvm开启/关闭偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程:
在代码进入同步块的时候,如果同步对象锁状态为无锁状态且不允许进行偏向(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
不同锁的比较
相关面试题分析
Synchronized修饰普通方法和静态方法的区别?什么是可见性?
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
可见性是指多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可以并不会及时的刷到主内存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。
锁分哪几类
线程要不要锁住资源
-
悲观锁
访问资源前先加锁,确保同一时间只有一个线程能操作
代表:Java中的synchronized块,Lock接口 -
乐观锁
不加锁,而是在更新时检查数据是否被修改过(比如版本号、CAS操作)。如果发现冲突,则重试或失败。
代表:AtomicInteger的compareAndSet(),数据库中的version字段
悲观锁(加锁):
val count = AtomicInteger(0)fun increment() {var current: Intvar next: Intdo {current = count.get()next = current + 1} while (!count.compareAndSet(current, next))
}
乐观锁(无锁)
val count = AtomicInteger(0)fun increment() {var current: Intvar next: Intdo {current = count.get()next = current + 1} while (!count.compareAndSet(current, next))
}
锁住同步资源失败,线程要不要阻塞?
当线程尝试获取同步资源(锁)失败时,不同锁机制采用不同策略
- 阻塞
放弃CPU,进行等待队列,由操作系统调度唤醒,如synchronized,ReentrantLock
行为:线程获取锁失败 → 进入 阻塞状态(BLOCKED) → 释放 CPU → 等待锁释放后被唤醒。
优点:不浪费 CPU 资源。
缺点:上下文切换开销大(用户态 ↔ 内核态),唤醒有延迟。
典型:
synchronized (obj) { ... } // JVM 会根据情况升级为重量级锁(阻塞)
lock.lock(); // ReentrantLock 默认使用阻塞(基于 AQS 的 CLH 队列)
✅ 适合:锁持有时间较长(如 I/O、复杂计算)。
- 自旋锁
获取锁失败,不挂起,而是在循环里不断重试
行为:获取锁失败 → 不挂起,而是在循环里不断重试(while (!tryLock()) {})。
优点:避免上下文切换,响应快(如果锁很快释放)。
缺点:浪费 CPU!如果锁很久不释放,自旋线程白白占用核心。
@Volatile
private var locked = falsefun lock() {while (!locked.compareAndSet(false, true)) {// 自旋:什么也不做,继续循环}
}fun unlock() {locked = false
}
✅ 适合:锁持有时间极短(比如几纳秒到微秒级),且多核 CPU(自旋线程不影响持有锁的线程运行)
多个线程竞争同步资源的流程细节有没有区别?
-
无锁(CAS),不锁住资源,多个线程中只有一个能修改资源成功,其他线程会重试
-
偏向锁,同一个线程执行同步资源时自动获取资源,对象头记录线程 ID
JVM 启动后默认开启 偏向锁(JDK 15+ 默认关闭,但可开启)。
第一次进入 synchronized:
JVM 将对象头的 Mark Word 设置为偏向当前线程 ID。
后续该线程再次进入,只需检查 Mark Word 是否是自己,无需任何 CAS 或系统调用。
✅ 零开销重入! -
轻量级锁,多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放
每个线程在自己 栈帧中创建一个 Lock Record。
通过 CAS 尝试将对象头指向自己的 Lock Record。
成功 → 获得锁;失败 → 说明有竞争,升级为重量级锁。 -
重量级锁,多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒
多个线程竞争锁时要不要排队?
在 Java 并发中,“排队”指的是线程在获取锁失败后,进入一个 FIFO(先进先出)的等待队列(比如 AQS 的 CLH 队列),按顺序被唤醒。
排了队 → 谁先等,谁先得(公平)。
不排队,直接抢 → 谁抢到算谁的(非公平)。
源码佐证(ReentrantLock + AQS)
公平锁,排队
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 关键!先检查队列是否为空(hasQueuedPredecessors)if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// ... 重入逻辑return false;
}
非公平锁,先尝试插队,插队失败再排队
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 关键!不管有没有人排队,直接 CAS 抢!if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// ... 重入逻辑return false;
}
一个线程中多个流程能不能获取同一把锁?
可重入锁,能
同一个线程可以多次获取同一把锁,而不会导致死锁
- 每次获取锁,重入计数器+1
- 每次释放锁,计数器-1
- 只有当计数器归零时,锁才真正释放,其他线程才能获取
代表:
- Java中的synchronized
- ReentrantLock
val lock = ReentrantLock()fun outer() {lock.lock()try {println("进入 outer")inner() // 调用另一个需要锁的方法} finally {lock.unlock()}
}fun inner() {lock.lock() // 同一个线程再次获取同一把锁!try {println("进入 inner")} finally {lock.unlock()}
}// 调用
outer()
✅ 不会死锁! 因为 ReentrantLock 允许同一线程重复加锁。
底层原理:
锁内部记录:
持有锁的线程 ID
重入次数(hold count)
当前线程再次请求锁时:
如果是自己 → hold count++,直接成功。
如果是别人 → 阻塞或失败。
非可重入锁,不能
假设
class SimpleLock {@Volatileprivate var locked = falsefun lock() {while (!locked.compareAndSet(false, true)) {// 自旋等待}}fun unlock() {locked = false}
}
用它来调用上面的 outer() / inner():
val lock = SimpleLock()fun outer() {lock.lock()try {inner() // 再次 lock()} finally {lock.unlock()}
}
结果:
🔴 死锁!
第一次 lock():locked = false → true,成功。
第二次 lock()(在 inner 中):
locked = true,compareAndSet(false, true) 永远失败。
线程无限自旋,无法继续。
因为锁不知道是同一个线程在重入,把它当作“其他线程竞争”。
多个线程能不能共享一把锁
共享锁,能
排他锁,不能
锁类型 | 别名 | 多个线程能否同时持有? | 典型用途 |
---|---|---|---|
共享锁(Shared Lock) | 读锁(Read Lock) | ✅ 能(多个线程可同时持有) | 读操作(不修改数据) |
排他锁(Exclusive Lock) | 写锁(Write Lock)、互斥锁 | ❌ 不能(同一时间只能一个线程持有) | 写操作(修改数据) |
import java.util.concurrent.locks.ReentrantReadWriteLockval rwLock = ReentrantReadWriteLock()
var data = 0// 读操作:多个线程可同时执行
fun read(): Int {rwLock.readLock().lock()try {println("${Thread.currentThread().name} 读取: $data")return data} finally {rwLock.readLock().unlock()}
}// 写操作:同一时间只能一个线程执行
fun write(value: Int) {rwLock.writeLock().lock()try {data = valueprintln("${Thread.currentThread().name} 写入: $value")} finally {rwLock.writeLock().unlock()}
}
CAS无锁编程的原理
使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包括三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS的三大问题:
ABA问题:
- 现象:值从 A → B → A,CAS 误以为“没变”,其实中间被改过。
- 例子:栈顶指针 A → 弹出 → 其他线程压入新节点(地址碰巧也是 A)→ 原线程 CAS 成功,但栈结构已错。
- 解决:加版本号(stamp)
→ 使用 AtomicStampedReference 或 AtomicMarkableReference。
AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);
// CAS 时同时比较值和版本号
top.compareAndSet(expectedRef, newRef, expectedStamp, newStamp);
循环时间长开销大(高竞争下)
- 现象:100 个线程同时自增 AtomicLong,大量 CAS 失败,CPU 空转。
- 解决:
用 LongAdder(分段累加,最终合并)→ 写多读少场景首选
或退化为锁(如 synchronized)
LongAdder counter = new LongAdder();
counter.increment(); // 内部用 Cell 数组分散竞争
long sum = counter.sum(); // 读时合并
只能保证一个共享变量的原子性
- 现象:无法原子更新多个字段(如同时改 x 和 y)。
- 解决:
把多个变量封装成一个对象,用 AtomicReference
或使用锁
class Point {volatile int x, y;
}AtomicReference<Point> pointRef = new AtomicReference<>(new Point());// 更新时创建新对象
pointRef.updateAndGet(p -> new Point(p.x + 1, p.y + 1));
ReentrantLock的实现原理
线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。
底层则是利用了JUC中的AQS来实现的。
AQS原理
用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。
在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。
Synchronized的原理以及与ReentrantLock的区别
synchronized(this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
Synchronized做了哪些优化(京东)
锁消除
如果JVM发现某个锁根本不会被多个线程访问,就直接去掉这个锁
基于逃逸分析
如果一个对象只在当前线程内使用(未逃逸到其他线程),那么对它的synchronized就是多余的。
JVM在JIT编译时直接删除无用的同步代码。
public String concat(String a, String b) {StringBuffer sb = new StringBuffer(); // 局部对象,未逃逸sb.append(a);sb.append(b);return sb.toString();
}
- StringBuffer的方法是synchronized的
- 但sb是局部变量,不可能被其他线程访问
- JVM消除所有synchronized调用,性能等同于StringBuilder
锁粗化
避免频繁加锁/解锁带来的开销,把多个相邻的同步块合并成一个大的同步块
synchronized(lock) { /* code1 */ }
// 中间没有竞争
synchronized(lock) { /* code2 */ }
// 中间没有竞争
synchronized(lock) { /* code3 */ }
JVM会将其优化为:
synchronized(lock) {/* code1 *//* code2 *//* code3 */
}
减少锁获取/释放的次数,提升性能
偏向锁
如果只有一个线程反复进入同步块,则零成本获取锁
- 第一次获取锁时,JVM 将对象头 Mark Word 标记为“偏向当前线程”(记录线程 ID)。
- 后续该线程再次进入:
- 直接检查 Mark Word 是否是自己 → 无需任何 CAS 或系统调用。
- ✅ 重入开销为零!
⚠️ 现状:
JDK 15+ 默认禁用偏向锁(因现代应用多线程普遍,撤销偏向锁的 STW 开销得不偿失)。
可手动开启:-XX:+UseBiasedLocki
轻量级锁
多个线程交替执行(非并发),避免进入OS互斥量
- 每个线程在栈帧中创建 Lock Record。
- 通过 CAS 尝试将对象头指向自己的 Lock Record。
- 成功 → 获得锁;失败 → 升级为重量级锁。
无阻塞、无内核态切换。
重量级锁
当多个线程真正同时竞争时,使用OS级互斥量保证安全
- 基于 Monitor(监视器),底层是 OS Mutex。
- 获取失败的线程进入 Entry Set 阻塞队列,状态变为 BLOCKED。
- 由操作系统负责线程调度和唤醒。
💡 这就是传统意义上的“互斥锁”,开销最大,但保证强一致性。
自旋锁+适应性自旋
避免线程刚阻塞,锁就被释放了(上下文切换太重)
当线程尝试获取重量级锁失败时,先不挂起,而是自旋(空循环)几次。
如果持有锁的线程很快释放,自旋线程就能立即获得锁,避免阻塞/唤醒开销。
Synchronized static与非static锁的区别和范围(小米)
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
Volatile能否保证线程安全?在DCL上的作用是什么?
不能保证,在DCL的作用是:volatile是会保证被修饰的变量的可见性和有序性,保证了单例模式下,保证在创建对象的时候的执行顺序一定是
1.分配内存空间
2.实例化对象instance
3.把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null
通过这三个步骤,从而保证了instance要么为null,要么是已经完全初始化好的对象。
volatile和synchronized有什么区别?
volatile是最轻量的同步机制。
volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性,因此多线程下的写复合操作会导致线程安全问题。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
什么是守护线程?你是如何退出一个线程的?
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
线程的中止:
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。
线程的中止:
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。因为会导致程序可能工作在不确定状态下。
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
sleep、wait、yield的区别,wait的线程如何唤醒它?
yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。
Wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。
wait 的线程使用notify/notifyAll()进行唤醒。
sleep是可中断的么
sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。
线程的生命周期
Java中线程的状态分为6种:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
ThreadLocal是什么
ThreadLocal是Java里一种特殊的变量。ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
在内部实现上,每个线程内部都有一个ThreadLocalMap,用来保存每个线程所拥有的变量副本。
线程池基本原理
复用有限的线程资源,避免频繁创建/销毁线程的开销,并通过任务队列实现生产者-消费者模型
在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。第二:提高响应速度。第三:提高线程的可管理性。
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
有三个线程T1,T2,T3,怎么确保它们按顺序执行?
可以用join方法实现