MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java多线程同步策略详解

2022-03-207.1k 阅读

Java 多线程同步基础概念

在 Java 多线程编程中,同步是一个至关重要的概念。当多个线程同时访问和修改共享资源时,可能会导致数据不一致或其他并发问题。例如,假设有两个线程同时对一个共享的整数变量进行加 1 操作,如果没有适当的同步机制,最终结果可能并不是我们期望的加 2。

Java 提供了多种同步策略来解决这些问题。其中最基本的就是使用 synchronized 关键字。synchronized 关键字可以用于方法和代码块,它的作用是确保在同一时刻只有一个线程能够进入被同步的区域。

synchronized 方法

下面是一个简单的 synchronized 方法的示例:

public class SynchronizedMethodExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在上述代码中,increment 方法被声明为 synchronized。这意味着当一个线程调用 increment 方法时,其他线程如果也尝试调用该方法,将会被阻塞,直到当前线程执行完该方法并释放锁。

synchronized 代码块

除了同步整个方法,我们还可以使用 synchronized 代码块来同步部分代码。这样可以更细粒度地控制同步,提高程序的并发性能。

public class SynchronizedBlockExample {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

在这个例子中,increment 方法使用了 synchronized 代码块,锁对象是 lock。当一个线程进入 synchronized 代码块时,它会获取 lock 对象的锁,其他线程如果也想进入该代码块,必须等待锁的释放。

锁的原理与特性

在 Java 中,每一个对象都可以作为一个锁。当一个线程进入 synchronized 修饰的方法或代码块时,它会获取对象的锁。当线程执行完同步区域的代码后,会释放锁,允许其他线程获取锁并进入同步区域。

可重入性

Java 的 synchronized 锁是可重入的。这意味着同一个线程可以多次获取同一个锁而不会发生死锁。例如:

public class ReentrantLockExample {
    public synchronized void method1() {
        System.out.println("Entering method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("Entering method2");
    }
}

在上述代码中,method1 调用了 method2,两个方法都是 synchronized 的。由于 synchronized 锁的可重入性,同一个线程在调用 method2 时不需要再次获取锁,从而避免了死锁。

公平性与非公平性

默认情况下,synchronized 锁是非公平的。这意味着在锁被释放时,等待队列中的线程不一定按照等待顺序获取锁,而是有可能新的线程直接获取到锁。非公平锁的性能通常比公平锁高,因为它减少了线程切换的开销。

然而,在某些情况下,我们可能需要使用公平锁,以确保所有线程都有公平的机会获取锁。Java 的 java.util.concurrent.locks.ReentrantLock 类提供了公平锁的实现:

import java.util.concurrent.locks.ReentrantLock;

public class FairLockExample {
    private final ReentrantLock lock = new ReentrantLock(true); // true 表示公平锁

    public void doWork() {
        lock.lock();
        try {
            // 执行任务
        } finally {
            lock.unlock();
        }
    }
}

线程安全的类与数据结构

Java 提供了一些线程安全的类和数据结构,它们内部已经实现了同步机制,以确保在多线程环境下的正确使用。

java.util.concurrent 包中的类

