线程资源共享

  • 1、不正确的访问资源
  • 2、解决共享资源竞争
  • 3、原子性和易变性
  • 4、原子类
  • 5、临界区
  • 6、在其他对象之上同步
  • 7、线程本地存储

1、不正确的访问资源

关于线程的不正确访问资源所导致的问题,这里就不过多的进行赘述了,有很多的问题,比如:银行存取款之类的。下面主要看一个例子,来看一下现象。

1.1、我们定一个抽象类,这个类的任务就是产出一个int的偶数,并且在里面维护者一个boolean值,这个boolean具体是做什么,等会大家就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 一个数字生成器的抽象类
* @author pengchengliu
*
*/
public abstract class IntGenerator {
private volatile boolean canceled = false ;
public void cancel () {
canceled = true ;
}
public boolean isCancel () {
return canceled ;
}
public abstract int next () ;
}

1.2、定义一个检查一个数字是否为偶数的任务,如果这个数字不是偶数,当前的任务取消,关于任务的取消,我们依赖于刚刚定义的抽象类里面的boolean值,为什么要这么做呢,因为IntGnerator在这例子里面扮演着共公资源,共享公共资源的任务可以观察该资源的终止信号。这样做可以消除所谓的竞争条件,竞争条件 :即两个或者更多的任务竞争响应某一个条件,

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
29
30
public class EventCheck implements Runnable{
private IntGenerator generator ;
private final int id ;
public EventCheck (IntGenerator generator, int ident) {
this.generator = generator ;
this.id = ident ;
}
@Override
public void run() {
while (!generator.isCancel()) {
int val = generator.next() ;
if(val % 2 != 0) {
System.out.println("var = " + val + ", id = " + id + ", not event");
//如果是奇数,则退出循环。
generator.cancel();
}
}
}
public static void test (IntGenerator generator, int count) {
System.out.println("press Control-C to exit");
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++) {
threadPool.execute(new EventCheck(generator, i));
}
threadPool.shutdown();
}
public static void test (IntGenerator generator) {
test(generator, 10);
}
}

即当终止信号一亮,所有的线程都必须退出当前的资源的访问,这样也方便所有的线程终止。

1.3、最后通过实现IntGenerator抽象类来让程序运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
*
* @author pengchengliu
*
*/
public class IntGeneratorImpl extends IntGenerator{
private int currentEventValue = 0 ;
@Override
public int next() {
++currentEventValue;
//——————flags——————//
++currentEventValue;
return currentEventValue;
}
public static void main(String[] args) {
EventCheck.test(new IntGeneratorImpl());
}
}

这里使用了两个++,为什么不是+2呢,因为这里要模拟一种场景,所以必须要保证+2这个操作不是原子性的,这种场景就是,一个线程调用到了flags标示的地方,然后另外一个线程已经到了next 方法返回的地方,这样就会看出相应的当前共享资源状态不一致的情况。好让读者看出线程的不确定访问资源的特性。

2、解决共享资源竞争

2.1、为什么会存在资源竞争

进程是操作系统分配资源的最小单位,而线程是操作系统进行调度的最小单位,一个进程当中往往包含着许多的线程,这里需要注意的一点就是:对于在同一个进程的线程来说,这些属于进程的资源是共享的。但是关于并发编程存在这一个基本问题,就是我们永远都不知道一个线程何时在运行,据一个例子,我们坐在餐桌前面准备吃饭,饭刚刚好,我们要吃的时候,突然时间暂停了,等我们回过神的时候,发现饭不见了。这就是一个典型的并发访问资源的例子。这个章节也主要就是处理上述这种问题。

2.2、资源竞争的解决方式

上述这种情况的解决方式便是当资源被一个任务使用的时候,在上面加锁,第一个访问某项资源的任务必须获取该资源的这个锁,才可以对这个资源进行操作,当我们操作完毕完毕以后释放这个锁,在一个任务获取锁之后,其他的任何任务都无法获取锁,并且无法访问这个资源,直到拿锁的那个任务释放锁以后,其他任务获取到这个锁,以此类推。

