Synchronized 和 Lock

Posted 做一个苦行僧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Synchronized 和 Lock相关的知识,希望对你有一定的参考价值。

 Sychronized关键字 在java多线程的问题中占有举足轻重的地位。这里记录一些易错的关于Sychronized的知识点。

1.一个是对象锁(作用在具体的对象实例上的),另外一个是作用在static修饰的方法addB是类上的,他们两个锁之间不存在竞争的关系,也没有范围大小关系,总之没有半毛钱的关系。获取了对象锁的线程A 调用addA ,不会影响另外一个线程B 获取锁调用addB方法,因为这个两个根本不是同一个锁。

synchronized public void addA();

synchronized public static void addB()

public class Service {
    synchronized  public void runMethod(){
        try{
            System.out.println(Thread.currentThread().getName()+" runMethod start");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName()+" runMethod end");

        }catch (Exception e){

        }
    }

    synchronized public static void stopMethod(){
        try{
            System.out.println(Thread.currentThread().getName()+" stopMethod start");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName()+" stopMethod end");
        }catch (Exception e){

        }
    }

}
public class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service){
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.runMethod();
    }
}

public class ThreadB extends Thread {
    private Service service;

    public ThreadB(Service service){
        super();
        this.service = service;
    }

    @Override
    public void run() {
        super.run();
        service.stopMethod();
    }
}

public static void main(String args[]){
        Service service = new Service();
        ThreadA a = new ThreadA(service);
        a.setName("ThreadA");
        a.start();
        ThreadB b = new ThreadB(service);
        b.setName("ThreadB");
        b.start();
    }

上述代码最后打印出:

ThreadA runMethod start
ThreadB stopMethod start
ThreadA runMethod end
ThreadB stopMethod end

这说明就算ThreadA获取了Service的对象锁,但是依然不能阻塞ThreadB 调用stopMethod方法。因为他获取的是Sevice这个class类锁

结论:对象锁和Class类锁没有半毛钱的关系,不能说那个作用域大一下。获取了对象锁的线程,只对 其他要争取对象所的线程取阻塞作用。获取了Class类锁的线程,只对其他要获取class类的线程起阻塞作用。都只能对争取同类别的锁的线程起作用。

2.  synchronized public static void addB()  和 Synchronized(xxxx.class) 需要竞争锁,也即是静态同步方法与synchronized(class)代码块需要竞争锁,实现线程阻塞。synchronized(this) 锁的是当前对象

public class Service {
    public void runMethod(){
        synchronized (Service.class){   //Serivice.class锁
            try{
                System.out.println(Thread.currentThread().getName()+" runMethod start");
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName()+" runMethod end");

            }catch (Exception e){

            }
        }
    }

    synchronized public static void stopMethod(){
        try{
            System.out.println(Thread.currentThread().getName()+" stopMethod start");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName()+" stopMethod end");
        }catch (Exception e){

        }
    }

}

和上面同样的ThreadA  和 ThreadB java类,还有同样的main方法。会有下面的运行结果,他们是同步执行的:

ThreadA runMethod start
ThreadA runMethod end
ThreadB stopMethod start
ThreadB stopMethod end
 

3 .对于synchronized(object) 代码块, object中的某个属性修改了,多线程还是会竞争同一个对象的对象锁。例如object为UserInfo,当我们同步代码块synchronized(userInfo实例)来说,改变userInfo实例中的某个属性,或者调用set方法,并不会改变对这个锁的竞争,多线程还是会竞争者同一把锁。

注意:一般最好不要以String对象作为synchronized代码块的锁对象

Java 虚拟机对 synchronized 的优化


       从 Java 6 开始,虚拟机对 synchronized 关键字做了多方面的优化,主要目的就是,避免 ObjectMonitor 的访问,减少“重量级锁”的使用次数,并最终减少线程上下文切换的频率 。其中主要做了以下几个优化: 锁自旋、轻量级锁、偏向锁。

锁自旋

线程的阻塞和唤醒需要 CPU 从用户态转为核心态,频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力,所以 Java 引入了自旋锁的操作。实际上自旋锁在 Java 1.4 就被引入了,默认关闭,但是可以使用参数 -XX:+UseSpinning 将其开启。但是从 Java 6 之后默认开启。

所谓自旋,就是让该线程等待一段时间,不会被立即挂起,看当前持有锁的线程是否会很快释放锁。而所谓的等待就是执行一段无意义的循环即可(自旋)

自旋锁也存在一定的缺陷:自旋锁要占用 CPU,如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的 CPU 时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁

轻量级锁

有时候 Java 虚拟机中会存在这种情形:对于一块同步代码,虽然有多个不同线程会去执行,但是这些线程是在不同的时间段交替请求这把锁对象,也就是不存在锁竞争的情况。在这种情况下,锁会保持在轻量级锁的状态,从而避免重量级锁的阻塞和唤醒操作。

轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

偏向锁

        轻量级锁是在没有锁竞争情况下的锁状态,但是在有些时候锁不仅存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作。偏向锁可以通过 -XX:+UseBiasedLocking 开启或者关闭。

偏向锁的具体实现就是在锁对象的对象头中有个 ThreadId 字段,默认情况下这个字段是空的,当第一次获取锁的时候,就将自身的 ThreadId 写入锁对象的 Mark Word 中的 ThreadId 字段内,将是否偏向锁的状态置为 01。这样下次获取锁的时候,直接检查 ThreadId 是否和自身线程 Id 一致,如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。

