synchronized为何能解决原子性?

何为原子性?

如果多个线程在做同一件事情的时候

1.如何产生

public class Demo {
    private int i =0;
    private void incr(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        Thread[] threads = new Thread[2];
        for (int i = 0; i <2; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.incr();
                }
            });
            //线程启动
            threads[i].start();
        }
        //join是主进程等待线程执行完毕
        threads[0].join();
        threads[1].join();
        System.out.println("计算的结果为------>"+ demo.i);
    }
}

  结果:由于 i++ 并不具备原子性则计算结果不正确

0
为何呢?那让我们来看一下class字节码的运行过程
将java文件编译成class文件,然后javap看一下class字节码他是如何的一种运行过程?
javap -v xxx.class
0
过程理解一下:
i++在class字节码中分为几个步骤?
  1. 先是通过getfield获取变量的值
  2. 然后将其放在操作数栈中
  3. 然后将其相加之后放回去
  4. 将计算好了的值在给i这个字段进行赋值
0
好了那么万一这个四步的其中一个环节线程切换到线程2去了。线程2执行完成回来再去拿i这个时候i还是线程1的i 线程2等于没有作用。这个就会发生问题
这就是在多线程环境下,存在的原子性问题,那么,怎么解决这个问题呢?

2.如何解决?

认真观察上面这个图,表面上是多个线程对于同一个变量的操作,实际上是i++这行代码,它不是原子的。所以才导致在多线程环境下出现这样一个问题。
也就是说,我们只需要保证,i++这个指令在运行期间,在同一时刻只能由一个线程来访问,就可以解决问题,那么接下来的重点就是同步锁Synchronized

Synchronized

Synchronized最终要实现互斥性不让让他们都去访问同一个对象

1.作用范围

synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:
1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
public static synchronized void one() { }
2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
public synchronized void two() { }
3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public synchronized void three() {synchronized (ThreadOneDemo.class){}}
这里里面看他传递进来的是什么对象 如果是static就是全局锁
可以控制锁的范围,影响锁的作用范围 其实就是对象的生命周期

2.锁的实现模型

0

3.如何实现?

Synchronized是如何实现锁的,以及锁的信息是存储在哪里?就拿上面分析的图来说,线程A抢到锁了,线程B怎么知道当前锁被抢占了,这个地方一定会有一个标记来实现,而且这个标记一定是存储在某个地方。

其实就是对象头里面存储了锁的信息

4.Markword对象头

这就要引出Markword对象头这个概念了,它是对象头的意思,简单理解,就是一个对象,在JVM内存中的布局或者存储的形式。
在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

 

 解释:

也就是说 每次线程进来的时候就会从 lock中获取这个对象 。从而知道这个对象的锁标记。争对是否有锁做对应的处理
可以用过ClassLayout打印对象头
添加依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

  例子

0
加上锁之后对象头是什么样的?
Object o = new Object();
public static void main(String[] args) {
    Demo demo = new Demo(); //o这个对象,在内存中是如何存储和布局的。
    System.out.println(ClassLayout.parseInstance(demo).toPrintable());
    synchronized (demo) {
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
    }
}

  输出

0
上面了解到对象头上储存了锁的类型,那么具体有哪些呢?往下看看
Synchronized锁升级
jdk6对锁的实现引入了大量的优化,比如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁操作的开销

1.Synchronized锁分类

  • 无锁
  • 偏量锁(默认延迟开启 4s)
  当前线程进入Synchronized的范围内但是没有其他线程进行竞争的前提下 他会偏向当前线程,当他下次再进来的时候就不需要再抢占锁 它能直接进来
  • 轻量级锁
    当之前偏向锁指向A,B来抢占锁,锁进行升级。会升级为轻量级锁通过自旋锁 用来保证 既不能让线程阻塞 还得让线程尽快获取
  • 重量级锁
    1. 用户得到内核态的交换需要用户空间到内核指令的发生
    1. 没有获得锁的线程会阻塞等待唤醒
这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现

2.锁的升级过程

  1. 去抢占锁查看一下偏量锁是否开启。
  2. 如果开启了就标记偏量锁 如果没有或者发送竞争就会锁升级为轻量级锁
  • 如果2个线程同时去抢占锁,这个时候 他是不会去直接标记偏量锁的他是会直接去标记为轻量级锁(因为偏量锁有延迟)
  • 一个线程抢占到了锁,那个线程在执行中 另外一个线程也不会一直等着。他会不停的循环重试。大概是10次这个过程他是不会阻塞的。(也可以将其称之为自旋锁)
  • 如果还是没等到的话 那么就把锁标记为重量级锁 第二个线程也就进入阻塞队列等待唤醒
  1. 如果其他线程都一直循环等待不到的话 就会把锁升级为重量级锁

2.0 锁的升级过程的具体解释:

1.默认情况下是偏向锁是开启状态,偏向的线程ID是0,偏向一个Anonymo0us BiasedLock 2.如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁,也就是把markword的线程ID改为当前抢占锁的线程ID的过程 3.如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁,线程在自己的线程栈帧中会创建一个LockRecord,用CAS操作把markword设置为指向自己这个线程的LR的指针,设置成功后表示抢占到锁。 4.如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋线程数超过CPU核心数的一般,在1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争的情况来自动控制自旋的时间。
升级到重量级锁,向操作系统申请资源, Linux Mutex,然后线程被挂起进入到等待队列。
0
以这张图的锁标志为参考下面用代码来看一下

2.轻量锁

public class ThreadTwoDemo3 {
    Object o =  new Object();
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(ClassLayout.parseInstance(person).toPrintable());
        synchronized (person){
            System.out.println("--------------------加锁之后-------------------");
            System.out.println(ClassLayout.parseInstance(person).toPrintable());
        }
    }
    public static class Person{
    }
}

  

0
 

3.偏量锁

默认情况下,偏向锁的开启是有个延迟,默认是4秒。为什么这么设计呢? 因为JVM虚拟机自己有一些默认启动的线程,这些线程里面有很多的Synchronized代码,这些Synchronized代码启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和撤销,效率较低。 通过下面这个JVM参数可以讲延迟设置为0.
-XX:BiasedLockingStartupDelay=0
设置成不延迟加载的话 就会默认使用偏量锁了
0
当前main获得了偏向锁
这里第一个对象和第二个对象都是偏向锁,因为打开的偏向锁 默认会有匿名对象去获取偏向锁

4.重量锁

Monitor监视器
在竞争比较激烈的情况下,线程一直无法获得锁的时候,就会升级到重量级锁。
public class ClassLayoutWeightDemo {
    public static void main(String[] args) {
        ClassLayoutWeightDemo testDemo = new ClassLayoutWeightDemo();
        Thread t1 = new Thread(() -> {
            synchronized (testDemo){
                System.out.println("t1 lock ing");
                System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
            }
        });
        t1.start();
        synchronized (testDemo){
            System.out.println("main lock ing");
            System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
        }
    }
}

从结果可以看出,在竞争的情况下锁的标记为 [010] ,其中所标记 [10]表示重量级锁

main lock ing
com.example.gupao_thread_v1.synchron02.ClassLayoutWeightDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           ca c9 e4 02 (11001010 11001001 11100100 00000010) (48548298)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

t1 lock ing
com.example.gupao_thread_v1.synchron02.ClassLayoutWeightDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           ca c9 e4 02 (11001010 11001001 11100100 00000010) (48548298)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 

 
posted @ 2022-05-14 16:10  Nuti  阅读(234)  评论(0编辑  收藏  举报