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

并发大全
2万字参透并发编程
-
中线程分为哪些状态jvm
- 在执行
方法后,线程是不是马上运行。Thread.start()
多线程导论
- 原子性是指在一个操作中就是
不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。cpu
- 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性即程序执行的顺序按照代码的先后顺序执行。
多线程设计模式解读—Producer-Consumer模式
请简述一下线程的sleep()方法和yield()方法有什么区别?
-
方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会sleep()
- 线程执行
方法后转入阻塞(blocked)状态,而执行sleep()
方法后转入就绪(ready)状态yield()
-
方法声明抛出InterruptedException,而sleep()
方法没有声明任何异常yield()
在Java中wait和seelp方法的不同
-
作用于对象,wait
作用于 Threadsleep
-
释放对象锁,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
-
:使一个线程处于等待状态,并且释放所持有的对象的lock。wait()
:使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。sleep()
-
:唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。notify()
-
:唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。notifyAll()
原子操作的实现原理是通过CAS实现的。
一般问CAS底层原理,我遇到这种问题,直接说比较并更新的过程,底层实现调用了unsafe类,unsafe类是用C++实现的,没研究过,代码。糊弄过去的
countDownLatch
countDownLatch
是在java1.5被引入,跟它一起被引入的工具类还有
CyclicBarrier
、
Semaphore
、
concurrentHashMap
和
BlockingQueue
。
-
这个类使一个线程等待其他线程各自执行完毕后再执行。countDownLatch
- 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
CountDownLatch和CyclicBarrier区别:
-
是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次countDownLatch
-
的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用CyclicBarrier
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
理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorexit
)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorenter
指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。monitorexit
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
可以通过构造函数实现公平锁. new RenentrantLock(boolean fair)ReentrantLock
- 绑定多个
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
进行修改。同时更新下一个链表中的线程等待节点。
可以看到在整个实现过程中,
lock
大量使用
CAS
+自旋。因此根据
CAS
特性,
lock
建议使用在低锁冲突的情况下。目前java1.6以后,官方对
synchronized
做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用
synchronized
做同步操作。
AQS
定义两种资源共享方式:
Exclusive(
独占,只有一个线程能执行,如
ReentrantLock
和
Share
(共享,多个线程可同时执行,如
Semaphore
/
CountDownLatch
)。
先大体说一下同步等待队列的特点:先进先出。获取锁失败的线程构造节点加入队列尾部,阻塞自己,等待唤醒。执行完成的线程从头部移出队列,并唤醒后续节点的线程。头结点是当前获取到锁的线程
公平锁:
非公平锁:
偏向锁
锁不存在多线程竞争,并且应由一个线程多次获得锁
当线程访问同步块时,会使用 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
-
:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。PriorityBlockingQueue
-
:基于DelayQueue
,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。PriorityQueue
也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。DelayQueue