Synchronized关键字详解

Synchronized提供线程安全机制。

一、如何使用Synchronized关键字?

1、修饰静态方法--->锁的是类对象

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

2、修饰一般方法--->锁的是实例对象

public synchronized void show() {
        System.out.println("Synchronized修饰的一般方法!");
    }
    public static void main(String[] args) {
        Test0105 test=new Test0105();
        test.show();
    }

3、修饰代码块--->锁的是当前代码块

public  void show() {
        Object o=new Object();
        synchronized (o) {
            System.out.println("Synchronized修饰的代码块!");
        }
    }

总而言之,Synchronized关键字锁的是堆内存中的对象。

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

二、Synchronized如何保证同步?(实现原理)

要理解Synchrinized底层实现原理,必须首先了解Java对象的内存布局,对象在内存中的布局包括三部分:对象头、实例数据、对齐填充数据。

其中,对象头是实现Synchronized的锁对象的基础。当给对象加锁的时,数据是存储在对象头中。当执行Synchronized同步方法或者同步代码块的时候,会在对象头中记录锁标记,锁标记指向的是monitor对象(也称为管道或者监视器锁)。

1、同步代码块的实现是基于虚拟机的指令monitorenter和monitorexit指令。monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁.。

2、同步方法的实现则是通过对象头实现的访问标志位为基础。synchronized方法会被翻译成普通的方法调用和返回指令,如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象。 


三、锁的种类

Java中的锁大致分为:偏向锁、轻量级锁、重量级锁、自旋锁(注意:锁只能升级,不能降级)

偏向锁、轻量级锁、重量级锁:这三种锁是指锁的状态,并且是针对Synchronized,在Java 5通过引入锁升级的机制来实现高效Synchronized,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价;轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能;重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

可重入锁:可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。可重入锁最大的作用是避免死锁。

乐观锁、悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

四、CAS(Compare and set)问题

CAS是一项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

CAS存在的问题:

1、ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2、性能消耗大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

五、总结

1、每个对象都有一个锁。用来在多线程访问的时候实现同步。
2、synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。
3、从执行效率的角度考虑,有时候我们未必要把整个方法都加上synchronized,而是可以采取 synchronized 块的方式,对会引起线程安全问题的那一部分代码进行 synchronized 就可以了。
4、两个 synchronized 块之间具有互斥性
5、synchronized 块获得的是一个对象锁,换句话说,synchronized 块锁定的是整个对象。
6、同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。

猜你喜欢

转载自blog.csdn.net/qq_40303781/article/details/85946177