如果您的程序只是一个单执行绪,单一流程的程序,那么通常您只要注意到程序逻辑的正确,您的程序通常就可以正确的执行您想要的功能,但当您的程序是多执行绪程序,多流程同时执行时,那么您就要注意到更多的细节,例如在多执行绪共享同一对象的数据时。
如果一个对象所持有的数据可以被多执行绪同时共享存取时,您必须考虑到「数据同步」的 问题,所谓数据同步指的是两份数据的整体性一致,例如对象A有 name与id两个属性,而有一份A1数据有name与id的数据要更新对象A的属性,如果A1的name与id设定给A对象完成,则称A1与A同步,如 果A1数据在更新了对象的name属性时,突然插入了一份A2数据更新了A对象的id属性,则显然的A1数据与A就不同步,A2数据与A也不同步。
数据在多执行绪下共享时,就容易因为同时多个执行绪可能更新同一个对象的信息,而造成对象数据的不同步,因为数据的不同步而可能引发的错误通常不易察觉, 而且可能是在您程序执行了几千几万次之后,才会发生错误,而这通常会发生在您的产品已经上线之后,甚至是程序已经执行了几年之后。
这边举个简单的例子,考虑您设计这么一个类别:
package onlyfun.caterpillar; public class PersonalInfo { private String name; private String id; private int count; public PersonalInfo() { name = "nobody"; id = "N/A"; } public void setNameAndID(String name, String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } private boolean checkNameAndIDEqual() { return (name.charAt(0) == id.charAt(0)) ? true : false; } }
在这个类别中,您可以设定使用者的名称与缩写id,并简单检查一下名称与id的第一个字是否相同,单就这个类别本身而言,它并没有任何的错误,但如果它被 用于多执行绪的程序中,而且同一个对象被多个执行存取时,就会"有可能"发生错误,来写个简单的测试程序:
package onlyfun.caterpillar; public class SynchronizedDemo { public static void main(String[] args) { final PersonalInfo person = new PersonalInfo(); Thread thread1 = new Thread(new Runnable() { public void run() { while(true) person.setNameAndID("Justin Lin", "J.L"); } }); Thread thread2 = new Thread(new Runnable() { public void run() { while(true) person.setNameAndID("Shang Hwang", "S.H");? } }); System.out.println("Start testing....."); thread1.start(); thread2.start(); } }
来看一下执行时的一个例子:
Start testing..... 822949) illegal name or ID..... 1443074) illegal name or ID..... 1750512) illegal name or ID..... 2587632) illegal name or ID..... 2805877) illegal name or ID..... 3705555) illegal name or ID..... 4000077) illegal name or ID.....?
看到了吗?如果以单执行绪的观点来看,上面的讯息在测试中根本不可能出现,然而在这个程序中却出现了错误,而且重点是,第一次错误是发生在第822949 次的设定(您的计算机上可能是不同的数字),如果您在程序完成并开始应用之后,这个时间点可能是几个月甚至几年之后。
问题出现哪?在于这边:
public void setNameAndID(String name, String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; }
虽然您设定给它的参数并没有问题,在某个时间点时,thread1设定了"Justin Lin", "J.L"给name与id,在进行测试的前一刻,thread2可能此时刚好呼叫setNameAndID("Shang Hwang",
"S.H"),在name被设定为"Shang Hwang"时,checkNameAndIDEqual()开始执行,此时name等于"Shang Hwang",而id还是"J.L",所以checkNameAndIDEqual()就会传回false,结果就显示了错误讯息。
您必须同步数据对对象的更新,也就是在有一个执行绪正在设定person对象的数据时,不可以又被另一个执行绪同时进行设定,您可以使用"synchronized"关键词来进行这个动作。
"synchronized"的一个使用方式是用于方法上,让方法作用范围内都成为被同步化区域,例如:
public synchronized void setNameAndID(String name,String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; }
每个对象内部都会有一个锁定(lock),当执行绪执行某个对象的同步化方法时,它会在对象上得到这个锁定,只有取得锁定的执行绪才可进入同步区,未取得锁定的执行绪则必须等待,直到有机会取得锁定,其它执行绪必须等目前执行绪先执行完同步化方法,并解除对对象的锁定,才有机会取得对象上的锁定。
就这个例子来说,简单的说,就是有执行绪在执行setNameAndID()时,会从对象上取得锁定,其它执行绪必须等待它执行完毕,释放锁定之后,才会有机会竞争锁定,取得锁定的执行绪才可以执行setNameAndID ()。
以上所介绍的是实例方法同步化(instance method synchronized),同步化的设定不只可用于方法上,也可以用于某个程序区块上,称之为实例区块同步化(instance block synchronized),例如:
public void setNameAndID(String name, String id) { synchronized(this) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) { System.out.println(count + ") illegal name or ID....."); } count++; } }
上面的意思就是在执行绪执行至"synchronized"设定的区块时取得对象的锁定,这么一来其它执行绪暂时无法取得锁定,因此无法执行对象同步化区块,这个方式可以应 用于您不想锁定整个方法区块,而只是想在共享数据在被执行绪存取时确保同步化时,由于只锁定方法中的某个区块,在执行完区块后即释放对对象的锁定,以便让 其它执行绪有机会取得锁定,对对象进行操作,在某些时候会比较有效率。
实例区块同步化的好处是,您也可以对某个对象进行同步化,而像实例方法同步化只针对this,例如在多执行绪存取同一个ArrayList对象时,ArrayList并没有实作数据存取时的同步化,所以它使用于多执 行绪时,必须注意是否必须对它进行同步化,多个执行绪存取同一个ArrayList时,有可能发生两个以上的执行绪将数据存入 ArrayList的同一个位置,造成数据的相互覆盖,为了确保数据存入时的正确性,您可以在存取ArrayList对象时对它进行同步化,例如:
// arraylist参考至一个ArrayList的一个实例 synchronized(arraylist) { arraylist.add(new SomeClass()); }
除了针对对象同步之外,您还可以针对静态方法同步化(static method synchronized),例如某个static成员会被多执行绪存取时,则可以如下设定:
public class Some { private static int value; public synchronized static void some() { value++; .... } }
进行锁定时,会锁定Some.class,因而static成员也受到保护。类似于实例区块同步化,您也可以在区块中锁定整个类别,称之为类别字面同步化(class literals synchronized),例如:
... public void doSomething() { synchronized(Some.class) { .... } } ...
事实上,您也可以使用Collections的synchronizedXXX()等方法来传回一个同步化的容器对象,例如传回一个同步化的List:
List list = Collections.synchronizedList(new ArrayList());
同步化所牺性的自然就是在于执行绪等待时的延迟,所以同步化的手法不应被滥用,您不用将整个对象的方法都加上"synchronized",有些方法只是单纯的传回某些数值,它并没有对共享数据进行修改的动作,那么它就不需要被同步化。