JAVA多线程(三)-- 线程同步-synchronized

线程同步机制
1、什么叫做线程同步?
线程有自己的私有数据,比如栈和寄存器,当多个线程同时读写同一份共享资源的时候,会引起冲突,这时候需要引入线程同步机制使得各个线程排队一个一个对共享资源进行访问,而不是同时进行。
实现线程同步的方法主要有三种:
1) synchronized关键字
2) 原子类 atomicXXXX (volatile + CAS操作)
3) ReentrantLock
这篇博客我们主要介绍 synchronized,volatile 和ReentrantLock在后面的博客我会介绍到。
2、概念:
1)同步:当对个线程同时访问一块资源,这些线程间就需要同步
2)临界资源:同一时刻只允许一个线程访问的资源
3)临界区:访问临街资源的代码块
4)临界区特点:临界区是一段供线程独占式访问的代码,也就是说若有一个线程正在访问该代码段,其他线程想要访问,只有等待当前线程离开方可进入,这样保证了线程的安全。
3、Synchronized(提供了一种排他机制,在同一时间只能有一个线程执行某些操作)
3.1 基本介绍
Synchronized提供了一种锁的机制,能够保证共享变量的互斥访问,从而防止数据不一致问题的出现。
3.2 基本用法
1)同步方法
public synchronized void func(){}
public synchronized static void func(){}
2)同步代码块
private final Object mutex = new Object();
public void func(){
sychronized(mutex){
}
}
下面我们来看一个例子:启动两个线程,顺序输出5,4,3,2,1,第一个线程输出完成之后第二个线程方可输出。

public class GY1 {
    public static void print() {
        synchronized (GY1.class) {
            int i = 5;
            while (i > 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i--);

                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread("t1"){
            @Override
            public void run() {
                print();
            }
        }.start();

        new Thread("t2"){
            @Override
            public void run() {
                print();
            }
        }.start();
        }
}

在这里插入图片描述
3.3 java中的对象头,monitor
①Java对象-> 由对象头、实例数据、对其填充的字节组成;
②对象头->由Mark Word、指向类的指针、数据长度(只有数组对象才会有)组成;
③Mark Word 记录了对象和锁的相关信息;(后文将会对其进行介绍)
④monitor->可以理解为一个同步工具,通常其被描述为一个对象。
需要注意的是:所有的java对象都是天生的monitor,每一个java对象自打创建除开就携带了一把锁, 这把锁称之为对象锁或者Monitor锁。
3.4 Synchronized关键字
首先先写一段代码:

public class GY1 {
    private final static Object mutex = new Object();

    public static void func(){
        synchronized (mutex){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        for(int i=0; i<5; i++){
            new Thread(){
                @Override
                public void run() {
                    func();
                }
            }.start();
        }
        }
}

对这个代码反编译之后,会看到两个特殊的指令monitorenter 和monitorexit,在官方的JVM规范中可以得到这两个指令的解释,如下:
当修饰代码块时:
在这里插入图片描述
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
这段话的大概意思为:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit: 
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段话的大概意思为:
执行monitorexit的线程必须是object ref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
在修饰方法时:
在这里插入图片描述
反编译之后,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
4.synchronized的演变(锁升级)
JDK1.6以后版本在处理同步锁才存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JVM在使用锁的过程会用到Java对象头中的Mark Word,Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
下面用一张图来说明Mark Word的结构以及锁的升级标志位:
在这里插入图片描述

  • 偏向锁:核心思想就是如果一个线程获得了锁,那么锁就会进入偏向模式,当这个线程再次请求该锁时,无需任何的同步操作操作,因此提高程序的性能。对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次同一个线程申请相同的锁,但是对于锁竞争比较激烈的场合,偏向锁就会失效。
  • 轻量级锁:对于绝大部分的锁,在整个同步周期内不存在竞争的,这只是经验数据
  • 自旋锁:介于轻量级锁和重量级锁之间的虚拟机的优化手段,给定一个循环次数,在经历若干次循环,如果得到锁,就顺序执行同步代码,反之就会将线程在操作系统层面挂起,这也是为了提升效率。
  • 重量级锁:未获取锁的线程会阻塞
    下来再说一下synchronized锁的使用过程(synchronized保证了并发编程的原子性、可见性、有序性)
    ① 当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
    ② 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
    ③ 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
    ④当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
    ⑤ 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的引用,同时在对象锁Mark Word中保存指向这片空间的引用。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
    ⑥ 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
    ⑦ 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
    在这里还需要考虑的一个问题是:为什么说重量级锁开销大呢?
    原因主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值