Java基础到高级:锁的全面解析与实践

前言

在项目开发过程中,当需要使用多线程去处理一些业务问题的时候,尤其涉及到多线程读写数据同时发生的操作时,就会产生一些线程安全的问题。那如何理解线程安全问题呢?

线程安全问题是指多线程环境中,由于存在数据共享,一个线程访问的共享数据已经被其他线程修改,导致数据异常的情况。那如何解决线程安全的问题呢?

在Java中,使用锁可以实现线程同步和互斥,避免线程安全问题。通过锁机制,可以确保一次只有一个线程访问某个特定的资源或代码区域,避免了多个线程同时读写共享数据导致的冲突和错误。同时,锁还可以用于实现线程之间的协作和通信,例如通过条件变量和锁实现生产者-消费者模型中的线程协作等。

值得注意的是要解决线程安全问题,需要深入理解多线程环境和数据共享的问题,并采取适当的同步和互斥措施来保护共享数据。同时,也需要理解不同的锁机制和适用场景,以便根据具体情况选择合适的锁来实现线程同步和互斥。

在Java中,常见的锁的代表名词包括:

互斥锁信号量读写锁自旋锁条件锁递归锁读写信号量顺序锁公平锁非公平锁乐观锁悲观锁原子类并发容器ThreadLocal

等等,那这么多的种类掌握起来还是比较困难的,接下来将会依次针对每种类型做出详细说明和代码样例。

正文

互斥锁

互斥锁是一种基本的线程同步机制,它用于确保在任何时刻只有一个线程可以访问某个共享资源或代码区域。互斥锁通过互斥量(mutex)来实现,当一个线程需要访问共享资源时,它必须先获取互斥量。如果其他线程已经持有互斥锁并正在访问共享资源,则该线程将被阻塞,直到持有互斥锁的线程释放互斥锁。

在Java中,可以通过synchronized关键字或ReentrantLock类来实现互斥锁。

通过synchronized关键字实现互斥锁的示例代码:

 public class Counter {  private int count = 0;  public synchronized void increment() {  count++;  }  public synchronized void decrement() {  count--;  }  public synchronized int getCount() {  return count;  }  }

在上面的代码中,synchronized关键字用于修饰increment()decrement()getCount()方法。这使得在同一时刻只有一个线程可以执行这些方法,从而保证了count变量的线程安全性(ps:因为使用的是互斥锁,所以多个线程无法同时调用increment,decrement,getCount方法)

通过ReentrantLock类实现互斥锁的实例代码:

 import java.util.concurrent.locks.Lock;  import java.util.concurrent.locks.ReentrantLock;  public class Counter {  private int count = 0;  private Lock lock = new ReentrantLock();  public void increment() {  lock.lock();  try {  count++;  } finally {  lock.unlock();  }  }  public void decrement() {  lock.lock();  try {  count--;  } finally {  lock.unlock();  }  }  public int getCount() {  return count;  }  }

在上面的代码中,我们使用了ReentrantLock类来实现互斥锁。通过调用lock()方法获取锁,并在finally块中调用unlock()方法释放锁,以确保锁总是被释放。这种方式提供了更大的灵活性,并避免了在发生异常时导致锁无法释放的问题。

当多个线程同时访问increment()decrement()方法时,由于互斥锁的存在,一次只有一个线程能够获取到互斥量并执行方法。其他线程将会被阻塞,直到持有互斥量的线程释放互斥量。因此,多个线程无法同时执行increment()decrement()方法。

嘿嘿,这是我自己的公众号,会分享一些项目管理的知识,相信工作几年后的朋友对于自己的职业规划也会有自己的认识,如果对项目管理感兴趣的朋友可以关注一下。

信号量

信号量(Semaphore)是一种同步机制,用于控制多个线程对共享资源的访问。信号量是一个整数值,通常用于表示可用资源的数量,也就说控制多少个线程可以去访问资源。信号量可以有两种操作:P(proberen,尝试)和V(verhogen,增加)。

P操作:线程尝试减少信号量的值。如果信号量的值为0,表示当前访问资源的线程数已经达到允许的阈值,则线程被阻塞,直到信号量的值增加。

V操作:线程增加信号量的值,并唤醒所有等待该信号量的线程。

在Java中,没有内置的Semaphore类,但可以使用java.util.concurrent.Semaphore来实现信号量。

