線程安全出現的原因
這裡我們讓多個線程共享同一個賣票資源:
public class RunnableImpl implements Runnable {
//總的票得數量
int tickets = 20;
@Override
public void run() {
while (true) {
if (tickets > 0) {
//為系統性能考慮,每個線程稍微休息一下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
tickets--;
}
}
}
}
public class DemoThreadSafe {
public static void main(String[] args) {
//建立Runnable接口的實作類對象
RunnableImpl run = new RunnableImpl();
//建立多個Thread類對象
Thread r0 = new Thread(run);
Thread r1 = new Thread(run);
Thread r2 = new Thread(run);
//用start方法開啟線程
r0.start();
r1.start();
r2.start();
}
}
輸出:
Thread-1的第20張票
Thread-2的第20張票
Thread-0的第20張票
Thread-2的第17張票
Thread-0的第16張票
Thread-1的第16張票
Thread-2的第14張票
Thread-0的第13張票
Thread-1的第12張票
Thread-2的第11張票
Thread-0的第11張票
Thread-1的第11張票
Thread-0的第8張票
Thread-2的第8張票
Thread-1的第6張票
Thread-2的第5張票
Thread-0的第4張票
Thread-1的第3張票
Thread-2的第2張票
Thread-0的第2張票
Thread-1的第0張票
Thread-2的第-1張票
從上面的例子可以看到, 有被重複賣出去的票,還有第-1,0張票,出現了線程安全問題。因為線程賣出了重複的票和不存在的票。
出現這個問題的原因是,我們建立的每個線程執行到
sleep()
語句之後就放棄了cpu的使用權。等到它睡醒之後,它照樣還是會往下執行, 不會管其它票的數量已經減少了。
線程安全的解決辦法
1. 線程同步(synchronized)
同步代碼塊
-
關鍵字可以用于方法中的某個區塊中,表示對這個區塊的互斥通路同步代碼塊synchronized
synchronized(同步鎖){
需要同步互斥操作的代碼
}
注意:
- 通過代碼塊中的鎖對象,可以使用任意的對象
- 但是必須保證多個線程使用的鎖對象是同一個
-
鎖對象作用:
把同步代碼塊鎖住,隻讓一個線程在同步代碼塊中執行
public class RunnableImpl implements Runnable {
//總的票得數量
int tickets = 20;
//建立一個鎖對象
Object obj = new Object();
@Override
public void run() {
while (true) {
//同步代碼塊
synchronized (obj){
if (tickets > 0) {
//為系統性能考慮,每個線程稍微休息一下
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
tickets--;
}
}
}
}
}
線程類中建立的obj鎖對象相當于作業系統中的pv操作,每個線程運作到synchronized的時候判斷鎖是否還有,有就拿來執行,沒有就等待别的線程歸還鎖。
2. 同步方法
- 使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其它線程隻能在方法外等候
public synchronized void method(){
可能會産生線程安全問題的代碼
}
同步鎖是誰?
對于非static方法,同步鎖就是this
對于static方法,是我們使用目前方法所在類的位元組碼對象(類名.class)
public class RunnableImpl implements Runnable {
//總的票得數量
int tickets = 20;
@Override
public void run() {
while (true) {
//同步代碼塊
sellTickets();
}
}
public synchronized void sellTickets(){
if (tickets > 0) {
//為系統性能考慮,每個線程稍微休息一下
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
tickets--;
}
}
}
Lock鎖
Java util.concurrent.locks.lock
機制提供了比
Synchronized代碼塊
和
synchronized方法
更加廣泛的鎖定操作,同步代碼塊、同步方法具有的功能Lock都有,除此之外更強大,更展現面向對象。
Lock鎖也稱作同步鎖,加鎖和釋放鎖方法化了:
-
:加同步鎖public void lock()
-
:釋放同步鎖public void unlock()
使用步驟:
- 在成員位置建立一個
ReentrantLock對象
- 在可能會出現安全問題的代碼前調用
中的方法Lock接口
lock擷取
- 在可能會出現安全問題的代碼後調用
中的方法Lock接口
unlock釋放鎖
public class RunnableImpl implements Runnable {
//總的票得數量
int tickets = 20;
//1. 在成員位置建立一個`ReentrantLock對象`
Lock l = new ReentrantLock();
@Override
public void run() {
while (true) {
//2. 在可能會出現安全問題的代碼前調用`Lock接口`中的方法`lock擷取`
l.lock();
if (tickets > 0) {
//為系統性能考慮,每個線程稍微休息一下
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
tickets--;
}
//3. 在可能會出現安全問題的代碼後調用`Lock接口`中的方法`unlock釋放鎖`
l.unlock();
}
}
}
為了確定鎖必須被釋放,推薦采用以下的寫法,将unlock()放在finally塊代碼語句中,無論是否出現異常,鎖都會被釋放。
public class RunnableImpl implements Runnable {
//總的票得數量
int tickets = 20;
//1. 在成員位置建立一個`ReentrantLock對象`
Lock l = new ReentrantLock();
@Override
public void run() {
while (true) {
//2. 在可能會出現安全問題的代碼前調用`Lock接口`中的方法`lock擷取`
l.lock();
if (tickets > 0) {
//為系統性能考慮,每個線程稍微休息一下
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "的第" + tickets + "張票");
tickets--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3. 在可能會出現安全問題的代碼後調用`Lock接口`中的方法`unlock釋放鎖`
l.unlock();
}
}
}
}
}