Java基础学习之并发篇:synsychronized和ReentrantLock

学习目标

  1. 了解锁的概念
  2. 锁的分类
  3. synchronized关键字的内涵
  4. 认识ReentrantLock类的实现
  5. synchronized和ReentranLock区别

锁的概念

锁单从字面来讲,决定进与出,上了锁,则拒绝进入。而在程序中,指的是拒绝线程的进入。

上锁的目的是解决多线程对资源竞争产生的数据不一致,总而言之上锁的过程是针对临界区代码的同步,例如线程A拿到锁则同步资源由线程A占领,其它线程进行挂起或排队等待锁的释放。
在Java中,我们一般会将锁分为悲观与乐观、可重入与不可重入、公平与非公平和共享与独占等。这只是在不同角度对于锁的划分,并不是表示一种锁的唯一归属,比如synchronized它是一种悲观锁,可重入且非公平的。ReentrantLock也是悲观锁,可重入且公平的(也支持非公平)
关于锁的具体分类和不同场景下的使用,可以看看这篇我转载的文章
>>Java基础学习之并发篇:不同的锁的适用场景
加锁是有代价的,加锁通常会严重地影响性能。阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长,或者如果一个线程在持有锁的情况下被延迟执行,例如发生了缺页错误、调度延迟或者其它类似情况,那么所有需要这个锁的线程都无法执行下去。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,所以对加锁过程作了一些优化引入了“偏向锁”和“轻量级锁”。

所以锁的使用是一把双刃剑,保证多线程下的资源同步,但又会有性能方面的考量,所以大家一直在追求使用锁过程的不断优化,甚至追求无锁编程


使用synchronized

synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的,保证资源的同步性。synchronized可以作用于普通方法上,静态方法上和代码块中,例如:

    /**
     * synchronized作用在普通方法中
     * @return
     */
    public synchronized String hello(){
        return "hello synchronized method";
    }
    /**
     * synchronized作用在静态方法中
     * @return
     */
    public synchronized static String helloStaticMethod(){
        return "hello synchronized method";
    }
  	synchronized (this) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("finished.");
            }

synchronized 可以修饰方法和代码块

  • 修饰代码块

    synchronized(this|object) {}

    synchronized(类.class) {}

  • 修饰方法

    修饰非静态方法

    修饰静态方法

在不同场景下使用到的锁是:

  • 对于普通方法,锁是对象锁(可以有多个,相当于synchronized(this))。
  • 对于静态方法,锁是类锁(相当于synchronized(类.class) )。
  • 对于同步代码块,锁可以是对象锁或类锁。

可以总结如下

  1. 普通方法的synchronized,在多个实例下锁是每个实例对象的 ;
  2. 对于静态方法,由于此时对象还未生成,所以只能采用类锁;
  3. 只要采用类锁,就会拦截所有线程,只能让一个线程访问;
  4. 对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。

正如我们上面提到的,Java团队一直在追求对synchronized的优化,例如引入偏向锁、轻量级锁,弄清楚优化的过程,我们要明白synchronized实现的方式:
synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步,之所以任何对象能够充当锁,在于Java在对象中有个Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
每个线程拥有Monitor变量,关联锁Mark Word 信息。Monitor拥有Owner确认当前自己是否是该锁的拥有者。也就是说一个线程在去竞争锁时,找到当前锁Mark Word发现锁的状态,当前锁拥有者,锁是否释放等信息。
Java基础学习之并发篇:synsychronized和ReentrantLock
以下是锁升级的过程

  1. 而第一线程A发现该锁是无锁状态,即未被占用时,拿到该锁,此时synchronized是偏向锁状态
  2. 如果下一个线程还是线程A在执行竞争该锁,此时判断线程占用者一致,则还是偏向锁,直接获取到该锁(即可重入),不需要竞争和等待走其它判定条件。
  3. 当然,如果发生了线程B竞争,此时升级为轻量级锁,线程B此时自旋去等待线程A释放锁,如果在自旋一定次数后,拿到了锁那么此时算比较走运
  4. 拿不到的话抱歉了,线程B挂起进入休眠,把CPU留给其它同志吧。此时便将锁升级为重量级锁,重量级锁就是需要CPU的唤醒嘛,有点浪费时间

ReentrantLock

我们在直接用synchronized时感觉用的还是不爽,某个线程tm程序炸了一直占用,导致其它线程全部失效,还要自己去实现个定时去超时处理。中断也不能及时响应,即使中断了还是在搞事情。所以Java团队听到了你们的心声,弄了一个Lock,老哥们拿去用吧。

Lock是在1.5之后提供的一个独占锁接口,它的实现类是ReentrantLock,相比较synchronized这种隐式锁(不用手动加锁和释放锁)的便捷性,但是提供了更加锁的可操作性、可中断的获取锁以及超时获取锁等多种synchronized不具备的特性

使用方法如下

        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {
            lock.lock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }

Java基础学习之并发篇:synsychronized和ReentrantLock

设置超时时间

       Thread t = new Thread(() -> {
            try {
                lock.tryLock(1000, TimeUnit.MICROSECONDS);
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
            lock.unlock();
        }
        });
         t.start();
         // 中断线程会立即响应
        t.interrupt();

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireNanos(AbstractQueuedSynchronizer.java:1245)
	at java.util.concurrent.locks.ReentrantLock.tryLock(ReentrantLock.java:442)
	at SynchronizedKeyWords.lambda$main$0(SynchronizedKeyWords.java:14)
	at java.lang.Thread.run(Thread.java:748)

使用的很爽了,那么里面的实现和synsychronized到底有啥区别,不得不要去瞄一下:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        abstract void lock();
        final boolean nonfairTryAcquire(int acquires) {
        }

        protected final boolean tryRelease(int releases) {        
        }
    }

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {        
        }
    }
 }

发现了一些好东西,ReentrantLock类实现接口Lock,抽象内部类Sync继承AQS(AbstractQueuedSynchronizer),有公平和非公平的两个实现类继承自Sync。

上一篇:【并发编程】synchronized在设计上的锁优化


下一篇:互斥锁解决原子性问题