zoukankan      html  css  js  c++  java
  • 并发编程-synchronized

      

    示例:

    public class Demo {
    
        private static int count=0;
        public static void inc(){
            try { Thread.sleep(1); }
            catch (InterruptedException e) {
            e.printStackTrace();
        } count++; }
        public static void main(String[] args) throws InterruptedException {
            for(int i=0;i<1000;i++){
                new Thread(()->Demo.inc()).start(); }
                Thread.sleep(3000);
            System.out.println("运行结果"+count);
        }
    
    }

    运行结果:

    997

    如何保证线程并行的数据安全问题?

    我们可以思考一下,问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使得线程的并行变成串行,那是不是就不存在这个问题呢?

    按照大家已有的知识,最先想到的应该就是锁吧。

    synchronized的基本认识

    在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。这块在后续我们会慢

    synchronized的基本语法

    synchronized有三种方式来加锁,分别是

    1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

    2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

    3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    不同的修饰类型,代表锁的控制粒度

    思考锁是如何存储的

    可以思考一下,要实现多线程的互斥特性,那这把锁需要哪些因素?

    1. 锁需要有一个东西来表示,比如获得锁是什么状态、无锁状态是什么状态

    2. 这个状态需要对多个线程共享

    那么我们来分析,synchronized锁是如何存储的呢?观察synchronized的整个语法发现,synchronized(lock)是基于lock这个对象的生命周期来控制锁粒度的,那是不是锁的存储和这个lock对象有关系呢?

    于是我们以对象在jvm内存中是如何存储作为切入点,去看看对象里面有什么特性能够实现锁

    对象在内存中的布局

    在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

    对象头包括mark word(锁信息、GC信息等)和类元信息(类元数据的首地址)

    Mark Word在32位虚拟机的长度是32bit、在64位虚拟机的长度是64bit。

    Mark Word里面存储的数据会随着锁标志位的变化而变化,Mark Word可能变化为存储以下5中情况

    线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor可以认为是一个同步对象,所有的Java对象是天生携带monitor。 

    多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识

    synchronized锁的升级

    使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。

    hotspot虚拟机的作者经过调查发现,大部分情况下,加锁

    的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的synchronized在JDK1.6之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。因此大家会发现在synchronized中,锁存在四种状态

    分别是:无锁、偏向锁、轻量级锁、重量级锁;锁的状态根据竞争激烈的程度从低到高不断升级。偏向锁

    当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了

    偏向锁的获取和撤销逻辑

    1. 首先获取锁对象的Markword,判断是否处于可偏向状态。(biased_lock=1、且ThreadId为空)

    2. 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到MarkWord

    a) 如果cas成功,那么markword就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块

    b) 如果cas失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行

    3. 如果是已偏向状态,需要检查markword中存储的ThreadID是否等于当前线程的ThreadIDa) 如果相等,不需要再次获得锁,可直接执行同步代码块

    b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

    偏向锁的撤销

    偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现cas失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

    对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

    1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于CAS重新偏向但前线程

    2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

    在我们的应用开发中,绝大部分情况下一定会存在2个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过jvm参数

    UseBiasedLocking来设置开启或关闭偏向锁

    轻量级锁的基本原理

    锁升级为轻量级锁之后,对象的Markword也会进行相应的的变化。升级为轻量级锁的过程:

    1. 线程在自己的栈桢中创建锁记录LockRecord。

    2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。

    3. 将锁记录中的Owner指针指向锁对象。

    4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针。

    自旋锁

    轻量级锁在加锁过程中,用到了自旋锁

    所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。

    注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。

    所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。

    自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。

    但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗CPU资源。默认情况下自旋的次数是10次,

    可以通过preBlockSpin来修改

    在JDK1.6之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。

    如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

    轻量级锁的解锁

    轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

    重量级锁的基本原理

    当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

    加了同步代码块以后,在字节码中会看到一个monitorenter和monitorexit。

    每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

    monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器

    monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

    任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

    示例总结:

    synchronized (lock) { // do something }

    情况一:只有Thread#1会进入临界区;

    情况二:Thread#1和Thread#2交替进入临界区,竞争不激烈;

    情况三:Thread#1/Thread#2/Thread3… 同时进入临界区,竞争激烈

    偏向锁

    此时当Thread#1进入临界区时,JVM会将lockObject的对象头Mark Word的锁标志位设为“01”,同时会用CAS操

    作把Thread#1的线程ID记录到Mark Word中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。

    轻量级锁

    偏向锁的场景太过于理想化,更多的时候是Thread#2也会尝试进入临界区,如果Thread#2也进入临界区但是Thread#1还没有执行完同步代码块时,会暂停Thread#1并且升级到轻量级锁。Thread#2通过自旋再次尝试以轻量级锁的方式来获取锁

    重量级锁

    如果Thread#1和Thread#2正常交替执行,那么轻量级锁基本能够满足锁的需求。但是如果Thread#1和Thread#2同时进入临界区,那么轻量级锁就会膨胀为重量级锁,意味着Thread#1线程获得了重量级锁的情况下,Thread#2就会被阻塞

    Synchronized结合Java Object对象中的wait,notify,notifyAll 

    前面我们在讲synchronized的时候,发现被阻塞的线程什么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁。那怎么做到显示控制呢?我们就需要借助一个信号机制:在Object对象中,提供了wait/notify/notifyall,可以用于控制线程的状态

    wait/notify/notifyall基本概念

    wait:表示持有对象锁的线程A准备释放对象锁权限,释放cpu资源并进入等待状态。

    notify:表示持有对象锁的线程A准备释放对象锁权限,通知jvm唤醒某个竞争该对象锁的线程X。线程A synchronized 代码执行结束并且释放了锁之后,线程X直接获得对象锁权限,其他竞争线程继续等待(即使线程X同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的notify ,notifyAll被调用)。

    notifyAll:notifyall和notify的区别在于,notifyAll会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程A释放锁之后,所有被唤醒的线程都有可能获得对象锁权限

  • 相关阅读:
    Google-C++编码规范中文版.pdf
    100个gdb小技巧(v1.0).pdf
    NSIS 3.0 发布,Windows 安装程序制作工具
    python爬取各类文档方法归类汇总
    【转】openwrt中ubus
    OpenWrt源码分析之ubus
    详解C语言中的fopen()函数和fdopen()函数
    IPsec技术介绍(转)
    mxml 详解
    Delphi IDE Theme Editor, Delphi IDE 主题编辑器,支持D7~Rad Studio 10.3 RIO及Lazarus
  • 原文地址:https://www.cnblogs.com/yintingting/p/6579681.html
Copyright © 2011-2022 走看看