Java良好的支持多線程。使用java,我們可以很輕松的編程一個(gè)多線程程序。但是使用多線程可能會(huì)引起并發(fā)訪問(wèn)的問(wèn)題。synchronized和ThreadLocal都是用來(lái)解決多線程并發(fā)訪問(wèn)的問(wèn)題。大家可能對(duì)synchronized較為熟悉,而對(duì)ThreadLocal就要陌生得多了。
并發(fā)問(wèn)題。當(dāng)一個(gè)對(duì)象被兩個(gè)線程同時(shí)訪問(wèn)時(shí),可能有一個(gè)線程會(huì)得到不可預(yù)期的結(jié)果。
一個(gè)簡(jiǎn)單的java類(lèi)Studnet
1
public class Student
{
2
private int age=0;
3
4
public int getAge()
{
5
return this.age;
6
7
}
8
9
public void setAge(int age)
{
10
this.age = age;
11
}
12
}
一個(gè)多線程類(lèi)ThreadDemo.
這個(gè)類(lèi)有一個(gè)Student的私有變量,在run方法中,它隨機(jī)產(chǎn)生一個(gè)整數(shù)。然后設(shè)置到student變量中,從student中讀取設(shè)置后的值。然后睡眠5秒鐘,最后再次讀student的age值。
1
public class ThreadDemo implements Runnable
{
2
Student student = new Student();
3
public static void main(String[] agrs)
{
4
ThreadDemo td = new ThreadDemo();
5
Thread t1 = new Thread(td,"a");
6
Thread t2 = new Thread(td,"b");
7
t1.start();
8
t2.start();
9
10
}
11
/**//* (non-Javadoc)
12
* @see java.lang.Runnable#run()
13
*/
14
public void run()
{
15
accessStudent();
16
}
17
18
public void accessStudent()
{
19
String currentThreadName = Thread.currentThread().getName();
20
System.out.println(currentThreadName+" is running!");
21
// System.out.println("first read age is:"+this.student.getAge());
22
Random random = new Random();
23
int age = random.nextInt(100);
24
System.out.println("thread "+currentThreadName +" set age to:"+age);
25
26
this.student.setAge(age);
27
System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
28
try
{
29
Thread.sleep(5000);
30
}
31
catch(InterruptedException ex)
{
32
ex.printStackTrace();
33
}
34
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
35
36
}
37
38
}
運(yùn)行這個(gè)程序,屏幕輸出如下:
a is running!
b is running!
thread b set age to:33
thread b first read age is:33
thread a set age to:81
thread a first read age is:81
thread b second read age is:81
thread a second read age is:81
需要注意的是,線程a在同一個(gè)方法中,第一次讀取student的age值與第二次讀取值不一致。這就是出現(xiàn)了并發(fā)問(wèn)題。
synchronized
上面的例子,我們模似了一個(gè)并發(fā)問(wèn)題。Java提供了同步機(jī)制來(lái)解決并發(fā)問(wèn)題。synchonzied關(guān)鍵字可以用來(lái)同步變量,方法,甚至同步一個(gè)代碼塊。
使用了同步后,一個(gè)線程正在訪問(wèn)同步對(duì)象時(shí),另外一個(gè)線程必須等待。
Synchronized同步方法
現(xiàn)在我們可以對(duì)accessStudent方法實(shí)施同步。
public synchronized void accessStudent()
再次運(yùn)行程序,屏幕輸出如下:
a is running!
thread a set age to:49
thread a first read age is:49
thread a second read age is:49
b is running!
thread b set age to:17
thread b first read age is:17
thread b second read age is:17
加上了同步后,線程b必須等待線程a執(zhí)行完畢后,線程b才開(kāi)始執(zhí)行。
對(duì)方法進(jìn)行同步的代價(jià)是非常昂貴的。特別是當(dāng)被同步的方法執(zhí)行一個(gè)冗長(zhǎng)的操作。這個(gè)方法執(zhí)行會(huì)花費(fèi)很長(zhǎng)的時(shí)間,對(duì)這樣的方法進(jìn)行同步可能會(huì)使系統(tǒng)性能成數(shù)量級(jí)的下降。
Synchronized同步塊
在accessStudent方法中,我們真實(shí)需要保護(hù)的是student變量,所以我們可以進(jìn)行一個(gè)更細(xì)粒度的加鎖。我們僅僅對(duì)student相關(guān)的代碼塊進(jìn)行同步。
1
synchronized(this)
{
2
Random random = new Random();
3
int age = random.nextInt(100);
4
System.out.println("thread "+currentThreadName +" set age to:"+age);
5
6
this.student.setAge(age);
7
8
System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
9
try
{
10
Thread.sleep(5000);
11
}
12
catch(InterruptedException ex)
{
13
ex.printStackTrace();
14
}
15
}
運(yùn)行方法后,屏幕輸出:
a is running!
thread a set age to:18
thread a first read age is:18
b is running!
thread a second read age is:18
thread b set age to:62
thread b first read age is:62
thread b second read age is:62
需要特別注意這個(gè)輸出結(jié)果。
這個(gè)執(zhí)行過(guò)程比上面的方法同步要快得多了。
只有對(duì)student進(jìn)行訪問(wèn)的代碼是同步的,而其它與部份代碼卻是異步的了。而student的值并沒(méi)有被錯(cuò)誤的修改。如果是在一個(gè)真實(shí)的系統(tǒng)中,accessStudent方法的操作又比較耗時(shí)的情況下。使用同步的速度幾乎與沒(méi)有同步一樣快。
使用同步鎖
稍微把上面的例子改一下,在ThreadDemo中有一個(gè)私有變量count,。
private int count=0;
在accessStudent()中, 線程每訪問(wèn)一次,count都自加一次, 用來(lái)記數(shù)線程訪問(wèn)的次數(shù)。

