APP下载
报价宝  ›  科技  › 

关于synchronized和ReentrantLock之多执行绪同步详解

报价宝 来源:baojiabao.com 发布时间:2019-11-13 15:02:00 10月25日更新
报价宝综合消息关于synchronized和ReentrantLock之多执行绪同步详解

本篇文章总结关于多执行绪程式设计的一些知识点,这其中两个重要的部分就是对于synchronized和ReentrantLock的使用和介绍。

一、执行绪同步问题的产生及解决方案

问题的产生:

Java允许多执行绪并发控制,当多个执行绪同时操作一个可共享的资源变数时(如资料的增删改查),将会导致资料不准确,相互之间产生冲突。

如下例:假设有一个卖票系统,一共有100张票,有4个视窗同时卖。

public class Ticket implements Runnable {

// 当前拥有的票数

private int num = 100;

public void run() {

while (true) {

if (num > 0) {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

}

// 输出卖票资讯

System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);

}

}

}

}

public static void main(String[] args) {

Ticket t = new Ticket();//建立一个执行绪任务物件。

//建立4个执行绪同时卖票

Thread t1 = new Thread(t);

Thread t2 = new Thread(t);

Thread t3 = new Thread(t);

Thread t4 = new Thread(t);

//启动执行绪

t1.start();

t2.start();

t3.start();

t4.start();

}

输出部分结果:

Thread-1.....sale....2

Thread-0.....sale....3

Thread-2.....sale....1

Thread-0.....sale....0

Thread-1.....sale....0

Thread-3.....sale....1

显然上述结果是不合理的,对于同一张票进行了多次售出。这就是多执行绪情况下,出现了资料“脏读”情况。即多个执行绪访问余票num时,当一个执行绪获得余票的数量,要在此基础上进行-1的操作之前,其他执行绪可能已经卖出多张票,导致获得的num不是最新的,然后-1后更新的资料就会有误。这就需要执行绪同步的实现了。

问题的解决:

因此加入同步锁以避免在该执行绪没有完成操作之前,被其他执行绪的呼叫,从而保证了该变数的唯一性和准确性。

一共有两种锁,来实现执行绪同步问题,分别是:synchronized和ReentrantLock。下面我们就带着上述问题,看看这两种锁是如何解决的。

二、synchronized关键字

1.synchronized简介

synchronized实现同步的基础:Java中每个物件都可以作为锁。当执行绪试图访问同步程式码时,必须先获得物件锁,退出或丢掷异常时必须释放锁。Synchronzied实现同步的表现形式分为:程式码块同步和方法同步。

2.synchronized原理

JVM基于进入和退出Monitor物件来实现程式码块同步和方法同步,两者实现细节不同。

程式码块同步:在编译后通过将monitorenter指令插入到同步程式码块的开始处,将monitorexit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个物件都有一个monitor与之关联,执行绪执行monitorenter指令时,会尝试获取物件对应的monitor的所有权,即尝试获得物件的锁。

方法同步:synchronized方法在method_info结构有ACC_synchronized标记,执行绪执行时会识别该标记,获取对应的锁,实现方法同步。

两者虽然实现细节不同,但本质上都是对一个物件的监视器(monitor)的获取。任意一个物件都拥有自己的监视器,当同步程式码块或同步方法时,执行方法的执行绪必须先获得该物件的监视器才能进入同步块或同步方法,没有获取到监视器的执行绪将会被阻塞,并进入同步伫列,状态变为BLOCKED。当成功获取监视器的执行绪释放了锁后,会唤醒阻塞在同步伫列的执行绪,使其重新尝试对监视器的获取。

物件、监视器、同步伫列和执行执行绪间的关系如下图:

3.synchronized的使用场景

①方法同步

public synchronized void method1

锁住的是该物件,类的其中一个例项,当该物件(仅仅是这一个物件)在不同执行绪中执行这个同步方法时,执行绪之间会形成互斥。达到同步效果,但如果不同执行绪同时对该类的不同物件执行这个同步方法时,则执行绪之间不会形成互斥,因为他们拥有的是不同的锁。

②程式码块同步

