天天看点

Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

我的博客中有更多后端开发面试题,点我查看!

Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

并发大全

2万字参透并发编程

  • jvm

    中线程分为哪些状态
  • 在执行

    Thread.start()

    方法后,线程是不是马上运行。

多线程导论

  • 原子性是指在一个操作中就是

    cpu

    不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
  • 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性即程序执行的顺序按照代码的先后顺序执行。

多线程设计模式解读—Producer-Consumer模式

Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

请简述一下线程的sleep()方法和yield()方法有什么区别?

  • sleep()

    方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
  • 线程执行

    sleep()

    方法后转入阻塞(blocked)状态,而执行

    yield()

    方法后转入就绪(ready)状态
  • sleep()

    方法声明抛出InterruptedException,而

    yield()

    方法没有声明任何异常

在Java中wait和seelp方法的不同

  • wait

    作用于对象,

    sleep

    作用于 Thread
  • wait

    释放对象锁,

    sleep

    不释放对象锁
  • wait

    运行于同步方法或同步代码块,

    sleep

    可运行于任何地方
  • wait

    不需要捕获异常,

    sleep

    需要捕获异常

Thread类中的yield方法有什么作用?

暂停当前线程,将当前线程从

Running

状态转到

Ready

状态,让出控制权给其他线程

谈谈 wait / notify 关键字的理解

  • wait / notify 存在于所有对象;

    使用时需要

    synchronized

    ,否则

    IllegalMonitorStateException

  • 调用

    wait

    方法会让当前线程阻塞,让出对象锁;若

    wait

    有设置时间参数,到时间后自动唤醒;
  • notify

    一次唤醒一个等待的线程;

    notifyAll

    一次性唤醒所有该对象上的等待线程。

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

wait,notify,notifyAll,sleep这些方法都跟线程的状态变化有关,为什么jdk把前三个方法放在Object类里面,而把sleep放在Thread类里面?
  • Java 内置锁机制中,锁是对象级而不是线程级,任何对象都能创建锁;
  • 一个线程可以有多个锁,若跟线程绑定可能会不够用。

进程和线程之间的通信方式

  • 进程:无名管道、有名管道、信号、共享内存、消息队列、信号量
  • 线程:互斥量、读写锁、自旋锁、线程信号、条件变量

创建线程的方法

  • 继承

    Thread

    类创建线程
  • 实现

    Runnable

    接口创建线程
  • 使用

    Callable

    Future

    创建线程
  • 使用线程池例如用

    Executor

    框架

如何停止一个线程

  • 使用退出标志,使线程正常退出,也就是当

    run

    方法完成后线程终止。
  • 使用

    interrupt

    方法中断线程。
  • 不推荐使用

    stop、suspend及resume

    方法。相当于电脑断电关机一样,是不安全的方法。

线程同步的方法。

同步的实现方面有两种,分别是

synchronized,wait

notify

  • wait()

    :使一个线程处于等待状态,并且释放所持有的对象的lock。

    sleep()

    :使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
  • notify()

    :唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
  • notifyAll()

    :唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

原子操作的实现原理是通过CAS实现的。

一般问CAS底层原理,我遇到这种问题,直接说比较并更新的过程,底层实现调用了unsafe类,unsafe类是用C++实现的,没研究过,代码。糊弄过去的

countDownLatch

countDownLatch

是在java1.5被引入,跟它一起被引入的工具类还有

CyclicBarrier

Semaphore

concurrentHashMap

BlockingQueue

  • countDownLatch

    这个类使一个线程等待其他线程各自执行完毕后再执行。
  • 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

CountDownLatch和CyclicBarrier区别:

  • countDownLatch

    是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
  • CyclicBarrier

    的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用

Runnable和Thread区别

1.

Runnable

Thread

