对于单线程的顺序编程而言,每次只做一件事情,其享有的资源不会产生什么冲突,但是对于多线程编程,这就是一个重要问题了,比如打印机的打印工作,如果两个线程都同时进行打印工作,那这就会产生混乱了。再比如说,多个线程同时访问一个银行账户,多个线程同时修改一个变量的值。这个时候,就很容易产生冲突了。

看一个例子: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)的更多相关文章

  1. java多线程02-----------------synchronized底层实现及JVM对synchronized的优化

    java多线程02-----------------synchronized底层实现及JVM对synchronized的优化 提到java多线程,我们首先想到的就是synchronized关键字,它在 ...

  2. Java多线程之内存可见性和原子性:Synchronized和Volatile的比较

    Java多线程之内存可见性和原子性:Synchronized和Volatile的比较     [尊重原创,转载请注明出处]http://blog.csdn.net/guyuealian/article ...

  3. “全栈2019”Java多线程第十六章:同步synchronized关键字详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  4. Java多线程(五) —— 线程并发库之锁机制

    参考文献: http://www.blogjava.net/xylz/archive/2010/07/08/325587.html 一.Lock与ReentrantLock 前面的章节主要谈谈原子操作 ...

  5. Java多线程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock

    在JDK5里面,提供了一个Lock接口.该接口通过底层框架的形式为设计更面向对象.可更加细粒度控制线程代码.更灵活控制线程通信提供了基础.实现Lock接口且使用得比较多的是可重入锁(Reentrant ...

  6. java多线程(五)之总结(转)

    引 如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个 ...

  7. Java多线程的同步方式和锁机制

    Object.wait(miliSec)/notify()/notifyAll() 线程调用wait()之后可以由notify()唤醒,如果指定了miliSec的话也可超时后自动唤醒.wait方法的调 ...

  8. Java 多线程(五)之 synchronized 的使用

    目录 1 线程安全 2 互斥锁 3 内置锁 synchronized 3.1 普通同步方法,锁是当前实例对象(this) 3.1.1 验证普通方法中的锁的对象是同一个. 3.1.2 验证不同的对象普通 ...

  9. java 多线程:线程通信-等待通知机制wait和notify方法;(同步代码块synchronized和while循环相互嵌套的差异);管道通信:PipedInputStream;PipedOutputStream;PipedWriter; PipedReader

    1.等待通知机制: 等待通知机制的原理和厨师与服务员的关系很相似: 1,厨师做完一道菜的时间不确定,所以厨师将菜品放到"菜品传递台"上的时间不确定 2,服务员什么时候可以取到菜,必 ...

随机推荐

  1. Chrome浏览器读写系统剪切板

    IE浏览器支持直接读写剪切板内容: window.clipboardData.clearData(); window.clipboardData.setData('Text', 'abcd'); 但是 ...

  2. IDEA上传项目至git

    今天来分享一下从idea上传项目至coding的过程. 本文基于windows系统. idea提供了很方便的控制git的界面化操作,除了安装git和一些必要的配置之外,用到命令行的地方会非常少. 1: ...

  3. Ext.chart.Chart 显示图标 -- 本地数据

    先来张直观的图,基于ext4.2制作 这就是弹出来的图表了. 今天先搭建一个本地数据版,也是静态数据版. 点击一个按钮执行一个function,来看function里面的东西: 一.依赖 Ext.re ...

  4. JavaWeb学习总结(一)——JavaWeb开发入门(转)

    转载自 http://www.cnblogs.com/xdp-gacl/p/3729033.html 一.基本概念 1.1.WEB开发的相关知识 WEB,在英语中web即表示网页的意思,它用于表示In ...

  5. openstack-glance API 镜像管理的部分实现和样例

    感谢朋友支持本博客,欢迎共同探讨交流,因为能力和时间有限.错误之处在所难免.欢迎指正. 假设转载,请保留作者信息. 博客地址:http://blog.csdn.net/qq_21398167 原博文地 ...

  6. TortoiseSVN的安装和使用

    TortoiseSVN是windows平台下Subversion的免费开源client. 一般我们都是先讲讲server的配置.然后再讲client的使用,可是在TortoiseSVN上.却能够反过来 ...

  7. java_抽象类

    抽象类1,语法定义抽象类前使用abstract关键字修饰,则该类为抽象类2.应用场景(1)在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确知道这些子类如何实现这些方法. (2)从多个 ...

  8. adb连接手机报错解决方案汇总(win7)

    >>adb devices常见错误:   >>解决方案汇总 检查端口是否占用:netstat -ano | findstr 5037 | findstr LISTENING 检 ...

  9. tools: idea 2017 激活

    1.下载并安装ideaIU (https://www.jetbrains.com/idea/download/#section=windows ,下载Ultimate版)2.下载 JetbrainsC ...

  10. Python学习日记:day1

    1.计算机基础 cpu:相当于人的大脑,用于计算. 内存:储存数据,运行速度快,成本高,断电数据消失. 硬盘 :固态硬盘(快).机械硬盘(有指针).储存数据,需要长期保持数据,重要文件 打开qq过程: ...