try
{
this.count++;
Thread.sleep(5000);

}catch(InterruptedException ex)
{
ex.printStackTrace();
}
為了模擬線程,所以讓它每次自加后都睡眠5秒。
accessStuden()方法的完整代碼如下:
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName+" is running!");

try
{
this.count++;
Thread.sleep(5000);

}catch(InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("thread "+currentThreadName+" read count:"+this.count);

synchronized(this)
{
Random random = new Random();
int age = random.nextInt(100);
System.out.println("thread "+currentThreadName +" set age to:"+age);
this.student.setAge(age);
System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());

try
{
Thread.sleep(5000);
}

catch(InterruptedException ex)
{
ex.printStackTrace();
}
}
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
運(yùn)行程序后,屏幕輸出:
a is running!
b is running!
thread a read count:2
thread a set age to:49
thread a first read age is:49
thread b read count:2
thread a second read age is:49
thread b set age to:7
thread b first read age is:7
thread b second read age is:7
我們?nèi)匀粚?duì)student對(duì)象以synchronized(this)操作進(jìn)行同步。
我們需要在兩個(gè)線程中共享count失敗。
所以仍然需要對(duì)count的訪問(wèn)進(jìn)行同步操作。
1
synchronized(this)
{
2
try
{
3
this.count++;
4
Thread.sleep(5000);
5
}catch(InterruptedException ex)
{
6
ex.printStackTrace();
7
}
8
}
9
System.out.println("thread "+currentThreadName+" read count:"+this.count);
10
11
12
synchronized(this)
{
13
Random random = new Random();
14
int age = random.nextInt(100);
15
System.out.println("thread "+currentThreadName +" set age to:"+age);
16
17
this.student.setAge(age);
18
19
System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
20
try
{
21
Thread.sleep(5000);
22
}
23
catch(InterruptedException ex)
{
24
ex.printStackTrace();
25
}
26
}
27
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
28
long endTime = System.currentTimeMillis();
29
long spendTime = endTime - startTime;
30
System.out.println("花費(fèi)時(shí)間:"+spendTime +"毫秒");
程序運(yùn)行后,屏幕輸出
a is running!
b is running!
thread a read count:1
thread a set age to:97
thread a first read age is:97
thread a second read age is:97
花費(fèi)時(shí)間:10015毫秒
thread b read count:2
thread b set age to:47
thread b first read age is:47
thread b second read age is:47
花費(fèi)時(shí)間:20124毫秒
我們?cè)谕粋€(gè)方法中,多次使用synchronized(this)進(jìn)行加鎖。有可能會(huì)導(dǎo)致太多額外的等待。
應(yīng)該使用不同的對(duì)象鎖進(jìn)行同步。
設(shè)置兩個(gè)鎖對(duì)象,分別用于student和count的訪問(wèn)加鎖。
1
private Object studentLock = new Object();
2
private Object countLock = new Object();
3
4
accessStudent()方法如下:
5
long startTime = System.currentTimeMillis();
6
String currentThreadName = Thread.currentThread().getName();
7
System.out.println(currentThreadName+" is running!");
8
// System.out.println("first read age is:"+this.student.getAge());
9
10
synchronized(countLock)
{
11
try
{
12
this.count++;
13
Thread.sleep(5000);
14
}catch(InterruptedException ex)
{
15
ex.printStackTrace();
16
}
17
}
18
System.out.println("thread "+currentThreadName+" read count:"+this.count);
19
20
21
synchronized(studentLock)
{
22
Random random = new Random();
23
int age = random.nextInt(100);
24
System.out.println("thread "+currentThreadName +" set age to:"+age);
25
26
this.student.setAge(age);
27
28
System.out.println("thread "+currentThreadName+" first read age is:"+this.student.getAge());
29
try
{
30
Thread.sleep(5000);
31
}
32
catch(InterruptedException ex)
{
33
ex.printStackTrace();
34
}
35
}
36
System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
37
long endTime = System.currentTimeMillis();
38
long spendTime = endTime - startTime;
39
System.out.println("花費(fèi)時(shí)間:"+spendTime +"毫秒");
這樣對(duì)count和student加上了兩把不同的鎖。
運(yùn)行程序后,屏幕輸出:
a is running!
b is running!
thread a read count:1
thread a set age to:48
thread a first read age is:48
thread a second read age is:48
花費(fèi)時(shí)間:10016毫秒
thread b read count:2
thread b set age to:68
thread b first read age is:68
thread b second read age is:68
花費(fèi)時(shí)間:20046毫秒
與兩次使用synchronized(this)相比,使用不同的對(duì)象鎖,在性能上可以得到更大的提升。
由此可見(jiàn)synchronized是實(shí)現(xiàn)java的同步機(jī)制。同步機(jī)制是為了實(shí)現(xiàn)同步多線程對(duì)相同資源的并發(fā)訪問(wèn)控制。保證多線程之間的通信。
可見(jiàn),同步的主要目的是保證多線程間的數(shù)據(jù)共享。同步會(huì)帶來(lái)巨大的性能開(kāi)銷(xiāo),所以同步操作應(yīng)該是細(xì)粒度的。如果同步使用得當(dāng),帶來(lái)的性能開(kāi)銷(xiāo)是微不足道的。使用同步真正的風(fēng)險(xiǎn)是復(fù)雜性和可能破壞資源安全,而不是性能。
ThreadLocal
由上面可以知道,使用同步是非常復(fù)雜的。并且同步會(huì)帶來(lái)性能的降低。Java提供了另外的一種方式,通過(guò)ThreadLocal可以很容易的編寫(xiě)多線程程序。從字面上理解,很容易會(huì)把ThreadLocal誤解為一個(gè)線程的本地變量。其它ThreadLocal并不是代表當(dāng)前線程,ThreadLocal其實(shí)是采用哈希表的方式來(lái)為每個(gè)線程都提供一個(gè)變量的副本。從而保證各個(gè)線程間數(shù)據(jù)安全。每個(gè)線程的數(shù)據(jù)不會(huì)被另外線程訪問(wèn)和破壞。
我們把第一個(gè)例子用ThreadLocal來(lái)實(shí)現(xiàn),但是我們需要些許改變。
Student并不是一個(gè)私有變量了,而是需要封裝在一個(gè)ThreadLocal對(duì)象中去。調(diào)用ThreadLocal的set方法,ThreadLocal會(huì)為每一個(gè)線程都保持一份Student變量的副本。所以對(duì)student的讀取操作都是通過(guò)ThreadLocal來(lái)進(jìn)行的。
1
protected Student getStudent()
{
2
Student student = (Student)studentLocal.get();
3
if(student == null)
{
4
student = new Student();
5
studentLocal.set(student);
6
}
7
return student;
8
}
9
10
protected void setStudent(Student student)
{
11
studentLocal.set(student);
12
}
accessStudent()方法需要做一些改變。通過(guò)調(diào)用getStudent()方法來(lái)獲得當(dāng)前線程的Student變量,如果當(dāng)前線程不存在一個(gè)Student變量,getStudent方法會(huì)創(chuàng)建一個(gè)新的Student變量,并設(shè)置在當(dāng)前線程中。
Student student = getStudent();
student.setAge(age);
accessStudent()方法中無(wú)需要任何同步代碼。
完整的代碼清單如下:
TreadLocalDemo.java
1
public class TreadLocalDemo implements Runnable
{
2
private final static ThreadLocal studentLocal = new ThreadLocal();
3
4
public static void main(String[] agrs)
{
5
TreadLocalDemo td = new TreadLocalDemo();
6
Thread t1 = new Thread(td,"a");
7
Thread t2 = new Thread(td,"b");
8
9
t1.start();
10
t2.start();
11
12
13
14
15
}
16
17
/**//* (non-Javadoc)
18
* @see java.lang.Runnable#run()
19
*/
20
public void run()
{
21
accessStudent();
22
}
23
24
public void accessStudent()
{
25
26
String currentThreadName = Thread.currentThread().getName();
27
System.out.println(currentThreadName+" is running!");
28
Random random = new Random();
29
int age = random.nextInt(100);
30
System.out.println("thread "+currentThreadName +" set age to:"+age);
31
Student student = getStudent();
32
student.setAge(age);
33
System.out.println("thread "+currentThreadName+" first read age is:"+student.getAge());
34
try
{
35
Thread.sleep(5000);
36
}
37
catch(InterruptedException ex)
{
38
ex.printStackTrace();
39
}
40
System.out.println("thread "+currentThreadName +" second read age is:"+student.getAge());
41
42
}
43
44
protected Student getStudent()
{
45
Student student = (Student)studentLocal.get();
46
if(student == null)
{
47
student = new Student();
48
studentLocal.set(student);
49
}
50
return student;
51
}
52
53
protected void setStudent(Student student)
{
54
studentLocal.set(student);
55
}
56
}
運(yùn)行程序后,屏幕輸出:
b is running!
thread b set age to:0
thread b first read age is:0
a is running!
thread a set age to:17
thread a first read age is:17
thread b second read age is:0
thread a second read age is:17
可見(jiàn),使用ThreadLocal后,我們不需要任何同步代碼,卻能夠保證我們線程間數(shù)據(jù)的安全。
而且,ThreadLocal的使用也非常的簡(jiǎn)單。
我們僅僅需要使用它提供的兩個(gè)方法
void set(Object obj) 設(shè)置當(dāng)前線程的變量的副本的值。
Object get() 返回當(dāng)前線程的變量副本
另外ThreadLocal還有一個(gè)protected的initialValue()方法。返回變量副本在當(dāng)前線程的初始值。默認(rèn)為null
ThreadLocal是怎么做到為每個(gè)線程都維護(hù)一個(gè)變量的副本的呢?
我們可以猜測(cè)到ThreadLocal的一個(gè)簡(jiǎn)單實(shí)現(xiàn)
1
public class ThreadLocal
2

