杰瑞科技汇

synchronized如何保证Java线程安全?

Of course! Let's dive deep into synchronized in Java. It's one of the most fundamental and important concepts for writing multi-threaded applications.

synchronized如何保证Java线程安全?-图1
(图片来源网络,侵删)

What is synchronized?

In simple terms, synchronized is a keyword in Java that is used to provide mutual exclusion and thread safety. It ensures that only one thread can execute a particular block of code or a method on a given object at any given time.

Think of it like a single restroom key in an office. Only the person holding the key (the thread) can use the restroom (the critical code). If the restroom is occupied, others must wait until it's free.


The Core Problem: Race Conditions

To understand why synchronized is needed, let's look at the problem it solves.

Imagine a simple counter:

synchronized如何保证Java线程安全?-图2
(图片来源网络,侵删)
class Counter {
    private int count = 0;
    public void increment() {
        count++; // This is NOT an atomic operation
    }
    public int getCount() {
        return count;
    }
}

In a multi-threaded environment, if two threads call increment() at the same time, what happens?

The count++ statement looks like a single action, but it's actually three separate steps:

  1. Read: Get the current value of count from memory.
  2. Modify: Add 1 to the value.
  3. Write: Store the new value back to memory.

Here's how a race condition can occur:

Thread A Thread B What happens to count?
Reads count (value is 10)
Reads count (value is 10) Both have the old value.
Adds 1 (value is now 11)
Adds 1 (value is now 11) Both have the new value.
Writes 11 to count
Writes 11 to count The final value is 11.

Expected Result: 12 Actual Result: 11

synchronized如何保证Java线程安全?-图3
(图片来源网络,侵删)

This is a race condition. The final result depends on the unpredictable timing of the threads. synchronized fixes this by ensuring that the three steps of count++ are executed as an atomic (indivisible) unit.


How to Use synchronized

There are three primary ways to use the synchronized keyword.

Synchronized Instance Methods

When you declare a method as synchronized, you are locking on the instance of the object (this).

public class MyObject {
    public synchronized void instanceMethod() {
        // Code here is thread-safe
        // Only one thread can execute this method per instance of MyObject at a time.
    }
}

How it works:

  • Thread A calls myObject.instanceMethod(). It acquires the lock on myObject.
  • While Thread A is inside the method, Thread B calls myObject.instanceMethod(). It blocks (waits) because the lock on myObject is already held by Thread A.
  • When Thread A finishes the method, it releases the lock. Now, Thread B can acquire the lock and enter the method.

Synchronized Static Methods

When you declare a static method as synchronized, you are locking on the class object (MyClass.class).

public class MyObject {
    public static synchronized void staticMethod() {
        // Code here is thread-safe
        // Only one thread can execute this method for the entire MyObject class at a time.
    }
}

How it works:

  • There is only one Class object for a class in the JVM (e.g., MyObject.class).
  • If Thread A calls MyObject.staticMethod() on any instance, it acquires the lock on MyObject.class.
  • Thread B calling MyObject.staticMethod() on any other instance will also block because it's trying to acquire the same class-level lock.

Synchronized Blocks (or Statements)

This is the most flexible way to use synchronization. You can synchronize on a specific object, which is useful when you don't want to lock an entire method.

public class MyObject {
    private final Object lock = new Object(); // A dedicated lock object
    public void doSomething() {
        // Some non-synchronized code
        System.out.println("Before synchronized block");
        synchronized (this) { // Lock on the current instance
            // Critical section - only one thread can execute this per instance
            System.out.println("Inside synchronized block on 'this'");
        }
        synchronized (lock) { // Lock on our dedicated lock object
            // Critical section - only one thread can execute this per instance
            // This is often preferred over 'this' to avoid external interference
            System.out.println("Inside synchronized block on 'lock'");
        }
        // More non-synchronized code
        System.out.println("After synchronized blocks");
    }
}

Why use a dedicated lock object?

  • Encapsulation: You expose less of your object's internal locking mechanism. If you synchronize on this, any other code that can access your object can also try to lock on it, leading to potential deadlocks.
  • Granularity: You can have multiple, independent locks within one object.

The Lock Mechanism (Intrinsic Lock)

When a thread encounters a synchronized block or method, it tries to acquire an intrinsic lock (also known as a monitor lock) associated with the specified object (this, the class object, or a custom object).

  • Rules:
    1. Only one thread can own the intrinsic lock for an object at a time.
    2. If a thread tries to enter a synchronized block and the lock is already held, it blocks until the lock is released.
    3. When a thread exits a synchronized block (normally or via an exception), it automatically releases the lock.

