并发编程(四)synchronized

#synchronized 关键字
synchronized关键字是Java 1.0就有的语法元素。在Java中,所有的object实例(class也是一种object)都可以作为多线程环境下得竞争资源,所以每个oject上都有一个锁的标记,在执行关键代码的时候,对非null的object加上synchronize关键字,标记一个代码块,可以自动对某个对象加解锁。
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
#synchronized的三种应用方式
synchronized有三种方式来加锁,分别是
\1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
\2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
\3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
#synchronized括号后面的对象
synchronized扩后后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。
#synchronized可能产生死锁

public class DeadLock {
    private Object a = new Object();
    private Object b = new Object();
    public static void main(String[] args) throws InterruptedException {
        DeadLock deadLock = new DeadLock();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (deadLock.a) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (deadLock.b) {
                        System.out.println("Thread 1 enter");
                    }
                }
            }
        }, "Thread1");
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (deadLock.b) {
                    synchronized (deadLock.a) {
                        System.out.println("Thread 2 enter");
                    }
                }
            }
        }, "Thread2");
        thread1.start();
        thread2.start();
    }
}

以上,thread1在请求到a的锁之后会带着锁睡一会儿,然后再请求b的锁,但是这是b的锁已经在thread2手里了,同时thread2还在请求a的锁,变成了循环等待并且是无限等待,于是产生了死锁。运行结果是两个线程都无限的等待下去。要想解这样的死锁,可以在竞争资源上加上是否被锁的标记位,然后引入等待超时的机制,使得有一方在请求资源超时之后做出让步,把手上已有的锁也释放了,改变循环等待的状态。但是,即使有了超时机制,也需要注意有过度退让的情况存在,形象的说,好比在一个只能容纳一个人通过的窄巷里,你和另一个人迎面走来,然后你们发现这样谁也过不去,于是都高风亮节的往后退出巷子,然后等待一会儿,又很默契的一起走了进去,结果是悲剧的又发生了死锁的情况,而且会持续下去。这就需要两个线程之间需要知道对方的情况而不是盲目的退让。
#Synchronize的可重入性
所以可重入性,是指在某个线程得到某个对象的锁之后,不需要额外申请该对象的锁也可以进入关键代码块。
#synchronized的字节码指令
通过javap -v 来查看对应代码的字节码指令,对于同步块的实现使用了monitorenter和monitorexit指令这两个指令,他们隐式的执行了Lock和UnLock操作,用于提供原子性保证。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。
这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器
线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。
#synchronized的锁的原理
jdk1.6以后对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁; 在了解synchronized锁之前,需要了解两个重要的概念,一个是对象头,另一个是monitor。
#####Java对象头
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
这里写图片描述
锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。
这里写图片描述
####Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit)
32位JVM的Mark Word的默认存储结构如下:
这里写图片描述
在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
这里写图片描述
这里写图片描述
####markOop实现
oop.hpp,每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应。先在oop.hpp中看oopDesc的定义
这里写图片描述
_mark 被声明在 oopDesc 类的顶部,所以这个 _mark 可以认为是一个 头部, 前面我们讲过头部保存了一些重要的
状态和标识信息,在markOop.hpp文件中有一些注释说明markOop的内存布局:
这里写图片描述
hash: 保存对象的哈希码
age: 保存对象的分代年龄
biased_lock: 偏向锁标识位
lock: 锁状态标识位
JavaThread*: 保存持有偏向锁的线程ID
epoch: 保存偏向时间戳
####Monitor
什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。从源码层面分析一下monitor对象:
oop.hpp下的oopDesc类是JVM对象的顶级基类,所以每个object对象都包markOop;
markOop.hpp中 markOopDesc继承自oopDesc,并扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象;
objectMonitor.hpp,在hotspot虚拟机中,采用ObjectMonitor类来实现monitor;
这里写图片描述
#synchronized的锁升级和获取过程
synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁;锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁.
####自旋锁(CAS)
自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。其实就是一段没有任何意义的循环。虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。
JDK1.6中-XX:+UseSpinning开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
####偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中
偏向锁的释放:
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程.
jvm开启/关闭偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
####轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
在当前线程的栈帧(stack frame)中生成一个锁记录(lock record),这个锁记录比前面说的那个对象锁(管理线程队列的monitor)简单多了,它只是对象头的一个拷贝。然后把对象头里的tag改成00,并把这个栈帧里的lock record地址放入对象头里。若操作成功,那就完成了轻量锁操作。如果不成功,说明有线程在竞争,则需要在当前对象上生成重量锁来进行多线程同步,然后将Tag状态改为10,并生成Monitor对象(重量锁对象),对象头里也会放入Monitor对象的地址。最后将当前线程t排队队列中。
轻量锁的解锁过程也很简单就是把栈帧里刚才的那个lock record拷贝到对象头里,若替换成功,则解锁完成,若替换不成功,表示在当前线程持有锁的这段时间内,其他线程也竞争过锁,并且发生了锁升级为重量锁,这时需要去Monitor的等待队列中唤醒一个线程去重新竞争锁。
####重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
这里写图片描述
偏向锁是比轻量锁还轻量的锁机制。当synchronized区域长期都由同一个线程加锁、解锁时,jvm就用偏向锁来做,它的加锁解锁比轻量锁操作起来指令更加简化。不过一旦有其他线程使用synchronized区域,即使没有线程间竞争,也会把偏向锁升级为轻量锁,当然如果发生线程竞争就再升级为对象锁。
锁的公平与不公平:公平锁是指线程获得锁的顺序按照fifo的原则,先排队的先得。非公平锁指每个线程都先要竞争锁,不管排队先后,所以后到的线程有可能无需进入等待队列直接竞争到锁。
非公平锁虽然可能导致某些线程饥饿,但是锁的吞吐率是公平锁好几倍,synchronized是一个典型的非公平锁方案,而且没法做成公平锁。
#wait和notify
wait和notify是用来让线程进入等待状态以及使得线程唤醒的两个操作
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
这里写图片描述
对于每个对象来说,都有自己的等待队列和阻塞队列。
当线程A(消费者)调用wait()方法后,线程A让出锁,自己进入等待状态,同时加入锁对象的等待队列。
线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。
线程A进入阻塞队列后,直至线程B释放锁后,线程A竞争得到锁继续从wait()方法后执行。

参考:http://www.jianshu.com/p/5dbb07c8d5d5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值