Java线程安全-了解同步的需要

Java线程安全-了解同步的需要,java,multithreading,Java,Multithreading,在运行多线程应用程序时,我很难理解同步方法和对象的概念,也很难理解不同步的主要问题 我知道synchronize关键字用于确保在一段时间内只有一个线程使用特定的对象或输入特定的块或方法,基本上锁定它并在执行结束时解锁,以便其他线程可以输入它 但我真的不理解这个问题,我完全糊涂了,我创建了一个演示应用程序,其中我有两个银行账户,一个银行类有5000个资金,还有一个方法可以将特定金额的资金转移到给定的账户,在它的构造函数中,它创建了两个银行帐户并启动线程,每个帐户都是一个线程 现在在银行账户的类中,

在运行多线程应用程序时,我很难理解同步方法和对象的概念,也很难理解不同步的主要问题

我知道synchronize关键字用于确保在一段时间内只有一个线程使用特定的对象或输入特定的块或方法,基本上锁定它并在执行结束时解锁,以便其他线程可以输入它

但我真的不理解这个问题,我完全糊涂了,我创建了一个演示应用程序,其中我有两个银行账户,一个银行类有5000个资金,还有一个方法可以将特定金额的资金转移到给定的账户,在它的构造函数中,它创建了两个银行帐户并启动线程,每个帐户都是一个线程

现在在银行账户的类中,我有一个资金字段和一个run方法,线程将调用该方法启动类继承线程,run方法将循环10次,并通过调用BanktakeFundsint amount从主银行提取20美元

现在我们开始,银行类:

public class Bank {

    private int bankmoney = 5000;

    public Bank() {
        Client a = new Client(this);
        Client b = new Client(this);

        a.start();
        b.start();
    }

    public void takeMoney(Client c, int amount) {
        if (bankmoney >= amount) {
            bankmoney -= amount;
            c.addFunds(amount);
        }
    }

    public void print() {
        System.out.println("left: " + bankmoney);
    }

    public static void main(String... args) {
        new Bank();
    }
}
和客户端类:

public class Client extends Thread {

    private Bank b;
    private int funds;
    Random r = new Random();
    public Client(Bank b) {
        this.b = b;
    }

    public void addFunds(int funds) {
        this.funds += funds;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            b.takeMoney(this, 20);
        }
        System.out.println(Thread.currentThread().getName() + " : " + funds);
        b.print();
    }
}
程序结束时每个帐户中有200美元,银行中还有4600美元,所以我没有真正看到问题所在,我没有演示线程安全问题,我想这就是我无法理解的原因

我试图得到最简单的解释,说明它到底是如何工作的,我的代码怎么会变成线程安全的问题

谢谢

例如:

static void transfer(Client c, Client c1, int amount) {
    c.addFunds(-amount);
    c1.addFunds(amount);
}

public static void main(String... args) {
    final Client[] clients = new Client[]{new Client(), new Client()};

    ExecutorService s = Executors.newFixedThreadPool(15);
    for (int i = 0; i < 15; i++) {
        s.submit(new Runnable() {
            @Override
            public void run() {
                transfer(clients[0], clients[1], 200);
            }
        });
    }

    s.shutdown();
    while(!s.isTerminated()) {
        Thread.yield();
    }

    for (Client c : clients) {
        c.printFunds();
    }
}

我测试了您的程序,实际上得到了以下输出:

结果与您的情况不同,不是4600

关键是,仅仅因为它只工作一次并不意味着它将永远工作。多线程可能会在同步程序中引入非确定性

想象一下,如果您的操作执行时间稍长,会发生什么情况。让我们用一个线程来模拟它。sleep:


现在再次尝试运行程序。

首先,线程不是对象。不要为每个客户端分配单独的线程。线程执行工作,对象包含指定必须执行的操作的代码

当您在客户机对象上调用方法时,它们不会在该客户机的线程上执行;它们在调用它们的线程中执行

为了让线程完成一些工作,您需要将它交给一个实现要在其上执行的代码的对象。这就是ExecutorService允许您简单地执行的操作


还要记住,锁不会锁定对象,synchronizedanObject自身不会阻止另一个线程同时调用对象的方法。锁只会阻止试图获取相同锁的其他线程继续进行,直到第一个线程使用它。

