多線程的相關關鍵詞理解 volatile,atomic,synchronized,lock
**
造成線程不安全的原因: 多線程操作一個變量的時候,每個線程會保存有自己的副本,線程的內部當進行操作的時候實際用的是副本,而不是主線程本身。 創建副本的時候(其他線程創建的時候),會去到當前主線程本身的值,不會去拿到其他線程副本里面的值,拿到的值可能是 其他線程操作過後的值,所以這個值不一定是最新的值。 當副本賦值完成之後會把自己當前變量的值賦值到主線程本身去,不管別線程有沒有賦值過,所以抹殺了別人的勞動成果造成線程不安全。
========================================================================
**
1. volatile
- 先看代碼
public class BankCard4 {
Account account;
public BankCard4() {
account = new Account();
//默認有100塊
account.setMoney(100);
}
/**
* 存款
*
* @param money
*/
public void deposit(long money) {
long old = account.getMoney();
account.setMoney(old + money);
System.out.println(Thread.currentThread().getName() + "充值成功" + account.getMoney());
}
/**
* 取款
*/
public void withdraw(long money) {
long old = account.getMoney();
if (old < money ) {
System.out.println(Thread.currentThread().getName() + " 餘額不足! 餘額:" + old);
}
if (old - money >= 0) {
account.setMoney(old - money);
System.out.println(Thread.currentThread().getName() + " 取款成功 : " + money + " 餘額 : " + account.getMoney());
}
}
}
public class Test {
//volatile 修飾
static volatile BankCard4 bankCard = new BankCard4();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankCard.deposit(10);
// bankCard.withdraw(10);
}).start();
}
}
//開了十個線程 去對同一個變量操作,經過 volatile 修飾。理論上我們充值完成後
//應該有200 , 原來有100 , 10個線程 每個線程 充值10塊。
}
結果:
Thread-2充值成功130
Thread-8充值成功180
Thread-7充值成功170
Thread-6充值成功160
Thread-4充值成功160
Thread-5充值成功140
Thread-3充值成功140
Thread-0充值成功130
Thread-1充值成功130
Thread-9充值成功190
Process finished with exit code 0
- 結果我們發現 充值過後 最大是 190,沒有得到想要的 結果,而且多次充值結果一樣。如果是真的銀行卡,那太可怕了。
- 事實證明了volatile並不是線程安全的 。
- 經過volatile修飾,只是保證在創建副本的時候去取其他副本里面的最新的值。
- 後面副本賦值到主線程的時候,會拿自己的值去覆蓋其他線程賦值的值,所以造成了線程不安全的原因。
結論: *volatile修飾不能保證線程安全,只能保證線程建立的時候副本的值是最新的。*保證此變量對所有線程的可見性(當一條線程修改這個變量值時,新值其他線程立即得知,不能保證在併發條件下是線程安全)
2.atomic類
- 原子包:java.util.concurrent.atomic
原理: 提供了一組原子類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具有排他性,即當某個線程進入方法,執行其中的指令時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另一個線程進入,這只是一種邏輯上的理解。
i++ 這就是一個原子操作,就是必須等這個線程的這個操作完成後才能進行其他操作。
這就是原子操作。
package bank;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by linving on 2016/11/15.
*/
public class BankCard3 {
// private AtomicReference<Long> moneny;
private AtomicLong moneny = new AtomicLong(); //用原子包下面的 AtomicLong 進行操作
public BankCard3() {
// moneny = new AtomicReference<>();
moneny.set(100L);
}
/**
* 存款
*
* @param
*/
public void deposit(long m) {
Long tmp = moneny.get();
if(moneny.compareAndSet(tmp, tmp + m)) ;
System.out.println(Thread.currentThread().getName() + "充值成功" + moneny.get());
}
/**
* 取款
*/
public void withdraw(long m) {
long old = moneny.get();
if (old < m) {
System.out.println(Thread.currentThread().getName() + " 餘額不足! 餘額:" + old);
}
if (moneny.compareAndSet(old, old - m)) {
System.out.println(Thread.currentThread().getName() + " 取款成功 : " + m + " 餘額 : " + moneny.get());
}
}
}
Thread-1充值成功110
Thread-0充值成功110
Thread-2充值成功110
Thread-3充值成功120
Thread-4充值成功130
Thread-5充值成功140
Thread-6充值成功150
Thread-7充值成功160
Thread-8充值成功170
Thread-9充值成功180
Process finished with exit code 0
從結果看:是線程不安全的。
也只能保證原子操作,也就是取錢,存錢的操作是不會被打斷。
也就是防止加到一半了變量變成其他的其他線程進來,不能保證線程安全。
3. synchronized
- 同步鎖的使用
可以鎖定對象或者方法 - synchronized(obj)
鎖定對象的時候 該對象只能被當前獲得鎖的線程使用,其他線程需要等待。
鎖定範圍是整個對象。 - public synchronized void method()
鎖定函數,鎖定的改函數只能被獲取到鎖定線程使用,其他線程需要等待。
鎖定範圍只是加鎖的函數。 其他沒有加鎖的函數可以被其他線程使用。
package bank;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by linving on 2016/11/15.
*/
public class BankCard2 {
// private AtomicReference<Account> atomicReference;
Account account;
public BankCard2() {
account = new Account();
account.setMoney(100);
}
/**
* 存款
*
* @param money
*/
public synchronized void deposit(long money) {
long old = account.getMoney();
account.setMoney(old + money);
System.out.println(Thread.currentThread().getName() + "充值成功" + account.getMoney());
}
/**
* 取款
*/
public synchronized void withdraw(long money) {
long old = account.getMoney();
if (old < money ) {
System.out.println(Thread.currentThread().getName() + " 餘額不足! 餘額:" + old);
}
if (old - money >= 0) {
account.setMoney(old - money);
System.out.println(Thread.currentThread().getName() + " 取款成功 : " + money + " 餘額 : " + account.getMoney());
}
}
}
結果:
Thread-9充值成功110
Thread-8充值成功120
Thread-6充值成功130
Thread-5充值成功140
Thread-4充值成功150
Thread-7充值成功160
Thread-3充值成功170
Thread-0充值成功180
Thread-1充值成功190
Thread-2充值成功200
Process finished with exit code 0
結果和我們預期的一樣。
4.Lock
- Lock 和 synchronized 同樣可以保證線程安全性。
- 使用比 synchronized 要靈活,但是複雜了一點:
- 可以把鎖鎖在所需要加鎖的代碼塊上面。
- 靈活,但是要只注意 死鎖問題。鎖需要自己釋放,synchronized 的鎖執行完代碼後會自己釋放,不存在死鎖問題。lock 則要自己釋放,忘記釋放很容易造成死鎖。
package bank;
import bank.Account;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by linving on 2016/11/15.
*/
public class BankCard {
// private AtomicReference<Account> atomicReference;
Account account;
Lock lock;
public BankCard() {
account = new Account();
account.setMoney(100);
lock = new ReentrantLock();
}
/**
* 存款
*
* @param money
*/
public void deposit(long money) {
lock.lock();
long old = account.getMoney();
account.setMoney(old + money);
System.out.println(Thread.currentThread().getName() + "充值成功" + account.getMoney());
lock.unlock();
}
/**
* 取款
*/
public void withdraw(long money) {
lock.lock();
long old = account.getMoney();
if (old < money ) {
System.out.println(Thread.currentThread().getName() + " 餘額不足! 餘額:" + old);
}
if (old - money >= 0) {
account.setMoney(old - money);
System.out.println(Thread.currentThread().getName() + " 取款成功 : " + money + " 餘額 : " + account.getMoney());
}
lock.unlock();
}
}
結果:
Thread-0充值成功110
Thread-2充值成功120
Thread-3充值成功130
Thread-1充值成功140
Thread-4充值成功150
Thread-5充值成功160
Thread-6充值成功170
Thread-7充值成功180
Thread-9充值成功190
Thread-8充值成功200
結果和我們想要的一樣。