相比优点有:

  • 由于Java不允许多继承,因此实现了

    Runnable

    接口可以再继承其他类,但是

    Thread

    明显不可以
  • Runnable

    可以实现多个相同的程序代码的线程去共享同一个资源,而

    Thread

    并不是不可以,而是相比于

    Runnable

    来说,不太适合,具体原因文章中有。
当以

Thread

方式去实现资源共享时,实际上源码内部是将

Thread

向下转型为了

Runnable

,实际上内部依然是以

Runnable

形式去实现的资源共享

2.

Runnable

为什么不可以直接run

阐述文章中已有,

Runnable

其实相对于一个Task,并不具有线程的概念,如果你直接去调用

Runnable

run

,其实就是相当于直接在主线程中执行了一个函数而已,并未开启线程去执行,带来的“灾害”文章中有。

Java线程池中submit() 和 execute()方法有什么区别?

submit()

可以获取返回值,

execute()

不行。

RUN()

Start()

方法区别是,

Start()

会重新创建一个线程去运行,而

RUN()

是只是去单独的调用。

可以用RUN()完成线程的同步

CompletableFuture,这个是JDK1.8里的新特性,通过它怎么实现多线程并发控制?

多线程实操

面试题,子线程10次子线程2执行20次与主线程100次来回循环执行50次

如何让多个线程交替执行?

一种是基于

synchronized

wait/notify

,另外一种是基于

Lock

Condition(ReentrantLock)

.

  • 循环内加锁
  • 判断共享状态变量,如果状态值表示还没轮到当前线程执行,则调用调用锁对象的

    wait

    方法
  • 等待状态变化的线程被唤醒,执行任务,然后改变状态,然后调用锁对象的

    notify

    方法

用户自己定义一个类,用这个类继承

thread

类,实现

thread

run

方法,然后再使用用户自己定义这个类创建一个对象,最后调用

thread

类的

start

方法激活线程。

VOLATILE

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。
  • 内存屏障
  • 内存可见

1.当写一个

volatile

变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;

2.这个写操作会导致其他线程中的缓存无效。

  • 禁止指令重排序优化。
  • 不能保证原子性
volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性

volatile

只保证可见性 有序性,不保证原子性的原因,在于寄存器无法被

volatile

刷新

  • 你说的本地缓存,指的是线程工作内存,不是cpu的寄存器
  • 线程工作内存,确实被设置为无效,下次直接从内存里面取,但是寄存器在之前已经进去了,这是导致

    volatile

    不能保证原子性的原因。如果

    volatile

    也能把寄存器的值给设置为无效,那么原子性就保证了

LOCK前缀指令篇(一):Java 之深入浅出解读 volatile

原创|《菜鸟读并发》java内存模型之volatile深入解读

一文带你理解Java中Lock的实现原理

Synchronized

volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

  • 既可以修饰方法也可以修饰代码块

对于同步方法,JVM采用

ACC_SYNCHRONIZED

标记符来实现同步。 对于同步代码块。JVM采用

monitorenter

monitorexit

两个指令来实现同步。

  • 同步代码块使用

    monitorenter

    monitorexit

    两个指令实现。可以把执行

    monitorenter

    指令理解为加锁,执行

    monitorexit

    理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行

    monitorenter

    )后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行

    monitorexit

    指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal
Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

sychronized

是java中最基本同步互斥的手段,可以修饰代码块,方法,类.

在修饰代码块的时候需要一个

reference

对象作为锁的对象.

在修饰方法的时候默认是当前对象作为锁的对象.

在修饰类时候默认是当前类的

Class

对象作为锁的对象.

synchronized

会在进入同步块的前后分别形成

monitorenter

monitorexit

字节码指令.在执行

monitorenter

指令时会尝试获取对象的锁,如果此没对象没有被锁,或者此对象已经被当前线程锁住,那么锁的计数器加一,每当

monitorexit

被锁的对象的计数器减一.直到为0就释放该对象的锁.由此

synchronized

是可重入的,不会出现自己把自己锁死.

sychronized

是java中最基本同步互斥的手段,可以修饰代码块,方法,类.

