一、概述
synchronized关键字是多线程安全十分重要的关键字。它解决的是多个线程之间访问资源的同步性,它可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
二、synchronized 关键字
2.1、 synchronized 关键字背景理解
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
2.2、与实俱进-synchronized 关键字在项目中的使用
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
消息队列可以将消费者持久化后的消息,在通过消费服务进行取出后作为参数传递给具体业务服务,比如用户阅读教程后赠送金币。由于是多个线程在跑,那么同一个时刻就只能一个线程处理存放参数,不然就会导致业务参数重复,因此需要将存放参数的方法进行同步。
public synchronized void putParameterValues(Object messageParameter) {
if (messageParameter !=null) {
parameterValues.add(messageParameter);
}
}
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。对消息对象进行加锁,保证同一时刻就一个线程处理该对象。
mq = MessageQueue.consume();
if (mq !=null) {
synchronized (mq) {
log.info("消息[" + mq.getMessageId() +"]执行消费开始......");
storage.updateHandleStatus(**, **,"");
|
2.3、synchronized 关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面
synchronized 修饰代码块的底层原理
public class SynchronizedDemo{
public void method() {
synchronized(this) {
System.out.println("synchronized修饰 代码块");
}
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class。
synchronized 修饰方法的底层原理
public class SynchronizedDemo2{
public synchronized void method2() {
System.out.println("synchronized 修饰方法");
}
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
2.4、Synchronized和ReentrantLock 的区别
在讲解区别之前,先说说ReentrantLock,在消息队列我们使用到ReentrantLock,具体为消息生产服务生产消息(将接受的消息放到消息队列),使用它可以保证在锁上等待时间最长的线程将获得锁的使用权。
可重入性
两者都是可重入锁。
锁的实现
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
性能的区别
ReentrantLock 比 synchronized 增加了一些高级功能:相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。
④ 性能已不是选择标准
重要词句剖析:
重量级锁:
可重入锁:相当于以家庭为单位排队吃火锅,只要家庭中的一个成员排到号了,家庭中的成员自动也为该号。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
ReentrantLock的使用场景:
场景1:如果已加锁,则不再重复加锁
a、忽略重复加锁。
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。
以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)
private ReentrantLock lock =new ReentrantLock();
if (lock.tryLock()) {// 如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
try {
// 操作
}finally {
lock.unlock();
}
}
场景2:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
这种其实属于场景1的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。
public class TimeLock implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
Thread.sleep(6 * 1000);
}else {
System.out.println(Thread.currentThread().getName()+" get Lock Failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 查询当前线程是否保持此锁。
if (lock.isHeldByCurrentThread()) {
System.out.println(Thread.currentThread().getName()+" release lock");
lock.unlock();
}
}
}
/**
* 在本例中,由于占用锁的线程会持有锁长达6秒,故另一个线程无法再5秒的等待时间内获得锁,因此请求锁会失败。
*/
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
Thread t1 = new Thread(timeLock, "线程1");
Thread t2 = new Thread(timeLock, "线程2");
t1.start();
t2.start();
}
}
上述例子中,由于占用锁的线程会持有锁长达 6 秒,故另一个线程无法在 5 秒的等待时间内获得锁,因此,请求锁失败。ReentrantLock.tryLock()方法也可以不带参数直接运行。这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁成功,立即返回 true。否则,申请失败,立即返回 false,当前线程不会进行等待。这种模式不会引起线程等待,因此也不会产生死锁。
场景3:如果发现该操作已经加锁,则等待一个一个加锁(同步执行,类似synchronized)
这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是非公平的。
这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)
ReentrantLock默认情况下为不公平锁
private ReentrantLock lock =new ReentrantLock();//参数默认false,不公平锁
private ReentrantLock lock =new ReentrantLock(true);//公平锁
try {
lock.lock();//如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
//操作
}finally {
lock.unlock();
}
不公平锁与公平锁的区别:
公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)
场景4:可中断锁
synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景3的另一种改进,没有超时,只能等待中断或执行完毕)
这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)
try {
final ReentrantLock lock =this.putLock;
lock.lockInterruptibly();
//操作
//消息列表里面放生产出来的产品,由于比较多,所有为了防止不正当操作产时间造成阻塞,使用可中断锁操作。
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}