1:synchronized和ReentrantLock的区别
1:含义不同
Synchronized 是关键字,属于 JVM 层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成;
Lock 是 java.util.concurrent.locks.lock 包下的,是 JDK1.5 以后引入的新 API 层面的锁;
2:使用方法不同
Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁;ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁;
3:等待是否可以中断
Synchronized 不可中断,除非抛出异常或者正常运行完成;ReentrantLock 可以中断。一种是通过 tryLock (long timeout, TimeUnit unit),另一种是 lockInterruptibly () 放代码块中,调用 interrupt () 方法进行中断;
4:加锁是否公平
Synchronized 是非公平锁;ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁;
2:锁的分类
3:乐观锁和悲观锁
1:悲观锁
悲观锁:synchronized和lock接口
适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗。典型情况:
①临界区有IO操作
②临界区代码复杂或循环量大
③临界区竞争非常激烈
2:乐观锁
乐观锁:典型例子就是原子类、并发容器等
适合并发写入少,大部分是读取的场景,不加锁的能让读取必能大幅提高。
4:可重入锁和非可重入锁
可重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
lock/reentrantlock/GetHoldCount.java
package lock.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
public class GetHoldCount {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
}
在递归调用中可重入锁的用法
lock/reentrantlock/RecursionDemo.java
package lock.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println("已经对资源进行了处理");
if (lock.getHoldCount()<5) {
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
5:公平锁和非公平锁
1:概述
公平指的是按线程请求的顺序来分配锁
非公平是指不完全按请求的顺序,在一定情况下可以插队。
2:为什么会设计非公平锁
为提高效率,避免唤醒带来的空档期
例如排队买火车票:早期买火车票需要通宵排队买票,如果我排在第2个位置,在开始卖票时,第一个位置的人买完票后轮到我买,但因为排了一个通宵,我有点不清醒发呆中(线程从阻塞状态唤醒),此时第一个人突然又回来去问下发车的时间,这时可以让第一个人插队,因为他的插队并不影响我买票,我还处于慢慢变清醒状态中。
在创建ReentrantLock对象时,参数填写为true,那么这就是个公平锁。
lock/reentrantlock/FairLock.java
package lock.reentrantlock;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 演示公平和不公平两种情况
*/
public class FairLock {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
//10个线程去打印
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
//启动10个线程
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}
class PrintQueue {
private Lock queueLock = new ReentrantLock(false);
//每份文档打印两份,模拟线程在完成第一次打印后,插队打印第二次
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
6:自旋锁和阻塞锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态完成,这种状态转换需要消耗处理器时间。
如果同步代码块中内容简单,状态转换消耗时间有可能比用户代码执行时间还长。
在两个或以上线程同时并行执行时,可以让请求锁的线程不放弃CPU的执行时间,看持有锁的线程是否很快会释放锁。为让当前线程等一下,需要让当前线程进行自旋,如果在自旋完成后,锁定同步资源的线程释放锁,则当前线程就可以不必阻塞而是直接获取同步资源,避免切换线程的开销。
1:自旋锁的实现原理
是lock方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
2:自旋锁存在的问题
如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
7:锁优化
1:减少锁持有时间
2:减少锁粒度
将大对象拆分成小对象,增加并行度,降低锁竞争。
ConcurrentHashMap允许多个线程同时进入
3:锁分离
根据功能进行锁分离
ReadWriteLock在读多写少时,可以提高性能。
4:锁消除
锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。
5:锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是在某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。