Java synchronized与死锁深入探究
作者:Node_Hao
1.synchronized的特性
1). 互斥性
当某个线程执行到 synchronized 所修饰的对象时 , 该线程对象会加锁(lock) , 其他线程如果执行到同一个对象的 synchronized 就会产生阻塞等待.
- 进入 synchronized 修饰的代码块 , 相当于加锁.
- 退出 synchronized 修饰着代码块 , 相当于解锁.
synchronized 使用的锁存储在Java对象里 , 可以理解为每个对象在内存中存储时 , 都有一块内存表示当前的锁定状态.类似于公厕的"有人" , "无人".
如果是"无人"状态 , 此时就可以使用 , 使用时需设置为"有人"状态.
如果是"有人"状态 , 此时就需要排队等待.
如果理解阻塞等待?
针对每一把锁 , 操作系统都会维护一个等待队列 , 当一个线程获取到这个锁之后 , 其他线性再尝试获取这个锁 , 就会获取不到锁 , 陷入阻塞等待. 一直等到之前这个线程释放锁后 , 操作系统才会唤醒其他线程来再次竞争这个锁.
2)可重入
synchronized 对同一个线程来说是可重入的 , 不会出现把自己锁死的情况.
如何理解把自己锁死?
观察下面这段代码可以发现 , 当某个线程调用add方法时 , 就会对 this 对象先加锁 , 接着进入代码块又会对 this 对象再次尝试加锁. 站在 this 对象的角度 , 它认为自己已经被另外的线程占用了 , 那么第二次加锁是否需要阻塞等待呢? 如果运行上述情况 , 那么这个锁就是可重入的 , 否则就是不可重入的.不可重入锁会导致出现死锁 , 而Java中的 synchronized 是可重入锁 , 因此没有上述问题.
synchronized public void add(){ synchronized (this) { count++; } }
在可重入锁内部 , 包含了"线程持有者"和"计数器"两个信息.
- 如果每个线程加锁时 , 发现锁以及被占用了 , 但加锁的人是它自己 , 那么仍然可以获取到锁 , 让计数器自增.
- 解锁的时候当计数器递减到0时 , 才真正释放锁.
2.synchronized使用示例:
1). 修饰普通方法
锁的是 Counter 对象.
class Counter{ public int count; synchronized public void add(){ count++; } }
2). 修饰静态方法
锁的是 Counter 类
class Counter{ public int count; synchronized public static void add(){ count++; } }
3).修饰代码块.明确指定锁哪个对象
锁当前对象:
class Counter{ public int count; public void add(){ synchronized (this) { count++; } } }
锁类对象:
class Counter{ public int count; public void add(){ synchronized (Counter.class) { count++; } } }
类锁和对象锁有什么区别?
顾名思义 , 对象锁用来锁住当前对象 , 类锁用来锁住当前类.如果一个类有多个实例对象 , 那么如果对其中一个对象加锁 , 别的线程只会在访问这个对象时阻塞等待 , 访问其他对象时没有影响.但如果是类锁 , 那么当一个线程对这个类加锁后 , 其他线程访问该类的所有对象都要阻塞等待.
3.Java标准库中的线程安全类
Java 标准库中有很多线程是不安全的 , 这些类可能涉及多线程修改共享数据 , 却又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- HashSet
- TreeSet
- StringBuilder
但还有一些是线程安全的 , 使用一些锁机制来控制.
- Vector
- HashTable
- CurrentHashMap
- StringBuffer
@Override @IntrinsicCandidate public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
这些线程之所以不加锁是因为 , 加锁会损失部分性能.
4.死锁是什么
死锁是这样一种情况 , 多个线程同时被阻塞 , 其中一个或全部都在等待某个资源被释放.由于线程被无限期的阻塞 , 因此程序不可能正常终止.
死锁的三个典型情况
1). 一个线程一把锁 , 连续加两次 , 如果锁是不可重入锁 , 就会死锁. Java中的synchronized和ReentranLock 都是可重入锁 , 因此不会出现上述问题.
2). 两个线程两把锁 , t1 和 t2 线程各种先针对锁A和锁B加锁 , 再尝试获取对方的锁.
例如 , 张三和女神去吃饺子 , 需要蘸醋和酱油 , 张三拿到醋 , 女神拿到酱油 , 张三对女神说:"你先把酱油给我 , 我用完就把醋给你" , 女神对张三说:"你先把醋给我 , 我用完就把酱油给你". 这时两人争执不下 , 就构成了死锁 , 醋和酱油就是两把锁 , 张三和女生就是两个线程.
public static void main(String[] args) { Object jiangyou = new Object(); Object cu = new Object(); Thread zhangsan = new Thread(()->{ synchronized (jiangyou){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (cu ){ System.out.println("张三把酱油和醋都拿到了"); } } }); Thread nvsheng = new Thread(()->{ synchronized (cu ){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou){ System.out.println("女神把酱油和醋都拿到了"); } } }); zhangsan.start(); nvsheng.start(); }
执行代码后 , 发现没有打印任何日志 , 说明没有线程拿到两把锁.
通过jconsole查看线程的情况:
3)多个线程多把锁
例如常见经典案例--"哲学家就餐问题"
假设有五个哲学家围着桌子吃饭 , 每个人中间放一个筷子 , 哲学家有两种状态 , 1.思考人生(相当于线程的阻塞状态) , 2.拿起筷子吃面条(相当于线程获取到锁执行计算) , 由于操作系统的随机调度 , 这五个哲学家随时都可能想吃面条 , 也随时都可能思考人生 , 但是想要吃面条就得同时拿起左右两个筷子.
假设同一时刻 , 所有哲学家同时拿起左手的筷子 , 所有的哲学家都拿不起右手的筷子 , 就会产生死锁.
死锁是一个严重的"BUG" , 导致一个程序的线程"卡死"无法正常工作.
5.如果避免死锁
死锁的四个必要条件:
1.互斥使用: 当资源被一个线程占有时 , 别的线程不能使用
2.不可抢占: 资源请求者不能从资源获取者手中夺取资源 , 只能等资源占有者主动释放.
3.请求和保持: 当资源请求者请求获取别的资源时 , 保存对原有资源的占有.
4.循环等待: 即存在一个等待队列 , P1占有P2的资源 , P2占有P3的资源 , P3占有P1的资 源, 这样就形成一个等待回路.
当上述四个条件都成立就会形成死锁 , 当然破坏其中一个条件也可以打破死锁 , 对于synchronized 来说 , 前三个条件是锁的基本特性 , 因此想要打破死锁只能从"循环等待"入手.
如何破除死锁?
如果我们给锁编号 , 然后指定一个固定的顺序来加锁(必然从小到大) , 任意线程加多把锁的时候都遵循上述顺序, 此时循环等待自然破除.
因此解决哲学家就餐问题就可以给每个筷子编号 , 每个人都遵守"先拿小的再拿大的顺序".此时1号哲学家和2号哲学家为了竞争筷子其中一个人就会阻塞等待 , 这时5号哲学家就有了可乘之机 , 5号哲学家拿起4号和5号筷子吃完面条 , 四号哲学家重复上述操作也吃完面条 , 这样就完美的打破了循环等待的问题.
同样 , 最初的张三和女神吃饺子问题也是同样的解决方式 , 规定两人都按"先拿醋再拿饺子"的顺序执行 , 就可以完美解决死锁问题.
public static void main(String[] args) { Object jiangyou = new Object(); Object cu = new Object(); Thread zhangsan = new Thread(()->{ synchronized (cu){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou ){ System.out.println("张三把酱油和醋都拿到了"); } } }); Thread nvsheng = new Thread(()->{ synchronized (cu ){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (jiangyou){ System.out.println("女神把酱油和醋都拿到了"); } } }); zhangsan.start(); nvsheng.start(); }
到此这篇关于Java synchronized与死锁深入探究的文章就介绍到这了,更多相关Java synchronized 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- Java同步锁Synchronized底层源码和原理剖析(推荐)
- java同步锁的正确使用方法(必看篇)
- 95%的Java程序员人都用不好Synchronized详解
- Java synchronized同步关键字工作原理
- Java synchronized偏向锁的概念与使用
- Java synchronized轻量级锁实现过程浅析
- Java synchronized重量级锁实现过程浅析
- Java @Transactional与synchronized使用的问题
- Java synchronized与CAS使用方式详解
- 浅析Java关键词synchronized的使用
- synchronized及JUC显式locks 使用原理解析
- java锁synchronized面试常问总结
- Java HashTable与Collections.synchronizedMap源码深入解析
- Java Synchronized锁的使用详解
- AQS加锁机制Synchronized相似点详解
- Java必会的Synchronized底层原理剖析
- 一个例子带你看懂Java中synchronized关键字到底怎么用
- 详解Java Synchronized的实现原理
- Synchronized 和 ReentrantLock 的实现原理及区别
- Java同步锁synchronized用法的最全总结