Java锁的类型
Java语言提供的锁的类型
自旋锁:循环的使用CAS操作,直到成功。自旋锁是不会放弃CPU资源的。
乐观锁:假设数据的并发操作没有冲突,所有线程都可以修改数据,在修改时判断数据和之前是否一致,一致则修改成功,不一致修改失败。
悲观锁:假设数据的并发操作都有冲突,对数据的任何操作都采取同步。让所有的线程排队读写数据。
独享锁:给资源加写锁,只有当前线程能够读写数据,其他线程既不能读数据,也不能写数据。小结:只有一个线程能够读写数据。
共享锁:给资源加读锁,当前线程可以读数据,但是不能写数据。其他线程也可以对资源加读锁,但是不能加写锁。小结:多个线程能读数据,没有线程能写数据。
可重入锁/不可重入锁:一个锁,可以锁多个资源。可重入锁是指:当线程拿到锁后,能够进入锁的同步的其他代码块中。不可重入锁是指:线程拿到锁后,不能进入锁的同步的其他代码块中,如果要进入,需要再次拿到锁。
公平锁/非公平锁:是否按时间顺序拿到锁。公平锁就是先来后到,非公平锁就是允许插队获取锁。
同步关键字synchronized
synchronized简介
Java语言中,每一个对象都能作为锁,具体表现形式:对于synchronized修饰的普通同步方法,锁是当前对象,如果是静态的同步方法,锁是当前类的Class对象,如果是同步代码块,锁是括号里的对象。synchronized同步的本质是,每一个对象都有一个Monitor监视器对象,通过进出Monitor,实现方法同步和代码块同步。进入Monitor的指令是monitorenter,退出Monitor的指令是monitorexit,一次只有一个线程,能够进入Monitor,其他的线程试图进入Monitor就会进入阻塞状态。synchronized不仅能保证同步,还能保证可见性和原子性。
synchronized锁的特点是:悲观锁,独享锁,可重入锁。在一些场景下,JVM会对synchronized锁作出优化,例如锁粗化、锁消除。
锁粗化:把多个连续的锁,扩展为一个更大的锁,以减少频繁的进出同步代码块,频繁做互斥同步操作带来的开销。这一点同样适用于我们写代码,如果有一个for循环里面的操作是同步的,那应该对整个for循环加锁,而不是对for循环内的操作加锁。
锁消除:JVM能够针对同步代码块里的变量进行逃逸分析,如果同步代码块里的变量不会被其它线程访问,JVM在优化时就可以去除锁。我们写代码的时候,也不需要对方法变量进行加锁操作,因为方法变量是栈封闭的,只有当前线程能看到,不存在线程不安全问题。
举2个例子。
private int i;
private int j;
public void syn(){//锁粗化前
synchronized (this) {
i++;
}
synchronized (this) {
j++;
}
}
public void syn(){//锁粗化后
synchronized (this) {
i++;
j++;
}
}
public void test(){
//append方法是synchronized方法,如果下面的程序是热点代码,JVM就能够消除进出Monitor的指令。因为str1是线程安全的变量。
StringBuffer str1 = new StringBuffer();
str1.append("a");
str1.append("a");
str1.append("a");
}
线程阻塞的代价
在分析锁的原理之前,首先要了解操作系统在切换线程的时候,需要付出什么样的代价,这更有利于我们站在JDK的角度,思考锁为什么这么设计。
Java的线程,是在操作系统线程之上的,如果操作系统要阻塞/唤醒一个线程,就需要在用户态和内核态之间进行切换。用户态和内核态占用不同的内存空间,有各自专门的寄存器,用户态->内核态,需要传递很多参数给内核,内核也需要在切换的时候,保存用户的一些寄存器信息、变量等等。总之,这是一个比较消耗系统资源的过程。如果针对一些同步代码,代码执行需要的CPU资源,小于内核切换需要的CPU资源,那在高并发的环境下,等同于大量的资源用在了线程状态的切换。这也是synchronized在JDK1.6之前,性能被人诟病的原因,synchronized让所有线程排队进入同步代码块,拿不到Monitor锁的线程直接进入阻塞状态。
synchronized加锁的原理
在HotSpot虚拟机中,对象在内存中由三部分组成:对象头、实例数据、对齐填充。对象头中,又由三部分组成:Mark Word、Class Metadata Address、Array Length。这三者在32位虚拟机中,占用内存大小均为32bit,在64位虚拟机中,占用的内存大小均为64bit。synchronized锁的信息,就存在Mark Word区域里。
在32位JVM的Mark Word区域里,存储结构是这样的(图摘自网络),其中Epoch是偏向时间戳。当锁标志位是11时,这是给GC的标记清除算法使用的。
有了上述的基础概念,我们接下来详细说synchronized到底是怎么实现加锁的。在JDK1.6版本的时候,其对synchronized关键字做了大量的优化,目的是为了降低加锁、解锁的开销。synchronized锁有4个级别,无锁、偏向锁、轻量级锁、重量级锁。在使用synchronized关键字加锁时,会先尝试使用低级别的锁,如果竞争厉害了,才使用高级别的锁,因为高级别的锁会消耗更多资源。下面是synchronized锁的状态转换图。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。