并发编程当中,所有由线程冲突引起的问题,一般都是通过“序列化访问共享资源”这种方案,大体的意思就是在给定的一段时间内只允许一个任务访问共享资源。实现的时候,我们通常在要操作共享资源的线程代码中加入一条锁语句,这代表的便是在一段时间内只有一个任务可以运行这段代码。因为我们设置的锁语句导致的,通过锁语句实现这种机制,我们一般叫做互斥量

考虑一个例子,多个人上厕所,有一个人是这个厕所的老大(线程调度机制),为了能够上厕所,一个人先去问厕所老大里面人有人么,看看能否使用,如果没人,就进去上厕所并且锁上厕所门,这个时候其他人要使用厕所的时候,就会被阻挡到厕所外面,当第一个人出来的时候释放锁,并告诉厕所的老大,它是用完了,并且打开厕所门(释放锁),这个时候厕所的老大开始衡量下一个进入厕所的人是大便还是小便,选取最合适的人进入厕所。(厕所老大选择人的时候,我们可以给厕所老大通过yield和setPriority提供建议,注意:仅仅只是建议,具体情况需要通过具体平台和JVM的实现)。例子有点污,凑活看

2.3、Java实现

java对防止线程冲突提供了内置支持,也就是我们通过Synchronized关键字就可以做到,由Synchronized包裹的代码段,在执行的时候,jvm检查锁,执行,jvm释放锁。

共享资源一般都是以对象的形式存在,但是也可以是文件,输入输出端口,或者是打印机,如果要控制对共享资源的访问,我们首先要把它包装成为一个类,然后把这个类当中所有访问共享资源的方法加锁。如果某一个任务处于锁块当中,那么在这个任务释放锁之前,其他要调用包装有资源共享类中任何加锁方法的线程都会被阻塞。

在上述生成偶数的例子里面已经看出,我们应该将这个包装类的成员变量声明为private,并且只能通过方法来访问这个成员。如果访问资源只有通过包装类的方法,那么把方法设置成为Synchronized,保证在访问资源的时候只有一个任务在访问它。

2.4、Java内置锁Synchronized的运行原理

a、多个任务调用同一个类的多个Synchronized方法

每一个对象都含有单一的锁,当调用这个对象上面的Synchronized的方法的时候,就会对这个对象加锁,这时这个对象上面的其他的Synchronized方法只有等到上一个方法调用完毕释放锁以后才可以被调用,这里举例子,一个名为A的类里面有两个Synchronized方法,分别是Synchronized b()、Synchronized c()。当一个任务调用了b方法,对于a这个对象而言,其他任务就只有等到b方法调用完毕以后,才可以调用a这个对象的b、c方法。所以对于a这个对象而言,里面所有的Synchronized方法都是在共享一把锁,这样就可以防止多个任务同时访问a这个对象的内存。

b、一个任务在一个方法当中调用了同一个对象的多个Synchronized方法

一个任务可以多次获取同一个对象的锁,这种情况下,jvm内部是通过一个计数器来实现的,当一个任务调用了一次Synchronized方法,内部计数器就会是1,调用了第二次的时候,内部计数器就是2,以此类推,当第一个方法调用完毕的时候,Synchronized就会释放锁-1,直到为0时,该任务就完成了调用,此时别的任务就可以进入锁了。

c、static ,类层级的加锁

针对每一个类,同样也有一把锁(作为类的Class对象的一部分),所以Synchronized static 方法可以防止在类的范围对static数据的并发访问。

注意;我们应该什么时候进行同步控制呢,如果我们正在写一个变量,它可能接下来将被另一个线程所读取,或者正在读取一个上一次被另外一个线程所写过的变量时,我们就必须加锁。如果我们在类当中超过一个方法在处理临界数据,那么我们必须同步所有的相关方法,如果只同步一个方法,那么其他方法将会随意的忽略这个对象的锁。每一个访问临界数据的方法必须被同步,否则加锁的方法将毫无意义。还记得上述的例子么? 我们通过加锁来避免1小节的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 加锁避免线程的不确定访问
* @author pengchengliu
*
*/
public class IntGeneratorImpl1 extends IntGenerator{
private int currentEventValue = 0 ;
@Override
public synchronized int next() {
currentEventValue++ ;
//更有效的切换线程,让出错的可能增加。
Thread.yield();
currentEventValue++;
return currentEventValue;
}
public static void main(String[] args) {
EventCheck.test(new IntGeneratorImpl1());
}
}
press Control-C to exit, 程序将一直运行,不会结束。

