字节跳动大厂面试题详解:java中有哪些类型的锁

  • 作者简介:一名后端开发人员,每天分享后端开发以及人工智能相关技术,行业前沿信息,面试宝典。

  • 座右铭:未来是不可确定的,慢慢来是最快的。

  • 个人主页:极客李华-CSDN博客

  • 合作方式:私聊+

  • 这个专栏内容:BAT等大厂常见后端java开发面试题详细讲解,更新数目100道常见大厂java后端开发面试题。

字节跳动大厂面试题详解:java中有哪些类型的锁

Java中的锁类型及详解

在Java中,锁是用来控制对共享资源的访问的机制。它们提供了多线程环境下的同步和互斥,以确保线程安全性。Java中有多种类型的锁,包括对象锁、类锁、读写锁、自旋锁等。

1. 对象锁(Synchronized)

对象锁是Java中最基本的锁类型之一,使用关键字 synchronized 来实现。它可以用于同步对对象实例方法和代码块的访问。

示例代码:
public class SynchronizedExample { private int count = 0;
    // 对象实例方法使用对象锁
    public synchronized void increment() { count++;
    }
    // 对象实例方法也可以使用代码块来加锁
    public void decrement() { synchronized (this) { count--;
        }
    }
}
2. 类锁(Synchronized)

类锁与对象锁类似,但是作用于类的所有实例。使用 synchronized 关键字修饰静态方法或者通过 Class 对象实现。

示例代码:
public class ClassLockExample { private static int count = 0;
    // 静态方法使用类锁
    public static synchronized void increment() { count++;
    }
    // 通过Class对象实现类锁
    public void decrement() { synchronized (ClassLockExample.class) { count--;
        }
    }
}
3. 读写锁(ReentrantReadWriteLock)

读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这提高了读操作的并发性能。

import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample { private int value = 0;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    // 读取操作
    public void read() { lock.readLock().lock(); // 获取读锁
        try { System.out.println("Read value: " + value); // 输出当前值
        } finally { lock.readLock().unlock(); // 释放读锁
        }
    }
    // 写入操作
    public void write(int newValue) { lock.writeLock().lock(); // 获取写锁
        try { value = newValue; // 更新值
            System.out.println("Write value: " + value); // 输出更新后的值
        } finally { lock.writeLock().unlock(); // 释放写锁
        }
    }
}