在修饰代码块的时候需要一个

reference

对象作为锁的对象.

在修饰方法的时候默认是当前对象作为锁的对象.

在修饰类时候默认是当前类的Class对象作为锁的对象.

synchronized

会在进入同步块的前后分别形成

monitorenter

monitorexit

字节码指令.在执行

monitorenter

指令时会尝试获取对象的锁,如果此没对象没有被锁,或者此对象已经被当前线程锁住,那么锁的计数器加一,每当

monitorexit

被锁的对象的计数器减一.直到为0就释放该对象的锁.由此

synchronized

是可重入的,不会出现自己把自己锁死.

synchronized 和 ReentrantLock有什么区别

除了

synchronized

的功能,多了三个高级功能.

等待可中断,公平锁,绑定多个

Condition

.

  • 等待可中断:在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待. tryLock(long timeout, TimeUnit unit)
  • 公平锁:按照申请锁的顺序来一次获得锁称为公平锁.

    synchronized

    的是非公平锁,

    ReentrantLock

    可以通过构造函数实现公平锁. new RenentrantLock(boolean fair)
  • 绑定多个

    Condition

    通过多次

    newCondition

    可以获得多个

    Condition

    对象,可以简单的实现比较复杂的线程同步的功能.通过

    await(),signal()

    ;

JAVA两个字符串如何比较大小

compareTo() 的返回值是int, 它是先比较对应字符的大小(ASCII码顺序)

Lock与synchronized有以下区别:

  • synchronized

    会自动释放锁,而

    Lock

    必须手动释放锁。
  • Lock

    可以让等待锁的线程响应中断,而

    synchronized

    不会,线程会一直等待下去。
  • 通过

    Lock

    可以知道线程有没有拿到锁,而

    synchronized

    不能。
  • Lock

    能提高多个线程读操作的效率。
  • synchronized

    能锁住类、方法和代码块,而

    Lock

    是块范围内的。

volatile和synchronized的区别

  • volatile

    synchronized

    执行成本更低,因为它不会引起线程上下文的切换和调度
  • volatile

    本质是在告诉

    jvm

    当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;

    synchronized

    则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile

    只能用来修饰变量,而

    synchronized

    可以用来修饰变量、方法、和类。
  • volatile

    可以实现变量的可见性,禁止重排序和单次读/写的原子性;而

    synchronized

    则可以变量的可见性,禁止重排序和原子性。
  • volatile

    不会造成线程的阻塞;

    synchronized

    可能会造成线程的阻塞。
  • volatile

    标记的变量不会被编译器优化;

    synchronized

    标记的变量可以被编译器优化。

深入解读synchronized和ReentrantLock

  • synchronized

    阻塞的线程状态为

    BLOCKED

  • ReentrantLock

    阻塞的线程状态为

    BLOCKED

    或者

    WAITTING

深入解读synchronized和ReentrantLock

synchronized 和 ReentrantLock有什么区别

除了

synchronized

的功能,多了三个高级功能.

等待可中断,公平锁,绑定多个

Condition

.

  • 等待可中断:在持有锁的线程长时间不释放锁的时候,等待的线程可以选择放弃等待.

    tryLock(long timeout, TimeUnit unit)

  • 公平锁:按照申请锁的顺序来一次获得锁称为公平锁.

    synchronized

    的是非公平锁,

    ReentrantLock

    可以通过构造函数实现公平锁.

    new RenentrantLock(boolean fair)

  • 绑定多个

    Condition

    通过多次

    newCondition

    可以获得多个

    Condition

    对象,可以简单的实现比较复杂的线程同步的功能.通过

    await(),signal();

锁升级

synchronized

锁升级原理:在锁对象的对象头里面有一个

threadid

字段,在第一次访问的时候

threadid

为空,

jvm

让其持有偏向锁,并将

threadid

设置为其线程

id

,再次进入的时候会先判断

threadid

