synchronized和ReentrantLock

概述

关于synchronized和ReentrantLock的区别如下:

关于两种锁使用方式的伪代码如下:

// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
	synchronized (this) {}
}

// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
	// 1.初始化选择公平锁、非公平锁
	ReentrantLock lock = new ReentrantLock(true);
	// 2.可用于代码块
	lock.lock();
	try {
		try {
			// 3.支持多种加锁方式,比较灵活; 具有可重入特性
			if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
		} finally {
			// 4.手动释放锁
			lock.unlock()
		}
	} finally {
		lock.unlock();
	}
}

synchronized

synchronized的底层实现

synchronized是 Java内建的同步机制,所以也被称为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取锁的线程时只能等待或者阻塞在那里。Java5之前,synchronized是唯一的同步方式。synchronized关键字既可以用来修饰方法,也可以使用在特定的代码块上,本质上 synchronized方法相当于把方法全部语句用 synchronized 代码包起来。

synchronized是基于一对 monitorenter/monitorexit 指令实现的,Monitor对象是同步的基本实现单元,无论是显示同步,还是隐式同步都是如此。区别是同步代码块是通过明确的 monitorenter 和 monitorexit 指令实现,而同步方法通过ACC_SYNCHRONIZED 标志来隐式实现。

同步代码块实现:

public class Test1 {
    public void fun1(){
        synchronized (this){
            System.out.println("fun111111111111");
        }
    }
}

将.java文件使用javac命令编译为.class文件,然后将class文件反编译出来。

反编译的字节码文件截取:

通过反编译后的内容查看可以发现,synchronized编译后,同步块的前后有monitorenter/monitorexit两个 字节码指令。
在Java虚拟机规范中有描述两条指令的作用:

翻译一下如下:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

monitorexit:

  1. 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  2. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

问题:synchronized 代码块内出现异常会释放锁吗?
A:会自动释放锁,查看字节码指令可以知道,monitorexit插入在方法结束处和异常处。从Exception table异常表中也可以看出。

同步方法代码演示:

public class Test1 {
    //锁当前对象(this)
    public synchronized void fun2(){
        System.out.println("fun2222222222222222222222");
    }
     //静态synchronized修饰:使用的锁对象是当前类的class对象
    public synchronized static void fun3(){
        System.out.println("fun33333333333333");
    }
}

编译之后反编译截图:

从反编译的结果来看,同步方法表面上不是通过monitorenter/monitorexit指令来完成,但是与普通方法相比,常量池中多出来了ACC_SYNCHRONIZED标识符。java虚拟机就是根据ACC_SYNCHRONIZED标识符来实现方法的同步,当调用方法时,调用指令先检查方法是否有 ACC_SYNCHRONIZED访问标志,如果存在,执行线程将先获取monitor,获取成功之后才执行方法体,执行完后再释放monitor。在方法执行期间,其他线程都无法再获取到同一个monitor对象。 虽然编译后的结果看起来不一样,但实际上没有本质的区别,只是方法的同步是通过隐式的方式来实现,无需通过字节码来完成。

ACC_SYNCHRONIZED的访问标志,其实就是代表:当线程执行到方法后,如果检测到有该访问标志就会隐式的去调用monitorenter/monitorexit两个命令来将方法锁住。

小结

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized 的特性

synchronized 作为悲观锁,具有两个特性,一个是 可重入锁,一个是不可中断性。

可重入锁

ReentrantLock和synchronized都是可重入锁

定义
指的是 同一个线程的 可以多次获得 同一把锁(一个线程可以多次执行synchronized,重复获取同一把锁)。

/*  可重入特性    指的是 同一个线程获得锁之后,可以再次获取该锁。*/
public class Demo01 {
    public static void main(String[] args) {
        Runnable sellTicket = new Runnable() {
            @Override
            public void run() {
                synchronized (Demo01.class) {
                    System.out.println("我是run");
                    test01();
                }
            }

            public void test01() {
                synchronized (Demo01.class) {
                    System.out.println("我是test01");
                }
            }
        };
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
    }
}

原理
synchronized 的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就 + 1,在执行完一个同步代码块时,计数器数量就会减1,直到计数器的数量为0才释放这个锁。

优点
可以一定程度上避免死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);
更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入);

不可中断

定义
线程A获得锁后,线程B要想获得锁,必须处于阻塞或等待状态。如果线程A不释放锁,那线程B会一直阻塞或等待,阻塞等待过程中,线程B不可被中断。

synchronized 是不可中断的,处于阻塞状态的线程会一直等待锁。

public class Demo02_Uninterruptible {

        private static Object obj = new Object();        // 定义锁对象

        public static void main(String[] args) {
            // 1. 定义一个Runnable
            Runnable run = () -> {
                // 2. 在Runnable定义同步代码块;同步代码块需要一个锁对象;
                synchronized (obj) {

                    // 打印是哪一个线程进入的同步代码块
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "进入同步代码块");

                    Thread.sleep(888888);
                }
            };

