欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Synchronized 和 Volatile、Lock 以及 ReentrantLock的区别

程序员文章站 2022-05-05 10:00:55
...

一、线程安全在三个方面体现

1、原子性(atomic、synchronized、Lock)

(1)原子的意思代表着——“不可分”;
(2)在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

1.1、atomic实现原子性

JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean,等等,位于sun.misc.Unsafe类下。其基本特性是在多线程环境下,当有多个线程同时对单个(包括基本类型和引用类型)变量进行操作时,具有排他性,即多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,直至成功,其实质是通过CAS完成原子性。

1.2、同步锁实现原子性

synchronized是一种同步锁,通过锁实现原子操作。

JDK提供锁分两种:一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;另一种是Lock,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。

2、可见性(volitile、synchronized、Lock)

线程执行结果在内存中对其它线程的可见性。

变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,加了这个指令后,会引发两件事情:

  • 发生修改后强制将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效,重新从内存中读取。

3、有序性(volatile、synchronized、Lock)

在本线程内观察,所有操作都是有序的(即指令重排不会导致单线程程序执行结果与排序前有任何差别)。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

二、synchronized关键字的作用

synchronized 美 [ˈsɪŋkrənaɪzd] 同步 synchronize的过去分词和过去式

synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。 因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。

三、volatile关键字的作用

volatile 美 [ˈvɑːlətl] 不稳定的,易挥发的

单例模式为什么要用Volatile关键字

其实volatile关键字的作用就是保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么如果一个线程修改了这个共享变量后,其他线程是立马可知的。为什么是这样的呢?比如,线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。

volatile能禁止指令重新排序,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。

四、volatile和synchronized的区别

(1)volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
(2)volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的,使用范围比较广 (要说明的是,java里不能直接使用synchronized声明一个变量,而是使用synchronized去修饰一个代码块或一个方法或类。);
(3)volatile只能保证可见性和有序性,不能保证原子性;而可见性、有序性、原子性synchronized都可以保证;
(4)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
(5)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

为什么volatile不能保证原子性?

对于i=1这个赋值操作,由于其本身是原子操作,因此在多线程程序中不会出现不一致问题,但是对于i++这种复合操作,即使使用volatile关键字修饰也不能保证操作的原子性,可能会引发数据不一致问题。

 private volatile int i = 0;
 i++;

如果启了500条线程并发地去执行i++这个操作, 最后的结果i是小于500的。因为 i++操作可以被拆分为三步:

  1. 线程读取i的值;

  2. i 进行自增计算;

  3. 刷新回i的值。

假设某一时刻i=5,此时有两个线程同时从主存中读取了i的值,那么此时两个线程保存的i的值都是5, 此时A线程对 i 进行了自增计算,然后B也对i进行自增计算,此时两条线程最后刷新回主存的i的值都是6(本来两条线程计算完应当是7),所以说volatile保证不了原子性。
具体可以理解为:

线程读取i;
temp = i + 1;
i = temp;

当 i=5 的时候A,B两个线程同时读入了 i 的值, 然后A线程执行了 temp = i + 1的操作, 要注意,此时的 i 的值还没有变化,然后B线程也执行了 temp = i + 1的操作,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6, 然后A线程执行了 i = temp (6)的操作,此时 i 的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时B线程需要重新读取 i 的值那么此时B线程保存的 i 就是6,同时B线程保存的 temp 还仍然是6, 然后B线程执行 i=temp (6),所以导致了计算结果比预期少了1。

五、Lock和synchronized的区别

Lock:底层是CAS乐观锁,依赖AbstractQueuedSynchronizer(AQS)类,把所有的请求线程构成一个CLH队列(FIFO的双向双端队列),而对该队列的操作均通过Lock-Free(CAS)操作。

  • Synchronized是关键字,内置语言实现,Lock是接口;
  • Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁;
  • Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁;
  • Lock可以使用读锁提高多线程读效率。

六、ReenTrantLock(可重入锁)和synchronized的区别

ReenTrant 美 [ˌriˈɛntrənt] 可重入的

Java中的ReentrantLock和synchronized两种锁定机制的对比(24w阅读量,详细)

轻松学习java可重入锁(ReentrantLock)的实现原理(5w阅读量,深入浅出,村人打水的故事)

ReenTrantLock可重入锁(和synchronized的区别)总结(3w阅读量,直接对比总结,以下总结参照此文)

可重入锁:ReentrantLock理解使用(1w阅读量)

ReentrantLock(重入锁)功能详解和应用演示
ReentrantLock原理

1、可重入性:

从名字上理解,ReenTrantLock是Lock接口的实现类,它的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

2、锁的实现:

Synchronized(关键字)是依赖于JVM实现的,而ReenTrantLock(类)是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

3、性能的区别:

在synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

4、功能区别:

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

5、锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

ReenTrantLock独有的能力:

1、ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2、ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3、ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly() 来实现这个机制。
4、ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。(可以判断是否获取到锁

ReenTrantLock实现的原理:

Synchronized 和 Volatile、Lock 以及 ReentrantLock的区别

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。由于线程被唤醒需要时间,在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

ReentrantLock提供了两个构造器,分别是

public ReentrantLock() {
    sync = new NonfairSync();
}
 
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

默认构造器初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。

什么情况下使用ReenTrantLock?

答案是,如果你需要实现ReenTrantLock的独有功能时。

参考
1、Java面试题二:synchronized 和 volatile 、ReentrantLock 、CAS 的区别
2、为什么volatile能保证有序性不能保证原子性
3、Synchronized和Lock的区别