Java中的线程几种同步方式详解(附代码示例)

后端 潘老师 6天前 15 ℃ (0) 扫码查看

当多个线程同时访问和修改共享资源时,就可能出现数据不一致等问题。为了解决这些问题,我们需要用到Java线程同步机制。接下来,就给大家详细介绍Java中几种常见的线程同步方式。

一、synchronized关键字

synchronized是Java自带的一种锁机制,主要用来实现线程同步。它的使用方式比较灵活,可以修饰方法,也可以修饰代码块。

(一)修饰实例方法

synchronized修饰实例方法时,它会同步当前对象(也就是this)的锁。只有获取到这个对象锁的线程,才能执行该方法。例如:

public synchronized void method() {
    // 线程安全代码
}

这就好比一个房间(对象)只有一把钥匙(对象锁),拿到钥匙的线程才能进入房间执行任务。

(二)修饰静态方法

如果synchronized修饰的是静态方法,那么它同步的是类的Class对象锁。因为静态方法属于类,不属于某个具体的实例,所以用类的Class对象作为锁。示例如下:

public static synchronized void staticMethod() {
    // 线程安全代码
}

这时候,就好像整个类是一个大房间,所有静态方法共用一把钥匙(类的Class对象锁) 。

(三)修饰代码块

synchronized修饰代码块时,可以指定要锁定的对象。这种方式更灵活,能精确控制同步的范围。代码如下:

public void method() {
    synchronized (this) {
        // 线程安全代码
    }
}

这里,我们以this作为锁定对象,只有拿到this对象锁的线程,才能执行花括号里的代码。

二、ReentrantLock(可重入锁)

ReentrantLock位于java.util.concurrent.locks包中,它比synchronized提供了更丰富的功能。使用示例如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Example {
    // 创建一个可重入锁实例
    private final Lock lock = new ReentrantLock();

    public void method() {
        // 获取锁,后续代码进入同步区域
        lock.lock(); 
        try {
            // 线程安全代码
        } finally {
            // 确保无论是否发生异常,都能释放锁
            lock.unlock(); 
        }
    }
}