是否与其线程

id

一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了

synchronized

锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在

Java 6

之后优化

synchronized

的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

偏向锁

把MARKWORD的threadID改成自己的threadID

在锁竞争不强烈的情况下,通常一个线程会多次获取同一个锁,为了减少获取锁的代价 引入了偏向锁,会在

Java

对象头中记录获取锁的线程的

threadID

  • 当线程发现对象头的

    threadID

    存在时。判断与当前线程是否是同一线程。
  • 如果是则不需要再次加、解锁。
  • 如果不是,则判断

    threadID

    是否存活。不存活:设置为无锁状态,其他线程竞争设置偏向锁。存活:查找

    threadID

    堆栈信息判断是否需要继续持有锁。需要持有则升级

    threadID

    线程的锁为轻量级锁。不需要持有则撤销锁,设置为无锁状态等待其它线程竞争。

不是同一个对象就升级为轻量级锁

因为偏向锁的撤销操作还是比较重的,导致进入安全点,因此在竞争比较激烈时,会影响性能,可以使用

-XX:-UseBiasedLocking=false

禁用偏向锁。

轻量级锁

当偏向锁升级为轻量级锁时,其它线程尝试通过CAS方式设置对象头来获取锁。

给MARKWORD一个指针,指向自己的lock record

  • 会先在当前线程的栈帧中设置

    Lock Record

    ,用于存储当前对象头中的

    mark word

    的拷贝。
  • 复制

    mark word

    的内容到

    lock record

    ,并尝试使用

    cas

    mark word

    的指针指向

    lock record

  • 如果替换成功,则获取偏向锁
  • 替换不成功,则会自旋重试一定次数。
  • 自旋一定次数或有新的线程来竞争锁时,轻量级锁膨胀为重量级锁。
重量级锁

自旋是消耗CPU的,因此在自旋一段时间,或者一个线程在自旋时,又有新的线程来竞争锁,则轻量级锁会膨胀为重量级锁。重量级锁,通过

monitor

实现,

monitor

底层实际是依赖操作系统的

mutex lock

(互斥锁)实现。需要从用户态,切换为内核态,成本比较高

CAS

CAS

可能遇到

ABA

问题,即内存中的值为

A

,变为

B

后,又变为了

A

,此时

A

为新值,不应该替换。可以采取:

A-1

B-2

A-3

的方式来避免这个问题

ABA问题是一种异常现象:如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令时就可能出现这个问题(如果在没有垃圾回收机制的环境 中)。在CAS操作中将判断“V的值是否仍然为A?”,并且如果是的话就继续执行更新操作。在大多数情况下,这种判断是足够的。然而,有时候还需要知道 “自从上次看到V的值为A以来,这个值是否发生了变化?”在某些算法中,如果V值首先由A编程B,在由B编程A,那么仍然被认为发生了变化,并需要重新执 行算法中的某些步骤。

如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现

ABA

问题。在这种情况下,即使链表的头结点仍然只想之前观察到的节点,那么也不足 以说明链表的内容没有发生变化。

如果通过垃圾回收器来管理链表节点仍然无法避免

ABA

问题,那么还有一个相对简单的解决方法:不是只是更新某个引用的值, 而是更新两个值,包含一个引用和一个版本号。即使这个值由

A

变成

B

,然后又变为

A

,版本号也将是不同的。

AtomicStampedReference

以 及

AtomicMarkableReference

支持在两个变量上执行原子的条件更新。

AtomicStampedReference

将更新一个“对象 —-引用”二元组,通过在引用上加上“版本号”,从而避免

ABA

问题。类似地,

AtomicMarkableReference

将更新一个“对象引用—- 布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除节点”。

参考

  • Java synchronized 原理从开始到放弃
  • 再有人问你synchronized是什么,就把这篇文章发给他。

ReentrantLock

以对象的方式来操作对象锁.相对于

sychronized

需要在

finally

中去释放锁,需要手动释放。

