[英]Java Thread safety - Understanding the need of synchronization
在运行多线程应用程序时,我很难理解同步方法,对象的概念并理解不这样做的主要问题。
我了解到,使用synchronize
关键字可确保只有一个线程一次可与特定对象一起使用或一次输入特定的块或方法,基本上将其锁定并在执行结束时将其解锁,以便其他线程可以输入它。
但是我真的不明白这个问题,我完全感到困惑,我创建了一个演示应用程序,其中有2个银行帐户,一个银行类别有5000
资金,并且有一种方法可以将特定金额的资金转入给定帐户,并在其构造函数中创建2个银行帐户并启动线程(每个帐户都是一个线程)。
现在在银行帐户的类中,我有一个funds
字段,以及一个运行方法,该线程将在启动时调用(该类继承Thread),该运行方法将循环运行10次,并通过调用Bank#takeFunds(int amount)
从主银行收取20美元Bank#takeFunds(int amount)
这样,银行类:
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();
}
}
和Client类:
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();
}
}
和输出对我来说:
Thread-0 : 200
left: 4800
Thread-1 : 200
left: 4600
该程序在每个帐户中以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();
}
}
印刷品:
My funds: 2000
My funds: 8000
首先,线程不是对象。 不要为每个客户端分配单独的线程。 线程确实起作用,并且对象包含指定必须完成的代码。
当您在Client
对象上调用方法时,它们不会“在该客户端的线程上”执行; 它们在调用它们的线程中执行。
为了使线程能够完成某些工作,您需要将其移交给实现要在其上执行的代码的对象。 这就是ExecutorService
允许您ExecutorService
完成的工作。
还要记住,锁不会“锁定对象”,并且synchronized(anObject)
不会阻止另一个线程同时调用anObject
的方法。 锁只会阻止尝试获取同一锁的其他线程继续处理,直到第一个线程完成为止。
我测试了您的程序,实际上得到了以下输出:
(结果与您的情况不同,为4600。)
关键是,仅仅因为它碰巧一次工作并不意味着它会一直工作。 多线程可以(在一个不同步的程序中)引入不确定性。
想象一下,如果您的操作花费了更长的时间才能执行。 让我们用Thread.sleep
模拟它:
public void takeMoney(Client c, int amount) {
if (bankmoney >= amount) {
try { Thread.sleep(1000); } catch (InterruptedException e) { }
bankmoney -= amount;
c.addFunds(amount);
}
}
现在尝试再次运行程序。
您的程序运行正常,因为您只扣除2000的总数。这远小于初始值。 因此,此检查没有任何作用,即使您删除了代码,代码仍然可以使用。
if (bankmoney >= amount) {
在这种情况下唯一可能发生的坏事是:如果client1检查的金额超过了他需要提取的金额,但与此同时,其他客户端也提取了该金额。
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次。
让我们看一个更现实的例子,并为我们的银行实现transfer
功能:
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不会很高兴。 请注意,仅锁定不会给您任何保证,还需要将balance
声明为volatile
。
任何时候,如果要对多个线程共享的数据进行某种处理,如果需要执行多个步骤,则可能需要同步。
这需要三个步骤:
i++;
步骤是; (1)从存储器中将i
的值存入寄存器,(2)将1加到寄存器,(3)将寄存器的值存回存储器。
运行中的线程可以随时被抢占 。 这意味着,操作系统可以暂停它,并使用CPU轮换其他线程。 因此,如果没有同步,线程A可以执行使i
递增的步骤(1)(可以将值存入寄存器),然后可以抢占它。 当线程A等待再次运行时,线程B,C和D可以分别递增i
一千次。 然后,当线程A最终再次运行时,它将在其最初读取的值上加1,然后将其存储回内存中。 线程B,C和D的三千个增量将丢失。
每当一个线程将某些数据置于不希望其他线程看到或对其进行操作的临时状态时,就需要进行同步。 创建临时状态的代码必须synchronized
,并且可以对同一数据进行操作的任何其他代码必须同步,并且仅允许线程查看状态的任何代码都必须同步。
正如Marko Topolnik指出的那样,同步不会对数据进行操作,也不会对方法进行操作。 您需要确保所有修改或查看特定数据集合的代码都在同一对象上同步。 那是因为同步只做一件事,而只有一件事:
JVM不允许在同一对象上同时synchronized
两个线程。 这就是全部。 使用方式取决于您。
如果您的数据在容器中,则可以方便地在容器对象上进行同步。
如果您的数据是同一个Foobar实例的所有实例变量,则可以方便地在该实例上进行同步。
如果您的数据都是静态的,那么您可能应该在某个静态对象上进行同步。
祝好运并玩得开心点。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.