{
3
private Map values = Collections.synchronizedMap(new HashMap());
4
public Object get()
5
{
6
Thread curThread = Thread.currentThread();
7
Object o = values.get(curThread);
8
if (o == null && !values.containsKey(curThread))
9
{
10
o = initialValue();
11
values.put(curThread, o);
12
}
13
return o;
14
}
15
16
public void set(Object newValue)
17
{
18
values.put(Thread.currentThread(), newValue);
19
}
20
21
public Object initialValue()
22
{
23
return null;
24
}
25
}
由此可見(jiàn),ThreadLocal通過(guò)一個(gè)Map來(lái)為每個(gè)線程都持有一個(gè)變量副本。這個(gè)map以當(dāng)前線程為key。與synchronized相比,ThreadLocal是以空間換時(shí)間的策略來(lái)實(shí)現(xiàn)多線程程序。
Synchronized還是ThreadLocal?
ThreadLocal以空間換取時(shí)間,提供了一種非常簡(jiǎn)便的多線程實(shí)現(xiàn)方式。因?yàn)槎鄠€(gè)線程并發(fā)訪問(wèn)無(wú)需進(jìn)行等待,所以使用ThreadLocal會(huì)獲得更大的性能。雖然使用ThreadLocal會(huì)帶來(lái)更多的內(nèi)存開(kāi)銷(xiāo),但這點(diǎn)開(kāi)銷(xiāo)是微不足道的。因?yàn)楸4嬖赥hreadLocal中的對(duì)象,通常都是比較小的對(duì)象。另外使用ThreadLocal不能使用原子類(lèi)型,只能使用Object類(lèi)型。ThreadLocal的使用比synchronized要簡(jiǎn)單得多。
ThreadLocal和Synchonized都用于解決多線程并發(fā)訪問(wèn)。但是ThreadLocal與synchronized有本質(zhì)的區(qū)別。synchronized是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問(wèn)。而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問(wèn)到的并不是同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享。而Synchronized卻正好相反,它用于在多個(gè)線程間通信時(shí)能夠獲得數(shù)據(jù)共享。
Synchronized用于線程間的數(shù)據(jù)共享,而ThreadLocal則用于線程間的數(shù)據(jù)隔離。
當(dāng)然ThreadLocal并不能替代synchronized,它們處理不同的問(wèn)題域。Synchronized用于實(shí)現(xiàn)同步機(jī)制,比ThreadLocal更加復(fù)雜。