0
点赞
收藏
分享

微信扫一扫

来聊聊synchronized关键字

嚯霍嚯 2022-02-13 阅读 126

synchronized的基础使用

对象锁

自定义对象锁

/**
 * 对象锁示例2
 */
public class SyncObjLock2 implements Runnable{
    private static SyncObjLock2 instance=new SyncObjLock2();

    private Object lock1=new Object();
    private Object lock2=new Object();

    /**
     * 可以看到使用sync后会导致线程排队拿锁一起串行
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"来了");

        synchronized (lock1){
            System.out.println("得到了lock1锁,当前线程名: "+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        synchronized (lock2){
            System.out.println("得到了lock2锁,当前线程名: "+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(" "+Thread.currentThread().getName()+"走了");
    }

    public static void main(String[] args) {

        Thread thread1=new Thread(instance);
        Thread thread2=new Thread(instance);
        thread1.start();
        thread2.start();
    }
}

this锁

/**
 * 对象锁示例
 */
public class SyncObjLock implements Runnable{
    private static SyncObjLock instance=new SyncObjLock();

    /**
     * 可以看到使用sync后会导致线程串行
     */
    @Override
    public void run() {
        System.out.println(" "+Thread.currentThread().getName()+"来了");

        synchronized (this){
            System.out.println("得到了对象锁,当前线程名: "+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(" "+Thread.currentThread().getName()+"走了");
    }

    public static void main(String[] args) {

        Thread thread1=new Thread(instance);
        Thread thread2=new Thread(instance);
        thread1.start();
        thread2.start();
    }
}

方法锁

普通方法锁

/**
 * 对象锁示例3
 */
public class SyncObjLock3 implements Runnable {
    private static SyncObjLock3 instance = new SyncObjLock3();


    /**
     * 可以看到使用sync后会导致线程排队拿锁一起串行
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "来了");

        lockMethod();


        System.out.println(" " + Thread.currentThread().getName() + "走了");
    }

    public synchronized void lockMethod() {

        System.out.println("得到了锁,当前线程名: " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
    }
}

静态方法锁

/**
 * 对象锁示例3
 */
public class SyncObjLockStaticMethod implements Runnable {
    private static SyncObjLockStaticMethod instance = new SyncObjLockStaticMethod();
    private static SyncObjLockStaticMethod instance2 = new SyncObjLockStaticMethod();


    /**
     * 可以看到使用sync后会导致线程排队拿锁一起串行
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "来了");

        lockMethod();


        System.out.println(" " + Thread.currentThread().getName() + "走了");
    }

    public static synchronized void lockMethod() {

        System.out.println("得到了锁,当前线程名: " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
    }
}

类锁

/**
 * 对象锁示例3
 */
public class SyncObjLockClassMethod implements Runnable {
    private static SyncObjLockClassMethod instance = new SyncObjLockClassMethod();
    private static SyncObjLockClassMethod instance2 = new SyncObjLockClassMethod();


    /**
     * 可以看到使用sync后会导致线程排队拿锁一起串行
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "来了");

        lockMethod();


        System.out.println(" " + Thread.currentThread().getName() + "走了");
    }

    public static void lockMethod() {

        synchronized (SyncObjLockClassMethod.class) {
            System.out.println("得到了锁,当前线程名: " + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
    }
}

Synchronized的使用注意事项

  1. 一把锁同一时间只能有一个对象获取
  2. 如果用static或者.class修饰的锁,所有对象持有的锁都是当前class对象
  3. 当有异常抛出时,sync锁照样会释放锁

synchronized锁原理

加锁和解锁的工作原理

如下所示,我们有这样一段示例代码,一个很简单的sync锁示例

public class SynchronizedDemo2 {

    Object object = new Object();
    public void method1() {
        synchronized (object) {

        }
        method2();
    }

    private static void method2() {

    }
}


我们首先使用javac将其编译为字节码文件

javac SynchronizedDemo2.java

再使用javap命令对其反编译

javap -verbose SynchronizedDemo2.class

可以看到,反编译后的指令出现了monitorentermonitorexit两条指令,这就是sync锁加锁和解锁的核心指令
在这里插入图片描述
当有一个线程将要执行monitorenter指令时,他就会将自己的锁计数器+1,然后与monitor关联,而monitor单位时间内永远只允许一个线程获取。所以当其他线程尝试与monitor关联时,就无法获取到锁,只能等待。
当获取到锁的线程执行monitorexit时,他就会将锁计数器-1,如果减完不为0,说明这把锁它重入了好几次,后续还可以随意重入。若为0则说明这个对象用完这把锁了,可以让给别人用了。

具体工作原理可参考下图食用
在这里插入图片描述

可重入的工作原理

正如上文所说,每个锁对象都有一个锁计数器,当某个线程monitorenter获取到锁时计数器就会+1,下次再进来再+1,monitorexit就-1.

保证可见性的原理(注意引用自己之前的文章)

这就涉及到一个happends-before原则,对一个监视器的解锁happends-before与对一个监视器的加锁。如下图所示,正是以为happends-before原则,线程a释放锁发生于线程b加锁,这就保证了线程a的修改线程b是可见的。
在这里插入图片描述关于happends-before原则,读者可以参考笔者这篇文章
并发编程必知必会——Happens-before

jvm对锁的优化

在jdk1.6前使用sync都是调用操作系统底层的Mutex Lock来实现的,而调用Mutex Lock时,它会将线程挂起并从用户态切换到内核态来执行。这种操作开销是非常大的。
而且我们大部分场景都是以单线程环境(无锁竞争环境)运行的。所以jdk1.6使用了例如:锁粗化、锁消除、自适应自旋锁、偏向锁、轻量级锁等方式来减少性能开销。

锁的类型简介

锁粗化

对于某个代码段大量lock和unlock操作,jvm会自动将其扩大成一个lock和unlock

锁消除

通过jlt编译器逃逸技术判断当前同步代码块数据是否也有被其他同步代码块获取,若没有则会消除这把锁。

轻量级锁

这个概念源于一个大胆的假设,大部分情况下,线程都是在无锁竞争环境下运行的,这种情况下调用操作系统的Mutex Lock是非常没有必要的,所以我们不妨在monitorentermonitorexit操作改用cas操作来尝试取锁和释放锁,这样开销相对会少一些。当有其他线程尝试cas取锁失败时,我们在通过调用操作系统的Mutex Lock,等锁释放时再将其唤醒,具体笔者会在后文详述,这里说个大体概念。

适应性自旋锁

上文我们知道轻量级锁是通过cas尝试取锁和解锁,适应性锁的诞生就是为了解决,cas失败取不到锁后的处理。
当线程cas取锁失败时,会在当前线程调用与monitor相关联的重量级锁之前进行一次忙等待后再次尝试,若还是失败则会调用与monitor相关联的semaphore(互斥锁)进入阻塞状态

偏向锁

在无锁竞争环境下,避免执行没有必要的cas原子执行,原因也很简单,虽然cas锁相对Mutex Lock开销确实是相对小一些。但是还是会存在一定的本地延迟。

锁的升降级

按照上文我们大概就知道,锁的升级过程为

	无锁->偏向锁->轻量级锁->重量级锁

而且这个过程是不可逆的

锁的优化技术详细介绍

自旋锁和自适应自旋锁

首先我们先来说说自旋锁,由上文我们就知道为了减少性能开销我们将重量级锁改用自旋锁减小上锁的开销。但是自旋锁的缺点也很明显,他会在cpu中不算自旋如果长时间占用cpu资源很影响性能的一件事。所以jdk设置当自选一定时间后,就将线程挂起。
jdk默认自选次数为10次,用户可以使用-XX:PreBlockSpin这个指令来修改。
但是尴尬的事情又来了,假如自旋期间没拿到锁,这个线程被挂起的期间,锁刚刚好被释放了怎么办?jdk考虑了这种情况,所以由此诞生了自适应自旋锁,假如运行期间某个线程经过自选 拿到锁并正在运行中,那么jvm就会认为这个锁拿到的几率很大,就会增加自旋次数。反之,可能就会省略掉自旋这个步骤直接将锁挂起。

偏向锁

虽然说cas轻量级锁解决了使用重量级锁的开销,但是我们却忘记考虑一个线程反复获取锁的这个场景,这种操作也会带来很多没必要的性能开销以及上下文切换。
所以HotSpot也对此进行了优化,但一个线程获取到这个锁时,就会找到这个锁对象的对象头中的栈帧,将其锁记录存储偏向的线程id,假如这个线程再来访问时,我们也只需查看这个锁的markword中指向的local world是否是这个同一个线程id即可决定是否允许拿锁。

而偏向锁的释放也很特殊,只有出现线程竞争的时候才会进行释放,首先它会将偏向锁所指向的线程暂停,然后判断它是否活着。若死了则直接将锁对象的对象头的设置为无锁状态。反之,要么将偏向锁指向要拿锁的这个线程,要么设置为无锁,再不然就是标记对象不可作为偏向锁。

锁粗化

由于上文已经介绍了锁粗化了概念,这里就以代码形式进行分析,概念其实也很简单,以下代码为了保证线程安全使用了StringBuffer,这就使得append操作会不断上锁解锁,这对性能会造成很大影响,jvm就会对这种系列的append操作优化为之上一把锁,在这把锁中实现一系列append操作,这就是锁粗化

/**
     * 锁粗化
     * @param s1
     * @param s2
     * @param s3
     * @return
     */
    public static String test04(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }

锁消除

锁消除的概念上文已经描述过了,我们这里就拿一个实际的例子来说明,如下代码所示,我们都知道String字符串拼接操作底层是通过创建新的字符串对象来实现的。所以我们通过编译再反编译可能就会看到StringBuffer或者StringBuilder的指令。

 public static String test03(String s1, String s2, String s3) {
        String s = s1 + s2 + s3;
        return s;
    }

而上方代码是在无锁竞争环境下,所以聪明的jvm通过逃逸技术得知后,就会对其做优化,使用StringBuilder来实现字符串拼接,如下图所示

在这里插入图片描述

轻量级锁

关于轻量级锁的概念也很简单,如下图所示,当一个线程要执行同步代码块时,就会在自己的栈帧中创建**锁记录(Lock Record)**的空间
在这里插入图片描述

然后将这个通过cas尝试获取这个锁对象Object,若获取成功则将标记为设置为01,然后锁对象的MarkWord会被更新为00,意为此对象当前正处于获取轻量级锁状态。再将Object的栈帧的指针指向这个线程Lock Record的地址
在这里插入图片描述
假如没有获取到,那么jvm就会判断这个Object锁的栈帧的指针指向的是不是当前取锁线程的Lock Record,若是则说明取锁已经成功了,让它继续做它该做的。反之就将这把锁标记为10,即将其膨胀为重量级锁,等待持有锁的线程释放。

锁的对比

在这里插入图片描述

synchronized和lock的对比

synchronized的缺点

  1. 效率低,只有锁用完或者异常了才会释放,而且无法设置设置锁超时,线程中断锁使用。
  2. 使用单一,加锁条件单一,每个锁仅有一个单一的条件,还不如读写锁。
  3. 无法判断是否成功拿到锁了

synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。 多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。

更深层次的理解synchronized

synchronized是通过jvm实现的,简单易用。当然使用时也需要注意以下几点:

  1. 锁的作用不能过大
  2. 锁对象不可为空
  3. 非必要别用synchronized或者lock锁,尽可能使用juc包中的各种类
  4. 避免死锁

synchronized是公平锁吗

不是,新来的线程完全可以立即获得monitor,很可能会造成在等待的线程持续等待的情况,所以使用不当很可能出现线程饥饿的情况

参考文献

关键字: synchronized详解

源码地址

添加链接描述

举报

相关推荐

0 条评论