C# Static Variable: Lifetime and Thread Safety
c# static variable: Learn how static variables work in C#: declaration, initialization, lifetime, thread safety, and common pitfalls for shared state.
c# static variable requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
A static variable in C# is a field declared with the static keyword. It belongs to the type itself, not to any particular instance. That means all instances of the type share the same storage location, and the variable exists even if no instance of the type has been created. This is a fundamental distinction from instance fields, which get a separate copy for each object.
Declaring a Static Variable
To declare a static variable, you add the static modifier to a field declaration inside a class or struct:
public class Counter { public static int TotalCount; }
Here TotalCount is a static field of type int. You can access it without creating an instance of Counter:
Counter.TotalCount = 5; int current = Counter.TotalCount;
Static fields can be read-only, const, or have any accessibility. A const field is implicitly static, but you cannot use the static keyword with const in C#. For a value that should never change, const is often a better choice than a static readonly field, but they have different semantics: const values are compiled into the referencing code, while static readonly values are evaluated at runtime.
Static Field Initialization and Lifetime
Static fields are initialized when the type is first accessed, which happens before any static member is referenced or any instance is created. The exact timing is controlled by the runtime, but you can rely on the fact that the static field will have its default value (e.g., 0 for int, null for reference types) until a static constructor or field initializer runs.
You can provide an initial value directly:
public class Settings { public static string AppName = "MyApp"; public static readonly int MaxRetries = 3; }
Static field initializers run in the order they appear in the class, and they run before any static constructor. If you need more complex initialization, you can use a static constructor:
public class Database { public static string ConnectionString; static Database() { ConnectionString = LoadConnectionString(); } private static string LoadConnectionString() { // read from config, environment, etc. return "Server=..."; } }
The static constructor runs once per type, and it is thread-safe by default. The lifetime of a static variable is the lifetime of the application domain (or the process in .NET Core / .NET 5+). The variable is never garbage collected as long as the type is loaded.
Static vs Instance Variables
The core difference is scope and lifetime. An instance variable is tied to an object and exists only while that object is alive. A static variable exists for the entire duration of the type's load. This has memory and design implications.
| Aspect | Static Variable | Instance Variable |
|---|---|---|
| Storage | One per type | One per instance |
| Access | Via type name | Via instance reference |
| Lifetime | AppDomain/process | Object lifetime |
| Shared | Across all instances | Isolated per instance |
Use a static variable when you need shared state that is not specific to any one object. For example, a global counter, a cache, or a configuration value that is identical for all instances. Use an instance variable when each object needs its own state.
Common Uses for Static Variables
Static variables are commonly used for:
- Caching – store the result of an expensive operation once and reuse it.
- Configuration – hold application-wide settings that are loaded once.
- Counters – track the number of instances created or operations performed.
- Utility state – shared resources like a
HttpClientinstance in a static field to avoid socket exhaustion.
For example, a simple cache:
public class DataRepository { private static readonly Dictionary<string, string> Cache = new Dictionary<string, string>(); public static string GetData(string key) { if (Cache.TryGetValue(key, out var value)) { return value; } value = LoadFromDatabase(key); Cache[key] = value; return value; } }
This pattern is effective when the data is immutable or rarely changes, but it introduces a shared mutable state that must be handled carefully.
Thread Safety with Static Variables
Because static variables are shared across all threads, any mutable static variable is a potential concurrency hazard. If multiple threads read and write the same static field without synchronization, you can get race conditions, corrupted state, or unexpected behavior.
Consider a simple counter:
public static int Count = 0; // In multiple threads: Count++;
The ++ operation is not atomic. Two threads can read the same value, increment, and write back, losing an increment. To fix this, use Interlocked.Increment:
Interlocked.Increment(ref Count);
Or use a lock when you need to protect a larger critical section:
private static readonly object LockObject = new object(); public static void Update() { lock (LockObject) { // read and modify static state } }
If the static variable is a collection like a Dictionary, you must synchronize all access or use a concurrent collection like ConcurrentDictionary. The same rules apply to static properties and static methods that modify static state.
For read-only static data, such as a static readonly field that is initialized once, thread safety is not a concern because the value is immutable after initialization. However, if the field is a reference type that is mutable (like a List<T>), even a readonly reference does not make the object itself immutable.
Static Classes and Static Members
A class can be declared static to indicate that it contains only static members and cannot be instantiated. This is common for utility classes:
public static class MathHelper { public static double Square(double x) => x * x; }
Static classes are implicitly sealed and cannot have instance constructors. They are useful for grouping related functions that do not depend on object state. However, they are not a substitute for proper dependency injection if you need testability. Static methods and static state can make unit testing harder because you cannot easily replace them with mocks.
Pitfalls and Maintainability Concerns
Static variables are easy to misuse. Here are some practical concerns:
- Global mutable state – Static fields are global state, which can make code harder to reason about and test. Prefer immutable static data or dependency injection where possible.
- Initialization order – Static field initializers run in textual order. If one static field depends on another, ensure the dependency appears earlier. Static constructors can help but add complexity.
- Memory leaks – A static variable that references a large object will keep that object alive for the entire application lifetime. Be careful with static collections that grow unboundedly; they can cause memory pressure.
- Thread safety overhead – Synchronizing access to static variables can introduce contention. If the variable is read-mostly, consider using
volatileor a lock-free pattern, but only after profiling. - Testability – Static state persists across tests, which can cause test pollution. Reset static fields in test setup if needed, or avoid them altogether.
For example, a static HttpClient is recommended to avoid socket exhaustion, but you must ensure it is configured correctly and not disposed. A static Random instance is not thread-safe; use ThreadLocal<Random> or a shared Random with locking.
When you need a singleton-like behavior, consider whether a static variable is the right approach. In many cases, a dependency injection container with a singleton lifetime is cleaner and more testable than a static field.
The key to using static variables well is to limit their scope, prefer immutability, and document the thread-safety guarantees.