Java 线程中的同步语句

Synchronized Statements in Java Thread

提问人:user22844207 提问时间:11/10/2023 最后编辑:user22844207 更新时间:11/12/2023 访问量:132

问:

我正在阅读官方教程中的线程主题,我正在尝试理解内在外观。

我现在了解了同步方法、同步语句同步静态方法之间的不同基本原理。

在那篇文章的同步语句部分,给出一个例子,他们说

在此示例中,addName 方法需要同步对 lastName 和 nameCount 的更改,但也需要避免同步其他对象方法的调用。(从同步代码中调用其他对象的方法可能会产生有关活动部分所述的问题。如果没有同步语句,就必须有一个单独的、不同步的方法,其唯一目的是调用 nameList.add。

我真的不明白他们说了什么。请用一个简单的例子向我解释一下。

然后,他们说:

同步语句对于通过细粒度同步提高并发性也很有用。例如,假设类 MsLunch 有两个实例字段 c1 和 c2,它们从不一起使用。这些字段的所有更新都必须同步,但没有理由阻止 c1 的更新与 c2 的更新交错进行,这样做会通过创建不必要的阻塞来减少并发性。我们没有使用同步方法或以其他方式使用与此相关的锁,而是创建两个对象来提供锁。

带示例代码

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

我应该如何理解它?我认为,它只是提到创建了两个对象来管理各自的变量 c1、c2,还有什么吗?

更新

在阅读了所有评论以及@tgdavies和@michael的回答后,我非常感谢您的支持,帮助我清楚地理解了这些概念。

对于第二个引号,我尝试进行比较以理解它。 实际上比synchronized statementsynchronized method

  1. 带有同步语句的 MsLunch.java

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }

    public long getC1() {
        return c1;
    }

    public long getC2() {
        return c2;
    }
}

  1. MsLunch2.java 与同步方法
public class MsLunch2 {
    private long c1 = 0;
    private long c2 = 0;

    public synchronized void inc1() {
        c1++;
    }

    public synchronized void inc2() {
        c2++;
    }

    public long getC1() {
        return c1;
    }

    public long getC2() {
        return c2;
    }
}
  1. 测试 .java
public class Main {
    public static void main(String[] args) throws InterruptedException {
        // synchronized statements
//        MsLunch ms = new MsLunch();
        // synchronized method
        MsLunch2 ms = new MsLunch2();

        long start = System.currentTimeMillis();
        System.out.println(start);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                ms.inc1();
            }
            System.out.println(ms.getC1());
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                ms.inc2();
            }
            System.out.println(ms.getC2());
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        long end = System.currentTimeMillis();
        System.out.println(end);
        System.out.println("Time = " + (end - start)/1000.0);
    }
}

java 线程

评论

