Synchronized关键字/锁

Synchronized关键字/锁

image-20210116211024291

使用场景

可分为三种:

//修饰实例方法,对当前实例对象加锁
public synchronized void instanceLock(){

}

//修饰静态方法,对当前类的Class对象加锁 本质上也是对象锁
public static synchronized void classLock(){

}

//修饰代码块,指定一个加锁的对象,给对象加锁
public void blockLock(){
    Object o = new Object();
    synchronized (o){

    }
}

如何实现加锁,为什么说是重量级的?

Java对象头

在JVM中,对象在内存分为三块区域:

  • 对象头
内容 说明
Mark Word 存储对象的hashcode或锁信息等
Klass Point 指向对象的类元数据的指针
Array length 数组的长度
  • 实例数据 存放类的数据信息,父类信息
  • 对其填充 虚拟机要求对象起始地址必须是8字节的整数倍,所以它为了字节对齐。

当当前线程是重量级锁时,Mark Word为指向堆中的monitor对象的指针。

而重量级锁归根到底就是monitor对象的争夺

  • 当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
  • 如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.
  • 同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。

所有的互斥,其实在这里,就是看你能否获得monitor的所有权,一旦你成为owner就是获得者。

同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。

monitor监视器是C++写的,在虚拟机的ObjectMonitor.hpp文件中。而ObjectMonitor的实现又涉及到操作系统的互斥量(mutex),而互斥量又涉及到了用户态和内核态的转换。当线程的synchronized锁还没有释放时,另一个线程需要操作系统切换内核态去阻塞它,这种切换是很耗资源的,效率也是很低的,所以说synchronized(1,6前)是重量级锁。

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;  // 存储Monitor对象
    _owner        = NULL;  // 持有当前线程的owner
    _WaitSet      = NULL;  // wait状态的线程列表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 单向列表
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁状态block状态的线程列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

大家说熟悉的锁升级过程,其实就是在源码里面,调用了不同的实现去获取获取锁,失败就调用更高级的实现,最后升级完成。

Java1.6对锁的优化

偏向锁

Hotspot作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。于是引入了偏向锁。

偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不需要了,提高了程序的性能。

实现原理

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向线程ID。

当下次该线程进入这个同步块时,会先去检查Mark Word里是不是存放着自己的ID

如果是的话,直接执行代码;

如果不是,说明有线程正在竞争。使用CAS方式替换对象头中的线程ID,就是先去测试当前偏向锁字段是否为0

若为0,说明竞争线程退出同步代码块,不存活了。该线程会将偏向锁字段设置为1,再将Mark Work重新设置为自己的线程ID,仍然为偏向锁。

若为1,说明竞争线程还在,偏向锁已经被它获取了。则该线程会撤销偏向锁,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁

撤销偏向锁

偏向锁使用了一种**等到竞争出现才释放锁的机制**。

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程实际开销很大:

  • 在一个安全点上停止拥有锁的线程;
  • 遍历线程栈,如果存在锁记录,需要修复锁记录和Mark Word,使其变成无锁状态
  • 唤醒被停止的线程,将当前锁升级为轻量级锁。

轻量级锁

实现原理

JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们叫做Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word

然后线程尝试用CAS操作将锁的Mark Word替换为指向锁记录的指针。

如果成功,当前线程获得锁。

如果失败,说明Mark Word已经被替换成了其他线程的锁记录了,然后该线程就尝试使用自旋来获取锁。

但是自旋是非常消耗CPU资源的,所以JDK采取了适应性自旋。就是如果当前线程自旋成功了,那下次自旋的次数会更多,如果自旋失败了,次数则减少。

当自旋失败后,这个线程就会被堵塞,同时锁也会升级为重量级锁。

轻量级锁的释放

轻量级锁解锁时,会使用CAS将之前复制在栈帧中的Displaced Maek Word替换回Mark Word中。(因为替换回去后,Mark Word不再指向该线程,下次线程进入该同步代码块,就可以进行偏向操作了)

若替换成功,说明整个过程没有其他线程访问。

若替换失败,说明当前线程在执行同步代码块期间,有其他线程在访问,锁已经膨胀为重量级锁了。

锁的升级

每一个线程在准备获取共享资源时,第一步会去检查Mark Word里面存放的是不是自己的线程ID。如果是的话,获得偏向锁。

如果不是,先进行CAS操作去替换Mark Word里面的线程ID,若成功,还是偏向锁;

如果失败,升级为轻量级锁。每个线程都尝试通过CAS操作将Mark Word指向自己的锁记录。

若成功,获得轻量级锁。

若失败,不断自旋,自旋到一定程度时,就将锁膨胀为重量级锁。并且该线程被堵塞,等待之前线程释放锁唤醒自己。

synchronized和Lock的区别

image-20201227192102457

image-20201227192600399

lock() 和 lockInterruptibly() 的区别

lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。

lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。

image-20201227194055071

ReentrantLock的可重入、可中断、非公平锁和公平锁

可重入

就是一个线程在获取了锁之后,再次去获取同一个锁,这时候仅仅是把state状态值进行累加。如果该线程释放了一次锁,就将state-1。当减到0时,其他线程才有机会获取锁。(这个线程释放锁后,会通知AQS等待队列里的线程节点。)

可中断(lockInterruptibly

可以中断等待获取锁的线程而直接返回,不用等到获取锁才响应。

可中断可以解决死锁问题

public class IntLock implements Runnable{
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    /**
     * 控制加锁顺序,产生死锁
     */
    public IntLock(int lock) {
        this.lock = lock;
    }
    public void run() {
        try {
            if (lock == 1) {
                lock1.lockInterruptibly(); // 如果当前线程未被 中断,则获取锁。
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");
            } else {
                lock2.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+",执行完毕!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 查询当前线程是否保持此锁。
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + ",退出。");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(1);
        IntLock intLock2 = new IntLock(2);
        Thread thread1 = new Thread(intLock1, "线程1");
        Thread thread2 = new Thread(intLock2, "线程2");
        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        thread2.interrupt(); // 中断线程2
    }
}

上述例子中,线程 thread1 和 thread2 启动后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。这便形成 thread1 和 thread2 之间的相互等待。代码 56 行,main 线程处于休眠(sleep)状态,两线程此时处于死锁的状态,代码 57 行 thread2 被中断(interrupt),故 thread2 会放弃对 lock1 的申请,同时释放已获得的 lock2。这个操作导致 thread1 顺利获得 lock2,从而继续执行下去。

非公平锁和公平锁

ReentrantLock可以自己设置是否为公平锁。非公平锁就是上来就抢,抢不到再排队。(可以查看源码)公平锁是乖乖排队。

两者的不同体现在acquire()方法。

synchronized 关键字和 volatile 关键字的区别

它们两个是互补的存在!

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好。
  • volatile关键字能保证数据的内存可见性,但是只能对单个volatile变量的读写有原子性。但synchronized对整个临界区代码的执行具有原子性。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
全部评论

相关推荐

06-27 15:15
长安大学 Java
点赞 评论 收藏
分享
不要停下啊:大二打开牛客,你有机会开卷了,卷起来,去找课程学习,在牛客上看看大家面试笔试都需要会什么,岗位有什么需求就去学什么,努力的人就一定会有收获,这句话从来都经得起考验,像我现在大三了啥也不会,被迫强行考研,炼狱难度开局,啥也不会,找工作没希望了,考研有丝丝机会
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务