ReentrantLock

里面的

true

false

就是 公平锁和非公平锁的实现

ReentrantLock

重入锁,是实现

Lock

接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在

java

关键字

synchronized

隐式支持重入性,

synchronized

通过获取自增,释放自减的方式实现重入。与此同时,

ReentrantLock

还支持公平锁和非公平锁两种方式。

ReentrantLock

state

字段表示当前线程重入锁的次数,当state为0时候,表示锁是空闲的。

compareAndSetState(0, 1)

表示当

state

,直接更新为

1

,即空闲时候,直接

CAS

上锁;线程如果更新成功了,

setExclusiveOwnerThread()

设置当前线程为锁的占有

可重入锁原理

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;

当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁

ReentrantLock

还是使用上面的例子,

ReentrantLock

的使用很简单,使用

new ReentrantLock(boolean isFair)

来创建一个公平或者非公平锁,使用.lock()方法加锁。使用.unlock()方法,因此我们就从这三个方法入手,来简单的看一下它是如何实现锁的。

ReentrantLock

主要是用了一个巧妙的数据结构(带头尾指针的双链表)和

CAS

加自旋以及使用

LockSupport

park

,

unpark

(类似

wait

,

notify

)来实现加锁和解锁。

CAS原理

AQS (AbstractQueuedSynchronizer)

由一个state和cas 阻塞队列组成

什么是AQS

AQS(AbstractQueuedSynchronizer)

AQS

是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子

int

值来表示状态的同步器的基类。如果你有看过类似

CountDownLatch

类的源码实现,会发现其内部有一个继承了

AbstractQueuedSynchronizer

的内部类

Sync

。可见

CountDownLatch

是基于

AQS

框架来实现的一个同步器.类似的同步器在

JUC

下还有不少。(eg.

Semaphore

)

AQS用法

如上所述,

AQS

管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如,

Semaphore

用它来表现剩余的许可数,

ReentrantLock

用它来表现拥有它的线程已经请求了多少次锁;

FutureTask

用它来表现任务的状态(尚未开始、运行、完成和取消)

AQS AbstractQueuedSynchronizer

J.U.C

是基于

AQS

实现的,

AQS

是一个同步器,设计模式是模板模式。

  • 核心数据结构:双向链表 +

    state

    (锁状态)
  • 底层操作:

    CAS

    Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
           

AQS

中的

int

类型的

state

值,这里就是通过

CAS

(乐观锁)去修改

state

的值。

lock

的基本操作还是通过乐观锁来实现的。

获取锁通过

CAS

,那么没有获取到锁,等待获取锁是如何实现的?我们可以看一下

else

分支的逻辑,

acquire

方法:

  • tryAcquire

    :会尝试再次通过

    CAS

    获取一次锁。
  • addWaiter

    :通过自旋

    CAS

    ,将当前线程加入上面锁的双向链表(等待队列)中。
  • acquireQueued

    :通过自旋,判断当前队列节点是否可以获取锁。

可以看到,当当前线程到头部的时候,尝试

CAS

更新锁状态,如果更新成功表示该等待线程获取成功。从头部移除。基本可以确认,释放锁就是对

AQS

中的状态值

State

进行修改。同时更新下一个链表中的线程等待节点。

Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

可以看到在整个实现过程中,

lock

大量使用

CAS

+自旋。因此根据

CAS

特性,

lock

建议使用在低锁冲突的情况下。目前java1.6以后,官方对

synchronized

做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用

synchronized

做同步操作。

AQS

定义两种资源共享方式:

