众所周知,在JUC中常见的锁就是Lock和Synchronized了,主要是用于并发多线程的同步执行问题,用于在许多线程执行时对资源的限制。锁通常需要硬件支持才可以有效实施。这种支持通常采用一个或多个原子指令,测试单个线程是否空闲。
Lock是显式加锁,锁释放。而synchronized是隐式锁,出了作用域自动释放,Lock只有代码块锁,synchronized有代码块锁和方法锁。使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类ReentrantLock(). )
优先使用顺序:Lock > 同步代码块 (已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字。包括synchronized方法和synchronized块:synchronized方法控制对 对象 的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
使用synchronized缺陷: 将一个大方法申明为synchronized将会影响效率,一般只是将方法中的需要保证同步的代码放到synchronized块中,同步方法中的同步监视器就是this,对象本身或者class。
下面看一下使用小案例:
1、抢票小案例(不安全状态)
class Tickets1 implements Runnable{
private Integer ticketNums=10;
private boolean flag=true;
@Override
public void run() {
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void buy() throws InterruptedException {
if(ticketNums<=0){
System.out.println("抢票停止了!");
flag=false;
return;
}else{
Thread.sleep(100); //防止一个线程就抢完了
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票!");
}
}
public static void main(String[] args) {
Tickets1 tickets=new Tickets1();
new Thread(tickets,"张三").start();
new Thread(tickets,"李四").start();
new Thread(tickets,"王五").start();
}
}
显然这个案例在多线程环境下是不安全的,解决办法就是使用线程同步机制,限制线程对资源的使用。下面采用Java内置关键字synchronized
来保证线程安全。
class Tickets1 implements Runnable{
private int ticketNums=10;
private boolean flag=true;
@Override
public void run() {
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
void buy() throws InterruptedException { //或者直接锁方法上synchronized void
synchronized (this){
if(ticketNums<=0){
System.out.println("抢票停止了!");
flag=false;
return;
}else{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票!");
}
}
}
public static void main(String[] args) {
Tickets1 tickets=new Tickets1();
new Thread(tickets,"张三").start();
new Thread(tickets,"李四").start();
new Thread(tickets,"王五").start();
}
}
同理Lock版本如下,我们一般使用Lock的实现类ReentrantLock来创建lock锁(可重入锁:也称递归锁,再同一线程的外层方法获取锁的时候,在进入内层方法自动获取锁。可重入锁一个好处是在一定程度上避免死锁),一个boolean参数fair来确定是否使用公平策略,默认是false非公平策略:
关于ReentrantLock可参考:一文彻底理解ReentrantLock可重入锁的使用和Java多线程系列——深入重入锁ReentrantLock
class Tickets implements Runnable{
private Integer ticketNums=10;
private boolean flag=true;
private final ReentrantLock lock=new ReentrantLock();//可重入锁
@Override
public void run() {
while(flag){
buy();
}
}
/**
*Lock是显式锁、synchronized是隐式锁,出了作用域自动释放,Lock只有代码块锁,synchronized有代码块锁和方法锁
* 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
* 优先使用顺序:Lock > 同步代码块 (已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)
*/
void buy() {
lock.lock(); //显式加锁,别写在try中,万一获取锁时候超时了就会触发捕捉异常,然后进入finally直接解锁
try{
if(ticketNums<=0){
System.out.println("抢票停止了!");
flag=false;
return;
}else{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票!");
}
}catch (Exception ex){
}finally {
lock.unlock();//显式解锁,如果同步代码块有异常,要将unlock()写入finally语句块
}
}
public static void main(String[] args) {
Tickets tickets=new Tickets();
new Thread(tickets,"张三").start();
new Thread(tickets,"李四").start();
new Thread(tickets,"王五").start();
}
}
在OOP思想中,类中只包含属性和方法,因此我们可以采用lambda表达式简化该类型的例子。
public class Synchronized {
public static void main(String[] args) {
Tickets tickets=new Tickets();
//Thread.holdsLock(tickets);//判断是否获得了tickets对象的锁
//java.lang.Thread 中有一个方法叫做holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁
new Thread(()->{ //因为Thread第一个参数为Runnable接口,Runnable只有一个run方法,是一个函数式接口,因此可以使用Lambda表达式
for (int i = 0; i < 30; i++) tickets.saleLock();
},"A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) tickets.saleLock();
},"B").start();
new Thread(()->{
for (int i = 0; i < 40; i++) tickets.saleLock();
},"C").start();
}
}
/**
* OOP思想:属性和方法
* 如果Tickets继承Runnable重写run方法,会增加耦合性
*/
class Tickets{
int tickNums=30;
public synchronized void sale(){
if(tickNums>0){
System.out.println(Thread.currentThread().getName()+"抢到了第"+(tickNums--)+"张票----->"+"当前剩余"+tickNums+"张票!");
}else{
System.out.println("票已经售完!");
}
}
public void saleLock(){
Lock lock=new ReentrantLock(); //可重入锁(可以多次获取同一个锁,释放也需要多次释放),boolean参数决定是否是公平锁
lock.lock(); //加锁
//lock.tryLock();//尝试获取锁,
try{
if(tickNums>0){
System.out.println(Thread.currentThread().getName()+"抢到了第"+(tickNums--)+"张票----->"+"当前剩余"+tickNums+"张票!");
}else{
System.out.println("票已经售完!");
}
}catch (Exception ex){
ex.printStackTrace();
}finally {
lock.unlock(); //释放锁,否则可能导致死锁发生
}
}
}
更多使用可参见jdk文档下java.util.concurrent内容。
Lock和Synchronized区别:
1、Synchronized是Java的内置关键字,Lock是一个java接口。
2、Synchronized无法判断获取锁的状态,Lock可以通过java.lang.Thread 中的holdsLock()方法判断是否获得某个对象的锁。
3、Synchronized会自动释放锁,Lock必须手动释放锁,否则可能产生死锁。
4、如果一个线程获得了锁,对于Synchronized来说其他需要使用这个资源的线程就只能等待下去,如果获得了锁的线程进入阻塞状态,其他线程依旧会等待,Lock锁就不一定会一直等待下去,可以通过tryLock尝试获取锁。
5、Synchronized可重入锁,不可以中断,非公平的,Lock可重入锁,可以判断锁的状态,是否公平可以通过fair参数进行设置,相对而言比较灵活。
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
谈谈synchronized与ReentrantLock的区别?
① 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。 synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、轻量级锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
② 是否可手动释放:
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
③ 是否可中断
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时时间或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
④ 是否公平锁
synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
⑤ 锁是否可绑定条件Condition
synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
⑥ 锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。