synchronized(this){ //TODO }

描述同①

③方法同步

public synchronized static void method3

锁住的是该类,当所有该类的物件(多个物件)在不同执行绪中呼叫这个static同步方法时,执行绪之间会形成互斥,达到同步效果。

④程式码块同步

synchronized(Test.class){ //TODO}

同③

⑤程式码块同步

synchronized(o) {}

这里面的o可以是一个任何Object物件或阵列,并不一定是它本身物件或者类,谁拥有o这个锁,谁就能够操作该块程式程式码。

4.解决执行绪同步的例项

针对上述方法,具体的解决方式如下:

public class Ticket implements Runnable {

// 当前拥有的票数

private int num = 100;

public void run() {

while (true) {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

}

synchronized (this) {

// 输出卖票资讯

if(num>0){

System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);

}

}

}

}

}

输出部分结果:

Thread-2.....sale....10

Thread-1.....sale....9

Thread-3.....sale....8

Thread-0.....sale....7

Thread-2.....sale....6

Thread-1.....sale....5

Thread-2.....sale....4

Thread-1.....sale....3

Thread-3.....sale....2

Thread-0.....sale....1

可以看出实现了执行绪同步。同时改了一下逻辑,在进入到同步程式码块时,先判断现在是否有没有票,然后再买票,防止出现没票还要售出的情况。通过同步程式码块实现了执行绪同步,其他方法也一样可以实现该效果。

三、ReentrantLock锁

1.Lock界面

Lock,锁物件。在Java中锁是用来控制多个执行绪访问共享资源的方式,一般来说,一个锁能够防止多个执行绪同时访问共享资源(但有的锁可以允许多个执行绪并发访问共享资源,比如读写锁,后面我们会分析)。在Lock接口出现之前,Java程式是靠synchronized关键字(后面分析)实现锁功能的,而JAVA SE5.0之后并发包中新增了Lock界面用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

Lock界面的主要方法(还有两个方法比较复杂,暂不介绍):

void lock(): 执行此方法时, 如果锁处于空闲状态, 当前执行绪将获取到锁. 相反, 如果锁已经被其他执行绪持有, 将禁用当前执行绪, 直到当前执行绪获取到锁.

boolean tryLock():如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前执行绪被禁用, 当前执行绪仍然继续往下执行程式码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前执行绪并不继续向下执行. 通常采用如下的程式码形式呼叫tryLock()方法:

void unlock():执行此方法时, 当前执行绪将释放持有的锁. 锁只能由持有者释放, 如果执行绪并不持有锁, 却执行该方法, 可能导致异常的发生.

Condition newCondition():条件物件,获取等待通知元件。该元件和当前的锁系结,当前执行绪只有获取了锁,才能呼叫该元件的await()方法,而呼叫后,当前执行绪将缩放锁。

ReentrantLock,一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。(重入锁后面介绍)

2.ReentrantLock的使用

关于ReentrantLock的使用很简单,只需要显示呼叫,获得同步锁,释放同步锁即可。

ReentrantLock lock = new ReentrantLock(); //引数预设false,不公平锁

.....................

lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果

try {

//操作

} finally {

lock.unlock(); //释放锁

}

3.解决执行绪同步的例项

针对上述方法,具体的解决方式如下:

public class Ticket implements Runnable {

// 当前拥有的票数

private int num = 100;

ReentrantLock lock = new ReentrantLock();

public void run() {

while (true) {

try {

Thread.sleep(10);

} catch (InterruptedException e) {

}

lock.lock();

// 输出卖票资讯

if (num > 0) {

System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);

}

lock.unlock();

}

}

}

四、重入锁

当一个执行绪得到一个物件后,再次请求该物件锁时是可以再次得到该物件的锁的。

具体概念就是:自己可以再次获取自己的内部锁。

Java里面内建锁(synchronized)和Lock(ReentrantLock)都是可重入的。

public class SynchronizedTest {

public void method1() {

synchronized (SynchronizedTest.class) {

System.out.println("方法1获得ReentrantTest的锁运行了");

method2();

}

}

public void method2() {

synchronized (SynchronizedTest.class) {

System.out.println("方法1里面呼叫的方法2重入锁,也正常运行了");

}

}

public static void main(String[] args) {

new SynchronizedTest().method1();

}

}

上面便是synchronized的重入锁特性,即呼叫method1()方法时,已经获得了锁,此时内部呼叫method2()方法时,由于本身已经具有该锁,所以可以再次获取。

public class ReentrantLockTest {

private Lock lock = new ReentrantLock();

public void method1() {

lock.lock();

try {

System.out.println("方法1获得ReentrantLock锁运行了");

method2();

} finally {

lock.unlock();

}

}

public void method2() {

lock.lock();

try {

System.out.println("方法1里面呼叫的方法2重入ReentrantLock锁,也正常运行了");

} finally {

lock.unlock();

}

}

public static void main(String[] args) {

new ReentrantLockTest().method1();

}

}