1赞 tgdavies 11/10/2023
你读过活度(docs.oracle.com/javase/tutorial/essential/concurrency/...)的部分吗?
0赞 user22844207 11/10/2023
我还没读过。那句话包含两句话,我对第一句话很困惑
1赞 tgdavies 11/10/2023
我现在已经回答了你的第一个问题,但你应该阅读该部分,以了解为什么我们要保持同步块的范围较小。
0赞 user22844207 11/10/2023
Tks,请耐心等待,我正在阅读
2赞 michael 11/10/2023
...想象一下,是公共厕所;如果这些方法是同步的(即,有一把锁),则意味着当洗手间被占用时,没有人可以使用洗手间。但是,如果它们有单独的锁,那么两个洗手间可以同时使用。(现在想象一下,有 10 个洗手间,如果所有 10 个洗手间都用一把锁,那将是多么愚蠢,而建筑建筑师的意图显然是所有洗手间都应该同时使用。c1c2thisc1c2c1c10
1赞 Holger 12/5/2023
重要提示:为了使这些示例正确,并且 and 方法也必须使用,并且每个 getter 必须使用与修改 getter 读取的变量的方法相同的对象进行同步。getC1()getC2()synchronized

答:

0赞 tgdavies 11/10/2023 #1

第一个问题的示例代码是:

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

重要的是,我们不要在按住锁的同时调用对象的方法,因为我们不知道它的行为——例如,这可能需要很长时间。addnameListthisadd

如果没有同步语句(即,如果我们只有同步方法),我们需要编写:

public void addName(String name) {
    doSetLastName(name);
    nameList.add(name);
}

private synchronized doSetLastName(String name) {
        lastName = name;
        nameCount++;
}

短语的解释:

避免同步其他对象方法的调用

“避免”做某事意味着不做,所以我们应该“不同步其他对象方法的调用”

“调用”是一个调用,所以在上面的例子中,我们在对象上调用了该方法。addnameList

实例方法中的代码在特定对象(即引用的对象)的上下文中运行,因此“其他对象的方法”是不是该对象的对象上的方法。在上面的示例中,是对象 上的方法。thisthisaddnameList

因此,我们可以将其改写为“不要在按住锁时在其他对象上调用方法”(当然,除非您决定需要这样做以确保线程安全。

对于您的第二个问题,假设它是这样实现的:MsLunch

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;

    public synchronized void inc1() {
            c1++;
    }

    public synchronized void inc2() {
            c2++;
    }
}

这仍然是完全线程安全的,但是由于两个同步部分都锁定在实例中,因此线程调用将被线程调用阻止。这对于保持线程安全是不必要的。MsLunchinc1inc2

评论

0赞 user22844207 11/10/2023
谢谢,你能在我的第一句话中解释一下“但也需要避免同步其他对象方法的调用”,以便我更容易理解你的答案吗?
1赞 tgdavies 11/10/2023
这句话的哪一部分需要解释?
0赞 user22844207 11/10/2023
这整个组:“避免同步其他对象方法的调用”
0赞 user22844207 11/10/2023
哇,感谢您的惊人补充解释。我可能需要很多时间才能理解你的答案。所以我会尽快投赞成票。
1赞 Holger 12/5/2023
多么可怕的教程。在不提供其他上下文的情况下,最值得注意的是,是否引用了线程安全集合,开发人员不得将方法的调用移出块,而是将其保留在内部,并确保对的每个访问也受到保护。面向初学者的教程应该侧重于一致性,而不是 Liveness 优化。nameListaddsynchronizednameList
-1赞 Reilas 11/10/2023 #2

"...我正在阅读官方教程中的线程主题,我正在尝试理解内在外观。..."

的概念类似于单例类的概念。
这或多或少是“内在的”这个词所描述的——即内在的、定性的。

基本上只有一个实例,一次只有 1 个线程可以访问它。
当另一个线程遇到时,JVM 将委托它是必须等待还是继续——这在 Java 中称为阻塞”。

"...我现在了解了同步方法、同步语句同步静态方法之间的不同基本原理。..."

它们是非常基本的概念。您只需锁定一个类实例即可防止并发修改。
换句话说,您可以“微调”,以减少“阻塞”。

尽管如此,线程多线程的实际实践可能相当复杂。
我推荐阅读 O'Reilly MediaJava Threads, 3rd Edition

"...在那篇文章的同步语句部分,举个例子,他们说......

。我真的不明白他们说了什么。请用一个简单的例子向我解释一下。..."

从本质上讲,他们所说的是,最好不要在中包含方法调用

"...然后,他们说:......

。我应该如何理解它?我认为,它只是提到创建了两个对象来管理各自的变量 c1、c2,还有什么吗?..."

这意味着,与其锁定 MsLunch 实例,不如锁定关联的 Object

这是整个 Java 框架中的常见做法。
查看 OpenJDK 中的 BufferedReader 类源代码。
GitHub – java/io/BufferedReader.java

评论

2赞 michael 11/10/2023
“与其锁定 MsLunch 实例,不如锁定关联的对象”——不,这不太正确。使用“MsLunch 实例”也是“对象”(具体来说,),无论您使用“关联的对象”还是 — 关键是,如果您需要和/或想要拥有多个锁,那么您必须使用单独的实例,因为只有一个。(我不确定这是否能澄清任何事情,但开始与“Singleton”进行比较的答案 - 一个复杂/不相关的话题本身 - 并不能真正帮助任何人理解并发。thisthisObjectthis
0赞 user207421 11/12/2023
它一点也不像单例的概念,“内在的”并不意味着“定性的”。可以有许多锁实例。JVM 不必“委托它是否必须等待或继续”。答案在很大程度上是无稽之谈。