Java并发编程之synchronized

本文探讨了Java中的synchronized关键字及其在处理线程安全问题中的作用。文章详细介绍了线程安全的概念,提供了线程安全的实现方法,如互斥同步和非阻塞式同步。接着,文章深入剖析了synchronized的三种使用方式,包括实例方法、静态方法和代码块同步,并通过示例代码解释了其工作原理。文章进一步探讨了synchronized的锁优化,如偏向锁、轻量级锁和重量级锁,并分析了锁升级的过程。最后,文章结合源码解析了synchronized的底层实现,包括锁的获取和释放机制。
摘要由CSDN通过智能技术生成
2824145-4e592f4f3b8d49b5.jpeg
学习.jpeg

该文章属于《Java并发编程》系列文章,如果想了解更多,请点击《Java并发编程之总目录》

前言

上篇文章我们讲了volatile关键字,我们大致了解了其为轻量级的同步机制,现在我们来讲讲我们关于同步的另一个兄弟synchronized。synchronized作为开发中常用的同步机制,也是我们处理线程安全的常用方法。相信大家对其都比较熟悉。但是对于其内部原理与底层代码实现大家有可能不是很了解,下面我就和大家一起彻底了解synchronized的使用方式与底层原理。

线程安全的问题

线程安全的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

在具体讲解synchronized之前,我们需要了解一下什么是线程安全,为什么会出现线程线程不安全的问题。请看下列代码:

class ThreadNotSafeDemo {
    private static class Count {
        private int num;
        private void count() {
            for (int i = 1; i <= 10; i++) {
                num += i;
            }
            System.out.println(Thread.currentThread().getName() + "-" + num);
        }
    }
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            Count count = new Count();
            public void run() {
                count.count();
            }
        };
        //创建10个线程,
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}

上述代码中,我们创建Count类,在该类中有一个count()方法,计算从1一直加到10的和,在计算完后输出当前线程的名称与计算的结果,我们期望线程输出的结果是首项为55且等差为55的等差数列。但是结果并不是我们期望的。具体结果如下图所示:

2824145-cfa91431d1aaf3b3.png
输出结果.png

我们可以看见,线程并没有按照我们之间想的那样,线程按照从Thread-0到Thread-9依次排列,并且Thread-0与Thread-1线程输出的结果是错误的。

之所以会出现这样的情况,是CPU在调度的时候线程是可以交替执行的,具体来讲是因为当前线程Thread-0求和后,(求和后num值为55),在即将执行打印语句时,突然CPU开始调度执行Thread-1去执行count()方法,那么Thread-0就会停留在即将打印语句的位置,当Thread-1执行计算和后(求和后num值为100),这个时候CPU又开始调度Thread-0执行打印语句。则Thread-1开始暂停,而这个时候num值已经为110了,所以Thread-0打印输出的结果为110。

线程安全的实现方法

上面我们了解了之所以会出现线程安全的问题,主要原因就是因为存在多条线程共同操作共享数据,同时CPU的调度的时候线程是可以交替执行的。导致了程序的语义发生改变,所以会出现与我们预期的结果违背的情况。因此为了解决这个问题,在Java中提供了两种方式来处理这种情况。

互斥同步(悲观锁)

互斥同步是指当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行。

在Java中最基本的互斥同步就是synchronized(这里我们讨论的是jdk1.6之前,在jdk1.6之后Java团队对锁进行了优化,后面文章会具体描述),也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

除了synchronized之外,我们还可以使用java.util.concurrent包下的ReentrantLock来实现同步。

非阻塞式同步(乐观锁)

互斥同步主要的问题就是进行线程阻塞和唤醒锁带来的性能问题,为了解决这性能问题,我们有另一种解决方案,当多个线程竞争某个共享数据时,没有获得锁的线程不会阻塞,而是不断的尝试去获取锁,直到成功为止。这种方案的原理就是使用循环CAS操作来实现。

synchronized的三种使用方式

了解了synchronized的解决的问题,那么我们继续来看看在Java中在Java中synchronized的使用情况。

