Java线程同步失败

3

Java的线程同步功能似乎不是百分百准确的。这个示例的代码会打印一个静态整数的值,该整数被每个线程递增。如果输出重复包含相同的数字,则uniq将识别它。为了帮助说明问题,每个示例都由Makefile脚本运行。每个示例使用不同的同步/锁定方法,但似乎没有一种方法能够100%地正常工作。至少在这个系统上,大多数重复发生在循环的早期。

Makefile:

JAVA=/usr/local/jdk/bin/java
JAVAC=$(JAVA)c

build:
    $(JAVAC) Synchron.java
    $(JAVAC) SynchronVolatile.java
    $(JAVAC) SynchronFinal.java
    $(JAVAC) SynchronThis.java
    $(JAVAC) SynchronA.java
    $(JAVAC) SynchronObj.java

run:
    $(JAVA) Synchron | sort | uniq -c | egrep -v '^\s+1\s+' ; /bin/true
    $(JAVA) SynchronVolatile | sort | uniq -c | egrep -v '^\s+1\s+' ; /bin/true
    $(JAVA) SynchronFinal | sort | uniq -c | egrep -v '^\s+1\s+' ; /bin/true
    $(JAVA) SynchronThis | sort | uniq -c | egrep -v '^\s+1\s+' ; /bin/true
    $(JAVA) SynchronA | sort | uniq -c | egrep -v '^\s+1\s+' ; /bin/true
    $(JAVA) SynchronObj | sort | uniq -c | egrep -v '^\s+1\s+' ; /bin/true

Synchron.java:

import java.io.*;
import java.util.*;

public class Synchron implements Runnable {
        static int a;

        synchronized public void adder() {
        Synchron.a++;
        System.out.println( Synchron.a );
        }

        public void run() {
                while( Synchron.a < 65535 ) {
                        adder();
                }
        }
        public static void main( String []args ) {
                ArrayList <Thread>al = new ArrayList<Thread>();

                try {
                        int i;
                        for( i = 0; i<10 ; i++ ) {
                                Synchron s = new Synchron();
                                Thread t = new Thread( s );
                                al.add(t);
                                t.start();
                        }

                        for( Thread t : al ) {
                                t.join();
                        }
                }
                catch( Exception e ) {
                        e.printStackTrace();
                }

        }
}

SynchronVolatile.java:

import java.io.*;
import java.util.*;

public class SynchronVolatile implements Runnable {
        static int a;
    static volatile Object o = new Object();

        public void adder() {
        synchronized( SynchronVolatile.o ) {
            SynchronVolatile.a++;
        }
        System.out.println( SynchronVolatile.a );
        }

        public void run() {
                while( SynchronVolatile.a < 65535 ) {
                        adder();
                }
        }
        public static void main( String []args ) {
                ArrayList <Thread>al = new ArrayList<Thread>();

                try {
                        int i;
                        for( i = 0; i<10 ; i++ ) {
                                SynchronVolatile s = new SynchronVolatile();
                                Thread t = new Thread( s );
                                al.add(t);
                                t.start();
                        }

                        for( Thread t : al ) {
                                t.join();
                        }
                }
                catch( Exception e ) {
                        e.printStackTrace();
                }

        }
}

SynchronFinal: 这与SynchronVolatile.java相同,只是使用了final来替代volatile。

SynchronThis.java:

import java.io.*;
import java.util.*;

public class SynchronThis implements Runnable {
        static int a;
    static volatile Object o = new Object();

        public void adder() {
        synchronized( this ) {
            SynchronThis.a++;
        }
        System.out.println( SynchronThis.a );
        }

        public void run() {
                while( SynchronThis.a < 65535 ) {
                        adder();
                }
        }
        public static void main( String []args ) {
                ArrayList <Thread>al = new ArrayList<Thread>();

                try {
                        int i;
                        for( i = 0; i<10 ; i++ ) {
                                SynchronThis s = new SynchronThis();
                                Thread t = new Thread( s );
                                al.add(t);
                                t.start();
                        }

                        for( Thread t : al ) {
                                t.join();
                        }
                }
                catch( Exception e ) {
                        e.printStackTrace();
                }

        }
}

SynchronA.java:

