Java并发编程之synchronized

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"。
  • 类型指针:对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁)
  • 记录数组长度数据:对象头剩下的一部分是用于记录数组长度的数据(如果当前对象不是数
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值