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对象和锁有这样几个特点。
锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
锁可以管理试图进入临界区的线程
锁可以拥有一个或多个条件对象
每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程
(四)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.内部锁和条件的局限性
内部锁虽然简便,但是他存在着很多限制:
不能中断一个正在试图获得锁的线程
试图获得锁时不能设定超时
因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的
在代码中应该使用这两种锁中的哪一种呢?Lock和Condition对象还是同步方法?在core java一书中有一些建议:
最好既不使用ReentrantLock也不使用synchronized关键词。在许多情况下你可以使用java.util.concurrent包
如果synchronized符合你的代码需要,请优先使用它
直到如果特别需要ReentrantLcok,再去使用它
为了加快代码的运行速度,我们采用了多线程的方法。并行的执行确实让代码变得更加高效,但随之而来的问题是,有很多个线程在程序中同时运行,如果它们同时的去修改一个对象,很可能会造成讹误的情况,这个时候我们需要用一种同步的机制来管理这些线程。
以上就是java线程(二)—线程同步详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!