其实偏向锁并不适合所有应用场景, 因为一旦出现锁竞争,偏向锁会被撤销,并膨胀成轻量级锁,而撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的 synchronized 块时,才能体现出明显改善;因此实践中,还是需要考虑具体业务场景,并测试后,再决定是否开启/关闭偏向锁。

Lock

在java5中,使用Lock对象来实现同步效果。ReentrantLock 的使用同 synchronized 有点不同,它的加锁和解锁操作都需要手动完成。

运行效果如下:

可以看出,使用 ReentrantLock 也能实现同 synchronized 相同的效果

你可能已经注意到,在上面 ReentrantLock 的使用中,我将 unlock 操作放在 finally 代码块中。这是因为 ReentrantLock 与 synchronized 不同,当异常发生时 synchronized 会自动释放锁,但是 ReentrantLock 并不会自动释放锁。因此好的方式是将 unlock 操作放在 finally 代码块中,保证任何时候锁都能够被正常释放掉。

公平锁和非公平锁的实现

ReentrantLock 有一个带参数的构造器,如下:

 /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

默认情况下,synchronized 和 ReentrantLock 都是非公平锁。但是 ReentrantLock 可以通过传入 true 来创建一个公平锁。所谓公平锁就是通过同步队列来实现多个线程按照申请锁的顺序获取锁,也就是由于申请同步锁而阻塞的线程,按照阻塞的顺序,FIFO,先来就先获取到锁

看java中非公平锁的实现:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        // android-removed: @ReservedStackAccess from OpenJDK 9, not available on Android.
        // @ReservedStackAccess
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

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

当ReentrantLock的是非公平锁的时候,某个线程想要获取锁,会调用上面的lock()方法:

在NonfairSync中lock方法首先if 是直接去调用compareAndSetState方法,这个方法直接尝试去修改当前锁的状态,如果能够修改成功,就直接获得了锁 ,不能修改成功就执行else 分支里面的acquire(1)方法。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

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

其实最后调用的是tryAcquire方法,我们看nonfairTryAcquire的实现:

final boolean nonfairTryAcquire(int acquires) {
            //得到当前尝试获取锁的线程
            final Thread current = Thread.currentThread();
             //获得当前锁的状态
            int c = getState();
            //状态为0  表示没有人获取锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
             // 如果当前线程 就是锁的拥有者,那么就重入
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

正如上面解释所说,线程调用lock的时候,首先直接调用compareAndSetState方法,这样就没有进过队列的调度。就有可能造成别的线程在队列里面等待获取锁,而此时新进来的线程如果恰好遇到别的线程释放锁,那么它就可能直接获取锁,别的线程还在队列里面等待。这样就有可能造成线程饥饿,就是某种情况下,可能某个线程一直获取不到锁。

公平锁的实现如下:

 /**
     * 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.
         */
        // Android-removed: @ReservedStackAccess from OpenJDK 9, not available on Android.
        // @ReservedStackAccess
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

其中tryAcquire方法是先检测当前锁的状态为0的时候,就去调用hasQueuedPredecessors方法,检查同步队列中是否有其他线程,如果有,就不会去执行后面的操作,当然当前线程 current == getExclusiveOwnerThread()不会为true,当前线程就不可能获取锁。

读写锁(ReentrantReadWriteLock)

在常见的开发中,我们经常会定义一个线程间共享的用作缓存的数据结构,比如一个较大的 Map。缓存中保存了全部的城市 Id 和城市 name 对应关系。这个大 Map 绝大部分时间提供读服务(根据城市 Id 查询城市名称等)。而写操作占有的时间很少,通常是在服务启动时初始化,然后可以每隔一定时间再刷新缓存的数据。但是写操作开始到结束之间,不能再有其他读操作进来,并且写操作完成之后的更新数据需要对后续的读操作可见。

在没有读写锁支持的时候,如果想要完成上述工作就需要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行。这样做的目的是使读操作能读取到正确的数据,不会出现脏读。

但是如果使用 concurrent 包中的读写锁(ReentrantReadWriteLock)实现上述功能,就只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续的读写锁都会被阻塞,写锁释放之后,所有操作继续执行,这种编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

悲观锁与乐观锁。

锁从宏观上分类,分为悲观锁与乐观锁。

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试乐观锁去获取锁,获取不到,才会转换为悲观锁,如ReentrantLock。

Synchronized 和 ReentrantLock的区别和联系

1.synchronized和 ReentrantLock都能实现同步.

2.synchronized实现同步,未获取到锁的线程会阻塞。而ReentrantLock可以通过tryLock,tryLock(long timeout, TimeUnit unit)方法来尝试获取锁,没有获取到锁之后,可以执行其他操作,可以不用阻塞线程。

3.synchronized是实现的是非公平锁,而ReentranLock可以实现公平锁和非公平锁,可以根据开发者需要自己设定。

4.Synchronized关键字,对于获取到锁的线程,除非线程被中断或者抛出异常,否则只能等线程执行完成之后才能释放锁。而ReentrantLock需要主动释放锁,像上文中的在finally中调用lock.unlock()方法释放锁。

以上是关于Synchronized 和 Lock的主要内容,如果未能解决你的问题,请参考以下文章

多线程编程之synchronized和Lock

Java多线程与并发库高级应用-工具类介绍

lock 和synchronized 的区别

Java同步锁——lock与synchronized 的区别转

synchronized和lock的区别

Synchronized和lock的区别和用法