  1. ConcurrentHashMap ConcurrentHashMap 是一个线程安全的哈希表。它允许多个线程同时读取,并且在更新操作时采用了更细粒度的锁机制,提高了并发性能。
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    private static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            map.put("key1", 1);
        });

        Thread thread2 = new Thread(() -> {
            map.put("key2", 2);
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(map);
    }
}
  1. CopyOnWriteArrayList CopyOnWriteArrayList 是一个线程安全的列表。它在修改操作(如添加、删除元素)时,会创建一个原数组的副本,在副本上进行修改,然后将原引用指向新的副本。这种方式保证了读操作的高效性,因为读操作不需要加锁,但会增加内存开销。
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            list.add("element1");
        });

        Thread thread2 = new Thread(() -> {
            Iterator<String> iterator = list.iterator();
            while (iterator.hasNext()) {
                System.out.println(iterator.next());
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

原子类

Java 的 java.util.concurrent.atomic 包中包含了一系列原子类,如 AtomicIntegerAtomicLong 等。这些类提供了原子性的操作,不需要使用锁就可以保证多线程环境下的数据一致性。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                count.incrementAndGet();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                count.incrementAndGet();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(count.get());
    }
}

生产者 - 消费者模型与同步

生产者 - 消费者模型是多线程编程中的经典模型。在这个模型中,生产者线程生成数据并将其放入共享队列,消费者线程从队列中取出数据进行处理。

使用 wait()notify() 方法

Java 的 Object 类提供了 wait()notify() 方法,用于线程间的协作。wait() 方法会使当前线程等待,直到其他线程调用 notify()notifyAll() 方法唤醒它。

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumerExample {
    private static final int MAX_SIZE = 5;
    private static Queue<Integer> queue = new LinkedList<>();

    static class Producer implements Runnable {
        @Override
        public void run() {
            int value = 0;
            while (true) {
                synchronized (queue) {
                    while (queue.size() == MAX_SIZE) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.add(value++);
                    System.out.println("Produced: " + (value - 1));
                    queue.notify();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int value = queue.poll();
                    System.out.println("Consumed: " + value);
                    queue.notify();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());

        producerThread.start();
        consumerThread.start();
    }
}

在上述代码中,生产者线程在队列满时调用 queue.wait() 等待,消费者线程在队列空时调用 queue.wait() 等待。当生产者向队列中添加元素或消费者从队列中取出元素后,会调用 queue.notify() 唤醒等待的线程。

使用 BlockingQueue

Java 的 java.util.concurrent 包提供了 BlockingQueue 接口及其实现类,如 ArrayBlockingQueueLinkedBlockingQueue 等。这些类简化了生产者 - 消费者模型的实现,因为它们内部已经实现了线程同步和阻塞机制。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueExample {
    private static final int MAX_SIZE = 5;
    private static BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(MAX_SIZE);

    static class Producer implements Runnable {
        @Override
        public void run() {
            int value = 0;
            while (true) {
                try {
                    queue.put(value++);
                    System.out.println("Produced: " + (value - 1));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("Consumed: " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());

        producerThread.start();
        consumerThread.start();
    }
}

在这个例子中,BlockingQueueput() 方法会在队列满时阻塞生产者线程,take() 方法会在队列空时阻塞消费者线程,从而实现了生产者 - 消费者模型的同步。

线程池与同步

线程池是一种管理和复用线程的机制,它可以提高应用程序的性能和资源利用率。在多线程编程中,线程池中的线程可能会同时访问共享资源,因此需要同步机制来保证数据的一致性。

创建线程池

Java 的 java.util.concurrent.Executors 类提供了一些静态方法来创建不同类型的线程池,如 newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutor 等。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }

        executorService.shutdown();
    }
}

在上述代码中,Executors.newFixedThreadPool(3) 创建了一个固定大小为 3 的线程池。当提交 5 个任务时,线程池中的 3 个线程会依次执行这些任务。

线程池中的同步

当线程池中的线程访问共享资源时,同样需要使用同步机制。例如,如果多个线程需要更新一个共享的计数器,可以使用 AtomicIntegersynchronized 关键字。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolSyncExample {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                count.incrementAndGet();
                System.out.println(Thread.currentThread().getName() + " incremented count to " + count.get());
            });
        }

        executorService.shutdown();
    }
}

在这个例子中,使用 AtomicInteger 保证了线程池中的线程对共享计数器的原子性操作,避免了数据不一致的问题。

同步策略的性能优化

在多线程编程中,同步机制虽然可以保证数据的一致性,但也会带来一定的性能开销。因此,需要采取一些策略来优化同步的性能。

减少锁的粒度

尽量将同步代码块的范围缩小,只对需要保护的共享资源进行同步。例如,在一个包含多个操作的方法中,如果只有部分操作涉及共享资源,可以将这些操作放在一个单独的 synchronized 代码块中。

public class FineGrainedLockingExample {
    private int value1 = 0;
    private int value2 = 0;

    public void updateValues() {
        // 非共享资源的操作
        value1++;

        synchronized (this) {
            // 共享资源的操作
            value2++;
        }
    }
}

