一.目标
谈到多线程共享数据,理想情况下我们希望做到“同步”和“互斥”。这是目标我们暂且把它先放到这。
二.分类
多线程共享数据通常的场景有一下两种:
场景一:
卖票,我们都买过火车票。要买火车票我们可以去车站,也可以通过代售点(或网购),但不管有多少种方式火车票的总数是一定的。
场景抽象:
对于卖票系统每个线程的核心执行的代码都相同(就是票数–)。
解决方法:
只需创建一个Runnable,这个Runnable里有那个共享数据。
代码模拟:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package
多线程共享数据;
public
class
Ticket
implements
Runnable{
private
int
ticket =
10
;
public
void
run() {
while
(ticket>
0
){
ticket--;
System.out.println(
"当前票数为:"
+ticket);
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package
多线程共享数据;
public
class
SellTicket {
/**
* @param args
*/
public
static
void
main(String[] args) {
Ticket t =
new
Ticket();
new
Thread(t).start();
new
Thread(t).start();
}
}
|
场景二:比较常见的例子,银行问题,我们对账户可以存钱也可以取钱,怎么保证这样的数据共享呢?
场景抽象:
每个线程执行的代码不同(比如上面的问题,对每个账户可以执行++操作和–操作),这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable之间的数据共享
解决方案:
有两种方法来解决此类问题:
- 将共享数据封装成另外一个对象中封装成另外一个对象中,然后将这个对象逐一传递给各个Runnable对象,每个线程对共享数据的操作方法也分配到那个对象身上完成,这样容易实现针对数据进行各个操作的互斥和通信
- 将Runnable对象作为偶一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。
代码模拟:
以一道面试题为例:
“设计4个线程。,其中两个线程每次对j增加1,另外两个线程对j每次减1”
(第一种解法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MyData {
private
int
j=
0
;
public
synchronized
void
add(){
j++;
System.out.println(
"线程"
+Thread.currentThread().getName()+
"j为:"
+j);
}
public
synchronized
void
dec(){
j--;
System.out.println(
"线程"
+Thread.currentThread().getName()+
"j为:"
+j);
}
public
int
getData(){
return
j;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
class
AddRunnable
implements
Runnable{
MyData data;
public
AddRunnable(MyData data){
this
.data= data;
}
public
void
run() {
data.add();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
public
class
DecRunnable
implements
Runnable {
MyData data;
public
DecRunnable(MyData data){
this
.data = data;
}
public
void
run() {
data.dec();
}
}
|
测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
TestOne {
/**
* @param args
*/
public
static
void
main(String[] args) {
MyData data =
new
MyData();
Runnable add =
new
AddRunnable(data);
Runnable dec =
new
DecRunnable(data);
for
(
int
i=
0
;i<
2
;i++){
new
Thread(add).start();
new
Thread(dec).start();
}
}
|
解法分析:
优点:
1.这种解法代码写的有条理,简单易读,从main中很容易整理出思路
2.将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“
不足:
代码写的比较繁琐,需要有多个类,不是那么简洁
个人观点:为了有条理个人比较喜欢这种写法。
(第二种解法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MyData {
private
int
j=
0
;
public
synchronized
void
add(){
j++;
System.out.println(
"线程"
+Thread.currentThread().getName()+
"j为:"
+j);
}
public
synchronized
void
dec(){
j--;
System.out.println(
"线程"
+Thread.currentThread().getName()+
"j为:"
+j);
}
public
int
getData(){
return
j;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public
class
TestThread {
/**
* @param args
*/
public
static
void
main(String[] args) {
final
MyData data =
new
MyData();
for
(
int
i=
0
;i<
2
;i++){
new
Thread(
new
Runnable(){
public
void
run() {
data.add();
}
}).start();
new
Thread(
new
Runnable(){
public
void
run() {
data.dec();
}
}).start();
}
}
}
|
解法分析:
与第一种方法的区别在于第二种方法巧妙的用了内部类共享外部类数据的思想,即把要共享的数据变得全局变量,这样就保证了操作的是同一份数据。同时内部类的方式使代码更加简洁。但是不如第一种解法条理那么清楚。