首先synchronized是jdk的一个关键字,ReentrantLock是java.util.concurrent.locks并发包下的一个类。
1.从公平与非公平角度来看
什么是公平锁与非公平锁:
公平锁与非公平锁都会维护一个队列,在公平锁中,新来的线程一定会进入队列的尾部,直到轮到自己拿到锁,他能保证每个线程都能执行。而非公平锁中,多个线程抢锁时,获取锁的线程不一定是同步队列中等待时间最长的线程,有可能是同步队列之外的线程先抢到锁,因为当线程来到它会首先尝试能否获取倒锁,减少了进入队列被唤醒一次开销,因为进入同步队列的线程都会被阻塞,而阻塞到被唤醒,需要操作系统的介入是比较消耗系统资源的。所以一般情况下我们使用非公平锁即可。
公平锁:
非公平锁:
synchronized只能实现非公平
而ReentrantLock既可以实现公平和非公平,它默认是非公平的。它有一个构造方法当参数为true即可变为公平锁。
2.从锁的对象来看
synchronized当修饰静态方法或者当同步代码块中为xx.class的时候都是锁的当前XX类的所有实例
synchronized修饰普通方法的时候锁的是当前实例对象
synchronized修饰同步代码块的时候锁的是当前代码块里面的对象。
ReentrantLock.lock()没有参数, 不像synchronized(xx)可以指定被锁定的对象,那么它锁的就是本身自己。
证明锁的是本身自己的一个例子:
public class Test {
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
demo1.count();
}
}).start();
}
}
}
class Demo1 {
private int a = 0;
ReentrantLock reentrantLock = new ReentrantLock();
public void count() {
reentrantLock.lock();
System.out.println("执行a加操作");
a++;
System.out.println("a加操作执行完毕");
reentrantLock.unlock();
}
}
当吧Demo1实例放在for循环外面打印结果: 都是成对出现
当吧Demo1实例放在for循环里面打印结果:
可见锁的都是当前对象实例
3.从性能上看
从性能来看,两者需要再不同场景下来区别,网上有许多博主也用例子证明了两者性能,我这里就给出一个结论:在高并发下使用synchronized会打满cpu,从而反作用于获得锁的线程,在低并发,特别同时是轻量化的操作,synchronized可能可以获得更好的性能。现实情况中虽然我们可能有几百个线程,但是大多数情况下,对于共享资源的修改,同时只有几个或者十几个,那么使用synchronized也不失为一种好选择。
4.从是否需要手动释放锁
synchronized是独占锁即锁的期间只允许一条线程进入,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
5.从内部实现核心来看
synchronized在1.6之前是重量级锁,直接与操作系统打交道,性能很差,而在1.6之后呢对其做了大量的优化,进行一个锁升级的一个过程。 从偏向锁->轻量级锁(也叫自旋锁,CAS)->重量级锁 。
jvm认为大多数情况下你的这把锁竞争性不太强,我就举一个占坑的一个例子,一个厕所只有一个坑位,当第一个人来,肠胃很好,很顺畅的拉出来走人,此时第二个人也恰好来到这,直接顺利就进去了,没有一个等待,抢厕所的操作,后续的人皆是如此。这时候上的就是偏向锁,效率很高。但是咱们的女厕所就不一样了(你懂的),人越来越多,大家都在抢这个厕所,此时锁升级为自旋锁,它采用CAS这种无锁算法。何时自旋锁升级为重量级锁,大家知道自旋的线程多了,是非常占用cpu资源的,当自旋的线程到达某个值时,这个值jvm帮你自动设定的,它会升级为重量级锁。 然后操作系统直接不让他们自旋了,扔到这把锁的等待队列中,这时候的线程是不消耗cpu资源的,好比让他们排好队一个个上厕所。
而ReentrankLock内部采用的是AQS(AbstractQuenedSynchronizer)实现的,AQS里边采用的是双向链表,即如果当前线程未获取到锁将会加入到链表中。 公平锁采用判断当前Node是不是头结点,如果是的话就获取锁并做业务处理,不是头结点的不能获取锁。非公平锁没有判断当前结点,采用CAS,谁第一个拿到了state=0,则视为获取锁,并把state设置为1。
总的来说ReentrantLock功能更强大:
- ReentrantLock 提供了一种能够中断等待锁线程的机制,通过 lock.lockInterruptibly() 来实现这个机制,也就是说正在等待的线程可以选择放弃等待,改为处理其他事情,而synchronize是不能响应中断的;
- ReentrantLock 可以指定是公平锁还是非公平,而 synchronized 只能是非公平锁
- ReentrantLock 还提供了等待锁,即满足一定时间后还没获取到锁,便放弃获取锁。
它其中比较常用的方法:
1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁 . 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
2. boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false . 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
3. void unlock():执行此方法时, 当前线程将释放持有的锁 . 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
4. Condition newCondition(): 条件对象,获取等待通知组件 。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
5. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个 线程获得锁,此时返回的是 9
7. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件 (condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
10. hasQueuedThreads():是否有线程等待此锁
11. isFair():该锁是否公平锁
12. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分 别是 false 和 true
13. isLock():此锁是否有任意线程占用
14. lockInterruptibly():如果当前线程未被中断,获取锁
15. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
16. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持, 则获取该锁