【问题标题】:Java synchronized instance member doesn't work in a nested wayJava 同步实例成员不能以嵌套方式工作
【发布时间】:2018-11-13 06:30:14
【问题描述】:

在使用 Java 同步编程时,我碰巧发现了一个不能按预期工作的用法。也就是说,在线程 A 中,它访问两个嵌套同步块内的实例成员(内容):

synchronized(this){
    synchronized (content) {content = str;}
}

在线程B中,它只在一个同步块中访问相同的内容:

synchronized (content) {content = str;}

我希望这可以正常使用

synchronized (content) {content = str;}

然而,事实并非如此。它可以工作,因为没有同步。 完整的代码和日志如下:

public class JavaSync {
    public static void main(String[] args) {
        SyncContent syncContent = new SyncContent("JavaSync");

        ThreadA a = new ThreadA(syncContent);
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB(syncContent);
        b.setName("B");
        b.start();

        ThreadC c = new ThreadC(syncContent);
        c.setName("C");
        c.start();
    }
}

class SyncContent {
    volatile String content = new String();

    public SyncContent(String content) {
        this.content = content;
    }

    private double timeConsuming() {
        double a, b, c;
        double sum = 0;
        for (int i = 1; i < 2000000; i++) {
            a = i + sum / ( i * 19);
            b = a / 17;
            c = b * 23;
            sum += (b + c - a) / (a + i);
        }
        return sum;
    }

    synchronized public void syncFunc(String str) {
        System.out.println("syncFunc.Thread: " + Thread.currentThread().getName() + " enter: " + System.currentTimeMillis());
        synchronized (content) {
            System.out.println("syncFunc.Thread: " + Thread.currentThread().getName() + " content old: " + content);
            content = str;
            System.out.println("syncFunc.Thread: " + Thread.currentThread().getName() + " content new: " + content);
            //Thread.sleep(2000);  // InterruptedException
            System.out.println("syncFunc.Thread: dummy result: " + timeConsuming());
            System.out.println("syncFunc.Thread: " + Thread.currentThread().getName() + " content final: " + content);
        }
        System.out.println("syncFunc.Thread: " + Thread.currentThread().getName() + " exit: " + System.currentTimeMillis());
        /*try {
        } catch (Exception e) {
            e.printStackTrace();
        }*/
    }

    public void syncThis(String str) {
        synchronized(this) {
            System.out.println("syncThis.Thread: " + Thread.currentThread().getName() + " enter: " + System.currentTimeMillis());
            synchronized (content) {
                System.out.println("syncThis.Thread: " + Thread.currentThread().getName() + " content old: " + content);
                content = str;
                System.out.println("syncThis.Thread: " + Thread.currentThread().getName() + " content new: " + content);
                //Thread.sleep(2000);  // InterruptedException
                System.out.println("syncThis.Thread: dummy result: " + timeConsuming());
                System.out.println("syncThis.Thread: " + Thread.currentThread().getName() + " content final: " + content);
            }
            System.out.println("syncThis.Thread: " + Thread.currentThread().getName() + " exit: " + System.currentTimeMillis());
            /*try {
            } catch (Exception e) {
                e.printStackTrace();
            }*/
        }
    }

    public void syncVariable(String str) {
        synchronized(content) {
            System.out.println("syncVariable.Thread: " + Thread.currentThread().getName() + " enter: " + System.currentTimeMillis());
            System.out.println("syncVariable.Thread: " + Thread.currentThread().getName() + " content old: " + content);
            content = str;
            System.out.println("syncVariable.Thread: " + Thread.currentThread().getName() + " content new: " + content);
            System.out.println("syncVariable.Thread: " + Thread.currentThread().getName() + " exit: " + System.currentTimeMillis());
        }
    }
}

class ThreadA extends Thread {
    private SyncContent syncContent;
    private String me = "ThreadA";

    public ThreadA(SyncContent syncContent) {
        super();
        this.syncContent = syncContent;
    }

    @Override
    public void run() {
        syncContent.syncThis(me);
    }
}


class ThreadB extends Thread {
    private SyncContent syncContent;
    private String me = "ThreadB";

    public ThreadB(SyncContent syncContent) {
        super();
        this.syncContent = syncContent;
    }

    @Override
    public void run() {
        syncContent.syncFunc(me);
    }
}

class ThreadC extends Thread {
    private SyncContent syncContent;
    private String me = "ThreadC";

    public ThreadC(SyncContent syncContent) {
        super();
        this.syncContent = syncContent;
    }

    @Override
    public void run() {
        syncContent.syncVariable(me);
    }
}

日志:

syncThis.Thread: A enter: 1542076529822
syncThis.Thread: A content old: JavaSync
syncThis.Thread: A content new: ThreadA
syncVariable.Thread: C enter: 1542076529823
syncVariable.Thread: C content old: ThreadA
syncVariable.Thread: C content new: ThreadC
syncVariable.Thread: C exit: 1542076529824
syncThis.Thread: dummy result: 411764.5149938948
syncThis.Thread: A content final: ThreadC
syncThis.Thread: A exit: 1542076529862
syncFunc.Thread: B enter: 1542076529862
syncFunc.Thread: B content old: ThreadC
syncFunc.Thread: B content new: ThreadB
syncFunc.Thread: dummy result: 411764.5149938948
syncFunc.Thread: B content final: ThreadB
syncFunc.Thread: B exit: 1542076529897

为什么没有发生这种情况?

【问题讨论】:

  • 不清楚你在问什么。有问题吗?预期的结果是什么?实际结果如何?
  • consider synchronized (content) { content = str; // 字符串是不可变的 - 你在 locking 上已经改变了
  • 可能是因为这些锁都不常见。您正在对不同的对象进行同步。每次创建 SynConcent 时,都会创建一个新字符串。那是行不通的。
  • @Charlie,预计会同步实例成员内容。
  • @ScaryWombat 正确。

标签: java multithreading synchronized


【解决方案1】:

synchronized 适用于对象,而不是变量的名称。您通过锁定content 进入synchronized 块,但随后在块中,您为其分配了不同的对象。下次您尝试同步 content 时,您将同步 不同的对象,因此之前持有的锁不会干扰它。

如果您使用可变对象(例如 StringBuilder)并改为更改其数据,您将看到预期的行为:

synchronized (content) {
    content.setLength(0); // Remove the old content
    content.append(str); // Set the new content
    // Rest of the code...

【讨论】:

  • 没错。我修改了示例代码,现在可以正常工作了。
  • 这确实是一个很好的教训,因为它经常使用不可变变量,例如字符串。
  • 值得指出的是,始终遵循一个简单的规则可以避免这个错误:如果你写synchronized (foo) { ... },那么foo应该是一个final变量。如果编译器不允许你这样做,那么你要么在做一些非常棘手的事情,要么你犯了一个错误。而且由于“棘手”通常并不意味着“好”,因此通常最好遵守规则。
  • @SolomonSlow 好,一个简单但有用的规则
  • 顺便说一句,另一种解决方案是添加一个标志(例如整数)并在访问这些不可变变量的任何地方同步它,因为有时将这些不可变变量更改为可变变量可能很烦人或不可能。
猜你喜欢
  • 2020-04-07
  • 2021-09-14
  • 2012-03-12
  • 1970-01-01
  • 1970-01-01
  • 2012-05-09
  • 2016-01-31
  • 2017-06-06
相关资源
最近更新 更多