Important Considerations and Best Practices

Deadlocks

A deadlock is a situation where two or more threads are blocked forever, waiting for each other to release a lock.

Classic Example:

  • Thread 1 locks Lock A and then tries to lock Lock B.
  • Thread 2 locks Lock B and then tries to lock Lock A.
  • Both threads will wait forever.
// Deadlock Example
final Object lock1 = new Object();
final Object lock2 = new Object();
new Thread(() -> {
    synchronized (lock1) {
        System.out.println("Thread 1: Holding lock 1...");
        try { Thread.sleep(100); } catch (InterruptedException e) {} // Simulate work
        System.out.println("Thread 1: Waiting for lock 2...");
        synchronized (lock2) { // Deadlock!
            System.out.println("Thread 1: Acquired lock 2!");
        }
    }
}).start();
new Thread(() -> {
    synchronized (lock2) {
        System.out.println("Thread 2: Holding lock 2...");
        try { Thread.sleep(100); } catch (InterruptedException e) {} // Simulate work
        System.out.println("Thread 2: Waiting for lock 1...");
        synchronized (lock1) { // Deadlock!
            System.out.println("Thread 2: Acquired lock 1!");
        }
    }
}).start();

How to avoid deadlocks:

  • Lock Ordering: Always acquire locks in a consistent, global order.
  • Lock Timeout: Use java.util.concurrent.locks.Lock implementations which support tryLock() with a timeout.
  • Avoid Nested Locks: Keep synchronized blocks short and avoid calling unknown code from within them.

Performance Overhead

Synchronization has a performance cost due to the overhead of acquiring and releasing locks, and context switching when threads are blocked. Don't use it unnecessarily.

Liveness vs. Safety

  • Safety: Ensuring that nothing bad ever happens (e.g., no race conditions, data corruption). synchronized provides safety.
  • Liveness: Ensuring that something good eventually happens (e.g., the program doesn't deadlock or starve). synchronized can harm liveness if not used carefully (e.g., causing deadlocks).

synchronized vs. java.util.concurrent.locks.Lock

Feature synchronized java.util.concurrent.locks.Lock
How it's acquired Automatically when entering a block/method. Explicitly via lock.lock().
How it's released Automatically when exiting a block/method (even via exception). Explicitly via lock.unlock(). Must be in a finally block!
Interruptible No. A blocked thread cannot be interrupted. Yes. lock.lockInterruptibly() allows a thread to be interrupted while waiting.
Fairness Unfair (non-reentrant, but can be reentrant). Can lead to thread starvation. Can be fair (by passing true to constructor) or unfair. Fairness reduces starvation but is slower.
Condition Variables No. Yes. A Lock can have one or more Condition objects for more sophisticated thread signaling.

Modern Recommendation:

  • For most simple cases, synchronized is perfectly fine and often preferred for its simplicity.
  • For advanced scenarios like try-lock, timed locks, interruptible locks, or fairness, use the java.util.concurrent.locks package.

Modern Alternatives

Since Java 5, the java.util.concurrent package provides higher-level abstractions that are often better and safer than using synchronized directly.

  • java.util.concurrent.atomic classes: For simple atomic operations (like our count++ example). These classes use highly optimized, low-level hardware instructions (like compare-and-swap) and avoid the overhead of traditional locks.

    import java.util.concurrent.atomic.AtomicInteger;
    class Counter {
        private final AtomicInteger count = new AtomicInteger(0);
        public void increment() {
            count.getAndIncrement(); // This is atomic and lock-free
        }
    }
  • java.util.concurrent collections: Classes like ConcurrentHashMap, CopyOnWriteArrayList, etc., are designed for concurrent access and manage their own synchronization internally, often more efficiently than a simple synchronized wrapper.

Summary

Concept Description
Purpose To ensure thread safety by preventing race conditions.
Mechanism Uses intrinsic locks (monitors) on objects.
Usage synchronized methods or blocks.
Key Rule Only one thread can hold the lock for a given object at a time.
Pros Simple, easy to use, built into the language. Guarantees atomicity, visibility, and ordering.
Cons Can lead to deadlocks. Can have performance overhead. Less flexible than java.util.concurrent.locks.
Best Practice Keep synchronized blocks as short as possible. Use dedicated lock objects instead of this. Consider java.util.concurrent alternatives for complex scenarios.
分享:
扫描分享到社交APP
上一篇
下一篇