在上面的代码中,我使用ReentrantReadWriteLock实现了一个简单的读写锁示例。这个示例包括一个私有变量value用于存储数据,以及一个ReentrantReadWriteLock对象lock用于管理并发访问。

  • read() 方法用于读取数据。它首先获取读锁,然后输出当前的value值,并最终释放读锁。
  • write(int newValue) 方法用于写入数据。它首先获取写锁,然后更新value的值为newValue,输出更新后的值,并最终释放写锁。

    通过使用读写锁,我可以实现对共享资源的并发访问控制,提高了程序的并发性能。

    4. 自旋锁(Spin Lock)

    自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。

    import java.util.concurrent.atomic.AtomicBoolean;
    /**
     * 自旋锁是一种基于循环等待的锁,线程在获取锁时不会被挂起,而是不断地尝试获取锁。
     */
    public class SpinLockExample { private AtomicBoolean locked = new AtomicBoolean(false);
        /**
         * 获取锁的方法
         */
        public void lock() { // 使用自旋方式尝试获取锁
            while (!locked.compareAndSet(false, true)) { // 如果获取失败,继续尝试获取
                // 在高并发情况下,可能会导致线程长时间处于自旋状态,消耗CPU资源
            }
        }
        /**
         * 释放锁的方法
         */
        public void unlock() { // 释放锁,将锁状态设置为false
            locked.set(false);
        }
    }
    

    在上述代码中,我实现了一个简单的自旋锁(Spin Lock)示例。自旋锁使用了AtomicBoolean来表示锁的状态,false表示锁未被持有,true表示锁已被持有。

    • lock() 方法用于获取锁。它使用了自旋的方式来尝试获取锁,不断地循环检查锁的状态,直到成功获取锁。
    • unlock() 方法用于释放锁。它将锁的状态设置为false,表示锁已被释放。

      自旋锁的优势在于避免了线程的上下文切换,适用于短时间内持有锁的情况。然而,自旋锁可能会导致线程长时间处于忙等待状态,消耗CPU资源,因此在实际应用中需要谨慎使用。

      5. 重入锁(ReentrantLock)

      重入锁是一种与synchronized相似的锁,但提供了比synchronized更多的灵活性和功能,例如可中断锁、公平性等。

      import java.util.concurrent.locks.ReentrantLock;
      /**
       * ReentrantLock是Java并发包提供的可重入锁实现,允许同一个线程多次获取同一把锁。
       */
      public class ReentrantLockExample { private int count = 0;
          private ReentrantLock lock = new ReentrantLock();
          /**
           * 对计数器进行加一操作
           */
          public void increment() { lock.lock(); // 获取锁
              try { count++; // 对计数器进行加一操作
              } finally { lock.unlock(); // 释放锁
              }
          }
      }
      

      在上述代码中,我展示了使用ReentrantLock实现的一个简单示例。这个示例包括一个私有计数器count和一个ReentrantLock对象lock。

      • increment() 方法用于对计数器进行加一操作。在方法执行过程中,首先通过lock()方法获取锁,然后对计数器进行加一操作,最后通过unlock()方法释放锁。

        ReentrantLock是Java并发包提供的可重入锁实现,允许同一个线程多次获取同一把锁。相比于synchronized关键字,ReentrantLock提供了更多的锁定操作和更灵活的控制,适用于更复杂的并发场景。

        Java中锁的应用场景和详细案例

        对象锁的应用场景

        对象锁通常用于保护对对象实例的访问,例如多个线程对同一个对象进行操作时,可以使用对象锁确保线程安全。

        示例代码:
        public class ObjectLockExample { private int count = 0;
            // 对象实例方法使用对象锁
            public synchronized void increment() { count++;
            }
        }
        
        类锁的应用场景

        类锁作用于类的所有实例,常用于控制对静态变量的访问,或者对静态方法的调用。

        示例代码:
        public class ClassLockExample { private static int count = 0;
            // 静态方法使用类锁
            public static synchronized void increment() { count++;
            }
        }
        
        读写锁的应用场景

        读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。适用于读操作远远多于写操作的场景。

        示例代码:
        import java.util.concurrent.locks.ReentrantReadWriteLock;
        public class ReadWriteLockExample { private int value = 0;
            private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
            public void read() { lock.readLock().lock();
                try { System.out.println("Read value: " + value);
                } finally { lock.readLock().unlock();
                }
            }
            public void write(int newValue) { lock.writeLock().lock();
                try { value = newValue;
                    System.out.println("Write value: " + value);
                } finally { lock.writeLock().unlock();
                }
            }
        }
        
        自旋锁的应用场景

        自旋锁适用于锁保护时间短、线程竞争不激烈的情况。它避免了线程挂起和恢复的开销,适用于多核CPU并发度高的场景。

        示例代码:
        import java.util.concurrent.atomic.AtomicBoolean;
        public class SpinLockExample { private AtomicBoolean locked = new AtomicBoolean(false);
            public void lock() { while (!locked.compareAndSet(false, true)) { // 自旋等待锁释放
                }
            }
            public void unlock() { locked.set(false);
            }
        }
        
        重入锁的应用场景

        重入锁提供了比synchronized更多的灵活性和功能,例如可中断锁、公平性等。适用于复杂的同步需求场景。

        示例代码:
        import java.util.concurrent.locks.ReentrantLock;
        public class ReentrantLockExample { private int count = 0;
            private ReentrantLock lock = new ReentrantLock();
            public void increment() { lock.lock();
                try { count++;
                } finally { lock.unlock();
                }
            }
        }
        

        Java中锁的性能比较和最佳实践

        锁的性能比较

        在选择锁时,除了考虑功能和应用场景外,性能也是一个重要因素。不同类型的锁在性能上有所差异,因此需要根据具体情况进行选择。

        • 对象锁(Synchronized): JVM对Synchronized进行了优化,性能较高。但是,它是一种悲观锁,可能会导致线程阻塞和上下文切换。
        • 重入锁(ReentrantLock): 提供了比Synchronized更多的功能,例如可中断锁、公平性等。但是,它的性能略低于Synchronized。
        • 读写锁(ReentrantReadWriteLock): 适用于读操作远远多于写操作的场景,可以提高读操作的并发性能。
        • 自旋锁(SpinLock): 适用于锁保护时间短、线程竞争不激烈的情况,避免了线程挂起和恢复的开销。但是,如果锁保护时间过长或线程竞争激烈,会导致CPU消耗过多。
        • StampedLock: Java 8引入的新型锁,适用于读多写少的场景,性能优于ReentrantReadWriteLock。
          锁的最佳实践
          • 选择合适的锁类型: 根据具体场景选择合适的锁类型,避免过度同步。
          • 精细化锁的粒度: 尽量缩小锁的范围,以减少锁的竞争,提高并发性能。
          • 避免死锁: 设计良好的锁顺序,避免出现死锁情况。
          • 合理使用锁的超时和中断功能: 在获取锁时可以设置超时时间,避免线程长时间等待,提高系统的响应性。
          • 使用局部变量和线程封闭: 尽量使用局部变量和线程封闭的方式,避免共享资源的竞争。
          • 优化并发数据结构: 使用Java并发包提供的并发数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等,避免自己实现复杂的同步逻辑。
            示例代码
            import java.util.concurrent.locks.Lock;
            import java.util.concurrent.locks.ReentrantLock;
            public class LockExample { private int count = 0;
                private Lock lock = new ReentrantLock();
                public void increment() { lock.lock();
                    try { count++;
                    } finally { lock.unlock();
                    }
                }
            }
            

            以上示例代码展示了如何使用重入锁(ReentrantLock),是一种性能较好且灵活的锁实现方式,适用于大多数并发场景。

            Java并发编程锁的常见文体

            1. 竞态条件(Race Conditions)

            竞态条件是多线程环境下常见的问题,当多个线程同时访问共享资源,并且对资源的访问顺序产生依赖时,可能导致不确定的结果。

            解决方案:

            • 使用锁来保护共享资源,确保同一时间只有一个线程访问。
            • 使用原子类(Atomic类)来实现原子操作,避免非线程安全操作。
              2. 死锁(Deadlocks)

              死锁是指两个或多个线程被无限期地阻塞,彼此等待对方释放资源,从而无法继续执行的情况。

              解决方案:

              • 设计良好的锁顺序,避免出现循环等待的情况。
              • 使用tryLock()方法来避免死锁,及时释放已经获取的锁。
                3. 上下文切换(Context Switching)

                多线程之间的切换会带来上下文切换的开销,尤其是在多核CPU上,上下文切换可能成为性能瓶颈。

                解决方案:

                • 减少锁的粒度,尽量缩小同步代码块的范围,减少锁竞争。
                • 使用无锁数据结构,减少对共享资源的争用。
                  4. 内存可见性(Memory Visibility)

                  在多线程环境下,如果一个线程对共享变量的修改对另一个线程是不可见的,可能导致意想不到的结果。

                  解决方案:

                  • 使用volatile关键字来保证变量的可见性。
                  • 使用synchronized关键字或ReentrantLock来保证线程间的内存可见性。
                    5. 并发集合的安全性

                    Java提供了许多并发安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等,但是在特定场景下仍需注意安全性。

                    解决方案:

                    • 选择合适的并发集合类,并了解其特性和限制。
                    • 使用迭代器时,注意遍历过程中集合的修改操作,避免ConcurrentModificationException异常。

                      如果大家觉得有用的话,可以关注我下面的微信公众号,极客李华,我会在里面更新更多行业资讯,企业面试内容,编程资源,如何写出可以让大厂面试官眼前一亮的简历等内容,让大家更好学习编程,我的抖音,B站也叫极客李华。大家喜欢也可以关注一下我会定期更新作为一个新人如何回答面试题