JAVA中的锁事务

JVM其实不推荐你停止一个线程,而是希望让一个线程能够执行完

为什么不推荐停止一个线程,因为如果贸然的去停止一个线程可能会出现资源不能及时释放,那么Jvm怎么去让一个线程执行完。一个线程没有执行完,无外乎就两种情况

  • 这个线程在阻塞中
  • 这个线程一直在while(true)

对于第一种情况:要想让这个线程能够执行完,那就要去解阻塞

对于第二种情况:要想让这个线程能够执行完,就要跳出这个循环

基础入门

Thread类

package com.hzk.demo0;

import lombok.SneakyThrows;

/**
 * Created by 撸码的小孩 on 2021/12/15
 * time  11:31
 */
public class demo0 {
    static Thread thread;
    public static  boolean running = true;
    public static void main(String[] args) throws InterruptedException {
          trandition1();
        //Thread.sleep(1000);
          running = false;
    }

    public static  void  trandition1(){
        thread = new Thread() {
            @Override
            public void run() {
               while (running){ 
               }
            }
        };
        thread.start();
    }
}

定义一个变量 running ,并且创建一个线程,在这个新的线程里进行一个空的循环,然后再main方法里面去调用创建子线程的方法,并且修改running变量的值为false,讲道理来说,这个成员变量running没有被volatile修饰,主线程是不能将其修改为false,因此我们子线程将会一直循环。来看一下测试结果是不是符合我们的预期呢?

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RqZtFr5o-1662641675152)(images/1.png)]

线程尽然结束了,并没有出现我们所预期的一直死循环。那么为什么呢。此时你可能觉得是没有加volatile关键字的原因,因为该关键字有一个特点能够保证可见性,在这里我先不去否定你的猜想,来看一下下面这个例子。

package com.hzk.demo0;

import lombok.SneakyThrows;

/**
 * Created by 撸码的小孩 on 2021/12/15
 * time  11:31
 */
public class demo0 {
    static Thread thread;
    public static  boolean running = true;
    public static void main(String[] args) throws InterruptedException {
          trandition1();
          Thread.sleep(1000);
          running = false;
    }

    public static  void  trandition1(){
        thread = new Thread() {
            @Override
            public void run() {
               while (running){ 
               }
            }
        };
        thread.start();
    }
}

在上一个例子的running = false的上只是去添加一行代码 Thread.sleep(1000);,来让主线程进行1秒钟的暂停,那么讲道理来说 结果应该仍然是和我们上面的测试结果一样。因为我并没有对 running 这个变量添加volatile关键字。只是单纯的让主线程休息了1s,那么再来看一下测试结果。

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RFCpGu9r-1662641675153)(images/2.png)]

结果又另我们大跌眼界,发现测试结果尽然和上面的不一样,那么这是什么原因呢,

此时你可能觉得仍然是没有加volatile关键字的原因,因为该关键字有一个特点能够保证可见性,而子线程因为没有读到成员变量running的重修改值,因此runining仍然是个ture,那么循环就将一直继续。下面我来解释一下,即使我们没有添加volatile这个关键字,子线程也不可能永远得不到cpu呀,总有一刻子线程能够去获得到的。而且从第一个测试结果我们也能看到,running这个变量也已经改变成false了,不然也不会退出循环。到此至少能证明一点,这个running变量对子线程一定是可见的。

那么问题来了,只是单纯 的加了一行睡眠,就出现了这个问题呢?接着在往下看:

在上面的代码中的while(true){}循环体中 我加一行输出System.out.println(“hello”);

while(true){System.out.println(“hello”);}

package com.hzk.demo0;

import lombok.SneakyThrows;

/**
 * Created by 撸码的小孩 on 2021/12/15
 * time  11:31
 */
public class demo0 {
    static Thread thread;
    public static  boolean running = true;
    public static void main(String[] args) throws InterruptedException {
          trandition1();
          Thread.sleep(1000);
          running = false;
    }

