java 线程同步详细介绍及实例代码

java 线程同步

概要:

为了加快代码的运行速度,我们采用了多线程的方法。并行的执行确实让代码变得更加高效,但随之而来的问题是,有很多个线程在程序中同时运行,如果它们同时的去修改一个对象,很可能会造成讹误的情况,这个时候我们需要用一种同步的机制来管理这些线程。

(一)竞争条件

记得操作系统中,让我印象很深的有一张图。上面画的是一块块进程,在这些进程里面分了几个线程,所有这些线程齐刷刷统一的指向进程的资源。Java中也是如此,资源会在线程间共享而不是每个线程都有一份独立的资源。在这种共享的情况下,很有可能有多个线程同时在访问一个资源,这种现象我们叫做竞争条件。

在一个银行系统中,每个线程分别管理一个账户,这些线程可能会进行转账的操作。
在一个线程进行操作的时候,他首先,会把账户余额存放到寄存器中,第二步,它将寄存器中的数字减少要转出的钱数,第三步,它将结果写回余额中。
问题在于,这个线程在执行完1、2步时,另外一个线程被唤醒并且修改了第一个线程的账户余额值,但是这个时候第一个线程并不知情。第一个线程等待第二个线程执行完毕后,继续他的第三步:将结果写回余额中。这个时候,它把第二个线程的操作刷掉了,所以整个的系统的总钱数肯定会发成错误。
这就是java竞争条件发生的不良情况。

(二)ReentrantLock类

上面的例子告诉我们,如果我们的操作不是原子操作,被打断是肯定会发生的,即使有的时候概率真的非常小,但是也并不能排除这种情况。我们不能把我们的代码变成像操作系统中的原子操作,我们能做的是为我们的代码上锁来保证安全性。在并发程序中,如果我们想要访问数据,在这之前我们先给我们的代码套一个锁,在我们使用锁的期间,我们的代码中涉及的资源就像是被”锁上了“一样,不能被其他的线程访问,知道我们打开这个锁。

在java中,synchronized关键字和ReentrantLock类都有这种锁的功能。我们在这里首先一起来讨论一下ReentrantLcok的功能。

1.ReentrantLock构造器

在这个类中,提供了两个构造器,一个是默认构造器,没什么好说的,一个是带有公平策略的构造器。这个公平策略首先他比正常的锁要慢很多,其次在有的情况下他并不是真正公平的。而且如果我们没有特殊的理由真的需要公平策略的时候,尽量不要去研究这个策略。

2.获取与释放

ReentrantLock myLock = new ReentrantLock();
//创建对象
myLock.lock();
//获取锁
try{
...
}
finally{
myLock.unlock();
//释放锁
}

一定要记得在finally中释放锁!!我们之前说过,未检查的错误会导致线程的终止。莫名其妙的终止会让程序停止向下运行,如果不把释放放在finally中,这个锁将一直得不到释放。这种道理和我们在平时框架中用包后.close()是一个道理。说到close,值得一提的,当我们使用锁的时候,我们不能使用“带有资源的try语句”,因为这个锁并不是用close来关闭的。如果你不知道带有资源的try语句是什么,那就当我没说这句话吧。

3.锁具有可重入性

如果你要在递归或者循环程序中使用锁,那么就放心的用吧。ReentrantLock锁具有可重入性,他会在每次调用lock()的时候维护一个计数记录着被调用的次数,在每一次的lock调用都必须要用unlock来释放。

(三)条件对象

通常,线程在上了锁进入临界区之后发现了一个问题,他们所需要的资源,在别的对象中被使用或者并不满足他们能执行的条件,这个时候我们需要用一个条件对象来管理这些得到了一个锁,但是不能做有用工作的线程。

if(a>b){
  a.set(b-1);
}

1.”自己困住了自己“

上面是一个很简单的条件判断,但是我们在并发程序中不能这样写。存在的问题是,如果在这个线程刚刚做完判断之后,另外一个线程被唤醒,并且另外一个线程在操作之后使得a小于b(if语句中的条件已经不再正确)。

那么这个时候我们可能想到,我们把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但是如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句,这时,我们突然发现,if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确,这真的是非常尴尬,我们自己的锁把我们自己困住了,我们出不去,别人进不来。

2.Condition类

为了解决这种情况,我们用ReentrantLock类中的newCondition方法来获取一个条件对象。

