CodingGoldSource | Eloise's Paradise
0%

CodingGoldSource

这个文档用来搜集归纳日常见到的技术细节问题包括:Util code blocks, problem analysis,技术博客/podcasts 书签 etc ,后期可能会因为篇幅受限而将某些部份提炼为新的文章并通过hyperlink跳转。

公共代码块

AI

Java

Linux

SQL

Nifi

HTML

JS

问题分析

设计模式

多线程

UseCase01

在坦克大战游戏中,当碰撞检测到子弹击中敌方坦克时,会加载爆炸声音和动图,但是在原方案中声音的加载是阻塞的,也就是说声音加载完之前,爆炸动图的加载和frame的重画(repaint)都会被block,那么就会给用户一种丢帧甚至卡顿的感觉。

原方案代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ExplodeOld {
public static final int WIDTH = ResourceMgr.explosions[0].getWidth(), HEIGHT = ResourceMgr.explosions[0].getHeight();
private int x , y;
private boolean living = true;
private TankFrame tf = null;
/**
* 用来记录每次加载哪张图片
*/
private int step = 0 ;

public ExplodeOld(int x, int y, TankFrame tf) {
this.x = x;
this.y = y;
this.tf = tf;
new Audio("audio/explode.wav").play();
}

public void paint(Graphics g){
g.drawImage(ResourceMgr.explosions[step++], x, y, null);
if(step >= ResourceMgr.explosions.length) {
/**
* 爆炸结束后要从list移除
*/
tf.explodes.remove(this);
}
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

public boolean isLiving() {
return living;
}

public void setLiving(boolean living) {
this.living = living;
}

public TankFrame getTf() {
return tf;
}

public void setTf(TankFrame tf) {
this.tf = tf;
}

public int getStep() {
return step;
}

public void setStep(int step) {
this.step = step;
}
}

The Problem: Single-Threaded Blocking
🎭 Analogy: A Restaurant Kitchen
Imagine your game is a restaurant kitchen with only ONE chef (the Event Dispatch Thread - EDT):
Before the fix (blocking):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Chef (EDT) Timeline:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time: 0s 1s 2s 3s 4s 5s
│ │ │ │ │ │

Collision happens! 💥

├─ 🔊 Play explosion sound (2 seconds)
│ ┌─────────────────────┐
│ │ Playing audio... │ ← Chef is BUSY
│ │ Can't do anything │ making food!
│ │ else! │
│ └─────────────────────┘
│ │
┌───────────────┐
│ 🎨 Paint │ ← Customer waits!
│ explosion │ Food arrives late!
└───────────────┘

Root Cause Analysis:

  • Audio plays in constructor on EDT: The new Audio("audio/explode.wav").play() call in the Explode constructor runs on the Event Dispatch Thread (EDT), blocking the paint operations. 单线程阻塞
  • GIF animation not properly handled: You’re loading GIF frames as static BufferedImages, but GIFs need special handling to display their animation frames properly.
  • Step increment without bounds checking: In Explode.paint(), the step++ increments every time paint is called, but paint might be called multiple times per frame cycle.
  • No preloading of explosion images: Although images are loaded in ResourceMgr’s static block, GIF frames need to be extracted properly.

其他的原因我们暂不考虑, 只聚焦线程阻塞问题。

Before (blocking version)

1
2
3
4
5
6
7
// In Explode constructor - called from paint() method
public Explode(int x, int y, TankFrame tf) {
this.x = x;
this.y = y;
this.tf = tf;
new Audio("audio/explode.wav").play(); // ⚠️ BLOCKS HERE!
}

Call Stack Visualization:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Main Game Loop (T.java line 19-22)
↓ calls repaint() every 50ms

TankFrame.paint() (line 92)
↓ draws everything

Bullet.collidesWith() (line 97)
↓ detects collision at line 106

new Explode() at line 111

Explode constructor (line 15)

Audio.play() at line 19
╔═══════════════════════════╗
║ Reading audio file ║ ← STUCK HERE!
║ Writing to speaker ║ Takes ~1-2 seconds
║ WHILE LOOP running ║ ENTIRE GAME FROZEN!
(blocks 1000+ ms)
╚═══════════════════════════╝
(can't continue until audio finishes!)
paint() resumes

next frame renders (2 seconds later!) 😱

Timelines:

1
2
3
4
5
6
7
8
9
Frame 1 (t=0ms): Collision detected → Create Explode → Play audio starts
████████████████████████████████ (audio blocks for 1500ms)

Frame 2 (t=50ms): SKIPPED! EDT still playing audio ❌
Frame 3 (t=100ms): SKIPPED! EDT still playing audio ❌
Frame 4 (t=150ms): SKIPPED! EDT still playing audio ❌
...
Frame 30 (t=1500ms): Audio finishes → Finally can paint! ✅
BUT player saw nothing for 1.5 seconds!

Enhanced Codes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Explode {
public static final int WIDTH = ResourceMgr.explosions[0].getWidth(), HEIGHT = ResourceMgr.explosions[0].getHeight();
private int x , y;
private boolean living = true;
private TankFrame tf = null;
private int step = 0 ;
private long lastUpdate = 0;
private static final int FRAME_DELAY = 50;

public Explode(int x, int y, TankFrame tf) {
this.x = x;
this.y = y;
this.tf = tf;
new Thread(() ->{
System.out.println("currentThreadName = " + Thread.currentThread().getName());
new Audio("audio/explode.wav").play();
} ).start();
}

public void paint(Graphics g){
if(step >= ResourceMgr.explosions.length) {
tf.explodes.remove(this);
return;
}

long currentTime = System.currentTimeMillis();
if (currentTime - lastUpdate >= FRAME_DELAY) {
g.drawImage(ResourceMgr.explosions[step], x, y, null);
lastUpdate = currentTime;
step++;
} else {
if (step > 0) {
g.drawImage(ResourceMgr.explosions[step - 1], x, y, null);
}
}
}
}

After (Non-Blocking version):

1
2
3
4
5
6
7
// In Explode constructor
public Explode(int x, int y, TankFrame tf) {
this.x = x;
this.y = y;
this.tf = tf;
new Thread(() -> new Audio("audio/explode.wav").play()).start(); // ✨ NEW THREAD!
}

Call Stack Visualization:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Main Game Loop (T.java line 19-22)
↓ calls repaint() every 50ms

TankFrame.paint() (line 92)
↓ draws everything

Bullet.collidesWith() (line 97)
↓ detects collision at line 106

new Explode() at line 111

Explode constructor (line 15)

new Thread().start()
╔═══════════════════════════╗ ↗ New thread created!
║ Thread runs separately ║ ↗ Doesn't block EDT!
║ Audio plays in parallel ║ ↗
╚═══════════════════════════╝ ↗
(returns IMMEDIATELY!)
paint() continues immediately ✅

next frame renders in 50ms! 🎉

Timeline:

1
2
3
4
5
6
7
8
9
10
11
Main Thread (EDT):
Frame 1 (t=0ms): Collision → Create Explode → Start audio thread → Paint explosion ✅
Frame 2 (t=50ms): Paint next animation frame ✅
Frame 3 (t=100ms): Paint next animation frame ✅
Frame 4 (t=150ms): Paint next animation frame ✅

Audio Thread (separate):
t=0ms: Start playing audio 🔊
t=50ms: Still playing... 🔊
t=100ms: Still playing... 🔊
t=1500ms: Audio finishes 🔊

🎬 Animation: Thread Execution Flow

Single Thread (Blocking) - Like a Single-Lane Road:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
🚗 Cars = Tasks (paint, audio, input, etc.)
🛣️ Road = CPU/Thread

BEFORE FIX:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time →

Lane 1 (EDT):
🚗🚕🚙🚌 [🔊AUDIO TRUCK 🚚] 🚗🚕🚙
^^^^^^^^^^^^^^^^^^
This HUGE truck blocks
EVERYTHING behind it!

Result: Traffic jam! 🚦
- Paint tasks wait 2 seconds
- Input tasks wait 2 seconds
- Game appears frozen!

Multi-Thread (Non-Blocking) - Like a Multi-Lane Highway:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AFTER FIX:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time →

Lane 1 (EDT):
🚗🚕🚙🚌 🚗🚕🚙🚌 🚗🚕🚙🚌
Smooth traffic flow! ✅

Lane 2 (Audio Thread):
[🔊AUDIO TRUCK 🚚]
Runs in parallel!
Doesn't block lane 1!

Result: No traffic jam! 🎉
- Paint continues smoothly
- Audio plays simultaneously
- Game stays responsive!

🔍 Why Does Audio.play() Block?

Let’s look at what happens inside Audio.play():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void play() {
byte[] b = new byte[1024*5];
int len = 0;
sourceDataLine.open(audioFormat, 1024*5);
sourceDataLine.start();
audioInputStream.mark(12358946);

// ⚠️ THIS WHILE LOOP TAKES 1-2 SECONDS!
while ((len = audioInputStream.read(b)) > 0) {
sourceDataLine.write(b, 0, len); // Writes audio chunks
}

sourceDataLine.drain(); // Waits for all audio to finish
sourceDataLine.close();
}

What’s happening:

1
2
3
4
5
6
7
8
9
10
11
12
13
while loop iteration by iteration:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Iteration 1: Read 5KB → Write to speaker (takes ~1ms)
Iteration 2: Read 5KB → Write to speaker (takes ~1ms)
Iteration 3: Read 5KB → Write to speaker (takes ~1ms)
...
Iteration 300: Read 5KB → Write to speaker (takes ~1ms)

Total time: 300 iterations × ~5ms = ~1500ms (1.5 seconds!)

During ALL this time, the method doesn't return!
The calling thread is STUCK in this loop!

🧠 Key Concept: Thread Independence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Think of threads like smartphone apps:

📱 Your Phone = Computer CPU
📲 Apps = Threads

SCENARIO 1: Single-threaded phone (old phones)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Open music app → Play song
- Try to open camera → WAIT! Music must finish first! ❌
- Try to check messages → WAIT! Music must finish first! ❌

SCENARIO 2: Multi-threaded phone (modern phones)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Open music app → Play song in background ✅
- Open camera → Works fine! (separate thread)
- Check messages → Works fine! (separate thread)

Your game is the same!
- EDT = Main UI thread (handles painting, input)
- Audio thread = Background worker (handles sound)

📈 Performance Comparison

🎯 Summary
The Root Cause:

  1. Java’s paint() method runs on the Event Dispatch Thread (EDT)

  2. The EDT is single-threaded - it does ONE thing at a time

  3. Audio.play() has a long-running loop (1-2 seconds)

  4. When you call play() directly in the constructor, the EDT stops painting to play audio

  5. Result: You hear sound but see nothing until audio finishes!

The Solution:

  1. Create a new thread specifically for audio
  2. The new thread runs independently from the EDT
  3. EDT can continue painting while audio plays in parallel
  4. Result: Sound and visuals happen simultaneously! 🎉

The Magic:

-------------本文结束感谢您的阅读-------------