上面便是ReentrantLock的重入锁特性,即呼叫method1()方法时,已经获得了锁,此时内部呼叫method2()方法时,由于本身已经具有该锁,所以可以再次获取。

五、公平锁

CPU在排程执行绪的时候是在等待伫列里随机挑选一个执行绪,由于这种随机性所以是无法保证执行绪先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些执行绪(优先级较低的执行绪)可能永远也无法获取CPU的执行权,优先级高的执行绪会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证执行绪按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序伫列。

ReentrantLock便是一种公平锁,通过在构造方法中传入true就是公平锁,传入false,就是非公平锁。

public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}

以下是使用公平锁实现的效果:

public class LockFairTest implements Runnable{

//建立公平锁

private static ReentrantLock lock=new ReentrantLock(true);

public void run() {

while(true){

lock.lock();

try{

System.out.println(Thread.currentThread().getName()+"获得锁");

}finally{

lock.unlock();

}

}

}

public static void main(String[] args) {

LockFairTest lft=new LockFairTest();

Thread th1=new Thread(lft);

Thread th2=new Thread(lft);

th1.start();

th2.start();

}

}

输出结果:

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

Thread-1获得锁

Thread-0获得锁

这是撷取的部分执行结果,分析结果可看出两个执行绪是交替执行的,几乎不会出现同一个执行绪连续执行多次。

六、synchronized和ReentrantLock的比较

1.区别:

1)Lock是一个界面,而synchronized是Java中的关键字,synchronized是内建的语言实现;

2)synchronized在发生异常时,会自动释放执行绪占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的执行绪响应中断,而synchronized却不行,使用synchronized时,等待的执行绪会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个执行绪进行读操作的效率。

总结:ReentrantLock相比synchronized,增加了一些高阶的功能。但也有一定缺陷。

在ReentrantLock类中定义了很多方法,比如:

isFair() //判断锁是否是公平锁

isLocked() //判断锁是否被任何执行绪获取了

isHeldByCurrentThread() //判断锁是否被当前执行绪获取了

hasQueuedThreads() //判断是否有执行绪在等待该锁

2.两者在锁的相关概念上区别:

1)可中断锁

顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一执行绪A正在执行锁中的程式码,另一执行绪B正在等待获取该锁,可能由于等待时间过长,执行绪B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的执行绪中中断它,这种就是可中断锁。

lockInterruptibly()的用法体现了Lock的可中断性。

2)公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个执行绪在等待一个锁,当这个锁被释放时,等待时间最久的执行绪(最先请求的执行绪)会获得该锁(并不是绝对的,大体上是这种顺序),这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些执行绪永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的执行绪获取锁的顺序。ReentrantLock可以设定成公平锁。

3)读写锁

读写锁将对一个资源(比如档案)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个执行绪之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。

ReadWriteLock就是读写锁,它是一个界面,ReentrantReadWriteLock实现了这个界面。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

4)系结多个条件

一个ReentrantLock物件可以同时系结多个Condition物件,而在synchronized中,锁物件的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地新增一个锁,而ReentrantLock则无须这么做,只需要多次呼叫new Condition()方法即可。

3.效能比较

在效能上来说,如果竞争资源不激烈,两者的效能是差不多的,而当竞争资源非常激烈时(即有大量执行绪同时竞争),此时ReentrantLock的效能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

在JDK1.5中,synchronized是效能低效的。因为这是一个重量级操作,它对效能最大的影响是阻塞的是实现,挂起执行绪和恢复执行绪的操作都需要转入核心态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock物件,效能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的效能并不比Lock差。官方也表示,他们也更支援synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

自己是从事了七年开发的Android工程师,不少人私下问我,2019年Android进阶该怎么学,方法有没有?

没错,年初我花了一个多月的时间整理出来的学习资料,希望能帮助那些想进阶提升Android开发,却又不知道怎么进阶学习的朋友。【包括高阶UI、效能优化、架构师课程、NDK、Kotlin、混合式开发(ReactNative+Weex)、Flutter等架构技术资料】,希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

资料获取方式:转发+关注后私信回复我【学习】免费获取Android进阶开发资料!!!

资料大全

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 汽车价格 笔记本价格 笔记本电脑价格 小米手机价格 红米手机价格