一文详解synchronized

5.3 synchronized
1 底层原理

synchronized底层原理 = java对象头markword + 操作系统对象monitor

对象头的Mark word:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VpiXqROL-1643373380693)(Java面试题总结.assets/image-20220128191854648.png)]

Monitor结构:

//部分属性
ObjectMonitor() {
    _count        = 0;  //锁计数器 进入数
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  }

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eu1r5jnS-1643373380694)(Java面试题总结.assets/image-20220128192028194.png)]

1 synchronized无论是加在同步代码块还是方法上,效果都是加在对象上,其原理都是对一个对象上锁
2 如何给这个obj上锁呢?当一个线程Thread-1要执行临界区的代码时,首先会通过obj对象的markword指向一个monitor锁对象
3 当Thread-1线程持有monitor对象后,就会把monitor中的owner变量设置为当前线程Thread-1,同时计数器count+1表示当前对象锁被一个线程获取。
4 当另一个线程Thread-2想要执行临界区的代码时,要判断monitor对象的属性Owner是否为null,如果为null,Thread-2线程就持有了对象锁可以执行临界区的代码,如果不为null,Thread-2线程就会放入monitor的EntryList阻塞队列中,处于阻塞状态Blocked。
5 当Thread-0将临界区的代码执行完毕,将释放monitor(锁)并将owner变量置为null,同时计算器count-1,并通知EntryList阻塞队列中的线程,唤醒里面的线程

1》 synchronized作用在代码块时:

它的底层是通过monitorenter、monitorexit指令来实现的

monitorenter:

  • 每个对象都是一个监视器锁(monitor),当对象被占用时就会是锁定状态
  • **monitor进入数(锁计数器)**为0时代表无线程占用,当有线程进入时,进入数设置为1,且该线程就是monitor的拥有者owner
  • 当进入线程已经拥有了该monitor,则monitor进入数+1
  • 如果该monitor已被其他线程占用,则该线程进入monitor的阻塞队列中,等待monitor进入数为0

monitorexit:

  • 执行monitorexit的线程必须是objectref所对应的monitor持有者
  • 执行monitorexit后,monitor进入数减1,如果进入数减为0,则该线程释放monitor

2》synchronized作用在方法时:

  • 相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的
  • 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor
  • 在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
2 三种实现方式
作用于实例方法
public class Test8 implements Runnable {

    //静态变量 临界区
    static int count = 0;

    //synchronized修饰实例方法
    public synchronized void add() {
        count++;
    }

    @Override
    public void run() {
        //线程体
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test8 test8 = new Test8();
        //多个线程操作一个实例对象
        Thread thread1 = new Thread(test8);
        Thread thread2 = new Thread(test8);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count); //2000
    }
}

上述代码模拟了两个线程操作一个共享变量count,分别对count进行自加1000,最终结果是2000。

因为count++不是一个原子操作,分为先读值在加1两步操作,所以在并发执行时,如果不使用synchronized修饰实例方法,那么最终结果很大可能是小于2000的

问题:一个实例对象只有一把synchronized锁,如果有多个实例对象操作一个共享变量时,synchronized锁并不能保证线程的安全,如将上述代码的main方法修改为:

public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(new Test8());
    Thread thread2 = new Thread(new Test8());
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    System.out.println(count); //最终结果会小于2000
}

解决这种问题的方式是将synchronized作用于静态的add方法,这样的话,对象锁就当前类对象,无论创建多少个实例对象,类只有一个,所有在这样的情况下对象锁就是唯一的

作用于静态方法

当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作

public class Test8 implements Runnable {

    //静态变量 临界区
    static int count = 0;

    //synchronized修饰静态方法
    public static synchronized void add() {
        count++;
    }

    @Override
    public void run() {
        //线程体
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Test8());
        Thread thread2 = new Thread(new Test8());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count); //最终结果还是2000
    }
}
作用于同步代码块

当一个方法很大时,直接锁住整个方法会很费时;可以用同步代码快用于锁住一个方法中的小部分代码

synchronized不能修饰静态代码块

注:使用synchronized锁住同步代码块时,多个线程的实例对象也必须是同一个,不能作用于多个实例对象

public class Test8 implements Runnable {
    //全局静态实例
    static Test8 test8=new Test8();

    //静态变量 临界区
    static int count = 0;

    //synchronized修饰实例方法
    public void add() {
        //可以直接锁指定实例synchronized (test8)
        //也可以通过锁定传入的this实例
        synchronized (this) {
            count++;
        }
    }

    @Override
    public void run() {
        //线程体
        for (int i = 0; i < 1000; i++) {
            add();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(test8);
        Thread thread2 = new Thread(test8);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count); //2000
    }
}
3 synchronized升级机制

JDK 1.6之前,synchronized 还是一个重量级锁。但JDK1.6之后引入了偏向锁和轻量级锁 ,从此以后锁的状态就有了四种:无锁、偏向锁、轻量级锁、重量级锁

锁升级是不可逆的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4egQROu8-1643373380695)(Java面试题总结.assets/image-20220128201806521.png)]

1》 无锁

没有对资源进行锁定,所有线程都能访问并修改同一个资源,但只有一个线程能修改成功,其他线程会不断循环,直到修改成功

2》偏向锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁。并且执行完同步代码后不会释放锁

只依赖一次CAS原子指令,第二次进入退出同步区时检测Mark Word 里是否存储着指向当前线程的偏向锁。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

3》轻量级锁

当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能

轻量级锁的获取主要由两种情况:

  • 当关闭偏向锁功能时
  • 由于多个线程(两个及以上)竞争偏向锁导致偏向锁升级为轻量级锁

4》重量级锁

**忙等:**一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting),轻量级锁自旋就是忙等

如果自旋次数超过10次,则会升级锁重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值