java多线程(五)-访问共享资源以及加锁机制(synchronized,lock,voliate)
对于单线程的顺序编程而言,每次只做一件事情,其享有的资源不会产生什么冲突,但是对于多线程编程,这就是一个重要问题了,比如打印机的打印工作,如果两个线程都同时进行打印工作,那这就会产生混乱了。再比如说,多个线程同时访问一个银行账户,多个线程同时修改一个变量的值。这个时候,就很容易产生冲突了。
看一个例子:src\thread_runnable\EvenTest.java
class EvenChecker implements Runnable{
private IntGenerator generator;
public EvenChecker(IntGenerator generator) {
super();
this.generator = generator;
}
public void run() {
// TODO Auto-generated method stub
int val = 0;
while (!generator.isCanceled()){
val = generator.next();
if (val%2 != 0){
System.out.println("Error info --->" + val + " not even, threadInfo=" + Thread.currentThread().getName());
generator.cancel();
}
}
}
public static void test(IntGenerator gp, int count) {
System.out.println("start test " + count + " thread") ;
ExecutorService exec = Executors.newCachedThreadPool();
for (int i=0; i<count; i++){
exec.execute(new EvenChecker(gp));
}
exec.shutdown();
}
public static void test(IntGenerator gp) {
test(gp, 5);
}
}//end of "class EventChecker"
class IntGenerator {
private int currentEvenValue = 0;
private volatile boolean canceled = false;
/**
* 对于顺序执行的程序,该方法内的 currentEvenValue 的值每次都增加2,所以 该方法的返回值用于都为偶数,不可能为奇数。
* @return
*/
public int next(){
++currentEvenValue;
// Thread.yield();
++currentEvenValue;
return currentEvenValue;
}
public void cancel(){
canceled = true;
}
public boolean isCanceled(){
return canceled;
}
}//end of "class IntGenerator"
public class EvenTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
EvenChecker.test(new IntGenerator());
}
}
先来分析这个代码 ,
在IntGenerator对象中, currentEvenValue值初始值为0,在next()方法里每次加2,然后返回,所以next()方法返回的永远都为偶数,不可能为奇数。而EvenChecker对象默认开启了5个线程,循环获取 IntGenerator对象的next()方法产生的值,并进行判断,如果为奇数,则打印Error info,并停止循环。
当然,这个程序如果是顺序程序,那么永远不可能打印出Error Info,但是实际运行程序,某一次的输出结果如下:

很快的就产生了奇数的情况,原因就是因为 多个线程以交叉的顺序来修改了 currentEvenValue的值(当然对于多核cpu,可能就是在不同的核上同时运行),在 IntGenerator对象的next()方法中,有可能当currentEvenValue刚加一次时,另一个线程就又进入该方法进行修改。所以导致了产生了 奇数。
这就是多线程共享受限资源,而引起bug的一个明显的例子,我们可以想到,如果在 next()方法的两句++操作语句之间,加一句 Thread.yield()语句,就像下面这样,
++currentEvenValue;
Thread.yield();
++currentEvenValue;
那么 next()将会更快的产生奇数。
实际运行某一次输出结果如下:

