Synchronized锁机制

文章目录

    • 2.1 Synchronized
      • 2.1.1 Synchronized 关键字回顾
        • synchronized关键字的底层实现
          • synchronized修饰代码块
          • synchronized修饰普通方法
          • synchronized修饰静态方法
      • 2.1.2 售票案例

2.1 Synchronized

2.1.1 Synchronized 关键字回顾

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

另外,在 Java 1.6之前的版本中,synchronized 属于 重量级锁,效率低下。

因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{} 括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    1. 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这 个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方 法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。
  5. 关键字 synchronized 与 **wait()/notify()**这两个方法一起使用可以实现等待/通知模式

synchronized实现同步的基础:Java中的每一个队形都可以作为锁,具体表现有以下几点:

  • 对于普通方法,锁的是当前实例对象
  • 对于静态方法,锁的是当前类的class对象
  • 对于同步代码块,锁的是synchronized括号里的配置对象

尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

synchronized关键字的底层实现

synchronized修饰代码块
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过IDK自带的 == javap ==命令查看.class文件
在这里插入图片描述

从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitoropen in new window实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在这里插入图片描述

对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

Synchronized锁机制_第1张图片

如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized修饰普通方法
 public synchronized void method() {
        System.out.println("synchronized 方法");
    }

Synchronized锁机制_第2张图片

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized修饰静态方法
    public static synchronized void method() {
        System.out.println("synchronized 静态方法");
    }

Synchronized锁机制_第3张图片
可以看出synchronized修饰静态方法和实例方法没有区别,都是增加一个ACC_SYNCHRONIZED的flag,静态方法只是比实例方法多一个ACC_STATIC标识代表这个方法是静态的。

如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM会尝试获取当前class的锁

2.1.2 售票案例

package com;

//第一步  创建资源类,定义属性和和操作方法
class Ticket {
    //票数
    private int number = 30;
    //操作方法:卖票
    public synchronized void sale() {
        //判断:是否有票
        if(number > 0) {
            System.out.println(Thread.currentThread().getName()+" : 卖出:"+(number--)+" 剩下:"+number);
        }
    }
}

public class SaleTicket {
    //第二步 创建多个线程,调用资源类的操作方法
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket ticket = new Ticket();
        //创建三个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"AA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"BB").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //调用卖票方法
                for (int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        },"CC").start();
    }
}

如果一个代码块被Synchronzied修饰了,当一个线程获得了相应地锁,并执行了该代码块,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里 获取锁的线程释放锁只会有两种情况

  1. 获取锁的线程执行完了该代码块,然后线程释放对应的锁的占有。
  2. 线程执行发生异常,此时JVM会让线程自动释放锁

那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep 方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一 下,这多么影响程序执行效率。 因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。

你可能感兴趣的:(JUC,并发编程,java)