上述例子里面,我们使用了Thread.yield方法让出错的可能大大增加,

2.5、显示对象锁Lock对象

在JDK5.0之后,concurrent包还定义第二种加锁方式,那就是Lock对象,这种加锁对象和Synchronized加锁方式相比较,优点就是,我们必须显示的创建、加锁、释放锁。可以让整个加锁流程更加的灵活,更加的多变,缺点就是:使用起来增加了代码量,以及实现加锁没有Synchronized来的简洁明了。关于这种方式的使用,还是套用上述的例子来展示一个Demo来让大家多多了解。

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
/**
* 使用Lock来防止线程的不确定访问。
* @author pengchengliu
*
*/
public class LockIntGenerator extends IntGenerator{
private int currentEventValue = 0 ;
private Lock lock = new ReentrantLock();
@Override
public int next() {
lock.lock();
try{
currentEventValue++ ;
Thread.yield();
currentEventValue++;
return currentEventValue ;
} finally {
//为了防止出现死锁,所以在这里使用finally块当中释放lock,注意后台线程哦。
lock.unlock();
}
}
public static void main(String[] args) {
EventCheck.test(new LockIntGenerator());
}
}

上述例子当中,我们看到了如何使用Lock,但是这里有两点需要强调的地方。

  • 我们一定要使用try-finally字句,来确保我们的锁一定被释放掉了,因为我们使用显示锁的时候,如果我们不显示的释放掉锁,可能我们造成死锁。
  • return语句必须放在在try块当中,这样做的目的就是我们一定要确保在return之后释放锁,确保不是过早的释放锁。

Synchronized内置对象和Lock显示对象到底如何选择呢,这里有一篇博客推荐大家读一下,

3、原子性和易变性

原子操作:就是一组操作不能被线程调度器所中断,一旦操作开始,那么在它执行期间就不会发生上下文切换(线程调度器)。这样也就避免了很多的线程冲突问题。

仔细思考一个问题,如果我们的一组代码都是原子操作,那么我们是不是就没有必要去加锁,反正在其运行期间,线程调度器也不可能切换上下文。这个问题理论上是Ok的,那么我们如何确定一组操作是原子操作呢? 答案是专家及程序员也许可以做到,但是我们呢,很难,很难,首先我们不知道什么样的操作是原子操作,这个地方坑很多,所以我们要坚决抵抗完全依赖自己的能力去写出无锁的同步代码。我们可以看一个例子,来了解原子操作。

在Java当中,内置对象的读操作和写操作都是原子性操作,但是除了long类型的和double类型的,为什么会出现这一种情况,因为long、double 是64位的操作,它们的读写JVM是将其分离位32位进行操作的,这个时候,如果线程调度器在32位结束下一个32位开始之前切换了线程,那么我们的原子性将彻底失败,这种情况就是典型的Java字撕裂。如何避免字撕裂呢,我们可通过volatitle这个关键字来避免。

可视性问题:

在多处理器系统上,在一个应用程序当中不同的任务对应用程序的状态有着不同的视图,例如:一个任务做出了修改,即使在不中断的原子操作下,对其他任务也可能是不可视的(修改只是暂时的存在了本地处理器的缓存当中,并没有刷新到主村当中),那么如何让其他的任务对该操作可视呢,这里有两种解决方式

  • 使用java关键字volatile
  • 使用同步块,也就是加锁

3.1、使用同步块解决可视性问题。

其实当我们对一组操作加锁的时候(这里没有内置锁和显示锁的区别),JVM会将这一组操作所操作的域,强制的刷新到主存当中,这样一个任务做出的修改,一定在应用程序当中对于其他的任务而言是可视的。

3.2、使用Java关键字volatile解决可视性问题

当我们将一个域申明位volatile域的时候,那么只要对这个域产生了写操作,那么所有的读操作都会看到这个修改。即使使用了本地缓存,volatile域会立即的刷新到主存当中。而所有的读操作都是发生在主存当中。