解决共享资源竞争
要想避免类似上面demo中出现的不同步问题,做法就是当某一个受限资源在使用过程中加锁,每个线程在访问该资源前,都先检查一下该资源是否加锁了。没有则访问并加锁,否则就等待着,直到锁被(占有该资源的线程)释放了。
这种某个时刻只允许一个线程访问某个共享资源的方法,称为 序列化访问共享资源 的方案,通常这都是通过在代码片段开始时 加入特殊语句来实现的,然后同一时刻,只允许一个线程来访问这个代码片段。因为锁语句产生了一种相互排斥的效果,所以这种机制也常常被称为 互斥量(mutex)。
打印机的例子是很明显的,好几个人都挤在打印机前,都争着抢着打印自己的东西。但是如果某个人使用过程中,能随时被其他人打断抢走,那么最后的结果肯定是乱成一团。而通过加锁机制就可以避免这种情况。第一个挤上去的人,给打印机加了锁,然后开始打印自己的东西,这个时候其他人 虽然围在打印机周围,但是是没办法使用的,只有当第一个人使用完毕了,解锁后,才会有第二个人获得打印机资源,加锁,并开始使用打印机,不过谁会是第二个获得打印机的人,这就不确定了。
Java提供了 synchronized 关键字 来提供加锁支持,当某个线程执行某个被synchronized关键字保护的代码片段的时候,它将先检查其是否加锁,如果没有,则加锁,执行完毕后,再释放锁。如果已经加锁了,那就无法使用这个资源了。
在java中,一切都是对象,不管是要访问打印机,还是输入输出语句,都是要通过调用对象的方法来实现的,所以我们使用synchronized的方式可以是 在定义方法时加锁。
比如
class ClassA{
synchronized void g(){ /** do something */}
synchronized void f(){ /** do something */}
void m(){ /** do something */}
}
我们对ClassA的g()和f()方法进行了加锁。但是需要注意的是,synchronized 加锁,是加在整个ClassA对象上的,也就说,某个线程操作g()方法时,因为g()方法加锁了,其实是ClassA加锁了,所以f()方法也不能被其他线程调用,当然m()方法是可以被其他线程调用的。加锁都是加在对象上,而不是 某个方法上,这样设计是合理的,因为f()和g()既然都是一个对象的方法,那么从设计理念上来讲,他们都应该是属于和同一个受限资源有关系的方法。
具体的加锁,释放锁是JVM来负责的。
我们将上一个 demo中的 IntGenerator对象的next()方法进行加锁。
public synchronized int next(){
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
然后运行代码,输出结果如下

从控制台可以看出,程序一直在运行,但是不会再出现奇数,打印出Error info了。
使用 synchronized 关键字可以比较方便的来加锁,而java 5之后,引入了新的对象来加锁。例子如下:
void func(){
Lock lock = new ReentrantLock();
lock.lock();
try{
//do something
}finally{
lock.unlock();
}
}
Lock对象可以更加灵活,也可以提供更细粒度的控制,不过synchronized 写起来更加简单方便一些。
如果我们希望加锁的只是方法的部分代码而不是全部(这段代码被称为临界区 critical section),那么也可以使用 synchronized 关键字来操作。
void func(){
//do something
synchronized (this) {
//临界区
}
//do something
}
我们采用synchronized 来加锁除了防止争夺受限资源这个重要方面之外,其实还有一个方面,那就是:内存可见性.我们不仅希望防止线程A在访问某个对象状态时,另一个线程B同时也在修改该对象状态的这种情况的发生。同时也希望,当线程A修改完该对象的状态后,其他的线程在访问该对象时,都能看到这个变化。这就叫做内存可见性。
而实现内存可见性的方式,除了加锁方式,还有一个 volatile 关键字。
在java当中有个原子操作的概念,原子操作的意思就是 不能被线程调度机制所中断的操作。一般开始该操作,那么在它执行完之前,是不可能进行上下文切换的。比如对于 除了long,double之外的基本类型进行简单操作,就可以称为原子操作。(long,double都是64位,jvm在使用他们的时候,都是将他们当做两个32位的)。原子操作既然不会被线程调度机制中断,那么看起来不需要对它们进行同步控制。但是这种想法对于单核cpu也许使用,但是对于多核cpu,就不是这个样子了。
假设线程A,线程B 都需要访问一个int类型变量count,线程A在cpu的1号核上先执行任务,修改变量count的值,然后存储在了1号核本身的寄存器或者缓存上,然后访问完之后,线程B在cpu的2号核上开始运行,但是请注意这个时候,B读取的count的值是从 主存中读取的(有可能是内存,或者L1 ,L2 cache等).所以线程B读取到的值 和1号核的count值不同了。此时虽然对于count的修改是原子操作,没有被线程中断。但是却不同步。这也被称为 可视性问题。一个线程做出的修改,虽然是原子性的,没有被中断,但是对于其他线程也可能是不可视的。
Volatile关键字就是确保了可视性,当声明一个变量为volatile的,一个线程修改了该变量的值,其他线程也可以看到该修改。添加了volatile关键字的属性,会立刻被写入到主存中,这样就避免了不同步的问题。
synchronized和volatile有什么区别呢。
(1) volatile是一种比synchronized更加轻量级的同步机制。volatile不会执行加锁操作,也不会阻塞线程。
(2) 如果代码当中过度依赖volatile,那么将会使代码更脆弱,也更难以理解。
(3) 加锁机制既可以保证可见性又可以保证原子性。而volaitle只确保可见性。
总体来说,需要同步的时候,第一选择应该是synchronized,这是最安全的方式,虽然它可能性能差一些,不过随着jdk本身的优化,加锁机制的性能也在不断提升。
这几篇java多线程文章的demo代码下载地址 http://download.csdn.net/detail/yaowen369/9786452
-------
作者: www.yaoxiaowen.com
github: https://github.com/yaowen369
java多线程(五)-访问共享资源以及加锁机制(synchronized,lock,voliate)的更多相关文章
- java多线程02-----------------synchronized底层实现及JVM对synchronized的优化
java多线程02-----------------synchronized底层实现及JVM对synchronized的优化 提到java多线程,我们首先想到的就是synchronized关键字,它在 ...
- Java多线程之内存可见性和原子性:Synchronized和Volatile的比较
Java多线程之内存可见性和原子性:Synchronized和Volatile的比较 [尊重原创,转载请注明出处]http://blog.csdn.net/guyuealian/article ...
- “全栈2019”Java多线程第十六章:同步synchronized关键字详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Java多线程(五) —— 线程并发库之锁机制
参考文献: http://www.blogjava.net/xylz/archive/2010/07/08/325587.html 一.Lock与ReentrantLock 前面的章节主要谈谈原子操作 ...
- Java多线程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock
在JDK5里面,提供了一个Lock接口.该接口通过底层框架的形式为设计更面向对象.可更加细粒度控制线程代码.更灵活控制线程通信提供了基础.实现Lock接口且使用得比较多的是可重入锁(Reentrant ...
- java多线程(五)之总结(转)
引 如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个 ...
- Java多线程的同步方式和锁机制
Object.wait(miliSec)/notify()/notifyAll() 线程调用wait()之后可以由notify()唤醒,如果指定了miliSec的话也可超时后自动唤醒.wait方法的调 ...
- Java 多线程(五)之 synchronized 的使用
目录 1 线程安全 2 互斥锁 3 内置锁 synchronized 3.1 普通同步方法,锁是当前实例对象(this) 3.1.1 验证普通方法中的锁的对象是同一个. 3.1.2 验证不同的对象普通 ...
- java 多线程:线程通信-等待通知机制wait和notify方法;(同步代码块synchronized和while循环相互嵌套的差异);管道通信:PipedInputStream;PipedOutputStream;PipedWriter; PipedReader
1.等待通知机制: 等待通知机制的原理和厨师与服务员的关系很相似: 1,厨师做完一道菜的时间不确定,所以厨师将菜品放到"菜品传递台"上的时间不确定 2,服务员什么时候可以取到菜,必 ...
随机推荐
- CentOS7操作系统参数优化
生产环境配置需要标准化,将常用操作写成脚本用于操作系统的初始化. #!/bin/bash #Date:2017 #This Script is for centos7.3 init #01.配置yum ...
- 站在 Android 开发的角度,聊聊 Airbnb 的 Lottie!!!
一.前言 你有没有遇上一些设计师,对一些酷炫的动画着迷,喜欢根据场景设计出可爱而流畅的动画.但是在实际工作中,哪怕开发人员也觉得这些动画非常的棒,可我们知道,越是定制化的动画,实现起来将会越麻烦和耗时 ...
- 导入mysql数据的时候提示Field * doesn't have a default value解决方法
项目使用django+mysql 在linux中使用的是mysql5.7,导入数据提示:Field * doesn't have a default value 想要解决问题就需要知道在mysql5. ...
- iscroll4 input textarea不能获得焦点问题
最近在做移动端项目的时候,使用iscroll4实现页面滚动效果,之后发现页面中的input,textarea等不能得到焦点,输入内容. 问题原因是: 使用iscroll之后,输入框无法聚焦,页面文字等 ...
- Java---Ajax在Struts2框架的应用实例
Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术. 很久没有动过ajax了,趁此机会复习一下,写一个简单的例子 一.项目结构: 二.需要的jar包 三.具体代码: 1.web.x ...
- LDA主题模型学习笔记5:C源代码理解
1.说明 本文对LDA原始论文的作者所提供的C代码中LDA的主要逻辑部分做凝视,原代码可在这里下载到:https://github.com/Blei-Lab/lda-c 这份代码实现论文<Lat ...
- Android 四大组件学习之ContentProvider四
上节我们学习了怎样去读取系统短信以及插入一条短信到系统中. 本节我们学习怎样获取系统的联系人,以及插入一条联系人 好.废话不多说了,直接操作. 首先和读取短信一样,先找到联系人在数据库中的位置. wa ...
- Nginx事件处理中的connection和read、write事件的关联
/********************************************************************* * Author : Samson * Date ...
- Spring之AOP实现面向切面编程
近期在学Java的动态代理和Spring面向切面编程,越来越认为Spring设计的真的是太完美了.于是,想一个最简单的样例来跑一下.但问题多多,显示缺少,Aspectj里面的相应的类.导入Aspect ...
- DocFX生成PDF文档
使用DocFX生成PDF文档,将在线文档转换为PDF离线文档. 关于DocFX的简单介绍使用DocFX生成文档 使用docfx 命令 1.下载 https://github.com/dotnet/do ...