            // 3. 先开启一个线程来执行同步代码块
            Thread t1 = new Thread(run);
            t1.start();
            // 保证第一个线程先去执行同步代码块
            Thread.sleep(1000);

            /**
             4. 后开启一个线程来执行同步代码块(阻塞状态)到时候第二个线程去执行同步代码块的时候,
             锁已经被t1线程锁获取得到了;所以线程t2是无法获取得到Object obj对象锁的;
             那么也就将在同步代码块外处于阻塞状态。*/
            Thread t2 = new Thread(run);
            t2.start();


            /** 5. 停止第二个线程;观察此线程t2能否被中断;*/
            System.out.println("停止线程前");
            t2.interrupt();    // 通过interrupt()方法给t2线程进行强行中断
            System.out.println("停止线程后");

            // 最后得到两个线程的执行状态
            System.out.println(t1.getState());    // TIMED_WAITING
            System.out.println(t2.getState());    // BLOCKED
        }
    }

// 运行结果:
    Thread-0
    进入同步代码块
            停止线程前
    停止线程后
            TIMED_WAITING
    BLOCKED        // t2的状态依然为BLOCKED,说明synchronized是不可被中断的

结果分析:
通过interrupt()方法让 t2 线程强行中断,最后打印t2的状态,依然为BLOCKED,即线程不可中断。

锁升级过程及monitor

详情请看 Java中的锁

ReentrantLock

ReentrantLock的实现依赖于AQS,详情请看 AQS详解

公平锁和非公平锁

非公平锁

// java.util.concurrent.locks.ReentrantLock
static final class NonfairSync extends Sync {
	...
	final void lock() {
		if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
		else
			acquire(1);
	}
  ...
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer

public final void acquire(int arg) {
	if (!tryAcquire(arg) && //如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了
	    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果获取失败,就需要加入到等待队列中。
		selfInterrupt();
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer

protected boolean tryAcquire(int arg) {
	throw new UnsupportedOperationException();
}

可以看出,这里只是AQS的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的(以ReentrantLock为例)。如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。

原理

ReentrantLock对AQS的独占方式实现为:ReentrantLock中的state初始值为0表示无锁状态。在线程执行 tryAcquire()获取该锁后ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过tryAcquire() 获取锁时均会失败,直到该线程释放锁后state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1, 因此ReentrantLock也属于可重入锁。 但获取多少次锁就要释放多少次锁,这样才能保证state最终为0。如果获取锁的次数多于释放锁的次数,则会出现该线程一直持有该锁的情况;如果获取锁的次数少于释放锁的次数,则运行中的程序会报锁异常。

可/不可中断性

ReentrantLock 的lock方法是不可中断的,tryLock方法是可中断的。

演示不可中断:

public class Demo03_Interruptible {

    // 创建一个Lock对象
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        test01();
    }

    // 演示 Lock 不可中断
    public static void test01() {
        Runnable run = () -> {

            String name = Thread.currentThread().getName();
            try {
                lock.lock();    // lock() 无返回值
                System.out.println(name + "获得锁,进入锁执行");
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();    // unlock也是没有返回值的
                System.out.println(name + "释放锁");
            }
        };

        Thread t1 = new Thread(run);
        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(run);
        t2.start();

        System.out.println("停止t2线程前");
        t2.interrupt();
        System.out.println("停止t2线程后");

        Thread.sleep(1000);

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

----------------------------------------------
运行效果:
Thread-0获得锁,
进入锁执行
        停止t2线程前
停止t2线程后
        TIMED_WAITING    // t1线程在临界区睡88888ms,有时限的等待
WAITING        // t2线程处于等待状态,WAITING

演示可中断:

public class Demo03_Interruptible {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        test02();
    }

    // 演示 Lock 可中断
    public static void test02() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;
            try {

                b = lock.tryLock(3, TimeUnit.SECONDS);

                //说明尝试获取得到了锁;则进入if块当中
                if (b) {
                    System.out.println(name + "获得锁,进入锁执行");
                    Thread.sleep(888888);
                } else {
                    // 没有获取得到锁执行else,证明了Lock.tryLock()是可中断的;
                    System.out.println(name + "在指定时间内没有获取得到锁则做其他操作");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (b) {    //得到了锁才释放锁
                    lock.unlock();
                    System.out.println(name + "释放锁");
                }
            }
        };

        Thread t1 = new Thread(run);
        t1.start();
        Thread t2 = new Thread(run);
        t2.start();
    }
}
----------------------------------------------------
代码执行效果:
Thread-0获得锁,进入锁执行
Thread-1在指定时间没有得到锁做其他操作

synchronized和ReentrantLock的区别

  1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
  2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。
  3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值