首先,要明白一个问题:为什么多线程会有安全问题?明白这个问题也就知道在解决时的要注意的方面了。
以下面的程序来说明
public class TicketThread {
public static void main(String[] args) {
Ticket tick=new Ticket();
Thread t1=new Thread(tick,"窗口1");
Thread t2=new Thread(tick,"窗口2");
t1.start();
t2.start();
}
}
class Ticket implements Runnable{
public void run(){
int sum=10,good=0;
while(good<=sum){ System.out.println(Thread.currentThread().getName()+"::产品"+i);
good++;
}
}
}
显示结果如下
窗口2::产品1
窗口1::产品1
窗口2::产品2
窗口1::产品2
窗口2::产品3
窗口1::产品3
窗口2::产品4
窗口1::产品4
窗口2::产品5
窗口1::产品5
窗口2::产品6
窗口2::产品7
窗口1::产品6
窗口1::产品7
窗口1::产品8
窗口1::产品9
窗口1::产品10
窗口2::产品8
窗口2::产品9
窗口2::产品10
当然,这个结果是不唯一的,每次运行结果都会不同。但每个窗口卖的产品都是10个,都是按顺序卖的,这有什么不对吗?
你要说,产品只有10个啊,两个窗口怎么能各卖10个。那我说设定为每个窗口的卖的是自己独有的呢,就是各有10个呢,两个窗口,一个在学校南门,一个在北门,肯定是各卖各的,只是都是加盟店,卖的牌子一样而已嘛!
是的,这就是问题所在。只有当多线程涉及到了共享数据的问题,才会要考虑安全问题,否则纵然是多线程,两线程毫无相关的话也是没有安全问题的。
那么哪些是共享数据呢?是不是在run()外定义又在run()内使用的话就是共享数据呢?
那么我们把上面程序中的int sum=10放到run()外,来,用脚趾头想一想,运行程序的话,结果会怎样显示。
答案很简单(鉴于显示结果有点长,我就不冒着被认为是借代码增加博客长度的名声把答案再贴一遍了),和上面的结果几乎一样,只是具体顺序会有而已。
再把int good=1也放在run()外,显示结果呢。
结果如下:
窗口2::产品1
窗口1::产品1
窗口2::产品2
窗口1::产品3
窗口2::产品4
窗口1::产品5
窗口2::产品6
窗口1::产品7
窗口2::产品8
窗口1::产品9
窗口2::产品10
好的,现在呢,两个窗口合并成为一家店了,总共只有10个产品,问题也来了,1产品卖了两次。
总结下就是只有在run()里有被多个线程动态操作改变的共享数据才会发生安全问题,因此找到操作改变共享数据的代码并加以保护也正是解决安全问题的关键。
安全问题的解决,目前有两种方式,一种是synchronized(同步),一种是Lock和Condition(新版本)。
先来讲synchronized。常见的是同步代码块,可以自由选择作为锁(API里也称之为监视器)的载体。当把synchronized修饰函数时,函数就成了同步函数。其中静态同步方法的锁是该方法所在类的字节码,是唯一的;非静态同步方法的锁是所属对象的引用(即this),不唯一,使用时要小心。
关于锁或同步,有如下原则:
1.必须有多个线程动态操作改变共享数据。原因上面已经分析过。否则加锁无意义,还浪费时效。这个原则提出的另一个非必须要求是同步代码块范围能小就小,毕竟加载锁会拖慢程序运行。
2.必须是多个线程使用同一个锁,允许一个线程持有多个锁。
3.一次只能有一个线程持有锁,同时,锁的作用范围是这个线程内的所有使用这个锁的代码。即当此线程内有两个或以上的代码块(或函数)使用此锁时,任意一个持有锁后就等同于所有代码块持有了锁。也因此有了死锁的发生。
知道原则之后,我们来解决上面代码的安全问题。为何会发生产品1被卖出两次的情况?这里就是多线程最烧脑的所在了,你要考虑线程的随机发生性和按逻辑产生的结果了。这里产生的原因是:当窗口2(不要纠结为何不是窗口1,这是运行程序自己真实产生的,窗口1虽然先启动,但并未执行,随机性)读到good=1进入while,执行打印语句后停下了,还没执行good++,这时窗口1进来执行了,也读到good=1并打印了。
按这样分析,还可能发生卖出产品11的情况,多运行几次应该会有。
接下来解决这个问题,问题在于多线程读取good并在while的()里判断后,到打印时又发生了变化,因此把while的循环体设置成同步,同时为了防止最后一次打印出11,要在循环体里加个判断。
先不在while循环里加判断,
程序修改如下(只写了改写的部分,其他部分没变):
class Ticket implements Runnable{
int sum=10;
int good=1;
public void run(){
while(good<=sum)
synchronized(this){ System.out.println(Thread.currentThread().getName()+"::产品"+good);
good++;
}
}
}
显示结果为
窗口1::产品1
窗口1::产品2
窗口1::产品3
窗口1::产品4
窗口1::产品5
窗口1::产品6
窗口1::产品7
窗口1::产品8
窗口1::产品9
窗口1::产品10
窗口2::产品11
对于结果说两句:从1到10一直是窗口1不代表同步代码块的范围已经把这个while循环都包括了,同步代码块是只包含了循环体里的执行部分,外面的判断并没有包括。之所以前面一直窗口1,好吧,我觉得计算机执行时应该有我不知道的某种规则在选择执行吧。sum=10我试了很多遍,最好的一次是出现了前面是10个窗口2,最后一个窗口1。后来我用sum=100试,刷的一下,100个窗口1,最后一个窗口2.开始真有点怀疑把循环体全给同步了,后来一想不对,要不最后一个不会出窗口2:101的。我就多刷了几次,终于刷了一次看着有代表性的。也提醒大家注意,多线程这有时候显示结果不符合,不一定是代码有问题,毕竟随机是不是完全的纯随机不是我们能掌握的。
把那次刷出来的好一点的结果贴一部分:
窗口1::产品1
窗口1::产品2
窗口1::产品3
窗口2::产品4
窗口2::产品5
窗口2::产品6
窗口2::产品7
窗口2::产品8
窗口2::产品9
窗口2::产品10
窗口2::产品11
窗口2::产品12
对于最后多出的那个产品,在循环体里加上判断就行,让good>sum时就停止循环。