当继承并且扩展了一个现有类库中的类,并且想在多线程环境使用时,如果"不那么"了解这个类时,需要格外的小心。
看下面这个类,假设这个类是想要用于扩展的基类,它有俩个写方法writer1()、writer2()一个读方法read(),这个类已经充分同步,它是线程安全的类,在外部直接使用它不需要额外的同步。
@GrubBy(desc="内置锁")
class Sup {
protected int x;
protected int y;
//后面会用到
protected Object lock = new Object();
synchronized void write1() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "在写...");
x = 1;
y = 1;
}
synchronized void write2() {
System.out.println(Thread.currentThread().getName() + "在写...");
x = 2;
y = 2;
}
synchronized void read() {
System.out.println(Thread.currentThread().getName() + "在读...");
System.out.println("X:" + x + "-->" + "Y:" + y);
}
}
请注意上面 红色的字体,那是不需要 额外同步的前提条件,如果使用它时的 不变约束发生改变,那么我们必须为其添加额外的同步,例子如下
public class TestSubThread {
static int i = 0;
public static void main(String[] args) {
final Sup sup = new Sup();
final TestSubThread test = new TestSubThread();
final Object lock = new Object();
//在主线程开启一个新线程
//值得注意的是任务在哪里定义和它在哪里运行完全是两码事
//下面这个任务虽然在主线程里定义但却在运行在名为Thread-0的线程中
Runnable task = new Runnable () {
@Override
public void run() {
synchronized(lock) {
while ( test.getI() < 20) {
sup.read();
System.out.println(test.getI());
}
}
}
};
new Thread(task).start();
synchronized(lock) {
while ( test.getI() < 20) {
sup.write1();
test.increment();
System.out.println(test.getI());
}
}
}
public synchronized void increment() {
i++;
}
public synchronized int getI() {
return i;
}
}
虽然 TestSubThread和Sup都是线程安全的类,但Sup读、写操作的
不变约束已经改变,它必须依赖于正确的TestSubThread包含的状态变量 i ,它们应当是一个
原子操作。并且在主线程中对 i 的写操作,在Thread-0中应该被及时看到,也就是它们需要符合上篇文章的happens-before原则,所以请注意 俩个synchronized(lock){...}的同步代码块.
所以就算是全部类都是线程安全的,构成的应用程序也不一定是线程安全的,反之亦然。
这里的例子不太容易看清的原因是为了验证例子中的注释部分。
下面说一下,当扩展一个类用于多线程环境中时需要注意的事情
首先最简单的一点事,重写一个父类的方法,并不能继承它的synchronized
synchronized void write1() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "在写...");
x = 1;
y = 1;
}
synchronized void write2() {
System.out.println(Thread.currentThread().getName() + "在写...");
x = 2;
y = 2;
}
synchronized void read() {
System.out.println(Thread.currentThread().getName() + "在读...");
System.out.println("X:" + x + "-->" + "Y:" + y);
}
}
class Sub extends Sup {
@Override
void write2() {
System.out.println(Thread.currentThread().getName() + "在写...");
y = 3;
try {
Thread.sleep(1000);//这里动的手脚是为了让其更容易出现错误的情况,在修改完x后,让其sleep,交出控制权让而让其他写线程有机会进行修改
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 3;
}
}
显然,父类的writer2()虽然是同步的,但如果重写后忘记了添加synchronized显然就是很容易导致状态不一致的情况,writer2()的方法签名应该为synchronized void writer2()
,很显然,这里也间接说明了一个问题,如果采用内置锁策略,子类自己重写的同步方法和它从父类中继承的同步方法是使用同一个锁的(至于是this还是其父类对象,还无从验证)。
上面的情形只是最简单的情况,想通过继承而扩展一个已有类时,通常并不能获得它的源代码,只能通过文档来了解其同步策略,一旦没有正确了解其同步策略,而只是简单粗暴想当然为扩展方法加上synchronized会导致严重的问题。
public class TestSubThread {
public static void main(String[] args) {
final Sup sub = new Sub();
System.out.println(Thread.currentThread().getName());
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 20; i++) {
sub.write2();
}
}
};
t1.setName("运行重写write2方法的线程:");
t1.start();
Thread t2 = new Thread() {
public void run() {
for (int i = 0; i < 20; i++) {
sub.read();
}
}
};
t2.setName("运行继承父类read方法的线程:");
t2.start();
Thread t3 = new Thread() {
public void run() {
for (int i = 0; i < 20; i++) {
sub.write1();
}
}
};
t3.setName("运行继承父类write1方法的线程:");
t3.start();
}
}
@GrubBy(desc="内置锁")
class Sup {
protected int x;
protected int y;
protected Object lock = new Object();
void write1() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "在写...");
synchronized(lock) {
x = 1;
y = 1;
}
}
void write2() {
System.out.println(Thread.currentThread().getName() + "在写...");
synchronized(lock) {
x = 2;
y = 2;
}
}
void read() {
System.out.println(Thread.currentThread().getName() + "在读...");
synchronized(lock){
System.out.println("X:" + x + "-->" + "Y:" + y);
}
}
}
class Sub extends Sup {
@Override
synchronized void write2() {
System.out.println(Thread.currentThread().getName() + "在写...");
y = 3;
int i = 0;
while (i < 100000000) {
i++;
}
x = 3;
}
}
很显然,父类的读、写操作采用的是显示锁的同步策略,而子类中重写的writer2()采用的是内置锁的同步策略,也就是说当一个线程运行writer2()时,因为采用的是 不同的锁,并不能阻止别的线程同时运行read()或writer1()方法.
例如,
writer2():lock (this) --> y=3 --> sleep --> x=3 --> unlock(this)
read():lock(object) --> x=1 y=3 --> unlock(object)
重复一下,调用writer2()时加的锁并不能阻塞别的线程运行read()因为他们使用的是不同的锁。假如此时,读、写线程共享的内存中x=1,y=1,此时cpu执行writer2()方法至y=3并且写入内存而x=3,由于不能阻止read(),所以随后read会有可能读取到 x=1,y=3的错误值
可以做如下修改,让其同步策略一致。
class Sub extends Sup {
@Override
void write2() {
System.out.println(Thread.currentThread().getName() + "在写...");
synchronized(super.lock) {
y = 3;
int i = 0;
while (i < 100000000) {
i++;
}
x = 3;
}
}
}
下篇关于volatile和LocalThread。