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

看一个例子: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. SketchMaster 隐私政策

    隐私政策 本应用尊重并保护所有使用服务用户的个人隐私权.为了给您提供更准确.更有个性化的服务,本应用会按照本隐私权政策的规定使用和披露您的个人信息.但本应用将以高度的勤勉.审慎义务对待这些信息.除本隐 ...

  2. python自带库及第三方库api察看

    今天发现一个很有意思的功能,python自带了所有库的文档查看器,配置如下: 配置pydoc服务,cmd中输入如下代码: python –m pydoc –p 1234 回车后 ,使用过程中,该窗口不 ...

  3. javascript设计模式——组合模式

    前面的话 在程序设计中,有一些和“事物是由相似的子事物构成”类似的思想.组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的“孙对象”构成的.本文将详细介绍组合模式 宏命令 宏 ...

  4. python入门基础

    Python 入门 变量 什么是变量?变量就是变化的量,核心是"变"和"量"两个字,变就是变化,量就是衡量状态. 为什么需要变量?程序执行的本质就是一系列状态的 ...

  5. Scala并发编程react、loop代码实战具体解释

    演示样例代码及凝视: //scala并发编程中的react和loop,共同特点: //通过线程存用的方式让性能有所提升. //Actor本身的运行,被actor子系统管理的时候,会有一个或者多个远程的 ...

  6. POJ 2533 Longest Ordered Subsequence(DP 最长上升子序列)

    Longest Ordered Subsequence Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 38980   Acc ...

  7. C#基础知识 结构与类的区别

    网上看到struct与class之间的区别,都写的很多,当然说的是对的,也很详细.不过我个人不喜欢照本宣科,还是要有自己的理解和认识,方便记忆. (前提:对于值类型与引用类型有一定的认识) 结构最重要 ...

  8. Druid数据库连接池源码分析

    上一篇文章重点介绍了一下Java的Future模式,最后意淫了一个数据库连接池的场景.本想通过Future模式来防止,当多个线程同时获取数据库连接时各自都生成一个,造成资源浪费.但是忽略了一个根本的功 ...

  9. js复制链接

    需求:js复制一个并非当前页面的链接 实现: html <button class="btn btn-primary btn_copyurl">复制课程链接</b ...

  10. java表单重复提交常用解决办法

    最近在看些基础的东西,顺便做下笔记.相信大家在平时网页使用中,经常会有按钮重复点击,然后点不动刷新,还有当网络延时比较厉害点了没反应在点击的重复提交.为了避免这种情况,总结了一下4点处理方案 表单重复 ...