Javaマルチスレッド学習の道(3)-共有リソースのスレッド同期(更新)の問題を解決するため
マルチスレッドは、さまざまなメリットをもたらすだけでなく、プログラマーにとってより高い要件を提示します。その中で最も重要なのは、複数のスレッドと共有リソースの関係を解決する方法です。複数のスレッドは、互いに干渉して問題を引き起こすことがよくあります。たとえば、 、マルチスレッドバンキングシステムでは、ユーザーの口座残高は500元になりました。現在、ユーザーはモールで買い物をしていて、カードをスワイプして300元を支払います(1スレッド)。同時に、息子は密かに使用しています。彼のアカウントは400のオンラインゲーム(他の1つのスレッド)を再充電します。2つのスレッドが同時にデータにアクセスし、そのときにアクセスされるデータは500元のアカウント残高です。同期メカニズムがない場合、これら2つの操作は明らかに、これは銀行システムに莫大な損失をもたらすでしょう。
3.1マルチスレッド問題のシミュレーション
上記のシナリオをシミュレートするコードは次のとおりです。マルチスレッドが同期されていない場合、大きな問題が発生する可能性があります。
背景:銀行口座の残高は1000で、複数のユーザーが同時に100元を使います。
クレジットカード口座
public class CreditCard implements Runnable
{
private Double balance=1000D;
private Double spendMoney;
public CreditCard(Double spendMoney)
{
this.spendMoney = spendMoney;
}
public void run()
{
try
{
Thread.sleep(1000);//通过sleep方法增加多个线程同时访问变量的可能性
} catch (InterruptedException e)
{
e.printStackTrace();
}
if (balance>spendMoney)
{
balance-=spendMoney;
System.out.println(Thread.currentThread().getName()+" : "+balance);
}
}
}
main関数のクラス:複数のスレッド
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main
{
public static void main(String[] args)
{
CreditCard creditCard = new CreditCard(100D);
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++)
{
executorService.execute(creditCard);
}
}
}
演算結果:ロジックに従えば、そのようなスレッドタスクは10個しか完了できないことがわかります(バランスは1000、各スレッドは1つのタスクを実行し、バランスは100を差し引かれます)が、結果は予想外で、20スレッドタスクです。完成した実シーンに変換するとカードになり、20人のユーザーが非常に短時間でアカウントにアクセスして100元を消費しますが、すべての消費は成功していますが、残高はわずか1,000です。
[外部リンクの画像転送に失敗しました。ソースサイトにホットリンク防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします(img-l2acufvg-1604740641886)(C:\ Users \暗谷幽兰\ AppData \ Roaming \ Typora \ typora-user-images \ image-20201104223323637.png)]
では、そのようなことが起こらないようにするにはどうすればよいでしょうか?つまり、問題を引き起こす可能性のあるコードを同期する同期です。つまり、問題を引き起こす可能性のあるコードにスレッドがアクセスしているときに、他のスレッドがそのコードにアクセスするのを防ぎます。
3.2同期コードブロック
synchronizeキーワードを使用して、コードを「閉じ」、オブジェクトロックを付与します。このロックは任意のオブジェクトにすることができるため、どのスレッドがこのコードセグメントにアクセスする場合でも、ロックを取得する必要があります。スレッドがロックを取得するとき(ドアを閉じる)、他のスレッドは、スレッドが実行を終了した後(ドアを開く)にのみロックを取得できます。
変更されたコード:
public class CreditCard implements Runnable
{
private Double balance=1000D;
private Double spendMoney;
Object object=new Object();
public CreditCard(Double spendMoney)
{
this.spendMoney = spendMoney;
}
public void run()
{
synchronized (object)
{
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
if (balance>=spendMoney)
{
balance-=spendMoney;
System.out.println(Thread.currentThread().getName()+" : "+balance);
}
}
}
}
動作結果:今回は正常に戻ります
[外部リンクの画像転送に失敗しました。ソースサイトにホットリンク防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします(img-YGAb5oAQ-1604740641888)(C:\ Users \暗谷幽兰\ AppData \ Roaming \ Typora \ typora-user-images \ image-20201104225956046.png)]
3.3同期同期方式
同期コードセグメントと同様に、同期メソッドは、共有共有リソースにアクセスするメソッドの前に同期ステートメントを追加することであり、オブジェクトロックはメソッドを呼び出すオブジェクト自体であるため、オブジェクトロックを明示的に宣言する必要はありません。メソッドはまだ静的です。つまり、特定のオブジェクトに属していないため、そのオブジェクトは、この記事のCreditCard.classなど、このクラスのバイトコードファイルです。
public class CreditCard implements Runnable
{
private Double balance = 1000D;
private Double spendMoney;
public CreditCard(Double spendMoney)
{
this.spendMoney = spendMoney;
}
public void run()
{
spendMoney();
}
public synchronized void spendMoney()
{
try
{
Thread.sleep(200);
} catch (InterruptedException e)
{
e.printStackTrace();
}
if (balance >= spendMoney)
{
balance -= spendMoney;
System.out.println(Thread.currentThread().getName() + " : " + balance);
}
}
}
3.4明示的なLockクラスを使用して同期します。
Lockの使用法も非常に簡単です。Lockオブジェクトを作成し、必要なコードセグメントの前後にそのlock()メソッドとunlock()メソッドを呼び出すだけですが、lock(の使用に注意する必要があります。 )メソッド。unlock()を使用するには、tryfinallyを使用する必要があります。unlock()がないと、ロックオブジェクトが解放されないためです。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CreditCard implements Runnable
{
private Double balance = 1000D;
private Double spendMoney;
private Lock lock=new ReentrantLock();
public CreditCard(Double spendMoney)
{
this.spendMoney = spendMoney;
}
public void run()
{
spendMoney();
}
public synchronized void spendMoney()
{
lock.lock();
try
{
try
{
Thread.sleep(200);
} catch (InterruptedException e)
{
e.printStackTrace();
}
if (balance >= spendMoney)
{
balance -= spendMoney;
System.out.println(Thread.currentThread().getName() + " : " + balance);
}
} finally
{
lock.unlock();
}
}
}
楽観的ロックと悲観的ロック
1.楽観的ロック
楽観的ロックとは、並行プロセス中にデータを読み書きするときに楽観的な態度を取ることです。複数のスレッドが共有リソースに同時に適用されると、すべてのスレッドが共有リソースにアクセスできますが、共有リソースに更新を送信すると、データの競合チェックを実行します。競合がある場合、送信は拒否され、エラーメッセージがユーザーに返されます。ご想像のとおり、複数のスレッドがデータのみを読み取る場合は、楽観的ロックが非常に役立ちます。誰もが非常にシンプルです。データは地上からアクセスされます。**したがって、楽観的ロックは、読み取り操作が多いシナリオに適しています。**チェックする必要がある書き込み操作はごくわずかです。
2.悲観的なロック
楽観的ロックとは対照的に、悲観的ロックは比較的保守的で悲観的な動作を使用します。複数のスレッドが共有リソースにアクセスする場合、1つのスレッドのみが共有リソースのオブジェクトロックを取得でき、他のスレッドはブロック状態になります。...スレッドまでオブジェクトロックの解放を終了します。