Exclusive(

独占,只有一个线程能执行,如

ReentrantLock

Share

(共享,多个线程可同时执行,如

Semaphore

/

CountDownLatch

)。

先大体说一下同步等待队列的特点:先进先出。获取锁失败的线程构造节点加入队列尾部,阻塞自己,等待唤醒。执行完成的线程从头部移出队列,并唤醒后续节点的线程。头结点是当前获取到锁的线程

公平锁:

Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

非公平锁:

Java多线程导论及延申我的博客中有更多后端开发面试题,点我查看!多线程导论VOLATILESynchronizedReentrantLockThreadLocal

偏向锁

锁不存在多线程竞争,并且应由一个线程多次获得锁

当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 MarkWord 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。(检查thread id)

轻量级锁

当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录( LockRecord)区域,同时将锁对象的对象头中 MarkWord 拷贝到锁记录中,再尝试使用 CAS 将 MarkWord 更新为指向锁记录的指针。

如果更新成功,当前线程就获得了锁。

如果更新失败 JVM 会先检查锁对象的

MarkWord

是否指向当前线程的锁记录。

如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。

不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。

轻量锁能提升性能的原因是:认为大多数锁在整个同步周期都不存在竞争,所以使用 CAS 比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 CAS 的开销,甚至比重量锁更慢。

会自旋,否则进行锁膨胀

重量级锁

适应性自旋

在使用

CAS

时,如果操作失败,

CAS

会自旋再次尝试。由于自旋是需要消耗

CPU

资源的,所以如果长期自旋就白白浪费了

CPU

。 JDK1.6加入了适应性自旋:

如果某个锁自旋很少成功获得,那么下一次就会减少自旋。

ABA问题(CAS的应用:AtomicStampedReference)

  • 一个变量

    int state = 0

  • 线程

    A

    读取到

    state = 0

  • 线程

    B

    修改

    state = 1

    再改回

    state = 0

    此时线程 A 再读取

    state

    ,还是 ,但实际上已经

    state

    已经被修改过。

对于对改动敏感的变量操作,可以使用

AtomicStampedReference

,它会同时保存时间戳以确认是否发生改动。

参考

  • ReentrantLock原理从开始到放弃

ThreadLocal

Map的Key是弱引用类型(WeakReference),而Value是强引用类型,如果Key被回收,Value却不会被回收。

ThreadLocal

Synchronized

都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized

    是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal

    是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于

    Synchronized

    ThreadLocal

    具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

由于

Thread

包含变量

ThreadLocalMap

,因此

ThreadLocalMap

Thread

的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用

ThreadLocal

不会内存泄漏,对应的

value

在下一次

ThreadLocalMap

调用

set()

,

get()

,

remove()

的时候会被清除。

因此,

ThreadLocal

内存泄漏的根源是:由于

ThreadLocalMap

的生命周期跟

Thread

一样长,如果没有手动删除对应

key

就会导致内存泄漏,而不是因为弱引用。

参考

  • ThreadLocal内存泄露的原因

实现线程安全的方式

  • 加锁 利用Synchronized或者ReenTrantLock来对不安全对象进行加锁,来实现线程执行的串行化,从而保证多线程同时操作对象的安全性,一个是语法层面的互斥锁,一个是API层面的互斥锁.
  • 非阻塞同步来实现线程安全。原理就是:通俗点讲,就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生冲突,那就再采取其他措施(最常见的措施就是不断地重试,直到成功为止)。这种方法需要硬件的支持,因为我们需要操作和冲突检测这两个步骤具备原子性。通常这种指令包括CAS SC,FAI TAS等
  • 线程本地化,一种无同步的方案,就是利用Threadlocal来为每一个线程创造一个共享变量的副本来(副本之间是无关的)避免几个线程同时操作一个对象时发生线程安全问题。

阻塞队列

  • ArrayBlockingQueue

    :基于数组实现的一个阻塞队列,在创建

    ArrayBlockingQueue

    对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
  • LinkedBlockingQueue

    :基于链表实现的一个阻塞队列,在创建

    LinkedBlockingQueue

    对象时如果不指定容量大小,则默认大小为

    Integer.MAX_VALUE

  • PriorityBlockingQueue

    :以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。
  • DelayQueue

    :基于

    PriorityQueue

    ,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。

    DelayQueue

    也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。