注意:

  • 非volatile域上面的原子操作不必强制刷新到主存,因此其他的读取操作的任务是不会看到这个修改以后的值的。
  • 如果多个任务在同时访问这个域,那么这个域就必须是volatile的,否则,就应该让这个域加锁,让其他任务同步来访问,上述描述过这个,同步也会导致向主存当中进行刷新,
  • 一个域完全由Synchronized方法,或者同步块所包含,那就不必将其设置为volatile的。

两种方式的比较

使用volatile而不是Synchronized的唯一安全情况就是只有一个可变的域,例如:一个域的值依赖于另外一个域,(a++ )某个域的值收到其他域的值的限制,类似于(a < b)小于限制。

再次提醒,我们不是专家,不用写出无锁的代码,所以我们的第一选择是使用同步块,

我们如何查看一个操作是否是原子操作呢,使用java的元命令。 javap -c ClassName 即可。例如下面的操作,注意:必须先编译成为.class文件以后才可以使用javap命令

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
29
30
31
32
33
34
35
36
37
38
public class Commands{
int i ;
void f(){
i++ ;
}
void b(){
i+=3 ;
}
}
public class Commands {
int i;

public Commands();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

void f1();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return

void f2();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_3
6: iadd
7: putfield #2 // Field i:I
10: return
}

通过上述指令可以看到,在方法内部操作成员的时候,都是先获取然后再次赋值的,在这中间还有好几部操作,而就是因为这几步操作,导致了线程的上下文切换,所以上述的这两个方法都不是相应的原子操作。

下面看一个例子,来看看如果要通过原子操作来写出无锁的同步代码是有多么的难。

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
29
30
31
32
33
/**
*
*
* @author pengchengliu
*
*/
public class AtomicityTest implements Runnable{
private int i = 0 ;
public int getValue(){
return i ;
}
private synchronized void eventIncrement(){
i ++ ;
i ++ ;
}
@Override
public void run() {
while(true)
eventIncrement();
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
AtomicityTest test = new AtomicityTest();
threadPool.execute(test);
while(true){
int value = test.getValue();
if(value % 2 != 0){
System.out.println(value);
System.exit(-1);
}
}
}
}

上述的例子最终以失败告终,我们明明加了锁呀,怎么还会失败呢。尽管return i是原子操作,我们没必要加锁,但是缺少同步使得其数值可以在处于不稳定的情况下被读取。除此之外,还存在可视性的问题。

如果从上述加锁的规则来说,我们应该给所有访问共享资源的方法加上锁,这样来达到当前资源的所有状态统一。
解决方式便是给getValue加上锁

4、原子类

JDK5以后引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,他们提供了一些更新操作。

这些类被调整为可以使用在某些现代处理器上的可获得,并且是机器级别上面的原子性,因此在使用它们的时候,通常不用担心。下面来观察一个使用原子类的例子,

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
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 使用原子类,可以提供一些原子类操作
* @author pengchengliu
*
*/
public class AtomicIntegerTest implements Runnable{
private AtomicInteger i = new AtomicInteger(0);
@Override
public void run() {
while(true){
eventIncrement();
}
}
public int getVaule () {
return i.get();
}
private void eventIncrement(){
i.addAndGet(2);
}
public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Aborting");
System.exit(-1);
}
}, 5000);
ExecutorService threadPool = Executors.newCachedThreadPool();
AtomicIntegerTest atomicInteger = new AtomicIntegerTest();
threadPool.execute(atomicInteger);

while(true){
int vaule = atomicInteger.getVaule();
if(vaule % 2 != 0){
System.out.println(vaule);
System.exit(-1);
}
}
}
}

上述代码,我们没有加锁,但是却没有出现关于共享资源的状态不统一的状态,这里我们分为两个方向,来研究一下,分别是操作的原子性,和共享资源的可视性。

  • 操作的原子性,这一点上面,我们使用了Java提供的原子类,那么其操作不用说,肯定就是所谓的原子性了。
  • 资源的可视性,这一点上面,我们没有使用相应的volatile关键字,这些任务对于这个资源是可视的么?答案是一定的 在使用原子类的时候,会默认的向主存当中刷新最新数据,而不会向缓存当中去写。

