Synchronized 和 Lock 对比学习

wait 与 sleep 方法字节码分析

Object 里面定义了几个与线程相关的方法: wait 方法、notify方法、notifyAll方法。这些方法可以解决线程之间的通信问题

在这里插入图片描述

在这里插入图片描述
wait方法被 native 关键词修饰,底层是通过 C++ 代码实现。wait 方法会导致线程等待,直到其他的线程调用了这个对象的 notify / notifyAll 方法才会使得这个线程唤醒。

调用 wait() 方法时, 线程必须要持有被调用对象的锁,当 wait() 方法后,线程就会释放掉该对象的锁 。 换言之就是 wait 方法需要放在 synchronized 修饰的代码里面(如果直接调用也会抛出异常)

调用Thread 类的 sleep 方法时,线程是不会释放掉对象的锁的

关于wait 与 notify 和 notifyAll 方法的总结

  1. 当调用 wait 时,首先需要确保调用了 wait 方法的线程已经持有了这个对象的锁
  2. 当调用 wait 后,该线程就会释放掉这个对象的锁,然后进入到等待状态 (wait set)
  3. 当线程调用了 wait 后进入到等待状态时,它就可以等待其他线程调用相同对象的 notify 或 notifyAll 方法来使得自己被唤醒
  4. 一旦这个线程被其他线程唤醒后,该线程就会与其他线程一同开始竞争这个对象的锁(公平竞争);只有当该线程获取到了这个对象的锁后,线程才会继续往下执行
  5. 调用 wait 方法的代码片段需要放在一个 synchronized 块或是 synchronized 方法中,这样才可以确保线程在调用 wait 方法前,已经获取到了对象的锁
  6. 当调用对象的 notify 方法时,它会随机唤醒该对象等待集合 (wait set)中的任意一个线程,当这个线程被唤醒后,它就会与其他线程 一同竞争对象的锁
  7. 当调用对象的 notifyAll 方法时,它会唤醒该对象等待集合 (wait set)中所有的线程,这些线程被唤醒过后,又会开始竞争对象的锁
  8. 在某一时刻,只有唯一一个线程可以拥有对象的锁

下面一个例子能很好的帮助你去理解 wait 和 notify 这两个方法

案例剖析:
在这里插入图片描述
1.先把这个构建出来,并且把增1 和减1 方法写出来

public class MyObject {
    
    
    private int counter;

