Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。
synchronized给出的答案是在软件层面依赖JVM,而j.u.c.Lock给出的答案是在硬件层面依赖特殊的CPU指令。
加了Synchronized关键字后,反编译后可以看到
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
synchronized和volatile,它们俩特性上最大的区别就在于原子性,volatile不具备原子性
死锁的发生
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象
java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用 (占有) 时,别的线程不能使用。
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1 占有 P2 的资源,P2 占有 P3 的资源,P3 占有 P1 的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。
在java中我们常常使用加锁机制来确保线程安全,但是如果过度使用加锁,则可能导致锁顺序死锁。同样,我们使用线程池和
信号量来限制对资源的使用,但是这些被限制的行为可能会导致资源死锁。java应用程序无法从死锁中恢复过来,因此设计时一定要排序那些可能导致死锁出现的条件。
解决死锁:
通过破坏环路等待条件解决死锁问题
1. 设置优先级。争抢到时间片的概率变大或变小 setPriority (10)[1,10];
2. 线程休眠,进入阻塞状态。当休眠的时间到了设置点的时候就会自动换到就绪状态;
3. 礼让方法。就绪状态礼让出来,进行下一次的竞争时间片。
对于分布式项目,有多少台服务器就有多少片JVM内存,即使每片内存中各设置一把“独一无二”的锁,从整体来看项目中的锁就不是唯一的。此时,如何保证每一个JVM上的线程共用一把锁呢?
答案是:把锁抽取出来,让线程们在同一片内存相遇,从而就出现了分布式锁啦
分布式锁
一般需要使用分布式锁的场景如下:
-
效率:使用分布式锁可以避免不同节点重复相同的工作,比如避免重复执行定时任务等;
-
正确性:使用分布式锁同样可以避免破坏数据正确性,如果两个节点在同一条数据上面操作,可能会出现并发问题。
分布式锁特点
一个完善的分布式锁需要满足以下特点:
-
互斥性:互斥是所得基本特性,分布式锁需要按需求保证线程或节点级别的互斥。;
-
可重入性:同一个节点或同一个线程获取锁,可以再次重入获取这个锁;
-
锁超时:支持锁超时释放,防止某个节点不可用后,持有的锁无法释放;
-
高效性:加锁和解锁的效率高,可以支持高并发;
-
高可用:需要有高可用机制预防锁服务不可用的情况,如增加降级;
-
阻塞性:支持阻塞获取锁和非阻塞获取锁两种方式;
-
公平性:支持公平锁和非公平锁两种类型的锁,公平锁可以保证安装请求锁的顺序获取锁,而非公平锁不可以。
分布式锁的实现
分布式锁常见的实现有三种实现,下文我们会一一介绍这三种锁的实现方式:
-
基于数据库的分布式锁;
-
基于Redis的分布式锁;
-
基于Zookeeper的分布式锁。
Redis提供了setnx指令,如果某个key当前不存在则设置成功并返回true,否则不再重复设置,直接返回false