下面,我们再来看一个Demo,还记得我们之前的那个IntGenerator么?如果我们使用原子类,是否可以避免不用加锁的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 使用原子类来避免加锁
* @author pengchengliu
*
*/
public class AtomicEventGeneratorImpl extends IntGenerator{

private AtomicInteger currentEventValue = new AtomicInteger(0);
@Override
public int next() {
return currentEventValue.addAndGet(2);
}

public static void main(String[] args) {
EventCheck.test(new AtomicEventGeneratorImpl());
}

}

通过上述的代码,我们看到同样成功啦,其实如果我们要实现同步代码,并且不加锁的话,或者考虑到当前的资源操作必须是一些原子操作的时候,我们可以使用JDK5以后提供的这些类,但是这里给大家的建议就是尽量不要依靠自己的能力去写无锁代码,最好是使用锁来进行控制

5、临界区

如果我们要加锁的话,我们会让一个方法进行加锁,这种方式成为同步方法,但是有时候我们的需求是一个方法太过庞大,一下锁这么多代码。感觉执行效率会下降,所以我们解决方式就是使用加锁块,也称为同步块,在进入同步块之前我们需要得到同步块的锁对象,这个对象由我们自己来声明出来。

使用同步块的好处在于不是使用整个方法进行同步控制,可以使多个任务访问对象的时间性能显著提高,下面我们来看一个Demo

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* 类似于一个JavaBean,当中含有一个内部类,这个内部类是一个异常,
* 并且当中含有一个比较条件,如果条件不成立,则抛出异常。
* @author pengchengliu
*
*/
class Pair {
private int x, y ;
public Pair (int x, int y) {
this.x = x ;
this.y = y ;
}
public Pair () {
this(0, 0) ;
}
public int getX () {
return this.x ;
}

public int getY (){
return this.y ;
}

public void incrrementX () {
x ++ ;
}

public void incrrementY () {
y ++ ;
}

@Override
public String toString() {
return "x : " + x + ", y : " + y;
}

/**
* 一个异常类,如果当前Pair对象的x!= y的时候抛出异常
* @author pengchengliu
*
*/
public class PairVaulesNotEqualException extends RuntimeException{
public PairVaulesNotEqualException () {
super("Pair values not equal :" + Pair.this);
}
}

public void checkState() {
if (x != y) {
throw new PairVaulesNotEqualException();
}
}
}

abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>());
public synchronized Pair getPair() {
return new Pair(p.getX(), p.getY());
}
protected void store (Pair p) {
storage.add(p);
try{
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {}
}
abstract public void increment () ;
}
class PairManager1 extends PairManager {
@Override
public synchronized void increment() {
p.incrrementX();
p.incrrementY();
store(p);
}
}
class PairManager2 extends PairManager {
@Override
public void increment() {
Pair tempPair ;
synchronized (this) {
p.incrrementX();
p.incrrementY();
tempPair = getPair();
}
store(tempPair);
}
}
class PairManipulator implements Runnable {

private PairManager manager ;

public PairManipulator(PairManager manager) {
this.manager = manager ;
}
@Override
public void run() {
while(true) {
manager.increment();
}
}

@Override
public String toString() {
return "Pair : " + manager.getPair() + "checkCounter = " + manager.checkCounter.get();
}

}
class PairChecker implements Runnable {
private PairManager manager ;

public PairChecker(PairManager manager) {
this.manager = manager ;
}

@Override
public void run() {
while (true) {
manager.checkCounter.incrementAndGet() ;
manager.getPair().checkState();
}
}
}
/**
* 同步块的展示
* @author pengchengliu
*
*/
public class CriticalSection {
static void testApproaches (PairManager manager1, PairManager manager2) {
ExecutorService threadPool = Executors.newCachedThreadPool();

PairManipulator manipulator1 = new PairManipulator(manager1) ;
PairManipulator manipulator2 = new PairManipulator(manager2);

PairChecker checker1 = new PairChecker(manager1);
PairChecker checker2 = new PairChecker(manager2);

threadPool.execute(manipulator1);
threadPool.execute(manipulator2);

threadPool.execute(checker1);
threadPool.execute(checker2);

try{
TimeUnit.MILLISECONDS.sleep(500);
} catch (Exception e) {
System.out.println("sleep is interrupted");
}

System.out.println("pm1 : " + manipulator1 + "\npm2 : " + manipulator2);
System.exit(0);
}

public static void main(String[] args) {
PairManager pm1 = new PairManager1() ;
PairManager pm2 = new PairManager2() ;
testApproaches(pm1, pm2);
}
}
pm1 : Pair : x : 61, y : 61checkCounter = 3
pm2 : Pair : x : 61, y : 61checkCounter = 123286104

