Java程序死鎖問題定位與解決方法
1. 死鎖概述
1.1 什么是死鎖
- 一定是發(fā)生在并發(fā)中;
- 互不相讓:當(dāng)兩個(gè)(或更多)線程(或進(jìn)程)相互持有對(duì)方所要的資源,又不主動(dòng)釋放,導(dǎo)致程序陷入無盡的阻塞,這就是死鎖。
1.2 死鎖產(chǎn)生的必要條件
導(dǎo)致死鎖的條件有四個(gè),這四個(gè)條件同時(shí)滿足就會(huì)產(chǎn)生死鎖。
- 互斥條件:某些資源只能由一個(gè)線程獨(dú)占使用,其他線程在資源被占用時(shí)只能等待。
- 請(qǐng)求和保持條件:一個(gè)線程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
- 不可搶占條件:線程已獲得的資源,在未使用完之前,不能強(qiáng)行剝奪。
- 循環(huán)等待條件:若干線程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。
2. 死鎖的案例分析
public class Resource { private String name; private int count; public Resource(String name) { this.name = name; } public void staticResource() { synchronized (this) { System.out.println("static resource"); count++; } } public void saveResource(Resource resource) { synchronized (this) { System.out.println("save resource:" + Thread.currentThread().getName()); resource.staticResource(); } } }
public class DeadLock { public static void main(String[] args) { Resource resource1 = new Resource("resource1"); Resource resource2 = new Resource("resource2"); Thread threadA = new Thread(() -> { for (int i = 0; i < 100; i++) { resource1.saveResource(resource2); } }); Thread threadB = new Thread(() -> { for (int i = 0; i < 100; i++) { resource2.saveResource(resource1); } }); threadA.start(); threadB.start(); } }
打印結(jié)果:
save resource:Thread-0 save resource:Thread-1
死鎖原因分析:
- 線程 A 行為:
threadA
在調(diào)用resource1.saveResource(resource2)
時(shí):- 首先鎖住了
resource1
對(duì)象。 - 然后試圖鎖住
resource2
對(duì)象,進(jìn)入其staticResource
方法。
- 首先鎖住了
- 線程B行為:
threadB
在調(diào)用resource2.saveResource(resource1)
時(shí):- 首先鎖住了
resource2
對(duì)象。 - 然后試圖鎖住
resource1
對(duì)象,進(jìn)入其staticResource
方法。
- 首先鎖住了
- 死鎖發(fā)生的原因:
- 如果
threadA
已經(jīng)鎖住resource1
,并等待鎖住resource2
,而此時(shí)threadB
已經(jīng)鎖住resource2
并等待鎖住resource1
,就會(huì)發(fā)生循環(huán)等待。 - 兩個(gè)線程互相等待對(duì)方釋放鎖,從而陷入死鎖狀態(tài)。
3. 死鎖排查
- 首先,通過
jps
命令,查看 Java 進(jìn)程的 pid。
C:\Users\shawn>jps 22568 24488 Launcher 10060 DeadLock 28076 Jps
- 然后,通過
jstack <pid>
命令查看線程 dump 日志。當(dāng)發(fā)現(xiàn)死鎖時(shí),可以在打印的 dump 日志中找到Found one Java-level deadlock:
信息,根據(jù)信息的內(nèi)容可以分析死鎖出現(xiàn)的原因。
C:\Users\shawn>jstack 23128 2024-11-23 15:38:34 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.321-b07 mixed mode): ============================= Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x0000022b6c713f08 (object 0x000000076bdaa990, a com.atu.deadlock.Resource), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x0000022b6c7169a8 (object 0x000000076bdaa9e8, a com.atu.deadlock.Resource), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at com.atu.deadlock.Resource.staticResource(Resource.java:13) - waiting to lock <0x000000076bdaa990> (a com.atu.deadlock.Resource) at com.atu.deadlock.Resource.saveResource(Resource.java:21) - locked <0x000000076bdaa9e8> (a com.atu.deadlock.Resource) at com.atu.deadlock.DeadLock.lambda$main$1(DeadLock.java:18) at com.atu.deadlock.DeadLock$$Lambda$2/1096979270.run(Unknown Source) at java.lang.Thread.run(Thread.java:750) "Thread-0": at com.atu.deadlock.Resource.staticResource(Resource.java:13) - waiting to lock <0x000000076bdaa9e8> (a com.atu.deadlock.Resource) at com.atu.deadlock.Resource.saveResource(Resource.java:21) - locked <0x000000076bdaa990> (a com.atu.deadlock.Resource) at com.atu.deadlock.DeadLock.lambda$main$0(DeadLock.java:12) at com.atu.deadlock.DeadLock$$Lambda$1/1324119927.run(Unknown Source) at java.lang.Thread.run(Thread.java:750) Found 1 deadlock.
4. 線上發(fā)生死鎖應(yīng)該怎么辦
- 首先保存案發(fā)現(xiàn)場(chǎng),然后立刻重啟服務(wù)器(使用 java 相應(yīng)的命令把整個(gè)堆棧信息保存下來),不能進(jìn)一步影響用戶體驗(yàn);
- 暫時(shí)保證線上服務(wù)的安全,然后再利用剛才保存的信息,排查死鎖,修改代碼,重新發(fā)版。
5. 常見死鎖修復(fù)策略
前面我們說死鎖的四個(gè)必要條件,我們只需要破壞其中任意一個(gè),就可以避免死鎖的產(chǎn)生。其中,互斥條件我們不可以破壞,因?yàn)檫@是互斥鎖的基本約束,其他三個(gè)條件都可以破壞。
- 破壞請(qǐng)求和保持條件:線程在請(qǐng)求開始前,一次性申請(qǐng)所有的資源。
- 破壞不可搶占條件:占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到,可以主動(dòng)釋放它占有的資源。
- 破壞循環(huán)等待條件:靠按序申請(qǐng)資源來預(yù)防。按某一順序申請(qǐng)資源,釋放資源則反序釋放。破壞循環(huán)等待條件。
5.1 破壞請(qǐng)求和保持條件
要破壞占用資源所帶來的等待,可以一次性申請(qǐng)所有資源,保證同時(shí)申請(qǐng)這個(gè)操作是在一個(gè)臨界區(qū)中,然后通過一個(gè)單獨(dú)的角色來管理這個(gè)臨界區(qū)。
- 這個(gè)角色有兩個(gè)很重要的功能,就是同時(shí)申請(qǐng)資源和同時(shí)釋放資源,并且這個(gè)角色一定是一個(gè)單例。
先定義一個(gè) ApplyLock
類,用來實(shí)現(xiàn)統(tǒng)一鎖資源的申請(qǐng),該類中有兩個(gè)方法:
- 一個(gè)是
applyLock()
方法,用來申請(qǐng)鎖; - 另一個(gè)是
free()
方法,用來統(tǒng)一釋放鎖。
public class ApplyLock { private List<Object> list = new ArrayList<>(); public synchronized boolean applyLock(Resource resource1, Resource resource2) { if (list.contains(resource1) || list.contains(resource2)) { return false; } else { list.add(resource1); list.add(resource2); return true; } } public synchronized void free(Resource resource1, Resource resource2) { list.remove(resource1); list.remove(resource2); } }
修改 Resource
類,定義一個(gè)全局唯一的 ApplyLock
實(shí)例,然后在 saveResource
中調(diào)用 applyLock()
方法和 free()
方法進(jìn)行統(tǒng)一鎖資源的獲取和釋放。
public class Resource { private String name; private int count; static ApplyLock applyLock = new ApplyLock(); public Resource(String name) { this.name = name; } public void staticResource() { synchronized (this) { System.out.println("static resource"); count++; } } public void saveResource(Resource resource) { applyLock.applyLock(this, resource); try { System.out.println("save resource:" + Thread.currentThread().getName()); resource.staticResource(); } finally { applyLock.free(this, resource); } } }
由于當(dāng)前涉及的相關(guān)資源都實(shí)現(xiàn)了一個(gè)統(tǒng)一的鎖資源獲取和釋放,從而打破了請(qǐng)求和保持條件。
5.2 破壞不可搶占條件
破壞不可搶占條件的核心是當(dāng)前線程能夠主動(dòng)釋放嘗試占有的資源,這一點(diǎn) synchronized
無法實(shí)現(xiàn)。
- 原因是
synchronized
在申請(qǐng)不到資源時(shí)會(huì)直接進(jìn)入阻塞狀態(tài),一旦線程被阻塞就無法再釋放已經(jīng)占有的資源。 - 在
java.util.concurrent
包中的Lock
鎖可以輕松地解決這個(gè)問題。Lock
接口中有一個(gè)tryLock()
方法可以嘗試搶占資源,如果搶占成功則返回 true,否則返回 false,而且這個(gè)過程不會(huì)阻塞當(dāng)前線程。
import java.util.concurrent.locks.ReentrantLock; public class Resource { private String name; private int count; ReentrantLock lock = new ReentrantLock(); public Resource(String name) { this.name = name; } public void staticResource() { if (lock.tryLock()) { try { System.out.println("static resource"); count++; } finally { lock.unlock(); } } else { System.out.println("搶鎖失敗"); } } public void saveResource(Resource resource) { if (lock.tryLock()) { try { System.out.println("save resource:" + Thread.currentThread().getName()); resource.staticResource(); } finally { lock.unlock(); } } else { System.out.println("搶鎖失敗"); } } }
5.3 破壞循環(huán)等待條件
破壞循環(huán)等待條件的基本思想是:把資源按照某種順序編號(hào),所有鎖資源的申請(qǐng)都按照某種順序來獲取。 比如,可以根據(jù) hashCode
來確定加鎖順序,再根據(jù) hashCode
的大小確定加鎖的對(duì)象,實(shí)現(xiàn)代碼如下。
public class Resource { private String name; private int count; public Resource(String name) { this.name = name; } public void staticResource() { synchronized (this) { System.out.println("static resource"); count++; } } public void saveResource(Resource resource) { Resource lock = this.hashCode() > resource.hashCode() ? this : resource; synchronized (lock) { System.out.println("save resource:" + Thread.currentThread().getName()); resource.staticResource(); } } }
5.4 經(jīng)典的哲學(xué)家就餐問題
如圖所示:
- 有 5 個(gè)哲學(xué)家圍坐在一張圓桌旁。
- 每個(gè)哲學(xué)家都有一個(gè)吃飯和思考的狀態(tài)。
- 圓桌上放著 5 根筷子(與哲學(xué)家數(shù)量相同)。
- 哲學(xué)家必須同時(shí)拿起兩根筷子(左手和右手各一根)才能吃飯,吃完后放下筷子繼續(xù)思考。
問題描述:如果每個(gè)哲學(xué)家都拿起左邊的筷子并等待右邊的筷子,導(dǎo)致所有人相互等待,陷入死鎖。
- 編號(hào)為 0 的哲學(xué)家拿到編號(hào)為 0 的筷子,并等待編號(hào)為 1 的筷子。
- 編號(hào)為 1 的哲學(xué)家拿到編號(hào)為 1 的筷子,并等待編號(hào)為 2 的筷子。
- 編號(hào)為 2 的哲學(xué)家拿到編號(hào)為 2 的筷子,并等待編號(hào)為 3 的筷子。
- 編號(hào)為 3 的哲學(xué)家拿到編號(hào)為 3 的筷子,并等待編號(hào)為 4 的筷子。
- 編號(hào)為 4 的哲學(xué)家拿到編號(hào)為 4 的筷子,并等待編號(hào)為 0 的筷子。
哲學(xué)家就餐問題(死鎖):
public class DiningPhilosophers { public static class Philosopher implements Runnable { private Object leftChopstick; private Object rightChopstick; public Philosopher(Object leftChopstick, Object rightChopstick) { this.leftChopstick = leftChopstick; this.rightChopstick = rightChopstick; } @Override public void run() { while (true) { //思考 try { doAction("Thinking"); //吃飯 //拿起左邊筷子,拿起右邊筷子 放下右邊筷子 放下左邊筷子 synchronized (leftChopstick) { doAction("Picked up left chopstick"); synchronized (rightChopstick) { doAction("Picked up right chopstick -eating"); doAction("Put down right chopstick"); } doAction("Put down left chopstick"); } } catch (InterruptedException e) { e.printStackTrace(); } } } private void doAction(String action) throws InterruptedException { System.out.println(Thread.currentThread().getName() + " " + action); Thread.sleep((long) Math.random() * 10); } } public static void main(String[] args) { Philosopher[] philosophers = new Philosopher[5]; Object[] chopsticks = new Object[philosophers.length]; for (int i = 0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } for (int i = 0; i < philosophers.length; i++) { Object leftChopstick = chopsticks[i]; Object rightChopstick = chopsticks[(i + 1) % chopsticks.length]; philosophers[i] = new Philosopher(leftChopstick, rightChopstick); new Thread(philosophers[i], "哲學(xué)家" + (i + 1) + "號(hào)").start(); } } }
解決的方式有很多,這里我們通過改變一個(gè)哲學(xué)家拿筷子的順序,解決死鎖問題。
哲學(xué)家就餐的換手方案:
public class DiningPhilosophersFix { public static class Philosopher implements Runnable { private Object leftChopstick; private Object rightChopstick; public Philosopher(Object leftChopstick, Object rightChopstick) { this.leftChopstick = leftChopstick; this.rightChopstick = rightChopstick; } @Override public void run() { while (true) { //思考 try { doAction("Thinking"); //吃飯 //拿起左邊筷子,拿起右邊筷子 放下右邊筷子 放下左邊筷子 synchronized (leftChopstick) { doAction("Picked up left chopstick"); synchronized (rightChopstick) { doAction("Picked up right chopstick -eating"); doAction("Put down right chopstick"); } doAction("Put down left chopstick"); } } catch (InterruptedException e) { e.printStackTrace(); } } } private void doAction(String action) throws InterruptedException { System.out.println(Thread.currentThread().getName() + " " + action); Thread.sleep((long) Math.random() * 10); } } public static void main(String[] args) { Philosopher[] philosophers = new Philosopher[5]; Object[] chopsticks = new Object[philosophers.length]; for (int i = 0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } for (int i = 0; i < philosophers.length; i++) { Object leftChopstick = chopsticks[i]; Object rightChopstick = chopsticks[(i + 1) % chopsticks.length]; if (i == philosophers.length - 1) { philosophers[i] = new Philosopher(rightChopstick, leftChopstick); } else { philosophers[i] = new Philosopher(leftChopstick, rightChopstick); } new Thread(philosophers[i], "哲學(xué)家" + (i + 1) + "號(hào)").start(); } } }
6. 實(shí)際工程中如何有效避免死鎖
- 設(shè)置超時(shí)時(shí)間:
Lock
的tryLock(long timeout, TimeUnit unit)
;synchronized
不具備嘗試鎖的能力。
- 使用最小化鎖:減少鎖的數(shù)量和作用范圍,能顯著降低死鎖發(fā)生的概率。
- 避免嵌套鎖:盡量避免線程在持有一個(gè)鎖時(shí)嘗試獲取另一個(gè)鎖。
- 使用高級(jí)并發(fā)工具:
Semaphore
、CountDownLatch
、ReadWriteLock
。
7. 其他活性故障
死鎖是最常見的活躍性問題,除了死鎖之外,還有一些類似的問題,會(huì)導(dǎo)致程序無法順利執(zhí)行,統(tǒng)稱為活躍性問題。
7.1 活鎖
什么是活鎖:線程處于一種“忙碌但無效”的狀態(tài),始終無法完成任務(wù)。(俗稱內(nèi)耗)
特點(diǎn):
- 程序一直在運(yùn)行,但是一直在做沒有意義的工作。
活鎖代碼示例:
public class LiveLock { static class Spoon { private Diner owner; //就餐者 public synchronized void use() { System.out.printf("%s has eaten!", owner.name); } public Spoon(Diner owner) { this.owner = owner; } public Diner getOwner() { return owner; } public void setOwner(Diner owner) { this.owner = owner; } } static class Diner { private String name; private boolean isHungry; public Diner(String name) { this.name = name; isHungry = true; } public void eatWith(Spoon spoon, Diner spouse) { while (isHungry) { //只有餓的情況下才能進(jìn)來 //問題在此處:一直再謙讓 if (spouse.isHungry) { System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧"); spoon.setOwner(spouse); continue; } spoon.use(); isHungry = false; System.out.println(name + ": 我吃好了"); spoon.setOwner(spouse); } } } public static void main(String[] args) { Diner husband = new Diner("牛郎"); Diner wife = new Diner("織女"); Spoon spoon = new Spoon(husband); new Thread(new Runnable() { @Override public void run() { husband.eatWith(spoon, wife); } }).start(); new Thread(new Runnable() { @Override public void run() { wife.eatWith(spoon, husband); } }).start(); } }
打印結(jié)果:
牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 牛郎: 親愛的織女你先吃吧 織女: 親愛的牛郎你先吃吧 ...
解決:以太網(wǎng)的指數(shù)退避算法,加入隨機(jī)因素。
public class LiveLockFix { static class Spoon { private Diner owner; //就餐者 public synchronized void use() { System.out.printf("%s has eaten!", owner.name); } public Spoon(Diner owner) { this.owner = owner; } public Diner getOwner() { return owner; } public void setOwner(Diner owner) { this.owner = owner; } } static class Diner { private String name; private boolean isHungry; public Diner(String name) { this.name = name; isHungry = true; } public void eatWith(Spoon spoon, Diner spouse) { while (isHungry) { //只有餓的情況下才能進(jìn)來 Random random = new Random(); //問題在此處:一直再謙讓 if (spouse.isHungry && random.nextInt(10) < 9) { System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧"); spoon.setOwner(spouse); continue; } spoon.use(); isHungry = false; System.out.println(name + ": 我吃好了"); spoon.setOwner(spouse); } } } public static void main(String[] args) { Diner husband = new Diner("牛郎"); Diner wife = new Diner("織女"); Spoon spoon = new Spoon(husband); new Thread(new Runnable() { @Override public void run() { husband.eatWith(spoon, wife); } }).start(); new Thread(new Runnable() { @Override public void run() { wife.eatWith(spoon, husband); } }).start(); } }
活鎖的解決方法:
- 增加隨機(jī)性:通過引入隨機(jī)的等待時(shí)間(如使用隨機(jī)退避算法),避免線程/進(jìn)程按照相同的模式重復(fù)操作。
- 設(shè)置重試次數(shù)或超時(shí):為線程的嘗試次數(shù)或時(shí)間限制設(shè)置一個(gè)閾值。如果超過限制,則采用其他策略,如強(qiáng)制退出或降級(jí)處理。
7.2 饑餓
線程饑餓問題其實(shí)指的公平性問題。是指某個(gè)線程因無法獲取所需資源而無法執(zhí)行,一直處于等待狀態(tài)的情況。
饑餓代碼示例:
public class StarvationExample { private static final Object lock = new Object(); public static void main(String[] args) { Thread highPriorityThread = new Thread(() -> { synchronized (lock) { while (true) { System.out.println("High priority thread is running..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread lowPriorityThread = new Thread(() -> { synchronized (lock) { while (true) { System.out.println("Low priority thread is running..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); highPriorityThread.setPriority(Thread.MAX_PRIORITY); lowPriorityThread.setPriority(Thread.MIN_PRIORITY); highPriorityThread.start(); lowPriorityThread.start(); } }
問題解析:
- 上述代碼中,高優(yōu)先級(jí)線程(highPriorityThread)由于持有 lock 鎖資源,它可能會(huì)導(dǎo)致低優(yōu)先級(jí)線程(lowPriorityThread)一直無法執(zhí)行,從而出現(xiàn)線程饑餓的現(xiàn)象。
線程饑餓原因:
- 資源分配不均: 如果一個(gè)線程的優(yōu)先級(jí)一直較低,而系統(tǒng)的調(diào)度策略總是優(yōu)先執(zhí)行高優(yōu)先級(jí)的線程,那么低優(yōu)先級(jí)線程就可能一直得不到執(zhí)行的機(jī)會(huì),從而發(fā)生饑餓。
- 線程被無限阻塞:當(dāng)獲得鎖的線程需要執(zhí)行無限時(shí)間長(zhǎng)的操作時(shí)(比如 IO 或者無限循環(huán)),那么后面的線程將會(huì)被無限阻塞,導(dǎo)致被餓死。
饑餓的解決方法:
- 設(shè)置合適的線程優(yōu)先級(jí)
- 使用公平性調(diào)度算法
8. 總結(jié)
- 死鎖
- 特點(diǎn):兩個(gè)或多個(gè)線程(進(jìn)程)相互等待對(duì)方釋放資源,導(dǎo)致所有線程都無法繼續(xù)執(zhí)行。
- 解決方法:避免一個(gè)線程持有多個(gè)資源的情況,或使用超時(shí)機(jī)制,如果一個(gè)線程在一定時(shí)間內(nèi)沒能獲得鎖,就放棄等待。
- 活鎖
- 特點(diǎn):線程仍然在運(yùn)行,但由于不斷地響應(yīng)對(duì)方,始終沒有實(shí)際進(jìn)展。
- 解決方法:為避免活鎖,可以設(shè)置超時(shí)機(jī)制,或者使用協(xié)調(diào)機(jī)制來避免線程之間過度的反應(yīng)。
- 饑餓
- 特點(diǎn):線程無法獲得執(zhí)行機(jī)會(huì),但其他線程仍然在運(yùn)行,造成某些線程得不到資源。
- 解決方法:使用公平鎖或合理的優(yōu)先級(jí)策略,確保每個(gè)線程都有機(jī)會(huì)執(zhí)行,不會(huì)被長(zhǎng)時(shí)間忽略。
以上就是Java程序死鎖問題定位與解決方法的詳細(xì)內(nèi)容,更多關(guān)于Java程序死鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
程序包org.springframework.boot不存在的問題解決
本文主要介紹了程序包org.springframework.boot不存在的問題解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-09-09Java使用easyExcel導(dǎo)出數(shù)據(jù)及單元格多張圖片
除了平時(shí)簡(jiǎn)單的數(shù)據(jù)導(dǎo)出需求外,我們也經(jīng)常會(huì)遇到一些有固定格式或者模板要求的數(shù)據(jù)導(dǎo)出,下面這篇文章主要給大家介紹了關(guān)于Java使用easyExcel導(dǎo)出數(shù)據(jù)及單元格多張圖片的相關(guān)資料,需要的朋友可以參考下2023-05-05Spring Boot從Controller層進(jìn)行單元測(cè)試的實(shí)現(xiàn)
這篇文章主要介紹了Spring Boot從Controller層進(jìn)行單元測(cè)試的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04Java靜態(tài)代理和動(dòng)態(tài)代理總結(jié)
這篇文章主要介紹了Java靜態(tài)代理和動(dòng)態(tài)代理總結(jié),非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02Spring?Boot實(shí)現(xiàn)MyBatis動(dòng)態(tài)創(chuàng)建表的操作語句
這篇文章主要介紹了Spring?Boot實(shí)現(xiàn)MyBatis動(dòng)態(tài)創(chuàng)建表,MyBatis提供了動(dòng)態(tài)SQL,我們可以通過動(dòng)態(tài)SQL,傳入表名等信息然組裝成建表和操作語句,本文通過案例講解展示我們的設(shè)計(jì)思路,需要的朋友可以參考下2024-01-01基于BigDecimal.setScale的用法小結(jié)
這篇文章主要介紹了基于BigDecimal.setScale的用法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09Java實(shí)現(xiàn)文件上傳與文件下載的示例代碼
在開發(fā)中項(xiàng)目難免會(huì)遇到文件上傳和下載的情況,這篇文章主要為大家詳細(xì)介紹了Java中實(shí)現(xiàn)文件上傳與文件下載的示例代碼,希望對(duì)大家有所幫助2023-07-07java 運(yùn)行報(bào)錯(cuò)has been compiled by a more recent version of the J
java 運(yùn)行報(bào)錯(cuò)has been compiled by a more recent version of the Java Runtime (class file version 54.0)2021-04-04Java利用HttpClient模擬POST表單操作應(yīng)用及注意事項(xiàng)
本文主要介紹JAVA中利用HttpClient模擬POST表單操作,希望對(duì)大家有所幫助。2016-04-04