java笔记:熟练掌握线程技术---基础篇之解决资源共享的问题(中)--中篇
要真正理解如何解决线程资源共享冲突的问题,还真有点复杂,但是这个又是线程的精华所在,也是线程中最重要的知识,我要尽力讲清楚它,因此内容比较多了,从中篇里还出了个中篇。
上篇博文的末尾我写了一段实例代码,想表现线程抢占资源时候所发生的资源冲突问题,不知道大家真的看明白了那段代码的意思吗?反正我对这段代码琢磨了半天才领悟了其中的含义。这里我还是先把前面那段代码贴出来:
package cn.com.sxia;
public class Semaphore implements Invariant {
private volatile int semaphore = 0;
public void acquire(){
++semaphore;
}
public boolean available(){
return semaphore == 0;
}
public void release(){
--semaphore;
}
@Override
public InvariantState invariant() {
int val = semaphore;
if (val == 0 || val == 1){
return new InvariantOK();
}else{
return new InvariantFailure(new Integer(val));
}
}
}
package cn.com.sxia;
public class SemaphoreTester extends Thread {
private volatile Semaphore semaphore;
public SemaphoreTester(Semaphore semaphore){
this.semaphore = semaphore;
setDaemon(true);
start();
}
public void run(){
while(true){
if (semaphore.available()){
yield();
semaphore.acquire();
yield();
semaphore.release();
yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore();
new SemaphoreTester(semaphore);
new SemaphoreTester(semaphore);
new InvariantWatcher(semaphore).join();
}
}
在main函数里我们创建了两个SemaphoreTester对象,也就是启动了两个线程,两个线程操作的是同一个Semaphore对象,换句话说操作的是同一个资源了,我们不断运行main函数监控程序总会打印出一次失败的信息,比如下面的信息:
Invariant violated: -1
我们再看看所写的代码,我们改写下程序,我们没有启动两个线程,只启动了一个Semaphore线程时候,SemaphoreTester里的run方法使得Semaphore里的semaphore的值总不会变成非0或1的数值,那么监控的程序也就不会报出我们失败信息来。那么到底是什么原因产生了失败了?
原因在于一个线程可能从对available()的调用中返回真,但是当此线程调用acquire()时候,第二个线程可能已经调用了acquire()并且增加或者减少了semaphore字段的值了,此时的InvariantWatcher就可能发现我们的数值违反了我们制定的约束,不能为非0或1的值导致程序被停止了。
大家还要注意:监控程序线程我调用了join()方法,大家还记得这个方法吗?我在基础篇上篇里面介绍过他,加上这个方法会让前面两个线程一直执行直到发生了失败才会调用我们写的监控程序,join方法让我们的监控程序不会干扰两个线程的正常运行。
另外对于volatile关键字,这个也不是一句两句说的清楚的,我在后面会做进一步的解释的。
上面的实例代码很好的说明了多线程里资源竞争的难题,我学到这里,个人觉得根本原因还是在线程调度机制CPU时间片的随机切换所导致的。
写上面的实例代码我提到这段代码是模仿信息量的程序的简化版,这个又怎么理解了?
前面我提到过简单“信号量”的概念,它可以看做两个线程之间的通讯标志对象。假如信号量的值为0,则信号量所监控的资源是可用的,如果非0,则监控资源不可用,那么其他线程就要等待了,当资源可以被线程使用时候,线程就会增加信号量的值,该线程然后继续执行并使用这个监控资源,但是其他线程是不能使用监控资源的。我们的例子就是按这个原理写出来的,属性semaphore的初始值是0,这就是说各个线程在初始化状态下都是被激活的,线程们的赶紧抢占资源啊。acquire方法就是给信号量增加值,release方法释放信号量的数值,available方法就是对信号量值进行判断,如果值为0,其他线程就可以抢占资源,不为零其他线程就得等待了。为了很好的演示我写的程序功能,同时程序里面还加入了我写好的监控程序的代码。
下面我将讲述java里如何来解决资源冲突的方案,答案很多人都知道那就是:synchronized关键字了,但是大家使用synchronnied来解决资源冲突时候,我们想过它的原理吗?java到底是运用什么样的算法解决了资源冲突了?
其实现在所有主流程序解决线程冲突也就是共享资源竞争的问题都是采用一种叫做序列化共享资源的方案。这个方案的内容就是:在给定的时刻里只准许一个线程访问共享资源,通常这个是通过在代码前面加上一段能创建一个锁的语句,这就保证了在一定时间内只有一个线程运行这段代码。锁的作用让不同的线程之间产生相互排斥的效果,因此这种做法也叫做“互斥量”(mutex).
Java也是采用这样的方案来解决线程冲突的问题,在java语言里,java提供了关键字synchronized,这个关键字为了防止资源冲突提供了内置支持,换句话说,当我们使用了synchronized关键字时候就告诉了java语言,你要帮我解决资源冲突的问题了。其实java语言内部,准确的说法应该是java虚拟机内部就是按照“信息量”的原理解决了这个难题,而内部的行为很像我们写的Semaphore类:有一个方法检测信号量的值,根据值的不同授予线程不同的状态(例如available方法),有一个方法会用来增加信号量的值(例如acquire方法),还有一个方法会减少信号量的值(例如release方法),最后当然也有一个监控程序了例如我们写的那样的监控框架。
我们到底如何使用synchronized关键字的,这个问题是不是很搞笑了?我觉得有时看起来很简单的东西里面所蕴含的精髓可能相当丰富或者这个简单背后有我们难以理解的高深之处了?因此我要好好谈谈如何使用synchronized关键字的问题,当然synchronized的用法丰富多彩,我这里只讲用synchronized来解决资源共享时候的用法。这里要声明下,我对synchronized的理解还是比较有限的,而且这个关键字其他的用法我还没深入研究过,自己也写得少,假如后面的内容有些说法过于绝对或者不太正确的话还请大家多多包涵了,反正有错误希望博友们能及时指出来了。
Synchronized的产生是为了解决线程冲突换句话说是共享资源共享的问题。从这个概念里面我们发现它包含了两个实体,一个是共享资源,一个是使用共享资源的方法。我在这里设定一个场景,把这一切的要素都放到一个类中,那么共享资源就是这个类的一个属性(不是静态的,我们谈论这个问题前提是所有的属性和方法属于对象,而不是属于类),而且一般为了保护类里面的属性,这个属性往往是私有的(private),而所有在这个类中的能访问到这个属性的方法都加synchronized,这么一看我们就明白了,synchronized在为所有方法加锁了,这种做法的结果就是当某一个线程正在使用带有synchronized关键字的方法时候,只要这个方法还在运行没有结束,其他所有该类带有synchronized的方法的线程都会被锁住。
这里要强调一下,锁的机制一定是在对象的不同方法上的,如果是不同对象的同一个方法对于同一个资源的访问是不存在冲突的问题,换句话说线程里线程的调度是以方法为单位进行调度的,例如方法A抢占到了CPU的时间片,那么方法B就被挂起了,但是在内存中方法A应该是唯一的,因此不存在一个对象的A使用另一个对象的方法A是被挂起的,也许这个说法大家可能不太好理解,我举个例子吧,大家看下面的代码:
package cn.com.sxia;
public class SingleMethodThread{
private volatile int i = 10;
public void release(){
System.out.println("前数值是:" + i);
if (i != 0){
--i;
}else{
System.exit(0);
}
System.out.println("后数值是:" + i);
}
}
package cn.com.sxia;
public class SingleMethodThreadTester extends Thread {
private volatile SingleMethodThread smt;
public SingleMethodThreadTester(SingleMethodThread smt){
this.smt = smt;
start();
}
public void run(){
while(true){
smt.release();
yield();
}
}
public static void main(String[] args) {
SingleMethodThread smt = new SingleMethodThread();
new SingleMethodThreadTester(smt);
new SingleMethodThreadTester(smt);
}
}
结果如下:
前数值是:10
前数值是:10
后数值是:9
后数值是:8
前数值是:8
前数值是:8
后数值是:7
后数值是:6
前数值是:6
前数值是:6
后数值是:5
后数值是:4
前数值是:4
前数值是:4
后数值是:3
后数值是:2
前数值是:2
前数值是:2
后数值是:1
后数值是:0
前数值是:0
前数值是:0
这个代码里我建了两个线程,线程都传入同样的数据也就是同一个资源即共享资源了,运行程序,程序的结果都是正常的,没有数据违反了我们设定的约束条件既然还在运行。我们在看一段代码:
package cn.com.sxia;
public class MutiMethodThread {
private volatile int i = 10;
public boolean isExit() {
if (i == 0)
return false;
else
return true;
}
public void release() {
System.out.println("前数值是:" + i);
--i;
System.out.println("后数值是:" + i);
}
}
package cn.com.sxia;
public class MutiMethodThreadTester extends Thread {
private volatile MutiMethodThread mmt;
public MutiMethodThreadTester(MutiMethodThread mmt){
this.mmt = mmt;
start();
}
public void run(){
while(true){
mmt.release();
yield();
}
}
public static void main(String[] args) {
MutiMethodThread smt = new MutiMethodThread();
new MutiMethodThreadTester(smt);
new MutiMethodThreadTester(smt);
}
}
结果如下:
前数值是:-18572
后数值是:-18573
前数值是:-18573
后数值是:-18574
前数值是:-18574
后数值是:-18575
前数值是:-18575
后数值是:-18576
前数值是:-18550
后数值是:-18577
前数值是:-18577
后数值是:-18578
前数值是:-18578
后数值是:-18579
前数值是:-18579
后数值是:-18580
。。。。。。。。。
当线程run方法里调用了对象两个不同方法也就产生了线程冲突的问题了。
上面的问题是一个很小的细节,不过我认为它是一个很关键的细节,在我做过对这个技术交流的人中我发现很多人其实对该处的知识大多都有错误的认识,而这种错误又常被人忽略结果导致对自己写出的线程程序有了错误的解读,每一个知识点都是结构严密的逻辑体,半天马虎就会把驴子当马用了,看起来没错其实差之千里了。
我在前面一直都强调synchronized关键字会给代码加锁,那么这个锁到底存在哪个地方啊,是方法还是对象还是类了?假如有面试官问你这个问题,你又当如何回答呢?
解决资源共享的锁在对象里(也在类里,这个我后面会提到就是不在方法上),每个对象都包含一个单一的锁,有的地方会把这个锁称为监视器,它本身也是对象的一部分,当该对象的任意一个带有synchronized关键字方法被调用的时候,对象都会被上锁,加锁的对象除了现在被调用的方法可以运行,其他所有带synchronized方法只有在对象释放掉锁后才能执行。所以,在java语言里一个对象所有带synchronized方法都是共用同一个锁。
讲了这么多估计还是有许多的童鞋感觉还是在云里雾里,我想换个角度解释解决冲突的问题,可能会开阔一下大家的思路。首先是共享的资源,也就是同一个时间很多线程会抢夺的资源到底是啥东东,共享的资源在我的理解里就是一块一堆线程都可以访问存储数据的内存区域,然而不同线程对这块内存区域的修改都是独立的,不会有交互,就像有一碗饭,大家排队轮流吃一口,可以前面吃完一口的的那个人不会告诉下一个人这碗饭吃了多少还剩多少,就算吃完了饭也只有当事人知道,其他人不知道,终于某个人吃完了最后一口,下一位又来吃,但是饭已经吃完了,没有饭了我们还说吃饭就不符合逻辑了,根据逻辑我们是希望在饭吃完时候大家都知道,大家就不用排队等饭吃了,线程冲突的问题就和这个类似,我们设定的约束条件该如何被执行了?在线程中到底谁是吃饭的人呢?根据我上面的代码,我是在对象的范畴里讨论资源冲突,对象里的一个方法就是吃饭的人,不同的方法就是不同的人了。有些人认为不同对象调用同一个方法去访问共享资源也会有冲突,大家看我上面写的实例代码,这种不是会有冲突的,为什么呢?其实java里的某一方法也是唯一的,这个不难理解,我们写的方法说白了就是一段代码,程序运行时候代码进入内存,内存的代码还是唯一的,因为我们就写了那一段,聪明的计算机不会肆意去copy里的代码,不同的对象执行同一个方法,这个方法在执行时候是唯一的,不可能同时有两个相同的方法在被调用,所以同一个方法是不会产生线程冲突的。但是不相同的方法调用共享资源就会产生冲突的问题了。
我们回到对象的锁,一个线程执行时候我们可以获得这个线程调用对象的锁多次,这个可能不太好理解,我举个例子:我们调用对象的一个方法,这个方法里又调用了对象的另一个方法,那么对象的锁就被调用两次了。Java虚拟机会记录下我们调用对象锁的次数了,一个对象的所有的锁都被解开了,那么锁的计数就为0了,如果对象调用了n次方法锁的计数就是n了。在程序中只有首先获得锁的那个线程才有机会获得多个锁的特权。
对象可以调用内部的属性和方法,构建对象的类也是可以调用属于类的属性和方法,那么类级别的操作也会存在线程冲突的问题,虽然属于类,但是解决方法和对象是一致的。
解决理论知识的讲解结束了,我们可以改写下上篇博文里的代码了,代码如下:
package cn.com.sxia;
public class SynchronizedEvenGenerator implements Invariant {
private int i;
public synchronized void next(){
i++;
i++;
}
public synchronized int getValue(){
return i;
}
@Override
public InvariantState invariant() {
int val = getValue();
if (val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}
public static void main(String[] args) throws InterruptedException {
SynchronizedEvenGenerator gen = new SynchronizedEvenGenerator();
new InvariantWatcher(gen,4000);
while(true){
gen.next();
}
}
}
代码里我把所有的方法都加上了synchronized关键字了,有的童鞋会不会这样想过,我只加一个了,或者我加上自己认为要加的方法,我建议大家不要这么做,在一个类里要加就全加,有的方法不加,线程的随机调度将会成为最大的安全隐患了。对于我们写的监控程序就没必要加synchronized关键字了,我们需要它的随机,它的随机让我们随时掌控数据的变化。