在java开发中,使用锁的场景有很多,有时候我们必须保证某块业务的原子性操作,所以就有可能需要用到的锁。所谓锁就是独享一块内存和CPU计算空间。在java代码中,涉及到使用锁的有几个关键词synchronized、lock、volatile。本节我们就来梳理梳理这几种方法的使用和区别。
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“。
synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized的使用方式有三种:代码块、成员方法、静态方式
1、作用在代码块上:
synchronized(this) {if(null == this.searchByOrigPhone(encryPhone)) {//存在相同的openId,先删除原来的信息this.deleteByOpenId(mUser.getOpenId());this.save(mUser.encryptSelf()); };}
2、作用在成员方法上:
public synchronized void method(){
// 代码
}
3、作用在静态方法上:
public static synchronized void method(){// 代码
}
synchronized的特点:
原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
可见性:保证共享变量的修改能够及时可见。执行 synchronized时,会对应执行 lock 、unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。
有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。
悲观锁:synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。
非公平锁:synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许线程发出请求后立即尝试获取锁。
可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。
Lock是个接口,有个实现类是ReentrantLock。Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。
接口源码:
public interface Lock {/*** 获取锁,调用该方法的线程会获取锁,当获取到锁之后会从该方法但会*/void lock();/*** 可响应中断。即在获取锁的过程中可以中断当前线程*/void lockInterruptibly() throws InterruptedException;/*** 尝试非阻塞的获取锁,调用该方法之后会立即返回,如果获取到锁就返回true否则返回false*/boolean tryLock();/*** 超时的获取锁,下面的三种情况会返回* ①当前线程在超时时间内获取到了锁* ②当前线程在超时时间内被中断* ③超时时间结束,返回false*/boolean tryLock(long time, TimeUnit unit) throws InterruptedException;/*** 释放锁*/void unlock();/*** 获取等待通知组件,该组件和当前锁绑定,当前线程只有获取到了锁才能调用组件的wait方法,调用该方法之后会释放锁*/Condition newCondition();
}
Lock的使用:
Lock lock = new ReentrantLock();
lock.lock();
try{//处理任务
}catch(Exception ex){}finally{lock.unlock(); //释放锁
}
volatile是Java提供的一种轻量级的同步机制。与synchronized修饰方法、代码块不同,volatile只用来修饰变量。并且与synchronized、ReentrantLock等重量级锁不同的是,volatile更轻量级,因为它不会引起线程上下文的切换和调度。
volatile的使用,以单例为例:
public class Singleton {// 使用volatile修饰,赋值后,其他线程能立即感知到private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}}
各种同步机制的关系和区别:
原理:
synchronized关键字属于JVM层面的,通过monitorenter,monitorexit指令实现,底层是通过monitor对象来完成,其实wait notify等方法也依赖monitor对象,只有同步块或者同步方法中才能调用wait,notify等方法;
Lock是具体类(java.utils.concurrent.locks.Lock)是API层面的锁
使用方法:
synchronized不许要显式手动的获得和释放锁,系统会自动让线程释放对锁的占用
ReectrantLock:需要显式的获得和释放锁,如果没有正确的释放锁,有可能导致死锁;
等待是否可以中断
synchronized不可以中断,除非抛出异常或者正常运行完成
ReentrantLock可以中断,1.设置超时时间tryLock(timeout),2.lockInterruptibly放在代码块中,调用interrupter()方法可以中断
加锁是否公平:
synchronzed是非公平锁
ReentrantLock两者都可以,默认为非公平锁,构造方法传入boolean值,true为公平锁,false为
ReentrantLock:实现分组唤醒需要唤醒的线程们,可以实现精确唤醒,而不是像synchronized要么随机唤醒一个线程,要么唤醒所有的线程;
与synchronized修饰方法、代码块不同,volatile只用来修饰变量。并且与synchronized、ReentrantLock等重量级锁不同的是,volatile更轻量级,因为它不会引起线程上下文的切换和调度。
按锁的性质可以将锁分为以下几种类别:
悲观锁 or 乐观锁:是否一定要锁
共享锁 or 独占锁(排他锁):是否可以有多个线程同时拿锁
公平锁 or 非公平锁:是否按阻塞顺序拿锁
可重入锁 or 不可重入锁:拿锁线程是否可以多次拿锁
-------------------------------------------------------------
乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的MySQL关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
共享锁:
共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。相当于对于同一把门,它拥有多个钥匙一样。就像这样,你家有一个大门,大门的钥匙有好几把,你有一把,你女朋友有一把,你们都可能通过这把钥匙进入你们家,这个就是所谓的共享锁。
公平锁:
就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁:
上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
可重入锁:
当某个线程请求一个被其他线程所持有的锁的时候,该线程会被阻塞(后面的读写锁先不考虑在内),但是像synchronized这样的内置锁是可重入的,即一个线程试图获取一个已经被该线程所持有的锁,这个请求会成功。重入以为这锁的操作粒度是线程级别而不是调用级别。我们下面说到的ReentrantLock也是可重入的,而除了支持锁的重入之外,该同步组件也支持公平的和非公平的选择。