    public static  void  trandition1(){
        thread = new Thread() {
            @Override
            public void run() {
               while (running){ 
                    System.out.println("hello");
               }
            }
        };
        thread.start();
    }
}

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v054D2ue-1662641675154)(images/4.png)]

此时,你心里一定是崩溃的,千万个艹尼玛在崩腾,为什么循环体是空的,就不能结束该线程,而循环体调用out()方法就能够结束该线程呢。

主要原因是因为JVM虚拟机进行了指令的重排,该怎么理解这句话呢?

先来解释第一种结果

​ 第一种结果是没有加睡眠时间,循环能够退出,子线程能够结束,针对现在的计算机来说,如果开启一个子线程的后面执行了主线程的逻辑,那么百分之八十的情况下,不会先去执行子线程,而是会去执行主线程的业务代码。关于这点是为什么,鄙人技术太潜,不能给与证明,我也查了很多资料,都没有对这个问题进行去证明。我们姑且就这么去记。好,那么按照这个理论,在去看代码中,那么也就是说主线程就会先去将这个running变量修改为false,所以子线程在去执行的时候,此时running已经是false了,因此会退出循环,结束子线程。

在来解释第二种结果

第二种结果是加了睡眠时间,有了第一种的理论基础,第二种也就能解释通了,当主线程进行休眠后,子线程就一定会先被执行到,那么此时这个running变量仍然是个true,那么就会一直执行,然后当主线程睡眠时间过了1s之后,再去修改running变量,此时有两种解释 【

①:子线程一直处于执行中,主线程不能修改running变量值

②: 因为Jvm在执行子线程的时候发现此时循环体是一个空的轮训,因此会对其进行优化,会将这个running先进行一个临时变量接收,然后赋值。举个例子,在子线程中先获取到成员变量running的值,然后用一个临时变量temp来接收,接着while中的条件会优化成为这个temp值,而不会再次读取主线程中修改的这个running值。

最后解释第三种结果

第三种结果其实是用第二种结果的②条来解释,当Jvm在执行子线程的时候,发现此时子线程中while中循环体并不是一个空的轮训,而是有方法的调用【注意一定是方法的调用才可以,如果是变量的操作,仍然不会重新读取新的running值 还是复用临时变量】

偏向锁

想必关于偏向锁大家或多或少都有一些了解,我在这里在大致介绍一下,如果要了解偏向锁 首先要明白jdk1.6之前重量级锁,那么什么为重量级锁呢?

内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

说白了一句话 加锁过程中去调用操作系统的互斥信号量mutex而去加锁就可以认为是一把重量级锁。

明白了重量级锁之后 我们在来看一下偏向锁:

我们仔细想一下如果每次调用同步方法的时候都去像操作系统申请这个mutex互斥资源,那么就会导致你的jvm性能十分低下,因此jvm也不是个傻x,他也看到了这个痛点,因此从1.6之后开始对其进行优化。那怎么优化呢,能不能直接将Synchronized关键字去掉呢,这个显然不行,因为我们并不知道程序员写的某个方法什么时候会被多个线程调用,如果是单线程的情况下肯定是没有问题,但是谁能保证这个方法未来只会有一个线程去调用呢。因此这种优化是不可取的,接着jvm继续优化,在没有实际竞争的情况下,偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,在初始化时才需要一次CAS。也就是说只要是一个线程多次去加锁,那么我就没有必要让每一次的加锁都去像操作系统申请,而是在第一次加锁的时候像操作系统申请一次,如果记录成功,则偏向锁获取成功*,记录锁状态为偏向锁,*以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。

虽然有了上面这些苍白的话术得到这个结论,但是并没有充足的证据去证明偏向锁的存在。

你可以在网上翻阅很多的博客资料,但是都不会有人去证明这个偏向锁是存在的。所有的帖子几乎中无非大家都是同样的理论,不同的人去cv而已,也算是一种悲哀吧。

验证偏向锁的存在

要想去验证偏向锁到底存不存在 其实是一件非常难非常难的事 首先要求修改linux关于锁相关的一个类库源码,然后再将其重新编译。这个过程非常耗时和容易出错。

修改linux源码的步骤:

  1. 首先我们要了解linux安装完成之后,默认会有一个很关键的类库glibc,而这个类库的就相当于桥梁作用,即承接着我们的app应用,实现向操作系统中内核的调用,又承接着os向app的暴露出的内核服务。但是这个默认的glibc版本是2.7,直接修改它的话会有问题,因此需要重新下载一个开源的jlibc包2.9版本,注意我测试的时候是用2.9版本。然后去修改这个类库的lock相关源码,最后在对这个新的类库完成编译。
  2. 默认我认为你的linux中已经安装了jdk环境以及java-1.8.0-openjdk-devel【这个开发环境非常重要】接着将glibc-2.19上传上来并解压。
  3. 进入到glib-2.19文件的/nptl目录,找打pthread_mutex_lock.c文件
  4. 在pthread_mutex_lock方法中的assert下一行,添加上fprintf(stderr,“msg tid = %lu\n”,pthread_self())
  5. 修改完成之后 在上传到linux中进行覆盖原文件。【可以用ldd --version查看我们还没编译之前的glibc版本】
  6. 开始编译修改后的glib 先进到glibc-2.19目录里面 cd glibc-2.19
  7. 创建一个目录 mkdir out 然后进入到out里面
../configure --prefix=/usr --disable-profile --enable-add-ons  --with-headers=/usr/include --with-binutils=/usr/bin

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ELRFQ7w0-1662641675155)(images/5.png)]

9、执行make && make install【时间估计在10-15分钟】

10、再次使用ldd --version 查看 返现此时glibc 的版本是2.19。

如果中间没有出现错误,那么你输入java命令的时候会出现下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwIoDeC7-1662641675155)(JAVA中的锁事.assets/6.png)]

至此linux源码修改完成。

验证偏向锁的存在

类比:

先验证重量级锁的存在

因为重量级锁的加锁的过程每次都会像操作系统去申请,接着操作系统去调用自己的类库去执行pthread_metux_lock保证互斥机制,那么首先在JAVA中去开启两个线程,然后都去执行一段同步方法比如叫Sync(),在这个方法里去调用操作系统的获取线程Id的方法,如果通过这个方法获取到的线程Id和操作系统分配的线程Id相同,那么就能表示我通过java进行线程间的互斥是通过我们操作系统中的metux来实现。

1、通过Java程序调用C程序

①:新建一个JAVA程序用来创建两个线程并实现互斥


/**
 * Created by 撸码的小孩 on 2022/1/12
 * time  12:04
 */
public class HzkThread {
    Object o =new Object();
    static {
        System.loadLibrary( "HzkThreadIdNative" );
    }

    public static void main(String[] args)  {
        System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        HzkThread example4Start = new HzkThread();
        example4Start.start();
    }

    public void start()    {
        Thread thread = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(500);
                        sync();
                    } catch (InterruptedException e) {

                    }
                }
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(500);
                        sync();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.setName("t1");
        thread2.setName("t2");
        thread.start();
        thread2.start();

    }

    public native  void tid();

    public void sync(){
        synchronized (o){
            tid();
        }
    }

}

②:创建一个c文件,用来获取线程id

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include "HzkThread.h"
 

JNIEXPORT void JNICALL Java_HzkThread_tid(JNIEnv *env, jobject c1){
	
	printf("current tid:%lu-----\n",pthread_self());
	usleep(700);
} 

这个c文件中的方法名是有讲究的,JNICALL Java_HzkThread_tid(JNIEnv *env, jobject c1) 并不是随便写的,而是根据上面的java文件 生成的.h文件,然后用改文件中的这个方法来替换 c文件的这个方法,就会确定这个方法名。然后再这个方法里在调用获取当前线程id的方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值