Condition cd = myLock.newCondition();

获取了Condition对象之后,我们就应该来研究这个对象有什么方法和作用了。先不急于看API,我们回到主题发现现在亟待解决的就是if条件判断的问题,我们如何才能:在已经上锁的情况下,发现if判断错误时,给其他线程机会并自己一直等着if判断变回正确。

Condition类就是为了解决这个难题而生的,有了Condition类之后,我们在if语句下面直接跟上await方法,这个方法表示这个线程被阻塞,并放弃了锁,等其他的线程来操作。

注意在这里我们用的名词是阻塞,我们之前也说过阻塞和等待有很大不同:等待获得锁时,一旦锁有了空闲,他可以自动的去获得锁,而阻塞获得锁时,即使有空闲的锁,也要等待线程调度器允许他去持有锁的时候才能获得锁。

其他的线程在顺利执行if语句内容之后,要去调用signalAll方法,这个方法将会重新去激活所有的因为这个条件被阻塞的线程,让这些线程重新获得机会,这些线程被允许从被阻塞的地方继续进行。此时,线程应该再次测试该条件,如果还是不能满足条件,需要再次重复上述操作。

ReentrantLock myLock = new ReentrantLock();
//创建锁对象
myLock.lock();
//给下面的临界区上锁

Condition cd = myLock.newCondition();
//创建一个Condition对象,这个cd对象表示条件对象

while(!(a>b))
  cd.await();
//上面的while循环和await方法调用是标准写法
//如果不能满足if的条件,那么他将进入阻塞状态,放弃锁,等待别人去激活它

a.set(b-1);
//一直等到从while循环出来,满足了判断的条件,我们执行自己的功能

cd.signalAll();
//最后一定不能忘记调用signalAll方法去激活其他的被阻塞的线程
//如果所有的线程都在等待其他线程signalAll,则进入死锁

非常不妙的,如果所有的线程都在等待其他线程signalAll,则进入死锁的状态。死锁状态是指所有的线程需要的资源都被其他的线程形成环状结构而导致谁都不能执行的情况。最后调用signalAll方法激活其他因为cd而阻塞的“兄弟”是必须的,方便你我他,减少死锁的发生。

3.Condition对象和锁总结

总结来说,Condition对象和锁有这样几个特点。

  1. 锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
  2. 锁可以管理试图进入临界区的线程
  3. 锁可以拥有一个或多个条件对象
  4. 每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程

(四)synchronized关键字

我们上面介绍的ReentrantLock和Condition对象是一种用来保护代码片段的方法,在java中还有另外一种机制:通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。从版本开始,java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。

1.synchronized与ReentrantLock比较

我们先拿出上面的代码:

public void function(){
  ReentrantLock myLock = new ReentrantLock();
  myLock.lock();

  Condition cd = myLock.newCondition();

  while(!(a>b))
    cd.await();

  a.set(b-1);

  cd.signalAll();
}

如果我们用synchronized来实现这段代码,将会变成下面的样子:

public synchronized void function(){
  while(!(a>b))
    wait();

  a.set(b-1);

  notifyAll();
}

需要我们注意的是,在使用synchronized关键词时,无需再去用ReentrantLock和Condition对象,我们用wait方法替换了await方法,notifyAll方法替换了signalAll方法。这样写确实比之前的简单了很多。

2.静态方法的synchronized

将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。

3.内部锁和条件的局限性

内部锁虽然简便,但是他存在着很多限制:

  1. 不能中断一个正在试图获得锁的线程
  2. 试图获得锁时不能设定超时
  3. 因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的

