1、什么是公平锁和非公平锁
公平锁和非公平锁是指在多线程环境下,如何对锁进行获取的顺序和策略的不同。
公平锁是指多个线程按照申请锁的顺序来获取锁,即先到先得的策略。当一个线程释放锁之后,等待时间最长的线程将获得锁。公平锁的优点是保证了每个线程的公平性,不存在饥饿现象,但是由于需要维护一个等待队列,因此会增加系统的开销。
非公平锁是指多个线程获取锁的顺序是不确定的,不一定按照申请锁的顺序来获取锁。当一个线程释放锁之后,锁的获取是由系统的调度算法来决定的。非公平锁的优点是可以减少线程上下文切换的开销,提高系统的吞吐量,但是容易出现饥饿现象,即某些线程可能会一直获取不到锁。
一般来说,公平锁适用于对线程公平性要求比较高的场景,而非公平锁适用于对性能要求比较高的场景。但是在具体应用时,需要根据实际情况来选择合适的锁。
● 公平锁:是指多个线程按照申请的顺序来获取值。在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否者就会加入到等待队列中,以后会按照 FIFO 的规则获取锁
● 非公平锁:是指多个线程获取值的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在并发环境中一上来就尝试占有锁,如果失败再进行排队,可能会造成优先级翻转或者饥饿现象
// 常用的ReentrantLock无参构造默认是非公平锁
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、Java中的公平锁和非公平锁
Java中的公平锁和非公平锁主要有以下几种:
ReentrantLock的公平锁和非公平锁:ReentrantLock是Java中常用的锁实现类之一,它提供了公平锁和非公平锁两种模式。在创建ReentrantLock对象时,可以通过构造函数传入一个布尔类型的fair参数来指定锁的模式。如果fair为true,则创建公平锁;如果fair为false,则创建非公平锁。
ReentrantReadWriteLock的公平锁和非公平锁:ReentrantReadWriteLock是一个读写锁,它也提供了公平锁和非公平锁两种模式。在创建ReentrantReadWriteLock对象时,可以通过构造函数传入一个布尔类型的fair参数来指定锁的模式。如果fair为true,则创建公平锁;如果fair为false,则创建非公平锁。
StampedLock的乐观锁和悲观锁:StampedLock是Java 8中新增的一种锁实现类,它提供了乐观锁和悲观锁两种模式。在使用StampedLock时,可以通过调用tryOptimisticRead()方法来获取乐观锁,或者通过调用readLock()方法来获取悲观锁。乐观锁是一种无锁的机制,它不会阻塞线程,但是需要通过validate()方法来检查锁是否仍然有效。
Synchronized的非公平锁:Synchronized是Java中内置的锁机制,它默认采用非公平锁模式。在使用Synchronized时,如果多个线程同时请求锁,则会根据系统的调度算法来决定哪个线程获取锁。
公平锁和非公平锁的性能和效率都会受到多种因素的影响,如锁的争用情况、线程的数量、系统的负载等。在实际应用中,需要根据具体情况选择合适的锁模式。
3、为什么Synchronized是非公平锁
Synchronized是Java中内置的锁机制,它的锁模式是非公平锁,这是因为Synchronized的实现方式是基于对象监视器(monitor)的。
在Java中,每个对象都有一个与之关联的monitor,当一个线程需要获取某个对象的锁时,它会首先尝试获取这个对象关联的monitor。如果monitor已经被其他线程占用,那么这个线程就会进入monitor的等待队列中,等待其他线程释放锁。
在Synchronized中,当一个线程释放锁时,JVM会从等待队列中随机选择一个线程来获取锁,而不是按照申请锁的顺序来获取锁,因此Synchronized是一种非公平锁。这种实现方式的优点是可以减少线程上下文切换的开销,提高系统的吞吐量,但是容易出现饥饿现象,即某些线程可能会一直获取不到锁。
Synchronized也可以通过在代码中使用wait()和notify()方法来实现公平锁,但是这种方式比较复杂,容易出错,而且性能也不如ReentrantLock的公平锁实现方式。因此,在实际应用中,如果需要公平锁,建议使用ReentrantLock。
4、Synchronized代码示例
下面是一个简单的Synchronized使用示例:
4.1、修饰方式
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample example = new SynchronizedExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + example.count);
}
}
输入结果如下:
在这个示例中,Synchronized被用于修饰increment()方法,这样在同一时刻只有一个线程可以执行该方法。这个示例中创建了两个线程,分别对count变量进行自增操作。由于Synchronized的作用,这些操作是互斥的,所以最终输出的count值应该是20000。
4.2、修饰代码块
除了修饰方法,Synchronized还可以修饰代码块。下面是一个使用Synchronized修饰代码块的示例:
public class SynchronizedExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample example = new SynchronizedExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + example.count);
}
}
在这个示例中,Synchronized被用于修饰代码块,这样在同一时刻只有一个线程可以执行该代码块。在这个示例中,lock对象被用于作为Synchronized锁的对象。
5、总结
需要注意的是,Synchronized锁的是对象,而不是代码。在同一个对象上使用Synchronized修饰的代码块或方法是互斥的,但不同对象上的Synchronized代码块或方法并不互斥。
Synchronized虽然可以保证线程安全,但是它会导致性能问题。在使用Synchronized时需要注意以下几点:
尽量缩小Synchronized的作用范围,只在必要的代码块或方法上使用Synchronized。
尽量不要在Synchronized代码块或方法中调用其他耗时的方法,这会导致其他线程长时间等待。
尽量使用Lock接口代替Synchronized,因为Lock接口提供了更灵活的锁定机制。