下面是一个使用Semaphore实现线程安全的打印机的示例代码:

 import java.util.concurrent.Semaphore;  public class Printer {  private Semaphore semaphore = new Semaphore(1); // 控制允许同时访问打印机的线程数量  public void print() throws InterruptedException {  try {  semaphore.acquire(); // 获取信号量,如果信号量不可用则阻塞当前线程  } catch (InterruptedException e) {  Thread.currentThread().interrupt();  throw e;  }  //此处写业务代码try {  // 模拟打印操作,这里只是简单地休眠1秒钟 ,此处用于处理实际的业务 Thread.sleep(1000);  } finally {  semaphore.release(); // 释放信号量,允许其他线程获取信号量  }  }  }

在上面的代码中,我们使用了Semaphore类来控制同时访问打印机的线程数量。只有一个信号量,初始值为1。当线程需要打印时,它首先尝试获取信号量。如果信号量的值为0,则线程被阻塞,直到其他线程释放信号量。当线程成功获取信号量后,它可以进行打印操作。在finally块中,无论打印操作是否成功完成,我们都要释放信号量,以便其他线程可以获取信号量并执行打印操作。通过这种方式,我们实现了线程安全的打印机访问控制。

读写锁

读写锁(ReadWriteLock)是一种同步机制,用于控制多个线程对共享资源的访问。读写锁提供了两种类型的锁:读锁和写锁。多个线程可以同时持有读锁,但只能有一个线程持有写锁。写锁是互斥的,即在一个线程持有写锁时,其他线程无法获取读锁或写锁。

读写锁的主要优势在于,当多个线程只是读取共享资源时,它们可以并发地访问资源,从而提高程序的性能。但如果有线程需要写入共享资源,则其他线程无法访问资源,保证了数据的一致性。

在Java中,读写锁可以通过java.util.concurrent.locks包中的ReentrantReadWriteLock类来实现。

下面是一个使用Java中的ReentrantReadWriteLock实现读写锁的示例代码:

 import java.util.concurrent.locks.ReentrantReadWriteLock;  public class SharedResource {  private String data;  private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  public String readData() {  lock.readLock().lock(); // 获取读锁  try {  return data;  } finally {  lock.readLock().unlock(); // 释放读锁  }  }  public void writeData(String newData) {  lock.writeLock().lock(); // 获取写锁  try {  data = newData;  } finally {  lock.writeLock().unlock(); // 释放写锁  }  }  }

在上面的代码中,我们使用ReentrantReadWriteLock类来实现读写锁。readData()方法使用读锁来读取数据,而writeData()方法使用写锁来写入数据。在获取锁时,我们使用lock()方法,而在释放锁时,我们使用unlock()方法。通过这种方式,我们可以确保在读取或写入数据时,其他线程无法同时访问数据,从而保证了数据的一致性。

自旋锁

当一个线程需要访问共享资源时,它会尝试获取自旋锁。如果锁已经被其他线程持有,则该线程会一直循环检查(自旋)直到锁被释放。

下面通过原生的代码实现一个自旋锁:

 public class SpinLock {  private volatile boolean locked = false;  public void lock() {  while (true) {  if (locked) {  continue;  } else {  locked = true;  break;  }  }  }  public void unlock() {  locked = false;  }  }

这是用java原生代码实现的一个自旋锁,在这个示例中,我们定义了一个SpinLock类,它包含了一个locked变量来记录锁的状态。lock方法使用一个无限循环来检查锁的状态,如果锁已经被其他线程持有,则继续循环;否则将锁设置为已持有状态并退出循环。unlock方法将锁的状态设置为未持有状态。

那下面通过代码使用一下这个自旋锁:

 public class SharedResource {  private final SpinLock lock = new SpinLock();  private int count = 0;  public void increment() {  lock.lock(); // 获取自旋锁  try {  count++;  System.out.println("Count after increment: " + count);  } finally {  lock.unlock(); // 释放自旋锁  }  }  }

在上面的示例中,我们定义了一个SharedResource类,它包含了一个自旋锁lock和一个共享资源count。在increment方法中,我们首先调用lock.lock()方法获取自旋锁,然后在代码块中增加count的值并输出结果。无论是否发生异常,我们都在finally块中调用lock.unlock()方法来释放自旋锁。

要使用这个示例,可以创建多个线程,并在每个线程中调用increment方法来访问共享资源。由于使用了自旋锁来保护共享资源,因此只有一个线程能够访问共享资源,其他线程会等待直到锁被释放。

使用自旋锁时需要注意以下几点:

  1. 自旋锁可能会导致CPU资源的浪费,因为线程在等待获取锁时会一直循环检查。

  2. 自旋锁适用于短时间的等待,如果等待时间较长,则应该考虑使用其他同步机制,例如阻塞队列或条件变量。

  3. 自旋锁适用于共享资源的访问非常频繁的情况,如果共享资源的访问较少,则使用自旋锁可能会影响性能。

其中ReentrantLock是本身就是一种自旋锁。ReentrantLock通过循环调用CAS操作来实现加锁,性能比较好也是因为避免了使线程进入内核态的阻塞状态。

条件锁

条件锁(Condition Lock)是一种同步机制,用于在多线程环境中实现线程之间的协调。它允许一个线程等待某个条件满足,而其他线程可以修改这个条件,以便满足等待中的线程。

条件锁通常与互斥锁(Mutex)一起使用,以保护共享数据的访问。当一个线程需要访问共享数据时,它首先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程将等待条件锁。当其他线程修改了共享数据并改变了条件时,它将释放条件锁,从而使等待中的线程可以继续执行。

以下用代码演示如何使用条件锁实现线程之间的协调:

 class SharedResource {  private final Lock lock = new ReentrantLock();  private final Condition condition = lock.newCondition();  private int count = 0;  public void increment() throws InterruptedException {  lock.lock();  try {  condition.await(lock, () -> count < 10); // 等待条件满足  ++count;  System.out.println("Count: " + count);  condition.signalOne(); // 通知其他线程条件已满足  } finally {  lock.unlock();  }  }  public void decrement() throws InterruptedException {  lock.lock();  try {  condition.await(lock, () -> count > 0); // 等待条件满足  --count;  System.out.println("Count: " + count);  condition.signalOne(); // 通知其他线程条件已满足  } finally {  lock.unlock();  }  }  }​public class Test{public static void main(String[] args) {  SharedResource example = new SharedResource();  Thread t1 = new Thread(() -> {  try {  example.increment();  } catch (InterruptedException e) {  e.printStackTrace();  }  });  Thread t2 = new Thread(() -> {  try {  example.decrement();  } catch (InterruptedException e) {  e.printStackTrace();  }  });  t1.start();  t2.start();  try {  t1.join();  t2.join();  } catch (InterruptedException e) {  e.printStackTrace();  }  }  }

在上述示例中,SharedResource 类包含一个共享资源 count,以及两个方法 increment()decrement(),分别用于增加和减少 count 的值。使用条件锁来确保在多线程环境下对 count 的操作是正确的,并确保当 count 的值满足特定条件时,等待的线程可以继续执行。

嘿嘿,这是我自己的公众号,会分享一些项目管理的知识,相信工作几年后的朋友对于自己的职业规划也会有自己的认识,如果对项目管理感兴趣的朋友可以关注一下。

递归锁(可重入锁)

递归锁也叫可重入锁,是一种特殊类型的锁,它允许同一线程多次获取同一个锁。这对于需要反复访问同一资源的情况非常有用,例如递归算法或嵌套事务。

递归锁的实现通常基于一个计数器,当线程尝试获取锁时,计数器递增。当线程释放锁时,计数器递减。只有当计数器为零时,其他线程才能获取该锁。

在 Java 中,可以使用 ReentrantLock 类实现递归锁(可重入锁)

 import java.util.concurrent.locks.ReentrantLock;​public class RecursiveTask implements Runnable {private final ReentrantLock lock = new ReentrantLock();​public void performTask(int count) {lock.lock();try {System.out.println("Thread " + Thread.currentThread().getId() + ": Count = " + count);​// 当计数小于5时递归调用if (count < 5) {performTask(count + 1);}} finally {// 确保锁在操作完成后释放lock.unlock();}}​@Overridepublic void run() {performTask(1);}​public static void main(String[] args) {RecursiveTask task = new RecursiveTask();Thread thread1 = new Thread(task);Thread thread2 = new Thread(task);thread1.start();thread2.start();}}

在这个例子中,RecursiveTask 类实现了 Runnable 接口,每个线程运行时都会从计数1开始执行 performTask 方法。每个线程都有自己的计数器,因此即使一个线程完成了递归任务,其他线程仍然可以独立地开始和完成自己的递归任务。但是多个线程走到performTask方法时都会尝试获取锁,如果锁被其他线程占用,则会继续等待锁释放。

读写信号量

读写信号量(ReadWrite Semaphore)是信号量的一种变种,它允许多个线程同时读取共享资源,但在写入时则需要独占式的访问。

下面通过一个简单的代码示例:

 import java.util.concurrent.Semaphore;  public class ReadWriteSemaphoreDemo {  public static void main(String[] args) throws InterruptedException {  Semaphore readSemaphore = new Semaphore(2); // 允许2个读线程同时访问  Semaphore writeSemaphore = new Semaphore(1); // 允许1个写线程访问  // 模拟读线程  Thread readerThread1 = new Thread(() -> {  try {  readSemaphore.acquire(); // 获取读锁  System.out.println("Reader 1 is reading...");  Thread.sleep(1000); // 模拟读取操作耗时  readSemaphore.release(); // 释放读锁  } catch (InterruptedException e) {  e.printStackTrace();  }  });  Thread readerThread2 = new Thread(() -> {  try {  readSemaphore.acquire(); // 获取读锁  System.out.println("Reader 2 is reading...");  Thread.sleep(1000); // 模拟读取操作耗时  readSemaphore.release(); // 释放读锁  } catch (InterruptedException e) {  e.printStackTrace();  }  });  // 模拟写线程  Thread writerThread = new Thread(() -> {  try {  writeSemaphore.acquire(); // 获取写锁  System.out.println("Writer is writing...");  Thread.sleep(1000); // 模拟写入操作耗时  writeSemaphore.release(); // 释放写锁  } catch (InterruptedException e) {  e.printStackTrace();  }  });  readerThread1.start();  readerThread2.start();  writerThread.start();  }  }

这个示例中,我们创建了一个readSemaphore和一个writeSemaphore,分别用于控制读和写的访问。两个读线程可以同时获取读锁并执行读取操作,但写线程在执行写入操作时需要独占写锁。

顺序锁

顺序锁是一种同步机制,用于控制多个线程对共享资源的访问顺序。它通过维护一个线程的访问顺序列表来实现,以确保线程按照一定的顺序访问共享资源,避免出现竞态条件。

顺序锁的主要思想是,当一个线程访问共享资源时,需要先获取顺序锁,并将自己添加到访问顺序列表的头部。其他线程在访问共享资源时,需要等待顺序锁被释放,并按照列表中的顺序进行访问。

 import java.util.LinkedList;  import java.util.Queue;  public class SequentialLock {  private Queue<Thread> queue = new LinkedList<>();  public synchronized void lock() throws InterruptedException {  Thread currentThread = Thread.currentThread();  while (true) {  queue.add(currentThread);  if (queue.peek() == currentThread) {  break;  } else {  wait();  }  }  }  public synchronized void unlock() {  Thread removedThread = queue.poll();  if (removedThread != null && removedThread.equals(Thread.currentThread())) {  notifyAll();  }  }  }

在这个示例中,我们使用了一个队列来维护访问顺序列表。当一个线程需要访问共享资源时,它首先调用lock()方法获取顺序锁。在lock()方法中,线程将自己添加到队列中,并检查队列头部是否是当前线程。如果不是当前线程,则当前线程会等待,直到它成为队列头部。当线程成为队列头部后,它就可以访问共享资源了。

当线程访问完共享资源后,它会调用unlock()方法释放顺序锁。在unlock()方法中,我们将队列头部的线程移除,并通知所有等待的线程。这样,其他线程就可以按照队列中的顺序访问共享资源了。

 public class SharedResource {  private int resourceValue;  private SequentialLock lock = new SequentialLock();  public void setResourceValue(int value) {  lock.lock(); // 获取锁  try {  resourceValue = value;  } finally {  lock.unlock(); // 释放锁  }  }  public int getResourceValue() {  lock.lock(); // 获取锁  try {  // 在这里执行对共享资源的访问操作  return resourceValue;  } finally {  lock.unlock(); // 释放锁  }  }  }

在这个例子中,我们创建了一个SharedResource类,它包含了一个SequentialLock对象来控制对共享资源的访问。在访问共享资源时,线程需要先调用lock()方法获取锁,并在访问完成后调用unlock()方法释放锁。这样可以确保同一时刻只有一个线程能够访问共享资源,避免了多个线程同时访问共享资源的情况。

公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。公平锁通过维护一个等待队列来保证线程按照请求锁的顺序获取锁,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取出并占有锁。

在Java中,java.util.concurrent.locks.ReentrantLock类提供了公平锁的实现。下面是一个使用公平锁的示例代码:

 import java.util.concurrent.locks.ReentrantLock;  public class FairLockExample {  private final ReentrantLock fairLock = new ReentrantLock(true); // 创建公平锁  private final Object sharedResource = new Object();  public void doSomething() {  fairLock.lock(); // 获取锁  try {  // 访问共享资源  // ...  } finally {  fairLock.unlock(); // 释放锁  }  }  }

在上面的代码中,我们创建了一个公平锁fairLock,并将其传递给ReentrantLock构造函数以指定为公平锁。然后,在doSomething()方法中,我们使用fairLock.lock()来获取锁,并在访问共享资源后使用fairLock.unlock()释放锁。这样可以确保线程按照请求锁的顺序获取锁,并避免了饥饿问题。

什么是线程饥饿问题,在下面会用简洁明了的方式给大家介绍。

线程饥饿问题

针对线程饥饿问题,是比较难理解的,下面通过简单的比喻帮助大家理解线程饥饿问题:

 想象一下,有一家餐厅,里面有10个服务员。每当有顾客进来,服务员就会去为他们服务。如果每个服务员都均匀地服务顾客,那么餐厅的运行就会很顺畅。​但是,如果有些服务员总是忙不过来,而其他服务员却闲着没事干,那么那些闲着的服务员就会一直等,因为他们没有顾客可以服务。这就好比线程饥饿问题。有些线程可能总是得不到它们需要的资源,就像那些闲着的服务员一样。​这种情况可能会导致那些得不到资源的线程长时间等待,甚至无法执行。这就好比那些闲着的服务员一直等不到顾客,他们就会一直闲着,无法为餐厅创造价值。​为了解决这个问题,我们可以采取一些措施,比如让服务员轮流休息,或者增加服务员的数量,这样就可以保证每个服务员都有顾客可以服务,不会出现线程饥饿问题。

嘿嘿,这是我自己的公众号,会分享一些项目管理的知识,相信工作几年后的朋友对于自己的职业规划也会有自己的认识,如果对项目管理感兴趣的朋友可以关注一下。

非公平锁

非公平锁是一种线程同步机制,与公平锁相对。在非公平锁中,线程获取锁的顺序不一定按照申请锁的顺序,而是按照线程的优先级或其他因素来决定。

在Java中,java.util.concurrent.locks.ReentrantLock类提供了非公平锁的实现。下面是一个使用非公平锁的示例代码:

 import java.util.concurrent.locks.ReentrantLock;  public class NonFairLockExample {  private final ReentrantLock nonFairLock = new ReentrantLock(false); // 创建非公平锁  private final Object sharedResource = new Object();  public void doSomething() {  nonFairLock.lock(); // 获取锁  try {  // 访问共享资源  // sharedResource...  } finally {  nonFairLock.unlock(); // 释放锁  }  }  }

需要注意的是,由于非公平锁的特性,它可能会导致饥饿问题,因为高优先级的线程可能会一直获取到锁,而低优先级的线程则可能长时间等待。因此,在使用非公平锁时需要谨慎考虑线程的优先级设置和资源竞争情况。

乐观锁

乐观锁会首先认为不会发生线程安全的问题,然后在做数据更新的时候去和数据库中数据的版本号做比对,来判断数据是否已经被修改。乐观锁的实现通常需要数据库的支持,例如使用版本号(version)字段来表示数据的版本。当事务开始时,将当前版本号加1,并将新的版本号写入数据库。在更新数据时,先读取当前版本号,然后比较版本号和预期版本号,如果相等,则更新数据并将版本号加1;如果不相等,则说明数据已经被其他事务修改过,此时可以抛出异常或进行其他处理。

 public class OptimisticLocker {  private static final Object lock = new Object();  private int version = 0; // 版本号  public class Entity {  private int id;  private int value;  private int version;  // 省略构造方法、getter和setter方法  }  public Entity read(int id) {  // 模拟从数据库中读取数据  Entity entity = new Entity();  entity.setId(id);  entity.setValue(value); // 假设从数据库中读取的值是10  entity.setVersion(version); // 假设从数据库中读取的版本号是0  return entity;  }  public boolean update(Entity entity) {  synchronized (lock) {  int currentVersion = entity.getVersion(); // 获取当前版本号  try {  // 模拟长时间运行的操作或其他线程修改数据的情况,可能这期间其他的事务已经将数据修改了。  Thread.sleep(1000);  Entity data = read(entity.getId);//取出数据库中的数据做版本号的比对if (currentVersion != data.getVersion()) {  System.out.println("数据已被其他线程修改,当前线程的更新失败!");  return false; // 更新失败  } else {  entity.setValue(entity.getValue() + 1); // 更新数据值  entity.setVersion(currentVersion + 1); // 更新版本号  System.out.println("当前线程的更新成功!");  return true; // 更新成功  }  } catch (InterruptedException e) {  e.printStackTrace();  }  }  return false; // 异常情况,更新失败  }  }

在这个示例中,update() 方法尝试对实体进行更新。它首先获取当前版本号,并检查版本号是否与数据库中的版本号一致。如果一致,则进行更新操作,并返回 true 表示更新成功。如果版本号不一致,则说明数据已被其他线程修改,此时返回 false 表示更新失败。这样可以确保在多个线程并发访问时,不会出现数据冲突的情况。

悲观锁

悲观锁其实是和乐观锁是相反的,悲观锁思想认为一定会发生线程安全的问题,所以不管是会不会存在线程安全的问题,都给加上锁。

原子类

在Java中,原子类主要用于实现线程安全的操作,尤其是在对共享数据进行并发访问时。这些类提供了原子操作,这些操作在多线程环境中是安全的。

Java提供了几个原子类,如AtomicIntegerAtomicLongAtomicBoolean等,它们都是java.util.concurrent.atomic包的一部分。

下面是一个使用AtomicInteger的简单示例,演示了如何使用原子类实现线程安全的计数器:

 import java.util.concurrent.atomic.AtomicInteger;  public class AtomicCounter {  private AtomicInteger counter = new AtomicInteger(0);  public void increment() {  counter.incrementAndGet();  }  public int getCount() {  return counter.get();  }  public static void main(String[] args) {  AtomicCounter counter = new AtomicCounter();  // 创建10个线程来增加计数器值  for (int i = 0; i < 10; i++) {  new Thread(() -> {  counter.increment();  }).start();  }  // 等待所有线程执行完毕  try {  Thread.sleep(5000);  } catch (InterruptedException e) {  e.printStackTrace();  }  // 输出最终计数器值  System.out.println("Final counter value: " + counter.getCount());  }  }

但此时如果将每个线程中操作数据的counter.increment(); 增加一个for循环。

 import java.util.concurrent.atomic.AtomicInteger;  public class Counter {  private AtomicInteger count = new AtomicInteger(0);  public void increment() {  count.incrementAndGet(); // 使用CAS操作原子性增加计数器  }  public int getCount() {  return count.get(); // 返回当前计数值  }  public static void main(String[] args) {  Counter counter = new Counter();  // 启动多个线程增加计数器  for (int i = 0; i < 10; i++) {  new Thread(() -> {  for (int j = 0; j < 1000; j++) {  counter.increment();  }  }).start();  }  // 等待所有线程执行完毕  try {  Thread.sleep(1000);  } catch (InterruptedException e) {  e.printStackTrace();  }  // 输出最终计数值  System.out.println("Final count: " + counter.getCount());  }  }

在多线程环境下,由于线程的调度是异步的,因此无法保证各个线程的执行顺序。在这种情况下,如果有多个线程同时尝试增加计数器的值,那么AtomicInteger会使用CAS(Compare-and-Swap)操作来确保这些操作的原子性。CAS操作会检查计数器的当前值,并与预期值进行比较,如果相等则将计数器的值增加1并返回更新后的值,否则重新尝试。由于这个操作的原子性,即使多个线程同时尝试增加计数器的值,最终的计数值仍然是正确的。但是,由于无法保证线程的执行顺序,因此可能会出现一些线程先于其他线程完成的情况,导致最终的计数值大于预期的值。

所以使用AtomicInteger可以保证原子性更新,但是它并不能直接保证线程安全。

ThreadLocal

ThreadLocal的主要目的是为了解决多线程中的数据同步问题。在多线程环境下,当多个线程同时访问共享变量时,就可能会出现数据不一致的情况。而ThreadLocal通过为每个线程提供独立的变量副本,避免了这种情况的发生。每个线程都持有自己的数据副本,互不干扰。

 public class ThreadLocalExample {  // 创建一个ThreadLocal对象来存储每个线程的执行时间  private static final ThreadLocal<Long> startTime = new ThreadLocal<>();  public static void main(String[] args) {  // 启动多个线程,每个线程执行相同的任务  for (int i = 0; i < 10; i++) {  new Thread(() -> {  // 在每个线程开始时,记录其开始时间  startTime.set(System.currentTimeMillis());  try {  // 模拟一些工作  Thread.sleep(1000);  } catch (InterruptedException e) {  e.printStackTrace();  }  // 在每个线程结束时,输出其执行时间  Long startTime = startTime.get();  long endTime = System.currentTimeMillis();  long executionTime = endTime - startTime;  System.out.println("Thread " + Thread.currentThread().getId() + " executed in " + executionTime + " ms");  }).start();  }  }  }

在这个示例中,我们使用ThreadLocal来存储每个线程的开始时间。在每个线程启动时,我们使用ThreadLocalset方法将当前时间设置为该线程的开始时间。然后,当线程结束时,我们使用get方法获取该线程的开始时间,并计算其执行时间。最后,我们输出每个线程的执行时间。由于每个线程都有自己的开始时间副本,因此计算出的执行时间是准确的,并且不会与其他线程的执行时间混淆。

嘿嘿,这是我自己的公众号,会分享一些项目管理的知识,相信工作几年后的朋友对于自己的职业规划也会有自己的认识,如果对项目管理感兴趣的朋友可以关注一下。

结语

在本文中,我们深入探讨了Java中的锁机制,从互斥锁到读写锁,再到乐观锁和悲观锁等概念。了解了各种锁的特点和代码样例,以及它们在并发编程中的重要性和作用。通过实例代码,我们分析了各种锁的实现和应用方式。

然而,尽管锁为我们提供了同步和互斥的关键机制,但在使用时仍需谨慎。过度使用或不恰当使用锁可能导致死锁、性能下降等问题。因此,在实际应用中,我们需要根据具体情况选择合适的锁类型和策略,并时刻关注锁的使用情况,确保系统的健壮性和性能。

此外,随着技术的发展,Java中的锁机制也在不断演进和优化。例如,偏向锁、元锁等新型锁的出现,为开发者提供了更多的选择和灵活性。

最后,希望本文能为你提供关于Java锁的全面知识和实践经验,帮助你在多线程编程中更加从容地应对挑战。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/615277.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python入门-面向对象

1.类和对象 是不是很熟悉&#xff1f;和Java一样&#xff0c;在Python中&#xff0c;都可以把万物看成(封装成)对象。它俩都是面向对象编程 1.1 查看对象数据类型 a 10 b 9.8 c helloprint(type(a)) print(type(b)) print(type(c))运行结果&#xff1a; D:\Python_Home\v…

k8s-调度 13

调度器通过 kubernetes 的 watch 机制来发现集群中新创建且尚未被调度到 Node 上的 Pod。调度器会将发现的每一个未调度的 Pod 调度到一个合适的 Node 上来运行。 kube-scheduler 是 Kubernetes 集群的默认调度器&#xff0c;并且是集群控制面的一部分。 如果你真的希望或者有…

C#上位机与欧姆龙PLC的通信11----【爆肝】上位机应用开发(Winform版)

1、先上图 前面10讲&#xff0c;让你爽煹了肝&#xff0c;已经进入最后收尾阶段&#xff0c;这节来个常规应用&#xff0c;让前面的技能直接飞上天&#xff0c;我们要做的界面软件是这样的&#xff0c;虽然没有潘金莲漂亮&#xff0c;但也是爆抱&#xff1a; 2、如何爆&#x…

爬虫到底违法吗?你离违法还有多远?

最近&#xff0c;国家依法查处了部分编写爬虫程序&#xff0c;盗取其他公司数据的不良企业。一时间风声鹤唳&#xff0c;关于爬虫程序是否违法的讨论遍布程序员圈子。那么到底编写爬虫程序是否违法呢&#xff1f; 其爬虫下载数据&#xff0c;一般而言都不违法&#xff0c;因为…

【机器学习】模型调参工具:Hyperopt 使用指南

机器学习| 模型调参工具&#xff1a;Hyperopt 使用指南 前言1. Hyperopt是什么&#xff1f;2. Hyperopt的优缺点3. 如何使用 Hyperopt 进行调参3.1 安装 Hyperopt3.2 构建超参数空间3.3 定义目标函数3.4 运行 Hyperopt 优化3.5 获取最优超参数 4. XGB调参代码示例参考资料 前言…

Hyperledger Fabric Docker 方式多机部署生产网络

规划网络拓扑 3 个 orderer 节点&#xff1b;组织 org1 , org1 下有两个 peer 节点&#xff0c; peer0 和 peer1; 组织 org2 , org2 下有两个 peer 节点&#xff0c; peer0 和 peer1; 因为我只有 3 台虚拟机资源所以没法实现完全的多机部署&#xff0c;资源使用规划如下&#…

智能分析网关V4方案:太阳能+4G+AI识别搭建智慧果园/种植园远程视频监控监管方案

一、方案背景 我国是水果生产大国&#xff0c;果园种植面积大、产量高。由于果园的位置大都相对偏远、面积较大、看守人员较少&#xff0c;值守的工作人员无法顾及园区每个角落&#xff0c;果园财产安全成为了关注的重点。人为偷盗、野生生物偷吃等事件时有发生&#xff0c;并…

磷酸铁锂电池生产污废水需要哪些工艺及设备

磷酸铁锂电池作为一种常见的锂离子电池&#xff0c;已广泛应用于电动汽车、储能系统等领域。然而&#xff0c;在磷酸铁锂电池的生产过程中&#xff0c;难免会产生一定量的污废水。为了有效处理和处理这些污废水&#xff0c;我们需要合适的工艺和设备。 首先&#xff0c;针对磷酸…

制造业卓越管理从6S管理开始

在当今制造业环境中&#xff0c;企业间的竞争愈发激烈&#xff0c;而管理水平的卓越与否&#xff0c;直接决定了企业在市场竞争中的地位和成功与否。管理并非一蹴而就&#xff0c;需要经过不断的实践和探索&#xff0c;逐步提升。其中&#xff0c;6S管理作为现代企业管理的基础…

Ubuntu 卸载重装 Nvidia 显卡驱动

问题描述 我使用 airsim 的时候&#xff0c;发现 UE4 没法使用显卡&#xff0c;导致非常卡顿 输入 nvidia-smi 有显卡型号等信息的输出&#xff0c;但是进程 process 里面没有显示 airsim 和其他软件占用显卡情况 因此&#xff0c;我选择了卸载重装 一.卸载旧版本的驱动 …

内容分发功能升级!一站式搞定文案生成/违规检测/一键分发全流程

随着社交媒体的不断发展&#xff0c;越来越多的企业开始布局新媒体矩阵&#xff0c;从集团总部到区域门店、个人销售&#xff0c;从全品类到细分垂直类目、从单一平台到多平台&#xff0c;试图让品牌影响力覆盖更广泛群体&#xff0c; 当然&#xff0c;随之而来的&#xff0c;如…

深度学习”和“多层神经网络”的区别

在讨论深度学习与多层神经网络之间的差异时&#xff0c;我们必须首先理解它们各自是什么以及它们在计算机科学和人工智能领域的角色。 深度学习是一种机器学习的子集&#xff0c;它使用了人工神经网络的架构。深度学习的核心思想是模拟人脑神经元的工作方式&#xff0c;以建立…

RuntimeError: Placeholder storage has not been allocated on MPS device!解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

Web安全测试基础

SQL注入 当下最常用的一个攻击手段&#xff0c;就是通过SQL命令插入到Web表单中或页面请求查询字符串中&#xff0c;最终达到欺骗服务器执行恶意的SQL语句的目的&#xff0c;SQL注入一旦成功&#xff0c;轻则直接绕开服务器验证&#xff0c;直接登录成功&#xff0c;重则将服务…

【Flutter 开发实战】Dart 基础篇:条件循环表达式

Dart 是一门强大的编程语言&#xff0c;其条件和循环表达式在编写灵活、高效代码中起着关键作用。本文将深入介绍 Dart 中的条件表达式、循环表达式、以及关键字如 for、while、do...while、break、continue 等内容&#xff0c;旨在为初学者提供清晰的指导。 1. 条件表达式 D…

Netty开篇——BIO章(二)

Java BIO 基本介绍 属于传统的 java io编程&#xff0c;内容见java.io包中BIO(blocking I/O) : 同步阻塞&#xff0c;服务器实现模式为个连接一个线程&#xff0c;即客户端有连接请求时服务器端就需要启动一个线程进行处理&#xff0c;如果这个连接不做任何事情会造成不必要的线…

数字化转型的关键:JVS低代码的列表页导入功能解析

在当今数字化时代&#xff0c;数据的管理和迁移变得至关重要。为了满足广大用户的需求&#xff0c;JVS低代码精心打造了“列表页导入功能”。这个功能专为高效处理列表页数据而设计&#xff0c;为用户提供了简单、便捷的数据导入、导出和模板下载服务。 列表页导入功能 列表页…

1、理解Transformer:革新自然语言处理的模型

目录 一、论文题目 二、背景与动机 三、卖点与创新 四、解决的问题 五、具体实现细节 0. Transformer 架构的主要组件 1. 注意力、自注意力&#xff08;Self-Attention&#xff09;到多头注意力&#xff08;Multi-Head Attention&#xff09; 注意力到底是做什么的&…

基于JAVA+ssm智能旅游线路规划系统设计与实现【附源码】

基于JAVAssm智能旅游线路规划系统设计与实现【附源码】 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql…

Neo4j恢复

主要记录windows环境下从备份文件中恢复Neo4j&#xff0c; Linux环境同理 备份在上一篇中有介绍&#xff0c;参考: Neo4j备份-CSDN博客 误删数据 为了模拟误删除场景&#xff0c;我们查询Person&#xff0c;并模拟误操作将其进行删除&#xff1b; match(p:Person) return …