Java并发学习2【面试+工作】

Java并发学习2【面试+工作】

三.synchronized&volatile

synchronized

  关键字synchronized的作用是实现进程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性(即同步块每次应该只有一个线程可以执行)。

  关键字synchronized可以有多重用法,这里做一个简单的整理。

  • 制定加锁对象(同一对象)。对给定对象加锁,进入同步代码前需要获得给定对象的锁。
  • 直接作用于实例方法。相当于对当前示例加锁(同一对象),进入同步代码前要获得当前示例的锁。
  • 直接作用于静态方法。相当于对当前类加锁,进入同步代码前要获得当前类的锁。

  学习过《操作系统》的,这里非常容易理解,不在举具体的例子。

  除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便而已。就有序性而言,被synchronized限制的多线程其实是串行的执行同步代码的。

volatile

  用一句话概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道.

  首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存. 而在这个过程,变量的新值对其他线程是不可见的.而volatile的作用就是使它修饰的变量的读写操作都必须在内存中进行!

volatile与synchronized

  • volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
  • volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
  • volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
  • volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
  • volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.

  因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++ 等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。

  总结:volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。可以实现synchronized的部分效果,但当n=n+1,n++等时,volatile关键字将失效,不能起到像synchronized一样的线程同步的效果


四.重入锁&Condition条件

重入锁

  这里介绍一下synchronized、wait、notify方法的替代品(或者说是增强版)-重入锁。重入锁是可以完全替代以上的内容的。并且重入锁的性能是远高于synchronized的,但是jdk6.0开始,jdk对synchronized做了大量的优化,使得两者性能差距不大。

  重入锁使用java.util.concurrent.locks.ReentrantLock类来实现。它的几个重要方法如下:

注意:把解锁操作lock.unlock()放到finally子句非常重要。这样保证即使在临界区的代码抛出了异常,锁也必须释放,否则,其他线程将永远阻塞.

重入锁简单案例: lock与unLock

  上述代码使用重入锁保护临界区资源i,确保了多线程对i操作的安全性。从这段代码可以看到,与synchronized相比,重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远好于synchronized,但值得注意的是,在提出临界区时,必须记得释放锁,否则其他线程就没有机会再访问临界区了。

  对于重入锁,同一个线程可以多次获得锁,但是释放锁的时候,也必须释放相同次数。否则会产生异常。

重入锁的中断响应:lockInterruptibly

锁申请等待限时:lock.tryLock的两个方法

公平锁:ReentrantLock的构造函数可以增加一个参数

Condition条件

  如果大家理解了obj.wait和obj.notify方法的话,那么就很容易理解Condition对象了。它和wait和notify方法的作用是大致相同的。但是wait和notify方法是和synchronized关键字合作使用的,而Condition是与重入锁相关联的。通过Condition的newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。利用Condition对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时间得到通知,继续执行。

Condition提供的基本方法如下:

以上方法的具体含义如下:

  • await() 方法会是当前线程等待,同时释放当前锁,当其他线程中使用signal() 或signalAll() 方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和obj.wait方法很相似。
  • awaitUninterruptibly() 方法与await() 方法基本相同,只不过它不会在等待过程中响应中断。
  • signal() 方法用于唤醒一个在等待中的线程,这和obj.notify方法很类似。

简单示例

  代码中,听过lock生成一个与之绑定的Condition对象。代码15行要求线程在Condition对象上进行等待。代码32行,由主线程发起通知,告知等待在Condition上的线程可以继续执行了。

  和obj.wait和notify方法一样,当线程使用Condition.await时,要求线程持有相关的重入锁,在Condition.await调用后,这个线程会释放这把锁。同理,在Condition.signal方法调用时,也要求线程先获得相关的锁。在signal方法调用后,系统会从当前Condition对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在signal方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让他可以继续执行。比如,在本例中,第33行就释放了重入锁,如果省略第24行,那么,虽然已经唤醒了线程t1,但是由于它无法重新获得锁,因而也就无法真正的继续执行。

  在jdk内部,重入锁和Condition对象被广泛的使用,后面讲到的线程安全的容器,他们的内容时候都有重入锁和Condition对象的影子。


五.信号量

信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。信号量主要提供了一下构造函数:

  在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多少个线程可以访问某一个资源。信号量的主要逻辑方法有:

这里只讲几个常用的方法:

  • acquire() 方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断。
  • acquireUninterruptibly方法和acquire方法类似,但是不响应中断。
  • tryAcquire尝试获得一个许可,如果成功返回true,失败返回false,它不会进行等待,立即返回。
  • release用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

一个案例:

  上述代码中,15、16行为临界区,程序会限制执行这段代码的线程数。这里在第7行,声明了一个包含5个许可的信号量。这就意味着同时可以有5个线程进入代码段15,16行。申请信号量使用semp.acquire,在离开时,务必使用semp.release释放信号量。这就和释放锁一个道理。

原文发布于微信公众号 - Java帮帮(javahelp)

原文发表时间:2018-05-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JackieZheng

Spring实战——缓存

缓存 提到缓存,你能想到什么?一级缓存,二级缓存,web缓存,redis…… 你所能想到的各种包罗万象存在的打着缓存旗号存在的各种技术或者实现,无非都是宣扬缓...

200100
来自专栏云计算教程系列

如何在Debian 9上使用Python 3设置Jupyter笔记本

Jupyter Notebook为交互式计算提供了一个命令shell作为Web应用程序。该工具可以与多种语言一起使用,包括Python,Julia,R,Hask...

24830
来自专栏码洞

Lettuce快速入门

最近在开发一个使用Redis协议包装HBase的Proxy服务器,一路写的很顺,客户端使用redis-py提供的execute_command方法也轻松搞定。但...

41410
来自专栏CDA数据分析师

深入解读Python解析XML的几种方式

本文将介绍深入解读利用Python语言解析XML文件的几种方式,并以笔者推荐使用的ElementTree模块为例,演示具体使用方法和场景。文中所使用的Pytho...

30170
来自专栏黑泽君的专栏

day58_BOS项目_10

之前的请假流程,是没有实际意义的,我们要使得我们流程变得有意义(有实际意义),需要在流程向下推进的过程中带着数据推进才有意义。如下图所示:

10440
来自专栏腾讯Bugly的专栏

Android 平台 Native 代码的崩溃捕获机制及实现

一、背景 在Android平台,native crash一直是crash里的大头。native crash具有上下文不全、出错信息模糊、难以捕捉等特点,比jav...

1.2K70
来自专栏前端杂货铺

巧妙复制一个流

实际业务中可能出现重复消费一个可读流的情况,比如在前置过滤器解析请求体,拿到body进行相关权限及身份认证;认证通过后框架或者后置过滤器再次解析请求体传递给业务...

10830
来自专栏JackieZheng

Spring实战——缓存

缓存 提到缓存,你能想到什么?一级缓存,二级缓存,web缓存,redis…… 你所能想到的各种包罗万象存在的打着缓存旗号存在的各种技术或者实现,无非都是宣扬缓...

218100
来自专栏LEo的网络日志

shell技巧分享(六)

15850
来自专栏Python爱好者

Java基础笔记11

14340

扫码关注云+社区

领取腾讯云代金券