    //加1的方法
    //这里必须要用 while 循环,等程序被 notify 唤醒过后不能立即往下执行,还需要重新竞争这个锁。
    public synchronized void increase() {
    
    
        while (counter != 0) {
    
    
            try {
    
    
                wait();  //wait方法调用过过后线程会释放锁,允许别的线程拥有这个对象的 moniter
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        counter++;
        System.out.println(counter);
        notify();
    }

    //减1的方法
    public synchronized void decrease() {
    
    
        while (counter == 0) {
    
    
            try {
    
    
                wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

        counter--;
        System.out.println(counter);
        notify();
    }
}

2.增加的线程调用对象的增加方法,减少的线程调用减少的方法

//增加线程
public class IncreaseThread extends Thread {
    
    

    private MyObject object;

    public IncreaseThread(MyObject object) {
    
    
        this.object = object;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 30; i++) {
    
    
            try {
    
    
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            object.increase();
        }
    }
}
//减少线程
public class DecreaseThread extends Thread {
    
    
    private MyObject object;

    public DecreaseThread(MyObject object) {
    
    
        this.object = object;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 30; i++) {
    
    
            try {
    
    
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            object.decrease();
        }
    }
}
public class Client {
    
    

    public static void main(String[] args) {
    
    
        MyObject myObject=new MyObject();

        Thread increaseThread1=new IncreaseThread(myObject);
        Thread decreaseThread1 = new DecreaseThread(myObject);
        
        increaseThread1.start();
        decreaseThread1.start();

    }
}

透过字节码理解 synchronized 关键字

写点儿伪代码:

方法3和方法4都是被 static 修饰的
在这里插入图片描述
假设一个线程先进入到了 method1方法,众所周知此时其余线程是不能进入到 method2方法的,因为其他的线程是无法拿到这个对象的 moniter 的。那么此时又能不能进入到 method3 方法或者是 method4 方法呢??

答案是:可以的。因为被 static 修饰的方法,锁的是所在类的 Class 对象,和 method1 锁的对象根本不是一个

------------------------------------------分割线--------------------------------------------------
------------------------------------------分割线--------------------------------------------------

通过一些Java反编译工具看看 Synchronized 底层实现

比如你自己的 Idea 开发工具可以装一个 jclasslib 这样的一个插件,就能很好的看到反编译的结果

1.synchronized修饰代码块

public class MyTest1 {
    
    

    private Object object = new Object();

    public void method() {
    
    
        synchronized (object) {
    
    
            System.out.println("hello world");
        }
    }

    public void method2() {
    
    
        synchronized (object) {
    
    
            System.out.println("welcome");
        }
    }

}

反编译结果:
在这里插入图片描述
这里为什么有一个 monitorerter,monitorexit 指令却有两个呢??

在字节码层面上多加了一条 monitorexit 指令来使得,异常情况的出现不能释放掉锁

总结:
当我们使用 synchronized 关键字修饰代码块时,字节码层面上是通过 monitorenter 与 monitorexit 指令实现锁的获取与释放动作

当线程进入到 monitorenter 指令后,线程将会持有 monitor对象,退出 monitorenter 指令后,线程将会释放 monitor对象

2. synchronized修饰方法

public class MyTest2 {
    
    

    public synchronized void method() {
    
    
        System.out.println("hello world");
    }
}

反编译结果:
在这里插入图片描述
总结:
synchronized 修饰方法时: 并没有出现 monitorenter 和 monitorexit 指令,而是出现了 ACC_SYNCHRONIZED 标志

是因为 JVM 使用了 ACC_SYNCHRONIZED 访问标志来区分了一个方法是否为同步方法;当方法被调用时,调用指令就会检查该方法是否拥有 ACC_SYNCHRONIZED 标志

如果有,执行线程将会先持有方法所在对象的 monitor 对象,然后再去执行方法;在该方法执行期间,其他任何线程无法再获取到这个 monitor 对象 。当线程执行完这个方法后,它会释放这个 Monitor 对象

最后对 synchronized 的总结

  • JVM 中的同步是基于进入与退出监视器对象 Monitor 来实现的,每个对象实例都会有一个 monitor 对象,monitor对象会和Java对象一同创建并销毁,monitor对象是由C++来实现的
  • 当多个线程同时访问一段同步代码时,这些线程会放到一个 EntryList 集合中,处于阻塞状态的线程会被放到该列表中。接下来,当线程获取到对象的 monitor时,monitor是依赖底层操作系统的 mutex lock 来实现互斥的,线程获取 mutex 成功,则会持有该 mutex,这时其他线程就无法获取到该 mutex
  • 如果线程调用了 wait 方法,该线程就会释放掉所持有的 mutex,并且该线程会进入到 WaitSet 集合(等待集合),等待下一次其他线程调用 notify/notifyAll 唤醒。如果当前线程顺利执行完毕,那么它也会释放掉所持有的 mutex
  • 那些处于 EntryListWaitSet 中的线程处于阻塞状态,阻塞操作是由操作系统完成的,在linux 下是通过 pthread_mutex_lock 函数实现的。线程被阻塞后会进入到内核调度状态,这会导致系统在用户态和内核态之间来回切换,严重影响锁的性能
  • 解决上述问题的办法便是自旋,其原理是:当发生 monitor 的争用时,如果 owner 能够在很短的时间内释放掉锁,那些正在争用的线程就可以稍微等待一下(即所谓的自旋),在 Owner 线程释放锁之后,争用线程可能立即获取到锁,从而避免了系统阻塞。不过,当 Owner 运行时间超过了临界值后,争用线程自旋一段时间后依然无法获取到锁,这时争用线程则会停止自旋进入到阻塞状态,所以总体的思想是:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有极大的性能提升。显然,自旋在多处理器(多核心)上才有意义

Synchronized 锁升级与偏向锁深入解析

在 JDK 1.5 之前,我们若想实现线程同步,只能通过 synchronized 关键字这一种方式来达成。底层 Java 也是通过 synchronized 关键字来做到数据的原子性维护的。synchronized 关键字是 JVM 实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是由 JVM 帮助我们实现的

很多人都说 synchronized 是一种重量级的锁,使用它都非常消耗资源。其实这种说法对 JDK 1.6 以后来说(包括JDK1.6),是不准确的

从JDK 1.6开始, synchronized 锁的实现发生了很大的变化, JVM 引入了相应的优化手段来提升synchronized锁的性能。这种提升涉及到了偏向锁、轻量级锁和重量级锁。从而减少锁的竞争带来的用户态、内核态之间的切换。这种锁的优化机制实际上是通过 Java 对象头中的一些标志位去实现的。对于锁的访问与改变,实际上都与 Java 对象头息息相关

从 JDK 1.6 开始,对象实例在堆当中会被划分为三个组成部分:

  • 对象头
  • 实例数据
  • 对齐填充
    在这里插入图片描述

对象头主要也是由3块内容来构成(HotSpot虚拟机):

1. markword
第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。

2. klass
对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.

3. 数组长度(只有数组对象有)
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度.

对于锁的演化过程,它会经历如下阶段:
无锁->偏向锁->轻量级锁->重量级锁

  • 偏向锁:
    针对一个线程来说的,它的主要作用就是优化同一个线程,多次获取一个锁的情况;如果一个 synchronized 方法被一个线程访问,那么这个方法所在的对象就会在其 Mark word 中将偏向锁进行标记。同时还有一个字段来存储该线程的 ID,当这个线程再次被访问同一个 synchronized 方法时。它将会检查这个对象的 Mark word 的偏向锁以及是否指向了线程ID.如果是,那么该线程就无需再进去管程(Monitor),而是直接进入该方法体重

(如果是另外一个线程访问这个 synchronized 方法,那么偏向锁就会被取消掉。锁升级变成轻量级锁)

  • 轻量级锁:
    若第一个线程已经获取到对象的锁,这是第二个线程又开始尝试争抢该对象的锁。由于该对象的锁已经被第一个线程获取到,因此它是偏向锁。而第二个线程在争抢时,会发现该对象头中的 Mark word 已经是偏向锁,但里面的存储的线程ID不是自己(是第一个线程),那么它会运行CAS(compare and swap)

1).锁获取成功,那么它会将 Mark word 中的线程ID 由第一个变成自己(偏向锁标记位保持不变),这样该对象依然会保持偏向锁的状态

2).锁获取失败,则表示这是可能会有多个线程同时尝试争取该对象的锁,那么该对象偏向锁会进行升级,升级为轻量级锁

  • 重量级锁:
    线程最终从用户态进入到内核态

----------------- -------------------------分割线--------------------------------------------------------------
---------------- --------------------------分割线--------------------------------------------------------------
--------------------- ---------------------分割线--------------------------------------------------------------

Lock 锁机制深入理解

JDK 1.5开始,并发包引入了Lock锁,Lock锁的同步锁是基于Java实现的,因此锁的获取和释放都是通过Java代码来实现和控制的。然后 synchronized 是基于底层操作系统的 mutex lock 来实现的,每次锁的获取与释放都是带来用户态和内核态之间的切换,这种切换回增加系统的负担,当并发量较高时,锁的竞争比较激烈的时候, synchronized 同步锁在性能上的表现就非常差。

Lock 的注释有点儿多,我就截取第一段来说说
在这里插入图片描述
第一段因为翻译过来:Lock 实现提供了更为广泛的一种锁的操作,要比使用 synchronized 方法或者是语句更能获得锁。它们允许更灵活的结构,可能有完全不同的属性,并且可以支持多个相关的 Condition 对象

稍后会额外讲讲这个 Condition 对象的作用

Lock 锁方法原理详解

在这里插入图片描述

  • lock()
    获取到锁。如果当前线程获取不到锁,那么它会一直陷入到等待,直到这个锁获取到

  • lockInterruptibly()
    去获取到锁,但允许可以被中断的…
    如果它等在获取锁的过程中,有其它线程中断了它,那么它就可以不陷入等待。

  • tryLock()
    这个方法是我们开发中用的最多的
    仅当在调用时锁是空闲的时才获取锁。
    如果这个锁是可以拿到的,会立刻返回 true 。如果不能返回,就返回 false

     * Lock lock = ...;
     * if (lock.tryLock()) {
    
    
     *   try {
    
    
     *     // manipulate protected state
     *   } finally {
    
    
     *     lock.unlock();
     *   }
     * } else {
    
    
     *   // perform alternative actions
     * }}</pre>

这种写法可以很友好的去支持我们拿到一把锁。假设我们可以拿到一把锁,我们就去拿这把锁,执行我们逻辑,并且在 finally 释放我们锁;如果我们没拿到这把锁,我们也不需要释放这把锁

  • boolean tryLock(long time, TimeUnit unit)
    在给定的时间内,线程没被中断,我们就去看能不能获取一把可用的锁。
    (如果第一时间没拿到,我们还可以等一会儿~~~~)

  • unlock()
    释放锁,一定要放 finally 中,确保锁的释放
    (当然我也没说放 finally 就能保证锁一定会被释放~~~~)

  • newCondition()
    返回一个绑定在 Lock 实例上面的 Condition 实例

猜你喜欢

转载自blog.csdn.net/weixin_43582499/article/details/113994420