杰瑞科技汇

synchronize java

Of course! Synchronization in Java is a fundamental concept used to control access to shared resources in a multithreaded environment. Its primary goal is to prevent race conditions and ensure thread safety.

synchronize java-图1
(图片来源网络,侵删)

Let's break it down, from the core concept to practical examples and best practices.


The Core Problem: Race Conditions

Imagine two threads, Thread A and Thread B, trying to increment the same shared counter:

class SharedCounter {
    private int count = 0;
    public void increment() {
        count++; // This is NOT an atomic operation!
    }
    public int getCount() {
        return count;
    }
}

The count++ statement seems simple, but it's actually three separate operations at the JVM level:

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

A race condition occurs if these operations are interleaved incorrectly:

synchronize java-图2
(图片来源网络,侵删)
Time Thread A (Incrementing) Thread B (Incrementing) Shared count Value
T1 Reads count (value is 0) 0
T2 Reads count (value is 0) 0
T3 Modifies value to 1 0
T4 Modifies value to 1 0
T5 Writes new value (1) to count 1
T6 Writes new value (1) to count 1

Result: Both threads incremented, but the final value is 1 instead of the expected 2. This is a race condition. Synchronization solves this by ensuring that only one thread can execute the critical section (the increment method) at a time.


The Core Mechanism: Monitors and Locks

Java uses a mechanism built around monitors and intrinsic locks.

  • Intrinsic Lock (or Monitor Lock): Every object in Java has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object's fields must acquire the object's intrinsic lock.
  • Monitor: The monitor is an entity that enforces rules about how threads access an object's lock. When a thread acquires a lock, it "enters the monitor."

How it works:

  1. A thread attempts to execute a synchronized block/method.
  2. It tries to acquire the intrinsic lock associated with the object specified in the synchronized statement.
  3. If the lock is available: The thread acquires the lock, executes the code, and releases the lock when it's done (either normally or via an exception).
  4. If the lock is NOT available: The thread is blocked and must wait until the lock is released by the thread that currently holds it.

Only one thread can hold the intrinsic lock for a given object at any one time. This guarantees that only one thread can execute a synchronized method or block on that object.


How to Use Synchronization in Java

There are three primary ways to use the synchronized keyword.

A. Synchronized Instance Methods

When you declare a method as synchronized, a thread must acquire the intrinsic lock of the instance (this) on which the method is being invoked before it can execute the method.

class Counter {
    private int count = 0;
    // Only one thread can execute this method on a specific Counter object at a time.
    public synchronized void increment() {
        count++;
    }
    public synchronized int getCount() {
        return count;
    }
}

How it works: If thread1 calls myCounter.increment(), it acquires the lock on myCounter. Any other thread that tries to call myCounter.increment() or myCounter.getCount() will be blocked until thread1 finishes and releases the lock.

B. Synchronized Static Methods

When you declare a static method as synchronized, a thread must acquire the intrinsic lock of the Class object associated with the class.

class SharedService {
    // Only one thread can execute this method for ALL instances of SharedService at a time.
    public static synchronized void doSomething() {
        // ... critical section ...
    }
}

How it works: The lock is on the SharedService.class object, not on an instance. This means the lock is shared across all instances of the class. If thread1 calls SharedService.doSomething() from any object, thread2 calling the same method from any other object will be blocked.

C. Synchronized Blocks (Statements)

This is the most flexible way to use synchronization. You can synchronize on any object, not just this or class. This allows you to fine-tune which part of your code is protected and which lock you use.

class BankAccount {
    private double balance;
    private final Object lock = new Object(); // A dedicated lock object
    public void deposit(double amount) {
        // No lock needed here, as it's not modifying the shared state.
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        // Synchronize only on the critical section
        synchronized (lock) {
            // Critical section: only one thread can execute this block for a given BankAccount instance.
            balance += amount;
        }
    }
    public void withdraw(double amount) {
        synchronized (lock) {
            // Critical section
            if (balance < amount) {
                throw new IllegalArgumentException("Insufficient funds");
            }
            balance -= amount;
        }
    }
}

