![](/style/images/good.png)
![](/style/images/bad.png)
Java 的线程安全,以及死锁
source link: https://www.boris1993.com/java-thread-security-deadlock.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
刚才面试的时候被问到了关于线程安全和死锁的问题,有点露怯,故赶紧查漏补缺,记录于此。
线程安全是程序设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成。
乐观锁与悲观锁
- 乐观锁:认为在使用数据时,不会有别的线程修改数据,所以不会加锁,只在更新时判断之前有没有被别的线程更新了数据。比如在数据库中设置一个
version
字段,在更新前先查询该字段的值,然后在写入时比较数据库中的值是否与之前查询到的值相同。 - 悲观锁:认为自己在使用数据的时候,一定有别的线程来修改数据,因此在获取数据的时候先加锁,确保数据不会被线程修改。
如何保证线程安全
syncronized
关键字,举例:ConcurrentHashMap
。是悲观锁。- 锁升级机制:
它是指在锁对象的对象头里面有一个
threadid
字段,在第一次访问的时候threadid
为空,JVM 让其持有偏向锁,并将threadid
设置为其线程 ID,再次进入的时候会先判断threadid
是否与其线程 ID 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized
锁的升级。- 偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的 id 会记录在对象的
Mark Word
中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。 - 轻量级锁(CAS):就是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过 CAS 操作尝试将
Mark Word
更新为指向LockRecord
的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。 - 重量级锁:虚拟机使用 CAS 操作尝试将
MarkWord
更新为指向LockRecord
的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord
是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
- 偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的 id 会记录在对象的
- 锁升级机制:
Lock
接口的实现类,常用ReentrantLock
。是悲观锁。lock()
加锁,unlock()
解锁,不解锁会造成死锁。- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。 - 锁绑定多个条件:一个
ReentrantLock
对象可以同时绑定多个Condition
对象,而在synchronized
中,锁对象的wait()
和notify()
或notifyAll()
方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock
则无须这样做,只需要多次调用newCondition()
方法即可。
ThreadLocal
。当多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal
来解决。它会在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。ThreadLocal
线程容器保存变量时,底层其实是通过ThreadLocalMap
来实现的。它是以当前ThreadLocal
变量为 key,要存的变量为 value。获取的时候就是以当前ThreadLocal
变量去找到对应的 key,然后获取到对应的值。
两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作 “资源” 的东西。
可以使用 jstack
检查死锁。
命令:jstack $(jps -l | grep 'DeadLockExample' | cut -f1 -d ' ')
。
示例输出:
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadLockExample$2.run(DeadLockExample.java:58)
- waiting to lock <0x000000076ab660a0> (a java.lang.Object)
- locked <0x000000076ab660b0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at DeadLockExample$1.run(DeadLockExample.java:28)
- waiting to lock <0x000000076ab660b0> (a java.lang.Object)
- locked <0x000000076ab660a0> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
- 以确定的顺序获锁
- 尽量降低锁的使用粒度
- 尽量使用同步代码块,而不是同步方法
- 避免嵌套锁
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK