synchronized常见加锁误区,你知道几个?

简介: synchronized的实现原理大致是什么样的?你能想到多少synchronized的加锁误区呢?

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

本文的主要围绕着下面这个问题展开的,在阅读之前可以先自己思考一下问题的答案是什么?

  • synchronized的实现原理大致是什么样的?
  • 你能想到多少synchronized的加锁误区呢?

一、基础原理

在谈到误区之前,我们先了解一下synchronized的基本实现原理,在JDK6之前,synchronized都是重量级锁,也就是通过操作系统加锁,这时候可能会导致加锁花费的系统时间比加锁以外需要使用的时间还要多,性能偏低。从JDK6开始,开始进行大量优化,引入偏向锁,自选锁,轻量级锁等,性能得到很大的提升。

在HotSpot的实现中,当线程第一访问对象时,会被对象头中的MarkWord中记录下线程的ID,当线程再次访问同步代码块时,发现其中记录的是自己的线程ID,那么可以直接进入同步代码块,这里也是偏向锁。

当有其他线程来竞争资源时,那么锁就要升级为轻量级锁,当有线程在执行同步代码块时,后来的线程并不会进入等待队列,而是不断的尝试获取锁,这就是自选锁,自己不断的旋转转圈,这里要消耗CPU资源的。

如果尝试10次之后还没有获取执行同步代码块的机会,那么这时候就需要升级为重量级锁,也就是通过操作系统上锁,这时候所有尝试获取锁的线程都变为阻塞状态,也会释放CPU资源。

这里也能获得一些其他结论:

同步代码执行时间短,线程数少的时候,可以考虑使用自选锁,减少使用重量级锁带来的时间消耗。当同步代码执行时间长,线程数多的时候,可以考虑使用重量级锁,减少CPU资源的消耗。

二、synchronized加锁的误区

1、在同一个类中,在静态方法和非静态方法上使用synchronized保证两者同步

这种情况下,这两者的锁标志是不一样的,非静态方法锁的是类的实例,静态方法锁的是类,这时候两者是可以同时执行的。

示例代码

public class T {
    /**
     * 相当于方法的代码执行时要synchronized(this)
     */
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + "m1 start");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m1 end");
    }
​
    /**
     * 相当于方法的代码执行时要synchronized(T.class)
     */
    public synchronized static void m2() {
        System.out.println(Thread.currentThread().getName() + "m2 start");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m2 end");
    }
​
    public static void main(String[] args) {
        T t = new T();
        // 两者使用的不是同一个锁,方法1执行完之前方法2就开始执行了
        new Thread(t::m1, "t1").start();
        new Thread(T::m2, "t2").start();
    }
}

2、创建Lock对象,代码使用 synchronized (lock) lock.lock()的混合双打保证不同的方法同步

这两者加锁的原理也是不一样的,synchronized会把lock实例当作锁标志,lock方法加锁时,锁的是其他东西。

示例代码

public class T {
    private final Lock lock = new ReentrantLock();
​
    public void m1() {
        System.out.println(Thread.currentThread().getName() + " m1 start");
        synchronized (lock) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " m1 end");
        }
    }
​
    public void m2() {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + " m2 start");
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " m2 end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
​
    public static void main(String[] args) {
        T t = new T();
        // 此处两者使用的不是同一把锁
        new Thread(t::m1, "t1").start();
        new Thread(t::m2, "t2").start();
    }
}

3、使用实体类加锁保证静态变量的同步

这里是行不通的,当加锁的是对象时,锁只能保证对象的变量是同步的。此时如果有其他线程修改静态变量时,就会导致该线程访问静态变量时获取到错误的值。

示例代码

public class T {
    private static volatile int count = 10;
​
    public void m() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            --count;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
​
    public static void main(String[] args) {
        // 此处锁的颗粒只限于实例本身,修改静态变量时无法保证同步
        for (int i = 0; i < 10; i++) {
            new Thread(new T()::m, "t" + i).start();
        }
    }
}

4、使用String,Integer(-128~127),Boolean等对象作为锁

在这里String有常量池,Integer也有缓冲池等,这会导致本来执行顺序的代码,因为使用了缓冲池中的同一个变量作为锁标志,变得有先后次序,出现不可预期的后果。

示例代码

public class T {
    public static void main(String[] args) {
        // T1 和 T2 没有顺序性,因为使用了同一把锁,变成了先后顺序执行
        // Boolean、封包的Integer对象(-128~127)、String常量等可能被重用的对象
        new Thread(new T1()::m, "T1").start();
        new Thread(new T2()::m, "T2").start();
    }
}
​
class T1 {
    private final Integer lock = 10;
​
    public void m() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " T1 start");
            try {
                for (int i = 0; i < 3; i++) {
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " T1 end");
        }
    }
}
​
class T2 {
    private final Integer lock = 10;
​
    public void m() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " T2 start");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " T2 end");
        }
    }
}

5、程序出现异常,synchronized锁会被释放吗?

程序在执行过程中,如果出现异常,默认情况锁会被释放,在第一个线程抛出异常,其他线程就会进入同步代码块,有可能访问到异常产生的数据,所以这里要注意数据的一致性

/**
 * 程序在执行过程中,如果出现异常,默认情况锁会被释放
 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况
 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适
 * 在第一个线程抛出异常,其他线程就会进入同步代码块,有可能访问到异常产生的数据
 * 因此要非常小心的处理同步业务逻辑中的异常
 */
public class T {
    int count = 0;
​
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while (true) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                // 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                int i = 1 / 0;
                System.out.println(i);
            }
        }
    }
​
    public static void main(String[] args) {
        T tx = new T();
        Runnable runnable = tx::m;
        new Thread(runnable, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(runnable, "t2").start();
    }
}

6、同步方法和非同步方法可以同时调用执行吗?

当然是可以的呀,你上厕所蹲坑的时候会影响别人在你后面擦马桶吗?

/**
 * 同步方法和非同步方法可以同时调用执行吗?
 */
public class T {
    public synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + "m1 start");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m1 end");
    }
​
    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m2 end");
    }
​
    public synchronized void m3() {
        System.out.println(Thread.currentThread().getName() + "m3 start");
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "m3 end");
    }
​
    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start();
        new Thread(t::m3, "t3").start();
        new Thread(t::m2, "t2").start();
    }
}
目录
相关文章
|
29天前
Synchronized锁原理和优化
Synchronize是通过对象头的markwordk来表明监视器的,监视器本质是依赖操作系统的互斥锁实现的。操作系统实现线程切换要从用户态切换为核心态,成本很高,此时这种锁叫重量级锁,在JDK1.6以后引入了偏向锁、轻量级锁、重量级锁 偏向锁:当一段代码没有别的线程访问,此时线程去访问会直接获取偏向锁 轻量级锁:当锁是偏向锁时,有另外一个线程来访问,偏向锁会升级为轻量级锁,这个线程会通过自旋方式不断获取锁,不会阻塞,提高性能 重量级锁:轻量级锁自旋一段时间后线程还没有获取到锁,线程就会进入阻塞状态,该锁会升级为重量级锁,重量级锁时,来竞争锁的所有线程都会阻塞,性能降低 注意,锁只能升
26 5
|
7月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
112 3
|
6月前
|
Java 程序员
【面试官】知道synchronized锁升级吗
线程A获取了某个对象锁,但在线程代码的流程中仍需再次获取该对象锁,此时线程A可以继续执行不需要重新再获取该对象锁。既然获取锁的粒度是线程,意味着线程自己是可以获取自己的内部锁的,而如果获取锁的粒度是调用则每次经过同步代码块都需要重新获取锁。此时synchronized重量级锁就回归到了悲观锁的状态,其他获取不到锁的都会进入阻塞状态。来获得锁,CAS操作不需要获得锁、释放锁,减少了像synchronized重量级锁带来的。轻量级锁通过CAS自旋来获得锁,如果自旋10次失败,为了减少CPU的消耗则锁会膨胀为。
170 4
|
5月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
96 0
|
安全 算法 Java
可重入锁,不可重入锁,死锁的多种情况,以及产生的原因,如何解决,synchronized采用的锁策略(渣女圣经)自适应的底层,锁清除,锁粗化,CAS的部分应用
可重入锁,不可重入锁,死锁的多种情况,以及产生的原因,如何解决,synchronized采用的锁策略(渣女圣经)自适应的底层,锁清除,锁粗化,CAS的部分应用
|
Java 编译器
Java多线程(4)---死锁和Synchronized加锁流程
Java多线程(4)---死锁和Synchronized加锁流程
79 0
|
7月前
|
安全 Java
大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
字节跳动大厂面试题详解:synchronized的偏向锁和自旋锁怎么实现的
79 0
|
7月前
|
存储 算法 Java
JUC并发编程之Synchronized锁优化
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
|
7月前
|
Java 编译器 程序员
synchronized 原理(锁升级、锁消除和锁粗化)
synchronized 原理(锁升级、锁消除和锁粗化)
|
存储 Java
面试~Synchronized 与 锁升级
面试~Synchronized 与 锁升级
61 0