前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >笔记08 - Java的线程同步Synchronized和ReentrantLock

笔记08 - Java的线程同步Synchronized和ReentrantLock

作者头像
码农帮派
发布2021-01-12 14:54:57
3670
发布2021-01-12 14:54:57
举报
文章被收录于专栏:码农帮派码农帮派

Synchronized

synchronized可以用来修饰以下3个层面:

  • 1. 修饰实例的方法;
  • 2. 修饰静态类的方法;
  • 3. 修饰代码块

synchronized修饰实例方法:

synchronized修饰实例方法的时候,锁对象是当前的实例对象,同一个实例调用此方法的时候才会产生互斥效果,不同的实例对象之间不会有互斥效果。

上面的代码中,在不同的线程中调用不同对象的printLog方法,两者相互不排斥,两个线程会随机竞争CPU资源:

上面的打印效果可以看出,两个线程的执行互不影响,打印信息是随机的。

如果将代码改成两个线程公用一个对象进行printLog:

代码执行的效果如下:

可以看出只有在一个线程执行完毕之后,另一个线程的调用才会执行,不同线程中的调用是相互排斥的。

修饰静态类方法

如果synchronized修饰的静态类的方法,那么锁对象就是当前Class。在不同的线程中调用不同的实例对象,也会有互斥的效果。

下面将printLog修改为静态方法:

执行代码进行打印:

上面的打印结果可以看出,两个线程是依次执行的。

synchronized修饰代码块

synchronized作用于代码块的时候,锁对象是跟在synchronized后面的对象。任何的对象都可以当作synchronize的锁对象。

实现细节

synchronized既可以作用于方法,也可以作用于代码块。但不同的锁对象的实现是有区别的。

下面的代码中,synchronized作用于代码块上:

使用javap查看对应的字节码:

从上面编译好的字节码文件可以看出,synchronized修饰代码块的时候,会把被修饰的代码用monitorenter和monitorexit进行包裹。另外字节码中有一个monitorenter指令,和两个monitorexit指令。这是虚拟机为了保证在异常发生的情况下,锁也能够被释放,所以两个monitorexit,一个是正常流程释放锁,另一个是异常流程释放锁。

当synchronized修饰方法的时候

从上面的代码和字节码中可以看到,synchronized修饰方法的时候,代码编译时会在方法的flags中标示ACC_SYNCHRONIZED标志。当虚拟机访问一个ACC_SYNCHRONIZED方法的时候,会自动在方法的开始和结束的位置添加monitorenter和monitorexit指令。

monitorenter和monitorexit指令可以理解为一把锁,这个锁有两个重要的属性:计数器和指针。

  • 计数器:当前线程访问锁的次数;
  • 指针:指向持有这把锁的线程。

锁计数器默认为0,当执行monitorenter指令的时候,如果这把锁的计数器为0,说明这把锁没有被任何线程锁持有,此时线程会将锁计数器加1,并将锁的指针指向自己。当执行monitorexit的时候,会将计数器减1。

ReentrantLock

ReentrantLock和synchronized不同,ReentrantLoock的加锁解锁都是需要手动完成的:

上面的代码中lock和unlock分别是加锁和解锁的过程。

上面的打印可以看出ReentrantLock可以实现和synchronized一样的效果。

【注意】ReentrantLock的加锁和解锁需要手动完成,为了保证在异常流程中也能够成功的解锁,我们需要在try-catch的finally中解锁,从而保证任何时候锁都可以被正常释放。

公平锁

ReentrantLock有一个带参数的构造函数:

默认情况下,synchronized和ReentrantLock都是非公平锁。但是ReentrantLock可以通过传入fair = true来创建一个公平锁。公平锁是通过一个同步队列来实现多线程按申请锁的顺序获得锁。

运行代码:

【分析】

  • 1. 3个线程依次start,一个线程持有锁之后,另外两个线程就会被加入到ReentrantLock的同步队列中,等到当前持有锁的线程进行一次自加之后,锁被释放,此时由同步队列中下一个线程持有锁;
  • 2. 上面代码的run方法中有sharedNumber < 20的判断,但是打印结果会打印到22。三个线程,在争夺CPU资源的时候,同时经过while的判断进入了while内部,到lock.lock()的时候,只有一个线程持有锁并获得了操作sharedNumber的机会,等这个线程执行完毕释放锁的时候,另外两个线程由于已经通过while判断,所以不会再次进行while中的sharedNumber的判断,因此会形成这种情况。要是为了避免这种情况的发生,可以在lock.lock()之后,真正操作sharedNumber的时候再加一层sharedNumber < 20的判断。

读写锁(ReentrantReadWriteLock)

concurrent包中提供了ReentrantReadWriteLock,在读操作的时候获取读锁,在写操作的时候获得写锁。

1. 创建一个读写锁:

代码语言:javascript
复制
ReadWriteLock rwLock = new ReentrantReadWriteLock();

2. 通过rwLock对象分别获得读锁(ReadLock)和写锁(WriteLock):

代码语言:javascript
复制
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); 
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

3. 使用读锁和写锁:

当写入操作在执行的时候,读取数据的操作会被阻塞,当写入操作执行完毕之后,肚脐数据的操作继续执行,并且读取的数据是最新写入的数据;多个读操作之间是互不排斥的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农帮派 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档