import java.io.*;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SynchronA implements Runnable {
        static int a;
    private volatile Lock lock = new ReentrantLock();

        public void adder() {
        lock.lock();
        SynchronA.a++;
        System.out.println( SynchronA.a );
        lock.unlock();
        }

        public void run() {
                while( SynchronA.a < 65535 ) {
                        adder();
                }
        }
        public static void main( String []args ) {
                ArrayList <Thread>al = new ArrayList<Thread>();

                try {
                        int i;
                        for( i = 0; i<10 ; i++ ) {
                                SynchronA s = new SynchronA();
                                Thread t = new Thread( s );
                                al.add(t);
                                t.start();
                        }

                        for( Thread t : al ) {
                                t.join();
                        }
                }
                catch( Exception e ) {
                        e.printStackTrace();
                }

        }
}

SynchronObj.java:

import java.io.*;
import java.util.*;

public class SynchronObj implements Runnable {
        static int a;
    Object o;

    public SynchronObj( Object obj ) {
        o = obj;
    }

        public void adder() {
        synchronized( o ) {
            SynchronObj.a++;
        }
        System.out.println( SynchronObj.a );
        }

        public void run() {
                while( SynchronObj.a < 65535 ) {
                        adder();
                }
        }
        public static void main( String []args ) {
                ArrayList <Thread>al = new ArrayList<Thread>();

        final Object o = new Object();

                try {
                        int i;
                        for( i = 0; i<10 ; i++ ) {
                                SynchronObj s = new SynchronObj( o );
                                Thread t = new Thread( s );
                                al.add(t);
                                t.start();
                        }

                        for( Thread t : al ) {
                                t.join();
                        }
                }
                catch( Exception e ) {
                        e.printStackTrace();
                }

        }
}

当代码运行时,上述线程同步方法并不能百分之百地保证有效。有什么想法是哪里出了问题?
2个回答

6
你遇到的问题是,在某些情况下,你的锁正在不同的锁对象实例上锁定,因此它们实际上永远不会相互干扰。
更改:
Object o;

to

public static final Object o = new Object();

现在,所有的synchronized语句都将尝试在同一个对象上进行锁定,从而发生正确的锁争用。
此外,这看起来很可疑:
while (SynchronObj.a < 65535) {...}

由于您没有进行同步操作就读取了变量a的值,这绝对是一个问题。

同时,您似乎是通过搜索重复输出来测试同步性的方法来进行测试。相反,尝试执行以下操作:

    public void run() {
            for (int i=0; i<10000; i++) {
                    adder();
            }
    }

由于您正在运行10个线程,只需验证最终答案是否为10000*10。任何少/多的结果都意味着线程同步不正确。


2
在没有同步对象的情况下,还可以将方法adder()更改为静态同步。 - Andrew Williamson
1
未同步的读取可能会成为一个问题,但对于作者正在检查的特定条件来说,只要“adder”的同步工作正常,它就不会造成问题。像这样的结构确实会在实际情况中出现,例如在无锁并发数据结构的实现中。 - pvg

0
Martin Konecny指出了您代码中的一个主要问题。所有线程必须为同一个锁对象内容以进行正确的同步。这就是为什么您的尝试“Synchron”,“SynchronThis”和“SynchronObj”都不会起作用的原因。
然而,另一个问题是您对该调用的处理。
System.out.println(SynchronObj.a)

发生在同步块之外。这也会导致一些问题。考虑以下可能的执行场景:

假设“a”的值为30。 线程1进入adder()方法并锁定对象o。它增加了“a”并释放了锁。现在a == 31。然而,在打印“a”的值之前,线程1被暂停,线程2开始执行。线程2获取锁,增加“a”并释放锁。现在a == 32。线程2继续执行并调用

System.out.println(SynchronObj.a)

屏幕上打印的值从30到32。31被“吞掉”了。因此,请确保您也在同步块内打印。始终同步共享变量的读取和写入。即使您认为可能不必要,编译器也可能重新排列您的代码。阅读此处以获取有关此行为的更多信息。
您还应避免在while循环中非同步访问“a”。一个替代方案是:
static int a;
static Object o = new Object();
static boolean cont = true;

public void adder() {
    synchronized( o ) {
        if (cont) {
            MainClass.a++;
            System.out.println( MainClass.a );
            cont = (MainClass.a < 65535);
        }
    }
}

public void run() {
    while(MainClass.cont) {
        adder();
    }
}

最后,您不需要将锁对象声明为volatilevolatile关键字告诉JVM不要在线程本地缓存变量值。当前值应从内存中读取。这在同步块内部自动发生。有关volatile的更多信息也可以参见第17.4章内存模型


哇,把打印语句放在同步块的外面,这是我做过的最愚蠢的事情之一。 - Ed Neville

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接