您的程序运行正常,因为您只扣除了2000的总数。这远远小于初始值。因此,该检查没有作用,即使您删除它,代码也会工作

 if (bankmoney >= amount) {
在这种情况下,唯一可能发生的坏事是,如果客户1检查金额超过了他需要提取的金额,但同时其他客户提取了该金额

public void run() {
    for (int i = 0; i < 100; i++) {
        b.takeMoney(this, 200);
    }
    System.out.println(Thread.currentThread().getName() + " : " + funds);
    b.print();
}


 public void takeMoney(Client c, int amount) {
    if (bankmoney >= amount) {
    system.println("it is safer to withdraw as i have sufficient balance")
        bankmoney -= amount;
        c.addFunds(amount);
    }
}
有一段时间,客户一会检查银行存款是否大于金额,但当他取款时,金额将达到负值。因为其他线程将接受该数量。
运行程序4-5次,您将了解。

让我们看一个更现实的示例,并为我们的银行实现一个转账功能:

public boolean transfer(long amount, Client source, Client recipient) {
    if(!source.mayTransferAmount(amount)) return false; // left as an exercise
    source.balance -= amount;
    recipient.balance += amount;
}
现在让我们想象两条线。线程A将单个单元从客户端x传输到客户端y,而线程B将单个单元从客户端y传输到客户端x。现在您必须知道,如果没有同步,您无法确定CPU如何命令操作,因此可能是:

A: get x.balance (=100) to tmpXBalance
B: get x.balance (=100) to tmpXBalance
B: increment tmpXBalance (=101)
B: store tmpXBalance to x.balance (=101)
A: decrement tmpXBalance (=99)
A: store tmpXBalance to x.balance (=99)
(rest of exchange omitted for brevity)

哇!我们刚刚丢了钱!客户x不会很高兴的。请注意,单独锁定并不能给您任何保证,您还需要将平衡声明为volatile。

任何时候,您都需要对多个线程共享的数据执行某些操作,如果这需要多个步骤,那么您可能需要同步

这需要三个步骤:

i++;
步骤如下:;1将i的值从内存中输入寄存器,2将1添加到寄存器,3将寄存器的值存储回内存

正在运行的线程可以随时抢占。这意味着,操作系统可以暂停它,并使用CPU给其他线程一个回合。因此,如果没有同步,线程A可以执行递增i的步骤1,它可以将值放入寄存器,然后它可以被抢占。当线程A等待再次运行时,线程B、C和D可以将i增加1000倍。然后,当线程A最终再次运行时,它会将它最初读取的值加1,然后将其存储回内存。线程B、C和D的三千个增量将丢失

每当一个线程可以将一些数据放入临时线程时,就需要同步 声明不希望其他线程看到或在其上操作。创建临时状态的代码必须同步,可以对相同数据进行操作的任何其他代码都必须同步,任何只允许线程查看状态的代码都必须同步

正如Marko Topolnik所指出的,同步不会对数据进行操作,也不会对方法进行操作。您需要确保修改或查看特定数据集合的所有代码都在同一对象上同步。这是因为同步只做一件事:

JVM将不允许在同一对象上同时同步两个线程。这就是它的全部功能。你如何使用它取决于你自己

如果数据位于容器中,则可以方便地在容器对象上进行同步

如果您的数据是同一Foobar实例的所有实例变量,那么您可以方便地在该实例上进行同步

如果您的数据都是静态的,那么您可能应该在某个静态对象上进行同步


祝你好运,玩得开心。

同步就像在十字路口的停车标志处停车,以确保十字路口是免费的。如果没有它,冲突是可能的。事实上,你运行了程序并得到了你期望的输出,这就是为什么这些bug如此令人讨厌的原因。代码中可能存在一个bug很长一段时间。然后,在这一天,有人创造了一个超高速处理器和繁荣!崩溃代码。这发生在我身上,是的。使takeMoney方法同步将确保获得一致的结果。我实现了与您告诉我的类似的示例,它在没有任何锁定或波动的情况下运行良好,我做错了什么?取决于您的系统架构,例如x86,您可能仅在某些系统事件发生时获得重新排序,例如,您有高系统负载,您的进程经常被中断,或者您只有一个双核CPU,这降低了真正并发的机会。请注意,仅仅因为它似乎在您的系统上工作,并不能保证它在任何情况下都能工作。我当然不会把银行押注在你的实施上-@杰克,艾奥贝所说的很重要:仅仅因为它只发生过一次并不意味着它会一直起作用。我想补充一点:仅仅因为它总是在一个环境中工作并不意味着它在每个环境中都能工作。并发错误可能很难重现,因此在测试中很难捕捉到。处理并发错误的最佳方法是首先不要创建它们。要做到这一点,必须坚持严格的设计模式,这些模式在数学上已经证明是有效的;医生让人群从它身边经过。
public boolean transfer(long amount, Client source, Client recipient) {
    if(!source.mayTransferAmount(amount)) return false; // left as an exercise
    source.balance -= amount;
    recipient.balance += amount;
}
A: get x.balance (=100) to tmpXBalance
B: get x.balance (=100) to tmpXBalance
B: increment tmpXBalance (=101)
B: store tmpXBalance to x.balance (=101)
A: decrement tmpXBalance (=99)
A: store tmpXBalance to x.balance (=99)
(rest of exchange omitted for brevity)
i++;