当一个对象访问Synchronized方法时,锁类型为对象锁。
此时该对象不能同时访问类中其他Synchronized方法。若一个对象中有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,则其它线程不能同时访问这个对象中任何一个synchronized方法。
此外,不同对象实例的synchronized方法是不相干预的。也就是说,其它线程可以同时访问此类下的另一个对象实例中的synchronized方法;
当对静态方法加锁时,锁类型为类锁。相当于给类加锁(synchronized.Class),静态方法被调用时(类名.方法名),会先检测调用类有没有持有锁,持有锁时,等待。
使用一个monitor对象,由ObjectMonitor实现(C++代码)
Class字节码中
1)对象加锁时,使用monitorenter指令得到monitor对象。检查owner是否为空
monitorenter操作的目标一定是对象,类型是reference。Reference实际就是堆里的一个存放对象的地址。每个对象(reference)都有一个monitor对应。
2)方法加锁时
常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
1)类锁与对象锁互不干扰。
个人推测原因:
静态方法:与静态成员变量一样,属于类的本身,在类装载的时候被装载到内存,
非静态方法:又叫实例化方法,属于实例对象,实例化后才会分配内存,必须通过类的实例来引用。
不同地址,所以访问互不干扰
2)wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,
否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
3)Synchronized缺点:当一个线程持有锁时,不能立即返回,也不能被打断。其他线程只能阻塞。
ReentrantLock :核心为AQS(AbstractQueueSynchronzer)。AQS基于FIFO双向队列实现。
队列的基本元素是Node,AQS为每个线程创建一个Node进行调度
ReentrantLock根据构造方法中传入的boolean值来创建一个静态抽象内部类Sync的子类,即公平锁或非公平锁
公平锁与非公平锁的比较
不公平锁:可能会产生饥饿现象。效率较高
公平锁:不会产生饥饿现象,效率较低
1) 使用CAS操作尝试修改当前同步状态。
a) 状态为0时,可直接修改AQS状态为1,成功得到锁
i. 将AQS的父类中exclusiveOwnerThread 设为当前线程(表示当前线程为独占拥有线程)
b) 状态不为0时,判断当前线程是否和执行线程Owner一致。
i. 一致时,state+1(锁的重入)
ii. 不一致时,调用addWaiter(Node.EXCLUSIVE)方法。为线程创建node节点
iii.当队列为空时,调用enq方法。方法内部自旋,首先判断尾是否为空
1.为空时,创建一个空节点。赋head = tail
2.方法内自旋,此时尾不为空,将节点插入尾部
iv. 调用方法,设置Node中waitState值为-1,标志该Node中线程被取消。
v. 调用parkAndCheckInterrupt()方法,方法内部park(this),将线程状态由Runnable,修改为waiting
先调用release()方法。
1) 方法内部调用tryrelease()方法
a. tryrelease():对state-1操作。此时若state为0,返回true。否则返回false.
2) 当返回true时,进入if方法体,首先判断队列的head != null 且head.waitStatus != 0(当前实际值为-1),则调用unparkSuccessor(Node)方法
a. unparkSuccessor(Node)方法内部。
i. 修改waitState状态为0,
ii. 调用unpark(thread)方法,使线程由Waiting变为Runnable
同:
1)都为可重入锁:同一个线程每进入一次,锁的计数器都自增1,要等到锁的计数器下降为0时才能释放锁
2)优化思想:都试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
异:
1)Synchronized是依赖于JVM实现的(部分为C++代码),而ReenTrantLock是JDK实现的(Java代码实现)
2)ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
3)ReenTrantLock提供了一个Condition(条件)类,可实现分组唤醒线程,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
4)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。synchronized不可中断
分为三部分:对象头、实例数据、对齐填充
Header(对象头):
1)存放自身运行时数据: 哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
2)类型指针:虚拟机通过该指针确定对象是哪个类的实例
InstanceData(实例数据):对象存储的有效信息,即字段内容
Padding(对齐填充):占位符。Java要求对象大小为8字节的整数倍,当数据没有对齐时,使用对齐填充
公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
非公平锁:在锁释放时,随机指定一个线程得到锁。
Synchronized为非公平锁,
ReentrantLock默认为非公平锁,但可通过带Boolean值的构造函数来指定使用公平锁
背景:
1.许多应用中,共享数据的锁定常常为短暂持有。
2.挂起线程和恢复线程的操作都需要转入内核态中完成。系统高并发时,大量挂起和恢复十分影响性能。
3.当一个机器中有多个处理器,可以使多个线程并发执行时,我们可以让后一个请求锁的线程。执行一个短暂自循环(继续占用处理器),看看持有锁的线程是否会很快释放锁。
自旋锁:短暂占用处理器,节省挂起和恢复线程的资源。
自适应自旋锁:自旋的时间不再固定,而是根据持有相同锁的前一个线程的自旋时间和锁的状态来决定。
目的:在没有的多线程的竞争下,减少 重量级锁使用操作系统中互斥量的开销
实现:使用CAS操作修改对象头中标志位。
00表示持有锁
01表示未锁定。
10表示膨胀为重量级锁
当两个以上线程竞争时,轻量级锁不再有效,会膨胀为重量级锁
优点:轻量级锁使用CAS操作标志位,避免了重量级锁使用互斥量的开销
缺点:当存在锁竞争时,除了互斥量开销外,还有CAS操作,因此在存在竞争的情况下,轻量级锁比重量级锁更慢
目的:消除数据在无竞争情况下的同步原语,进一步提升程序运行性能。
实现:对于持有偏向锁的线程,在接下来程序的执行过程中,如果该锁没有被其他线程获取,则永远不进行同步操作。
轻量级锁在无竞争情况下,使用CAS操作。
偏向锁在无竞争情况下,直接消除同步,不做任何操作。
当程序总是存在多线程竞争的情况下,可通过禁止偏向锁优化以提升性能
使用参数-XX:-UseBiaseLocking
reentrantlock :已经实现偏向锁。
1.在加锁时,会判断是否为当前持锁线程(使用TreadID 或 Thread本身来进行判断)
2.是当前持锁线程时,不进行CAS操作,直接对count+1。减少了一次CAS操作锁带来的性能开销。