| 如果您的程序只是一个单线程,单一流程的程序,那么通常您只要注意到程序逻辑的正确,您的程序通常就可以正确的执行您想要的功能,但当您的程序是多线程程序,多流程同时执行时,那么您就要注意到更多的细节,例如在多线程共用同一对象的数据时。 如果一个对象所持有的数据可以被多线程同时共享存取时,您必须考虑到“数据同步”的 问题,所谓数据同步指的是两份数据的整体性一致,例如对象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; 在这个类中,您可以设定使用者的名称与缩写id,并简单检查一下名称与id的第一个字是否相同,单就这个类本身而言,它并没有任何的错误,但如果它被 用于多线程的程序中,而且同一个对象被多个执行存取时,就会"有可能"发生错误,来写个简单的测试程序:
package onlyfun.caterpillar; 来看一下执行时的一个例子:
看到了吗?如果以单线程的观点来看,上面的讯息在测试中根本不可能出现,然而在这个程序中却出现了错误,而且重点是,第一次错误是发生在第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的一个实例
除了针对对象同步之外,您还可以针对静态方法同步化(static method synchronized),例如某个static成员会被多线程存取时,则可以如下设定:synchronized(arraylist) { arraylist.add(new SomeClass()); } 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",有些方法只是单纯的传回某些数值,它并没有对共用数据进行修改的动作,那么它就不需要被同步化。 |