1.
如果一个对象所持有的数据可以被多线程同时共享存取,必须考虑到数据同步的问题。所谓数据同步指的是两份数据的整体性和一致性。数据在多线程下共享时容易由于同时多个线程可能更新同一个对象的信息,而造成对象数据的不同步,因为数据的不同步可能引发的错误通常不易察觉,而且可能是在程序执行了几千几万次之后,才会发生错误。这通常会发生在产品已经上线之后,甚至是程序已经执行了几年之后。
2. 举个简单的例子,设计了一个PersonalInfo类:
-
package
ysu.hxy;
-
-
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
;
-
}
-
}
package ysu.hxy;
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;
}
}
单就这个类本身而言,它并没有任何的错误,但如果它被用于多线程的程序中,而且同一个对象被多个线程存取时,就会有可能发生错误。下面是一个简单的测试程序,看看PersonalInfo类在多线程共享数据下会发生什么问题。
-
package
ysu.hxy;
-
-
public
class
PersonalInfoTest
-
{
-
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 Lin"
,
"S.H"
);
-
}
-
}
-
});
-
-
System.out.println();
-
-
thread1.start();
-
thread2.start();
-
}
-
}
package ysu.hxy;
public class PersonalInfoTest
{
public static void main(String[] args)
{
final PersonalInfo person = new PersonalInfo();
//假设会能两个线程可能更新person对象
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 Lin","S.H");
}
}
});
System.out.println();
thread1.start();
thread2.start();
}
}
执行结果:
D:\hxy>java ysu.hxy.PersonalInfoTest
开始测试...
23466451) illegal name
or ID...
78044494) illegal name or ID...
101630476) illegal name or
ID...
106496643) illegal name or ID...
145330181) illegal name or
ID...
169674022) illegal name or ID...
174072203) illegal name or
ID...
214717201) illegal name or ID...
219668799) illegal name or
ID...
240921750) illegal name or ID...
265875722) illegal name or
ID...
270920923) illegal name or ID...
281256783) illegal name or
ID...
这个程序出现了错误,在23466451次的setNameAndID()执行时就开始了。如果程序完成并开始应用于实际场合之后,这个时间点可能是几个月甚至是几年之后。问题出在这里:
-
public
void
setNameAndID(String name,String id)
-
{
-
this
.name = name;
-
this
.id = id;
-
if
(!checkNameAndIDEqual())
-
{
-
System.out.println(count +
") illegal name or ID..."
);
-
}
-
count ++;
-
}
public void setNameAndID(String name,String id)
{
this.name = name;
this.id = id;
if(!checkNameAndIDEqual())
{
System.out.println(count + ") illegal name or ID...");
}
count ++;
}
虽然传递给setNameAndID()的变量并没有问题,在某个时间点时,thread1设定了Justin
Lin、J.L给name和id,在进行if测试的前一刻,thread2可能此时刚好调用setNameAndID("Shang
Hwang","S.H")。在name被设定为Shang HWang时,checkNameAndIDEqual()开始执行,此时name等于Shang
HWang,而id还是J.L。所以,checkNameAndIDEqual()就会返回false,结果就显示了错误信息。
必须同步数据对对象的更新,方法在有一个线程正在设定person对象的数据时,不可以被另一个线程同时进行设定。可以使用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 ++;
-
}
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 ++;
}
这是synchronized关键词的一个使用方式,用于方法上让方法的范围内都成为被同步化区域。被同步化区域在有一个线程占据时就像一个禁区,不允许其他线程进入。由于同时间只能有一个线程在被同步化区域,所以更新共享数据时,就像单线程程序在更新数据一样,以保证对象中的数据会与给定的数据同步。
sychronized的设定不只可用于方法上,也可以用于限定某个程序区块上被同步化区域。例如:
-
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..."
);
-
}
-
}
-
}
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...");
}
}
}
这个程序片段的意思是,在线程执行到synchronized设定的被同步化区块时锁定当前对象,这样就没有其他线程可以来执行这个被同步化区块。这个方式可以应用于您不想锁定整个方法区块,而只是想在更新共享数据时再确保对象与数据的同步化。由于只锁定方法中的某个区块,在执行完区块后即释放对对象的锁定,以便让其他线程能有机会对对象进行操作,相对于锁定整个方法区块效率较高。
也可以标示某个对象要求同步化。例如在多线程中存取同一个ArrayList对象时,由于ArrayList并没有实现数据存取时的同步化,所以当它使用多线程环境时,必须注意多个线程存取同一个ArrayList时,有可能发生两个以上的线程将数据存入ArrayList的同一个位置,造成数据的相互覆盖。为了确保数据存入时的正确性,可以在存取ArrayList对象时要求同步化。例如:
-
-
synchronized
(arraylist)
-
{
-
arrayList.add(
new
SomeClass());
-
}
//arraylist参考至一个ArrayList的一个实例
synchronized(arraylist)
{
arrayList.add(new SomeClass());
}
同步化确保数据的同步,但所牺牲的就是在于一个线程占据同步化区块,而其他线程等待它释放区块执行权时的延迟。这在线程少时可能看不出来,但在线程多的环境中必然造成一定的效率问题(例如大型网站的多人联机时)。