它有几个比较突出的特点:

  • 支持公平锁和非公平锁:公平锁会按照线程请求的顺序来分配锁,非公平锁则允许线程在锁可用时直接竞争,不按照顺序。
  • 支持尝试获取锁(tryLock():调用这个方法后,线程会尝试获取锁,如果获取成功就返回true,否则返回false,不会一直等待。
  • 支持中断获取锁(lockInterruptibly():在获取锁的过程中,如果线程被中断,它会抛出异常并停止等待。
  • 支持条件变量(Condition:通过条件变量,可以实现更精细的线程间协作。

三、volatile关键字

volatile关键字主要用于修饰变量,它能保证变量的可见性。也就是说,当一个线程修改了被volatile修饰的变量的值,其他线程能马上看到这个变化。不过,它不能保证对变量的操作是原子性的。比如下面这个例子:

private volatile boolean flag = true;

在实际应用中,volatile比较适合单个变量的读写操作场景。但如果涉及到复合操作,比如先读取变量的值,再根据这个值进行其他操作,volatile就不太适用了。

四、Atomic类

java.util.concurrent.atomic包下有一系列原子操作类,像AtomicIntegerAtomicLongAtomicBoolean等。这些类利用CAS(Compare-And-Swap,比较并交换)算法,实现了高效的线程安全操作。例如:

import java.util.concurrent.atomic.AtomicInteger;

public class Example {
    // 创建一个初始值为0的AtomicInteger对象
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        // 原子操作,对counter的值进行自增
        counter.incrementAndGet(); 
    }
}

这类原子操作类的优点是高效且无锁,很适合处理简单的数值操作场景。

五、ThreadLocal

ThreadLocal的作用是为每个线程提供独立的变量副本,这样不同线程之间对这个变量的操作就不会相互干扰,从而避免了线程间的竞争。示例代码如下:

public class Example {
    // 创建一个ThreadLocal对象,初始值为0
    private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void set(int value) {
        // 设置当前线程的变量副本的值
        threadLocal.set(value); 
    }

    public int get() {
        // 获取当前线程的变量副本的值
        return threadLocal.get(); 
    }
}

在实际开发中,如果某个变量只需要在线程内部使用,不涉及线程间共享,使用ThreadLocal就非常合适。

六、Semaphore(信号量)

Semaphore用于控制同时访问某个资源的线程数量。假设我们有一个资源,只允许一定数量的线程同时访问,就可以使用Semaphore来实现。代码示例如下:

import java.util.concurrent.Semaphore;

public class Example {
    // 创建一个信号量,允许最多3个线程同时访问资源
    private Semaphore semaphore = new Semaphore(3); 

    public void method() throws InterruptedException {
        // 获取许可,如果当前没有可用许可,线程会阻塞等待
        semaphore.acquire(); 
        try {
            // 线程安全代码
        } finally {
            // 释放许可,让其他等待的线程有机会获取
            semaphore.release(); 
        }
    }
}

这里创建的Semaphore对象允许最多3个线程同时访问资源,当有更多线程尝试获取许可时,它们就需要等待,直到有线程释放许可。

七、CountDownLatch(倒计时锁存器)

CountDownLatch主要用于让一个线程等待一组线程完成任务后,再继续执行。比如,有一个主线程需要等待多个子线程都完成各自的任务后,才能进行下一步操作,就可以用CountDownLatch。示例代码如下:

import java.util.concurrent.CountDownLatch;

public class Example {
    // 创建一个倒计时锁存器,初始计数值为3
    private CountDownLatch latch = new CountDownLatch(3); 

    public void task() {
        new Thread(() -> {
            System.out.println("Task finished");
            // 每次调用countDown(),计数值减一
            latch.countDown(); 
        }).start();
    }

    public void await() throws InterruptedException {
        // 主线程在这里等待,直到计数值变为0
        latch.await(); 
        System.out.println("All tasks completed");
    }
}

在这个例子中,latch的初始值为3,每一个子线程完成任务后调用countDown(),当计数值变为0时,等待的线程(比如主线程)就会继续执行。

八、CyclicBarrier(循环屏障)

CyclicBarrier的作用是让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。示例如下:

import java.util.concurrent.CyclicBarrier;

public class Example {
    // 创建一个循环屏障,需要3个线程到达屏障点,并设置一个屏障动作
    private CyclicBarrier barrier = new CyclicBarrier(3, () -> {
        System.out.println("All threads have reached the barrier");
    });

    public void task() {
        new Thread(() -> {
            try {
                System.out.println("Thread waiting at barrier");
                // 线程在这里等待,直到所有线程都调用了await()
                barrier.await(); 
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}

这里创建的CyclicBarrier要求3个线程都调用await()方法后,所有线程才会继续执行后续代码,并且在所有线程都到达屏障点时,会执行我们设置的屏障动作。

九、BlockingQueue(阻塞队列)

BlockingQueue是一种线程安全的队列,它支持阻塞操作。在多线程环境下,生产者-消费者模型经常会用到它。例如:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Example {
    // 创建一个容量为10的阻塞队列
    private BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); 

    public void producer() {
        new Thread(() -> {
            try {
                // 向队列中添加元素,如果队列已满,线程会阻塞
                queue.put(1); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

    public void consumer() {
        new Thread(() -> {
            try {
                // 从队列中取出元素,如果队列为空,线程会阻塞
                Integer value = queue.take(); 
                System.out.println("Consumed: " + value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

在这个例子中,生产者线程往队列里添加元素,消费者线程从队列里取出元素。如果队列满了,生产者线程会被阻塞;如果队列空了,消费者线程会被阻塞。

十、总结

不同的线程同步方式适用于不同的场景:

  • 简单同步场景:如果只是需要实现基本的同步功能,使用synchronized关键字就足够了。
  • 需要高级功能时:当需要更灵活的锁机制,比如支持公平锁、尝试获取锁、中断获取锁等功能,ReentrantLockCondition会是更好的选择。
  • 高效原子操作场景:对于简单的数值操作,Atomic类能提供高效且线程安全的解决方案。
  • 资源限制场景:如果要控制同时访问资源的线程数量,Semaphore是不错的选择。
  • 线程协作场景:涉及到线程之间的协作,比如等待一组线程完成任务,或者让一组线程互相等待,CountDownLatchCyclicBarrier就能派上用场。
  • 生产者-消费者场景:在生产者-消费者模型中,BlockingQueue可以很好地协调生产者和消费者之间的操作。

希望通过这篇文章,大家对Java中的线程同步方式有更深入的理解,在实际开发中能够根据不同的场景选择合适的同步方式,你学废了吗 :???:


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/17520.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】