在上述的例子当中,我们通过一个Pair来模拟我们的共享资源,它本来是不安全的,然后我们通过一个PairManager来统一将Pair的访问统一做了相应的加锁处理,而对于我们所要进行加锁的处理方式有两种,

  • 1、就是通过同步方法进行处理,
  • 2、通过同步块来进行处理,
    然后就是模拟我们的任务,我们的任务主要有两种,

  • 1、是控制增量

  • 2、检查增量是否正确,通过结果我们可以看出来,在500毫秒的时间当中,同步方法的执行效率远远的没有同步块的高,所以适当的选择同步块来增加我们代码的效率吧。

6、在其他对象之上同步

如果我们使用同步块的话,我们就必须要使用一个被加锁的对象,默认的写法是锁this,这个效果和同步方法差不多,但是有时候,我们开发的过程当中不得不在另外一个对象上面进行同步。下面我们来看一个Demo,给大家展示一下,如果我们没有在一个对象上面进行同步那么会出现什么情况。

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
29
30
31
32
33
/**
* 没有在同一个对象上面进行加锁
* @author pengchengliu
*
*/
public class SyncObject {
public static void main(String[] args) {
final Dualsynch dualsynch = new Dualsynch();
new Thread(){
public void run() {
dualsynch.fun();
};
}.start();
dualsynch.g();
}
}
class Dualsynch {
private Object syncObject = new Object () ;
public synchronized void fun () {
for (int i =0 ; i < 5; i++) {
System.out.println("fun()");
Thread.yield();
}
}
public void g () {
synchronized (syncObject) {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
}

由于锁的对象不是同一个对象,所以我们这里的同步方法将会失效,为什么会出现这一种情况呢,原因就是,当我们的任务进入同步方法的时候,获取了相对于this这个对象的锁,而当主线程的任务调用g()方法的时候,获取的是syncObject这个对象的锁,所以出现上述的情况,解决办法就是锁同一个对象就OK

注意:当我们在一个类当中同时需要给多个代码块加锁的话,那么我们就必须要保证相关任务都是在同一个对象上面进行同步的。

7、线程本地存储

其实我们要消除多个任务共享资源时,对资源的冲突操作,其实根本的解决方法就是消除掉这种情况,从根源上面进行消除,我们可以想象如果每一个线程可以像每一个进程一样,都有着自己特有的资源,那么也就从根本上面根除了资源的冲突。那就是线程的本地化存储。

线程本地化存储是一种自动化机制,可以为使用相同变量的不同线程创建不同的存储,比如:有5个线程都要使用变量x,那么线程本地化存储会生成5个x的不同存储块,最重要的便是它们可以将状态和线程相关联。

下面展示一个例子来展示关于线程本地化存储。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 线程的本地化存储
* @author pengchengliu
*
*/
public class ThreadLoaclVariableHolder {

//线程的本地化存储,在使用的时候,一般在类当中的静态变量进行使用
private static ThreadLocal<Integer> sLocalVar = new ThreadLocal<Integer>(){
private Random rand = new Random () ;
protected synchronized Integer initialValue(){
return rand.nextInt(10000);
}
};

public static void increment () {
sLocalVar.set(sLocalVar.get() + 1);
}

public static int get () {
return sLocalVar.get() ;
}

public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(new Accessor(i));
}
TimeUnit.MILLISECONDS.sleep(300);
threadPool.shutdownNow() ;
}
}
class Accessor implements Runnable {
private final int id ;
public Accessor (int id) {
this.id = id ;
}

@Override
public void run() {
while (!Thread.interrupted()) {
ThreadLoaclVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
@Override
public String toString() {
return "#" + id + ": " + ThreadLoaclVariableHolder.get() ;
}
}