第一章:併發編程中的三個問題
1、可見性問題
package com.itheima.demo01_concurent_problem;
/**
* 演示可見性問題
* 1、創建一個共享變量
* 2、創建一個線程讀取共享變量
* 3、創建一個線程修改共享變量
*
*
* 總結:併發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並沒有立即看到修改後的最新值
*/
public class Test01Visibility {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag)
{
}
}).start();
Thread.sleep(2000);
new Thread(()->{
flag = false;
System.out.println("線程修改了變量的值爲false");
}).start();
}
}
2、原子性問題
package com.itheima.demo01_concurent_problem;
import java.util.ArrayList;
import java.util.List;
/**
* 目標:演示原子性問題
* 1、定義一個共享變量number
* 2、對number進行1000次++操作
* 3、使用5個線程來進行
*/
public class Test02Atomicity {
public static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = ()->{
for (int i = 0; i < 1000; i++) {
number++;
}
};
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
//保證5個線程執行完之後才輸出number的值
for(Thread t :list)
{
t.join();
}
System.out.println("number="+number);
}
}
使用javap反彙編class文件,得到下面的字節碼指令:
其中,對於 number++ 而言(number 爲靜態變量),實際會產生如下的 JVM 字節碼指令:
9: getstatic #12 // Field number:I
12: iconst_1
13: iadd
14: putstatic #12 // Field number:I
由此可見number++是由多條語句組成,以上多條指令在一個線程的情況下是不會出問題的,但是在多 線程情況下就可能會出現問題。比如一個線程在執行13: iadd時,另一個線程又執行9: getstatic。會導 致兩次number++,實際上只加了1。
小結
併發編程時,會出現原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共
3、有序性問題
public static void main(String[] args)
{
int a = 10; int b = 20;
}
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</dependency
package com.itheima.demo01_concurent_problem;
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;
@JCStressTest
@Outcome(id = {"1","4"}, expect = Expect.ACCEPTABLE,desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING,desc = "danger")
@State
public class Test03Ordering {
int num = 0;
boolean ready =false;
//線程1執行的代碼
@Actor
public void actor1(I_Result r)
{
if(ready)
{
r.r1 = num + num;
}else {
r.r1 = 1;
}
}
//線程2執行的代碼
@Actor
public void actor2(I_Result r)
{
num = 2;
ready = true;
}
}
mvn clean install
java -jar target/jcstress.jar
第二章、Java內存模型(JMM)
中央處理器,是計算機的控制和運算的核心,我們的程序最終都會變成指令讓CPU去執行,處理程序中的數據
內存
Java內存模型
主內存與工作內存之間的交互
對應如下的流程圖:
lock -> read -> load -> use -> assign -> store -> write -> unlock
第三章:synchronized保證三大特性
synchronized (鎖對象)
{
// 受保護資源;
}
synchronized與原子性
使用synchronized保證原子性
package com.itheima.demo02_concurrent_problem;
import java.util.ArrayList;
import java.util.List;
/*
目標:演示原子性問題
1.定義一個共享變量number
2.對number進行1000的++操作
3.使用5個線程來進行
*/
public class Test02Atomicity {
// 1.定義一個共享變量number
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 2.對number進行1000的++操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (obj) {
number++;
}
}
};
List<Thread> list = new ArrayList<>();
// 3.使用5個線程來進行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("number = " + number);
}
}
synchronized與可見性(使用volatile也可以解決可見性問題)
使用synchronized保證可見性
package com.itheima.demo02_concurrent_problem;
/*
目標:演示可見性問題
1.創建一個共享變量
2.創建一條線程不斷讀取共享變量
3.創建一條線程修改共享變量
*/
public class Test01Visibility {
// 1.創建一個共享變量
private static boolean flag = true;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 2.創建一條線程不斷讀取共享變量
new Thread(() -> {
while (flag) {
//每次執行到synchronized,都會讓線程刷新工作內存裏面的值,從主存裏面重新獲取
synchronized(){
}
}
}).start();
Thread.sleep(2000);
// 3.創建一條線程修改共享變量
new Thread(() -> {
flag = false;
System.out.println("線程修改了變量的值爲false");
}).start();
}
}
synchronized保證可見性的原理
synchronized與有序性
int a = 1;
int b = a;
int a = 1;
int a = 2;
int a = 1;
int b = a;
int a = 2;
int a = 1;
int b = 2;
int c = a + b;
可以這樣:
int a = 1;
int b = 2;
int c = a + b;
也可以重排序這樣:
int b = 2;
int a = 1;
int c = a + b;
使用synchronized保證有序性
package com.itheima.demo02_concurrent_problem;
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;
@JCStressTest
@Outcome(id = {"1"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = {"4"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger2")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering {
private Object obj = new Object();
volatile int num = 0;
volatile boolean ready = false;
// 線程1執行的代碼
@Actor
public void actor1(I_Result r) {
synchronized(obj){
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
}
// 線程2執行的代碼
@Actor
public void actor2(I_Result r) {
synchronized(obj){
num = 2;
ready = true;
}
}
}
synchronized保證有序性的原理
第四章:synchronized的特性
可重入特性
package com.itheima.demo03_synchronized_nature;
/*
目標:演示synchronized可重入
1.自定義一個線程類
2.在線程類的run方法中使用嵌套的同步代碼塊
3.使用兩個線程來執行
*/
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
public static void test01() {
synchronized (MyThread.class) {
String name = Thread.currentThread().getName();
System.out.println(name + "進入了同步代碼塊2");
}
}
}
// 1.自定義一個線程類
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + "進入了同步代碼塊1");
Demo01.test01();
}
}
}
不可中斷特性
package com.itheima.demo03_synchronized_nature;
/*
目標:演示synchronized不可中斷
1.定義一個Runnable
2.在Runnable定義同步代碼塊
3.先開啓一個線程來執行同步代碼塊,保證不退出同步代碼塊
4.後開啓一個線程來執行同步代碼塊(阻塞狀態)
5.停止第二個線程
*/
public class Demo02_Uninterruptible {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 1.定義一個Runnable
Runnable run = () -> {
// 2.在Runnable定義同步代碼塊
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "進入同步代碼塊");
// 保證不退出同步代碼塊
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 3.先開啓一個線程來執行同步代碼塊
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
// 4.後開啓一個線程來執行同步代碼塊(阻塞狀態)
Thread t2 = new Thread(run);
t2.start();
// 5.停止第二個線程
System.out.println("停止線程前");
t2.interrupt();
System.out.println("停止線程後");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
package com.itheima.demo03_synchronized_nature;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
目標:演示Lock不可中斷和可中斷
*/
public class Demo03_Interruptible {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// test01();
test02();
}
// 演示Lock可中斷
public static void test02() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try {
b = lock.tryLock(3, TimeUnit.SECONDS);
if (b) {
System.out.println(name + "獲得鎖,進入鎖執行");
Thread.sleep(88888);
} else {
System.out.println(name + "在指定時間沒有得到鎖做其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b) {
lock.unlock();
System.out.println(name + "釋放鎖");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
// System.out.println("停止t2線程前");
// t2.interrupt();
// System.out.println("停止t2線程後");
//
// Thread.sleep(1000);
// System.out.println(t1.getState());
// System.out.println(t2.getState());
}
// 演示Lock不可中斷
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try {
lock.lock();
System.out.println(name + "獲得鎖,進入鎖執行");
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name + "釋放鎖");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2線程前");
t2.interrupt();
System.out.println("停止t2線程後");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
第五章:synchronized原理
javap 反彙編
通過javap反彙編學習synchronized的原理
我們編寫一個簡單的synchronized代碼,如下:
package com.itheima.demo01_concurent_problem;
public class Demo01 {
private static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj)
{
System.out.println("1");
}
}
public synchronized void test()
{
System.out.println("a");
}
}
javap -p -v D:\workspace\Synchronized\target\classes\com\itheima\demo01_concurent_problem\Demo01.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field obj:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String 1
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 9: 0
line 11: 6
line 12: 14
line 13: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String a
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 17: 0
line 18: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/itheima/demo01_concurent_problem/Demo01;
Each object is associated with a monitor.
A monitor is locked if and only if it has anowner.The thread that executes monitorenter attempts to gain ownership of the monitorassociated with objectref, as follows: • If the entry count of the monitor associated withobjectref is zero, the thread enters the monitor and sets its entry count to one. The threadis then the owner of the monitor. •
If the thread already owns the monitor associated withobjectref, it reenters the monitor, incrementing its entry count. • If anotherthreadalready owns the monitor associated with objectref, the thread blocks until the monitor'sentry count is zero, then tries again to gain ownership.
monitorexit
The thread that executes monitorexit must be the owner of the monitor associated with the
instance referenced by objectref. The thread decrements the entry count of the monitor
associated with objectref. If as a result the value of the entry count is zero, the thread
exits the monitor and is no longer its owner. Other threads that are blocking to enter the
monitor are allowed to attempt to do so
面試題synchroznied出現異常會釋放鎖嗎?
同步方法
面試題:synchronized與Lock的區別
深入JVM源碼
目標
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 線程的重入次數
_object = NULL; // 存儲該monitor的對象
_owner = NULL; // 標識擁有該monitor的線程
_WaitSet = NULL; // 處於wait狀態的線程,會被加入到該列表
_WaitSet _WaitSetLock = 0 ;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 多線程競爭鎖時的單向列表
FreeNext = NULL;
_EntryList = NULL; // 處於等待鎖block狀態的線程,會被加入到該列表
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object");
if (UseBiasedLocking) { //偏向鎖
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
}else //重量級鎖
{
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object");
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
// 通過CAS操作嘗試把monitor的_owner字段設置爲當前線程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
// 線程重入,recursions++
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
// 如果當前線程是第一次進入該monitor,設置_recursions爲1,_owner爲當前線程
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
// 省略一些代碼
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
// 如果獲取鎖失敗,則等待鎖的釋放;
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
Self->set_current_pending_monitor(NULL);
}
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
// Try the lock - TATAS
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
if (TrySpin (Self) > 0) {
assert (_owner == Self , "invariant") ;
assert (_succ != Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
// 省略部分代碼
// 當前線程被封裝成ObjectWaiter對象node,狀態設置成ObjectWaiter::TS_CXQ;
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;
// 通過CAS把node節點push到_cxq列表中
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
// Interference - the CAS failed because _cxq changed. Just retry.
// As an optional optimization we retry the lock.
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
}
// 省略部分代碼
for (;;) {
// 線程在被掛起前做一下掙扎,看能不能獲取到鎖
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;
if ((SyncFlags & 2) && _Responsible == NULL)
{
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
// park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000;
}else{
TEVENT (Inflated enter - park UNTIMED) ;
// 通過park將當前線程掛起,等待被喚醒
Self->_ParkEvent->park() ;
}
if (TryLock(Self) > 0) break ;
// 省略部分代碼
}
// 省略部分代碼
}
當該線程被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖,TryLock方法實現如下:
int ObjectMonitor::TryLock (Thread * Self) {
for (;;) {
void * own = _owner ;
if (own != NULL) return 0 ;
if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
// Either guarantee _recursions == 0 or set _recursions = 0.
assert (_recursions == 0, "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert that OwnerIsThread == 1
return 1 ;
}
// The lock had been free momentarily, but we lost the race to the lock.
// Interference -- the CAS failed.
// We can either return -1 or retry.
// Retry doesn't make as much sense because the lock was just acquired.
if (true) return -1 ;
}
}
monitor是重量級鎖
第六章:JDK6 synchronized優化
CAS
package com.itheima.demo05_cas;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/*
目標:演示原子性問題
1.定義一個共享變量number
2.對number進行1000的++操作
3.使用5個線程來進行
*/
public class Demo01 {
// 1.定義一個共享變量number
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
// 2.對number進行1000的++操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet(); // 變量賦值的原子性
}
};
List<Thread> list = new ArrayList<>();
// 3.使用5個線程來進行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("atomicInteger = " + atomicInteger.get());
}
}
CAS原理
Unsafe實現CAS
樂觀鎖和悲觀鎖
悲觀鎖從悲觀的角度出發:
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這
樣別人想拿這個數據就會阻塞。因此synchronized我們也將其稱之爲悲觀鎖。JDK中的ReentrantLock
也是一種悲觀鎖。性能較差!
樂觀鎖從樂觀的角度出發:
總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,就算改了也沒關係,再重試即可。所
以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去修改這個數據,如何沒有人修改則更
新,如果有人修改則重試。
CAS這種機制我們也可以將其稱之爲樂觀鎖。綜合性能較好!
CAS獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以
實現無鎖併發,適用於競爭不激烈、多核 CPU 的場景下。
1. 因爲沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一。
2. 但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。
小結
CAS的作用? Compare And Swap,CAS可以將比較和交換轉換爲原子操作,這個原子操作直接由處理
器保證。
CAS的原理?CAS需要3個值:內存地址V,舊的預期值A,要修改的新值B,如果內存地址V和舊的預期值
A相等就修改內存地址值爲B
1. 虛擬機將會把對象頭中的標誌位設爲“01”,即偏向模式。
2. 同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,如果CAS操作
成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何
同步操作,偏向鎖的效率高。
當鎖對象第一次被線程獲取的時候,
虛擬機將會把對象頭中的標誌位設爲“01”,即偏向模式。
同時使用CAS操 作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,
如果CAS操作成功,持有偏向鎖的線程以後每 次進入這個鎖相關的同步塊時,
虛擬機都可以不再進行任何同步操作,偏向鎖的效率高
偏向鎖是在只有一個線程執行同步塊時進一步提高性能,適用於一個線程反覆獲得同一鎖的情況。
偏向鎖可以 提高帶有同步但無競爭的程序性能
在多線程交替執行同步塊的情況下,可以避免重量級鎖引起的性能消耗。
synchronized (Demo01.class) { ... System.out.println("aaa"); }
public class Demo01 {
public static void main(String[] args)
{
contactString("aa", "bb", "cc");
}
public static String contactString(String s1, String s2, String s3)
{
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}
public class Demo01 {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append("aa");
}
System.out.println(sb.toString());
}
}
平時寫代碼如何對synchronized優化