synchronized会锁住谁呢:
synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个指令都需要一个reference类型的参数来指明要锁定和要解锁的对象。因为在Java中,一切都是对象,类也是一种对象,因此synchronized关键字会锁住某个类或者对象。
使用方法:
1. 修饰 代码块: 直接锁上括号里的对象或者类
synchronized ( object ) { ... }
synchronized ( className.class ) { ... }
2. 修饰 成员方法、静态方法 public synchronized void method() { ... }
注意:接口中的方法不能用;构造方法不能用; 该关键字无法继承(父类的方法用了该关键字,子类覆盖方法时,默认没有该关键字)
修饰成员方法时,会锁上这个方法的对象
修饰静态方法时,会锁上这个方法 对应的类
Synchronized是公平锁吗:不是
它的作用:原子性 可见性 有序性 可重入
首先要了解,Jvm中每个线程都有自己的工作内存,线程对变量的访问和修改应该现在自己的工作内存中,不能直接操作主存。线程也不能访问那其他线程的工作内存。
被synchronized锁上的对象只能被一个线程访问,在锁释放之前不会被其他线程访问,因此它是具有原子性的,因为要么获取不到锁无法执行,要么获得锁,在操作块中的代码执行完后 才释放锁。
它还能保证可见性。在线程获得锁之后,会先清空工作内存,将变量从主存复制过来。在解锁之前,要把被锁住的对象的信息 更新到主存中,之后再解锁。因此解锁后,其他的程序可以访问主存获得被修改的值。(其他的线程在被锁的对象被释放后 获取对象时,会从主存中读到刚刚被修改的值)
有序性:被synchronized括住的代码是有序的
可重入:
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。【引用:https://www.jianshu.com/p/5379356c648f】
synchronized的优化:
在1.6之前,它是一个重量锁,意思是当一个线程用synchronized锁住某个对象或者类时,如果发现该对象(类)被别的线程锁上了,该线程会被阻塞。 之所以叫“重量”锁,是因为Java中的线程映射到操作系统的原生线程中,如果要阻塞一个线程,那要操作系统来帮助完成。这意味着操作系统要从用户态转换为核心态,这个过程要消耗很多资源和处理器。如果我们的同步代码块(或者方法)进行的操作特别简单,那有可能执行这些语句耗费的时间都没有操作系统状态转换消耗的时间多。因此我们说synchronized是一个重量级的操作。
在1.6以后,synchronized变得智能了一些,除了重量锁,它还支持了偏向锁 和 轻量级锁。
首先需要了解Java中对象的对象头,对象是否被锁 以及 锁的类型等信息都被存储在对象头中。
Java对象可以被分为三个部分:对象头、实例数据以及对齐填充。
对象头又分为 Mark Word(存储对象运行时的数据,如hashcode,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间等) 和 Class Pointer(指向它的类的原数据指针; 对于数组,对象头中还要存储数组长度)【mark word图片源于网络】
分析一下该图,当JVM是32位时,mark word的长度为32位,且它的格式不是固定的,随着锁状态的不同,而不同。图中蓝色的部分是mark word可能的结构。下面的五行 是不同锁状态下具体结构。 不论什么状态,都有2bit的锁标志位。其余的部分如图。因此,当锁状态发生变化时,mark word中的32位数据都需要被修改。
偏向锁是指,在很多情况下,实际上不存在锁的竞争,总是同一个线程在获得同一个锁。
当一个对象的偏向锁 线程id为空时,则当有一个线程要对它上锁时,就将该线程的id写入这个 偏向锁线程id。
如果偏向锁 线程id不为空,判断要上锁的这个 线程id 与 偏向锁id 是否一致,如果一致,则不需要用对markword里的进行修改; 如果不一致,判断这个线程是否存活,如果不存活,则使锁变为无锁状态(唯一锁降级的情况)。再通过CAS竞争锁,如果竞争成功,则锁状态仍未偏向锁; 否则升级为轻量锁。如果线程存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么此时需要撤销偏向锁,升级为轻量级锁。
轻量锁:当一个对象的锁状态升级到轻量锁之后,如果发生锁竞争,未获得锁的线程不会立即被阻塞,而是会自旋(就是进行一个空循环)等一会儿,再尝试获得锁。
这样做的原因是,有可能拥有锁的线程 运行时间很短,很快就可以完成工作并释放锁。如果我们直接把未获得锁的线程阻塞,可能会导致,刚把该线程阻塞 锁就被释放了,这样消耗了资源和时间去阻塞和唤醒该线程。因此,我们让未获得锁的线程稍等一会儿。
如果它在自旋的过程中,成功等到了锁,那该对象的锁状态仍然为轻量锁;否则升级为重量锁。如果它在自旋时,又有第三个线程来要锁,则也要升级为重量锁。
如何进行锁升级 以及 释放锁 和 拥有锁:
当一个线程要一个未被锁住的对象,对象会从无锁状态升级为 偏向锁
当有一个线程要一个被偏向锁 锁上的对象,偏向锁升级为 轻量锁
当有一个线程要一个被轻量锁锁上的对象且自旋后获得失败(或自旋时又有线程来竞争),会升级为重量锁
每个线程中都会在栈帧中有一块空间,叫 DisplacedMarkWord,用于存储它获得的锁的对象的 mark word。
在一个线程想要占用一个对象时,像获取对象对象头,判断是否是可偏向的。
如果是是无锁且可偏向的,则通过CAS操作,把自己的线程id写入对象头。
如果CAS失败了,说明已经有别的线程抢先一步。这时,这个偏向锁需要升级为轻量级锁;
如果已经是偏向锁了且对象头中的threadId和自己的不一样,那执行之前的判断检查(因为线程不会主动释放可偏向锁,对象头中有占用偏向锁的线程ID并不代表他真的被占用了)。
如果锁其实没有被占用(占用该所的线程已经不要它了),则把对象头设置为无锁状态。需要抢占该对象的线程重新使用CAS操作进行抢占。
如果锁真的被占用了,该偏向锁也需要升级为轻量级锁。
升级锁的方式是,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。
【问题:当一个轻量级锁被线程释放后,对象头会变成什么样? 怎么释放呢?为什么有的博客会说 在释放的时候,会存在CAS 失败呢?
当一个轻量级锁被占用的时候,有一个线程来要锁,这个新的线程怎么判断当前锁被占用了?用CAS ?
】