在代码中应该使用这两种锁中的哪一种呢?Lock和Condition对象还是同步方法?在core java一书中有一些建议:

  1. 最好既不使用ReentrantLock也不使用synchronized关键词。在许多情况下你可以使用java.util.concurrent包
  2. 如果synchronized符合你的代码需要,请优先使用它
  3. 直到如果特别需要ReentrantLcok,再去使用它

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • Java中CountDownLatch进行多线程同步详解及实例代码

    Java中CountDownLatch进行多线程同步详解 CountDownLatch介绍 在前面的Java学习笔记中,总结了Java中进行多线程同步的几个方法: 1.synchronized关键字进行同步. 2.Lock锁接口及其实现类ReentrantLock.ReadWriteLock锁实现同步. 3.信号量Semaphore实现同步. 其中,synchronized关键字和Lock锁解决的是多个线程对同一资源的并发访问问题.信号量Semaphore解决的是多副本资源的共享访问问题. 今天

  • Java中的线程同步与ThreadLocal无锁化线程封闭实现

    Synchronized关键字 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(

  • 深入解析Java的线程同步以及线程间通信

    Java线程同步 当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization).像你所看到的,Java为此提供了独特的,语言水平上的支持. 同步的关键是管程(也叫信号量semaphore)的概念.管程是一个互斥独占锁定的对象,或称互斥体(mutex).在给定的时间,仅有一个线程可以获得管程.当一个线程需要锁定,它必须进入管程.所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程.这些其他的线程被

  • JAVA生产者消费者(线程同步)代码学习示例

    一.问题描述 生产者消费者问题是一个典型的线程同步问题.生产者生产商品放到容器中,容器有一定的容量(只能顺序放,先放后拿),消费者消费商品,当容器满了后,生产者等待,当容器为空时,消费者等待.当生产者将商品放入容器后,通知消费者:当消费者拿走商品后,通知生产者. 二.解决方案 对容器资源加锁,当取得锁后,才能对互斥资源进行操作. 复制代码 代码如下: public class ProducerConsumerTest { public static void main(String []args

  • Java多线程 线程同步与死锁

     Java多线程 线程同步与死锁 1.线程同步 多线程引发的安全问题 一个非常经典的案例,银行取钱的问题.假如你有一张银行卡,里面有5000块钱,然后你去银行取款2000块钱.正在你取钱的时候,取款机正要从你的5000余额中减去2000的时候,你的老婆正巧也在用银行卡对应的存折取钱,由于取款机还没有把你的2000块钱扣除,银行查到存折里的余额还剩5000块钱,准备减去2000.这时,有趣的事情发生了,你和你的老婆从同一个账户共取走了4000元,但是账户最后还剩下3000元. 使用代码模拟下取款过

  • 详解Java编程中线程同步以及定时启动线程的方法

    使用wait()与notify()实现线程间协作 1. wait()与notify()/notifyAll() 调用sleep()和yield()的时候锁并没有被释放,而调用wait()将释放锁.这样另一个任务(线程)可以获得当前对象的锁,从而进入它的synchronized方法中.可以通过notify()/notifyAll(),或者时间到期,从wait()中恢复执行. 只能在同步控制方法或同步块中调用wait().notify()和notifyAll().如果在非同步的方法里调用这些方法,在

  • Java多线程编程中synchronized线程同步的教程

    0.关于线程同步 (1)为什么需要同步多线程? 线程的同步是指让多个运行的线程在一起良好地协作,达到让多线程按要求合理地占用释放资源.我们采用Java中的同步代码块和同步方法达到这样的目的.比如这样的解决多线程无固定序执行的问题: public class TwoThreadTest { public static void main(String[] args) { Thread th1= new MyThread1(); Thread th2= new MyThread2(); th1.st

  • Java线程同步机制_动力节点Java学院整理

    在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系.可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题.现在就来学习多线程对数据访问的控制吧. 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 一.多线程引起的数据访问安全问题 下面看一个经典的问题,银行取钱的问题: 1).你有一张银行卡,里面有5000

  • java 多线程的同步几种方法

    java 多线程的同步几种方法 一.引言 前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊.闲话不多说,进入正题. 二.为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常.举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个

  • 解析Java线程同步锁的选择方法

    在需要线程同步的时候如何选择合适的线程锁?例:选择可以存入到常量池当中的对象,String对象等 复制代码 代码如下: public class SyncTest{    private String name = "name";public void method(String flag)    {        synchronized (name)        {            System.out.println(flag + ", invoke metho

  • Java中多线程同步类 CountDownLatch

    在多线程开发中,常常遇到希望一组线程完成之后在执行之后的操作,java提供了一个多线程同步辅助类,可以完成此类需求: 类中常见的方法: 其中构造方法: CountDownLatch(int count) 参数count是计数器,一般用要执行线程的数量来赋值. long getCount():获得当前计数器的值. void countDown():当计数器的值大于零时,调用方法,计数器的数值减少1,当计数器等数零时,释放所有的线程. void await():调所该方法阻塞当前主线程,直到计数器减

随机推荐