Best Practice: For instance-level locking, it's highly recommended to use a private final Object as the lock instead of synchronizing on this. This prevents other parts of your code from accidentally acquiring the same lock and potentially causing deadlocks or performance issues.


Important Concepts and Pitfalls

wait(), notify(), and notifyAll()

These methods are used for communication between threads within a synchronized block. They allow a thread to release its lock and wait for a specific condition to become true.

  • void wait(): Causes the current thread to wait until another thread invokes the notify() or notifyAll() method for this object. The releasing thread must be holding the lock.
  • void notify(): Wakes up a single thread that is waiting on this object's monitor. If many threads are waiting, one is chosen arbitrarily.
  • void notifyAll(): Wakes up all threads that are waiting on this object's monitor.

Crucial Rule: These methods can only be called from within a synchronized block or method. Calling them outside will throw an IllegalMonitorStateException.

Example: A simple Producer-Consumer using wait/notify

class Queue {
    private int n;
    private boolean valueSet = false;
    // Producer calls this
    public synchronized void put(int n) {
        // Wait until the consumer has taken the value
        while (valueSet) {
            try {
                wait(); // Releases lock and waits
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.n = n;
        valueSet = true;
        System.out.println("Put: " + n);
        notify(); // Notify the consumer that a new value is available
    }
    // Consumer calls this
    public synchronized int get() {
        // Wait until the producer has put a value
        while (!valueSet) {
            try {
                wait(); // Releases lock and waits
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Got: " + n);
        valueSet = false;
        notify(); // Notify the producer that the value has been taken
        return n;
    }
}

Deadlocks

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

Classic Example: The "Deadly Embrace"

class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    public void thread1() {
        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) { // Thread 1 needs lock 2
                System.out.println("Thread 1: Acquired lock 2. Doing work.");
            }
        }
    }
    public void thread2() {
        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) { // Thread 2 needs lock 1
                System.out.println("Thread 2: Acquired lock 1. Doing work.");
            }
        }
    }
}

If thread1 acquires lock1 and thread2 acquires lock2 at roughly the same time, they will both block forever, waiting for a lock the other thread holds.

How to avoid deadlocks:

  • Avoid Nested Locks: Minimize the number of locks a thread holds.
  • Lock Ordering: Always acquire locks in a predefined, global order. (e.g., always acquire lockA before lockB).
  • Lock Timeout: Use Lock objects (from java.util.concurrent.locks) that support tryLock(), which can be configured to time out instead of blocking indefinitely.
  • Deadlock Detection: Use tools like jstack or visual VM to detect deadlocks in a running application.

Modern Alternatives to synchronized

While synchronized is powerful and simple, it can be heavy-handed. The java.util.concurrent package provides more advanced and flexible tools.

Tool Use Case Advantage over synchronized
java.util.concurrent.locks.ReentrantLock More flexible locking. Supports try-lock, timed lock, fairness policies, and multiple condition variables (newCondition()).
java.util.concurrent.atomic package For single, shared variables (e.g., AtomicInteger, AtomicReference). Lock-free, highly performant, and often scale better under high contention. Uses low-level hardware instructions (CAS).
java.util.concurrent Collections For thread-safe data structures. ConcurrentHashMap, CopyOnWriteArrayList, etc. are highly optimized for concurrent access and avoid the bottlenecks of simply wrapping a collection with Collections.synchronizedMap().

Example: AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        // This operation is atomic and lock-free.
        count.incrementAndGet();
    }
    public int getCount() {
        return count.get();
    }
}

Summary: When to Use What?

Tool When to Use It
synchronized Good for most cases. It's simple, easy to understand, and works well for low-to-moderate contention. Perfect for protecting critical sections in methods or small blocks.
ReentrantLock When you need more control, such as timed locks, non-blocking tryLock, or fairness policies.
Atomic classes When you are performing atomic operations on a single variable (e.g., counters, flags). They are the most performant for this specific use case.
Concurrent Collections When you need a thread-safe data structure that is heavily accessed by many threads. They are designed for high concurrency.
分享:
扫描分享到社交APP
上一篇
下一篇