使用读写锁

如果共享资源的读取操作远远多于写入操作,可以使用读写锁。读写锁允许多个线程同时进行读操作,但只允许一个线程进行写操作。Java 的 java.util.concurrent.locks.ReentrantReadWriteLock 类提供了读写锁的实现。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int data = 0;
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        lock.readLock().lock();
        try {
            System.out.println("Reading data: " + data);
        } finally {
            lock.readLock().unlock();
        }
    }

    public void write(int newData) {
        lock.writeLock().lock();
        try {
            data = newData;
            System.out.println("Writing data: " + data);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

在上述代码中,read 方法使用读锁,允许多个线程同时读取数据;write 方法使用写锁,保证在写操作时其他线程不能进行读写操作。

锁粗化

有时候,如果一系列的连续操作都对同一个对象进行加锁和解锁,编译器可能会将这些锁操作合并成一个较大的锁操作,这就是锁粗化。例如:

public class LockCoarseningExample {
    private final Object lock = new Object();

    public void performOperations() {
        synchronized (lock) {
            // 操作 1
            // 操作 2
            // 操作 3
        }
    }
}

如果将每个操作都单独进行加锁和解锁,会增加锁的开销。通过锁粗化,可以减少锁的获取和释放次数,提高性能。

死锁与避免

死锁是多线程编程中一个严重的问题,它发生在两个或多个线程相互等待对方释放锁,从而导致所有线程都无法继续执行的情况。

死锁示例

下面是一个简单的死锁示例:

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("Thread 1 acquired lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 acquired lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread 2 acquired lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个例子中,thread1 先获取 lock1,然后尝试获取 lock2thread2 先获取 lock2,然后尝试获取 lock1。由于两个线程相互等待对方释放锁,从而导致死锁。

避免死锁的方法

  1. 按顺序获取锁:所有线程按照相同的顺序获取锁,这样可以避免死锁。例如,在上面的例子中,如果两个线程都先获取 lock1,再获取 lock2,就不会发生死锁。
  2. 使用定时锁:使用 tryLock 方法并设置超时时间。如果在指定时间内无法获取锁,线程可以放弃尝试并采取其他措施,避免无限期等待。
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockAvoidanceExample {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            boolean lock1Acquired = false;
            boolean lock2Acquired = false;
            try {
                lock1Acquired = lock1.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
                if (lock1Acquired) {
                    System.out.println("Thread 1 acquired lock1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock2Acquired = lock2.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
                    if (lock2Acquired) {
                        System.out.println("Thread 1 acquired lock2");
                    } else {
                        System.out.println("Thread 1 could not acquire lock2");
                    }
                } else {
                    System.out.println("Thread 1 could not acquire lock1");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock2Acquired) {
                    lock2.unlock();
                }
                if (lock1Acquired) {
                    lock1.unlock();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            boolean lock1Acquired = false;
            boolean lock2Acquired = false;
            try {
                lock1Acquired = lock1.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
                if (lock1Acquired) {
                    System.out.println("Thread 2 acquired lock1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock2Acquired = lock2.tryLock(1, java.util.concurrent.TimeUnit.SECONDS);
                    if (lock2Acquired) {
                        System.out.println("Thread 2 acquired lock2");
                    } else {
                        System.out.println("Thread 2 could not acquire lock2");
                    }
                } else {
                    System.out.println("Thread 2 could not acquire lock1");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock2Acquired) {
                    lock2.unlock();
                }
                if (lock1Acquired) {
                    lock1.unlock();
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这个改进的例子中,使用 tryLock 方法并设置超时时间,避免了死锁的发生。如果在指定时间内无法获取锁,线程会输出相应的信息并释放已经获取的锁。

通过以上对 Java 多线程同步策略的详细讲解,包括同步基础概念、锁的原理与特性、线程安全的类与数据结构、生产者 - 消费者模型、线程池与同步、同步策略的性能优化以及死锁与避免等方面,希望读者能够对 Java 多线程同步有更深入的理解和掌握,从而编写出高效、稳定的多线程程序。