在Java中synchronized主要有三种使用的情况。下面分别列出了这几种情况

  • 修饰普通的实例方法,对于普通的同步方法,锁式当前实例对象
  • 修饰静态方法,对于静态同步方法,锁式当前类的Class对象
  • 修饰代码块,对于同步方法块,锁是Synchronized配置的对象
证明当前普通的同步方法,锁式当前实例对象

为了证明普通的同步方法中,锁是当前对象。请观察以下代码:

class SynchronizedDemo {

    public synchronized void normalMethod() {
        doPrint(5);
    }
 
    public void blockMethod() {//注意,同步块方法块中,配置的是当前类的对象
        synchronized (this) {
            doPrint(5);
        }
    }
    //打印当前线程信息与角标值
    private static void doPrint(int index) {
        while (index-- > 0) {
            System.out.println(Thread.currentThread().getName() + "--->" + index);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        new Thread(() -> demo.normalMethod(), "testNormalMethod").start();
        new Thread(() -> demo.normalMethod(), "testBlockMethod").start();
    }
 }

在上诉代码中,分别创建了两个方法,normalMethod()与blockMethod()方法,其中normalMethod()方法为普通的同步方法,blockMethod()方法中,是一个同步块且配置的对象是当前类的对象。在Main()方法中,分别创建两个线程执行两个不同的方法。

程序输出结果
2824145-7616a244b81ac54f.png
输出结果.png

观察程序输出结果,我们可以看到normalMethod方法是由于blockMethod方法执行的,且blockMethod方法是在normalMethod方法执行完成之后在执行的。也就证明了我们的对于普通的同步方法锁式当前实例对象的结论。

证明对于静态同步方法,锁式当前类的Class对象
class SynchronizedDemo {
    public void blockMethod() {
        synchronized (SynchronizedDemo.class) {//注意,同步块方法块中,配置的是当前类的Class对象
            doPrint(5);
        }
    }
    public static synchronized void staticMethod() {
        doPrint(5);
    }
    /**
     * 打印当前线程信息
     */
    private static void doPrint(int index) {
        while (index-- > 0) {
            System.out.println(Thread.currentThread().getName() + "--->" + index);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedDemo demo = new SynchronizedDemo();
        new Thread(() -> demo.blockMethod(), "testBlockMethod").start();
        new Thread(() -> demo.staticMethod(), "testStaticMethod").start();
    }

}

在有了第一个结论的证明后,对于静态同步方法的锁对象就不再进行描述了(但是大家要注意一下,同步方法块中配置的对象是当前类的Class对象)。下面直接给出输出结果:


2824145-b44650256c9ed7b6.png
TIM截图20180821140901.png

观察结果,也很明显的证明了对于静态同步方法,锁式当前类的Class对象的结论

Synchronized的原理

下面文章主要是讲解jdk1.6之后Java团队对锁进行了优化之后的原理,优化之后涉及到偏向锁、轻量级锁、重量级锁。其中该文章都涉及jdk源码,这里把最新的jdk源码分享给大家----->jdk源码

在了解Synchronized的原理的原理之前,我们需要知道三个知识点第一个是CAS操作,第二个是Java对象头(其中Synchronized使用的锁就在对象头中)第三个是jdk1.6对锁的优化。在了解以上三个知识点后,再去理解其原理就相对轻松一点。关于CAS操作已经在上篇文章《Java并发编程之Java CAS操作》进行过讲解,下面我们来讲解关于Java对象头与锁优化的知识点。

Java对象的内存布局

在Java虚拟机中,对象在内存的存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)。其中虚拟机中的对象头包括三部分信息,分别为"Mark Word"、类型指针、记录数组长度的数据(可选),具体情况如下图所示:

2824145-81a3128a336b0a21.jpg
对象存储结构.png
Java对象头的组成
  • “Mark Word“:第一部分用于存储对象自身的运行时数据。如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向锁时间戳等,这部分的数据在长度32位与64位的虚拟机中分别为32bit和64bit,官方称为“Mark Word"。
  • 类型指针:对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(Ja
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值