單例模式是最基礎也最常被誤用的設計模式。本文透過遊戲伺服器負載平衡的真實場景,展示如何正確實作線程安全的單例,並探討其適用邊界。
確保一個類別只有一個實例存在,並提供一個全域存取點獲取該實例。
開發網路遊戲伺服器端時,需承載大量玩家連線。類似 Web 負載平衡器,需要一個統一入口將玩家分發到不同的大廳伺服器。即使在高併發多線程情況下,這個入口必須是唯一的,否則會導致分發邏輯混亂、玩家重複分配等嚴重問題。
using System;
using System.Collections.Generic;
using System.Threading;
namespace SingletonExample
{
class Program
{
static void Main(string[] args)
{
ParameterizedThreadStart ts = new ParameterizedThreadStart(EnterPlayer);
for (int i = 0; i < 20; i++)
{
Thread t = new Thread(ts);
t.Start("player" + i);
}
LoadBalanceServer.GetLoadBalanceServer().ShowServerInfo();
}
static void EnterPlayer(object playerName)
{
LoadBalanceServer lbs = LoadBalanceServer.GetLoadBalanceServer();
lbs.GetLobbyServer().EnterPlayer(playerName.ToString());
}
}
class LoadBalanceServer
{
private const int SERVER_COUNT = 3;
private List<LobbyServer> serverList = new List<LobbyServer>();
private static volatile LoadBalanceServer lbs;
private static readonly object syncLock = new object();
private LoadBalanceServer()
{
for (int i = 0; i < SERVER_COUNT; i++)
serverList.Add(new LobbyServer("LobbyServer" + i));
}
public static LoadBalanceServer GetLoadBalanceServer()
{
if (lbs == null)
{
lock (syncLock)
{
if (lbs == null)
{
Thread.Sleep(100); // 模擬高併發延遲,正式碼移除
lbs = new LoadBalanceServer();
}
}
}
return lbs;
}
public LobbyServer GetLobbyServer()
{
LobbyServer ls = serverList[0];
for (int i = 1; i < SERVER_COUNT; i++)
if (serverList[i].PlayerList.Count < ls.PlayerList.Count)
ls = serverList[i];
return ls;
}
public void ShowServerInfo()
{
foreach (LobbyServer ls in serverList)
{
Console.WriteLine("=================" + ls.ServerName + "=================");
foreach (string player in ls.PlayerList)
Console.WriteLine(player);
}
}
}
class LobbyServer
{
private List<string> playerList = new List<string>();
public List<string> PlayerList => playerList;
public string ServerName { get; }
public LobbyServer(string serverName) => this.ServerName = serverName;
public void EnterPlayer(string playerName) => playerList.Add(playerName);
}
}

GetLoadBalanceServer() 為唯一全域存取點。public sealed class LoadBalanceServer
{
private static readonly LoadBalanceServer instance = new LoadBalanceServer();
static LoadBalanceServer() { } // 確保 lazy initialization
private LoadBalanceServer() { /* 初始化邏輯 */ }
public static LoadBalanceServer Instance => instance;
// ... 其他方法
}
利用 CLR 保證的靜態建構函式線程安全與延遲初始化特性,程式碼更簡潔、效能更好,推薦作為首選。
單例模式核心在於「控制實例化過程」與「提供全域存取」。現代 C# 開發中,靜態初始化或 Lazy<T> 是更優雅的實作方式。但更重要的,是審視設計:這個類別真的只能有一個實例嗎?若答案不確定,請避免使用單例,改用依賴注入讓容器管理生命週期。