Java多线程核心技术二-synchronzied同步方法

1 概述

        关键字synchronzied保障了原子性、可见性和有序性。

        非线程安全问题会在多个线程对同一个对象中的同一个实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是读取到的数据其实是被更改过的。而线程安全是指获取的实例变量的值是经过同步处理的,不会出现脏读的现象。本篇将细化线程并发访问的内容,在细节上更多讲解并发时变量值的处理方法。

2 方法内的变量是线程安全的

        非线程安全问题存在时实例变量中,如果是方法内部私有变量,则不存在非线程安全问题,结果是线程安全的。

【示例1.1.1】演示方法内部声明一个变量时,是不存在“非线程安全”问题的。

public class HasSelfPrivateNum {
    public void addI(String username){
        try {
            int num = 0;
            if(username.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(4000);
            }else{
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + "num = " +num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

}

   

public class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }
    @Override
    public void run(){
        numRef.addI("a");
    }
}

   

public class ThreadB extends Thread{
    private HasSelfPrivateNum refNef ;

    public ThreadB(HasSelfPrivateNum refNef) {
        this.refNef = refNef;
    }
    @Override
    public void run(){
        refNef.addI("b");
    }
}
public class RunDemo201 {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(hasSelfPrivateNum);
        ThreadB b = new ThreadB(hasSelfPrivateNum);
        a.start();
        b.start();
    }
}

3 实例变量“非线程安全”问题及解决方案

        如果多个线程共同访问一个对象中的实例变量,则有可能出现非线程安全问题。线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况。把上面 HasSelfPrivateNum类中addI()方法的局部变量改为全局变量。在执行,num有可能被覆盖。

public class HasSelfPrivateNum {
    private int num = 0;
    public void addI(String username){
        try {

            if(username.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(4000);
            }else{
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + "num = " +num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

}

        这个实验是两个线程同时访问一个业务对象中的同一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能会出现非线程安全问题,根据前面的介绍,只需要在方法加上synchronized即可,更改后的代码:

public class HasSelfPrivateNum {
    private int num = 0;
    synchronized public void addI(String username){
        try {

            if(username.equals("a")){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(4000);
            }else{
                num = 200;
                System.out.println("b set over");
            }
            System.out.println(username + "num = " +num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

}

 

4 同步synchronzied在字节码指令中的原理

        在方法上使用synchronzied关键字实现同步的原因是使用了flag标记ACC_SYNCHRONZIED,当调用方法时,调用质量会检查方法的ACC_SYNCHRONZIED访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。

【示例1.3.1】测试代码

public class TestSynchronzied {
    synchronized public  static void testMethod(){

    }

    public static void main(String[] args) throws InterruptedException{
        testMethod();
    }
}

使用javap命令把class文件转换成字节码指令,如下:

javap -c -v TestSynchronzied.class

生成这个class文件对应的字节码指令,指令的核心代码如下:

 public com.jay.current.demo213.TestSynchronzied();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jay/current/demo213/TestSynchronzied;

  public static synchronized void testMethod();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 6: 0

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #7                  // Method testMethod:()V
         3: return
      LineNumberTable:
        line 9: 0
        line 10: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
    Exceptions:
      throws java.lang.InterruptedException

        在反编译的字节码指令中对public synchronzied void testMethod()方法使用了flag标记ACC_SYNCHRONZIED,说明此方法时同步的。

        如果使用synchronzied代码块,则使用monitorenter和monitorexit指令进行同步处理。测试代码如下:

public class TestSynchronzied_2 {
    public void testMethod(){
        synchronized (this){
            int age = 100;
        }
    }

    public static void main(String[] args) throws InterruptedException{
        TestSynchronzied_2 testSynchronzied2 = new TestSynchronzied_2();
        testSynchronzied2.testMethod();
    }
}

在CMD执行命令:

javap -c -v TestSynchronzied_2.class

生成的字节码指令如下:

 public void testMethod();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: bipush        100
         6: istore_2
         7: aload_1
         8: monitorexit
         9: goto          17
        12: astore_3
        13: aload_1
        14: monitorexit
        15: aload_3
        16: athrow
        17: return
      Exception table:
         from    to  target type
             4     9    12   any
            12    15    12   any
      LineNumberTable:
        line 5: 0
        line 6: 4
        line 7: 7
        line 8: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  this   Lcom/jay/current/demo213/TestSynchronzied_2;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ class com/jay/current/demo213/TestSynchronzied_2, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #7                  // class com/jay/current/demo213/TestSynchronzied_2
         3: dup
         4: invokespecial #9                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #10                 // Method testMethod:()V
        12: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            8       5     1 testSynchronzied2   Lcom/jay/current/demo213/TestSynchronzied_2;
    Exceptions:
      throws java.lang.InterruptedException

        有代码可知,在字节码使用了monitorenter和monitorexit指令进行同步处理。

5 多个对象多个锁

        【示例1.4.1】

public class HasSelfPrivateNum {
    synchronized public void testMethod(){
        try {
            System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

         上面的代码有同步方法testMethod(),说明此方法在正常情况下应该被顺序调用。在创建两个线程类。

public class ThreadA extends Thread{
    private  HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }
    @Override
    public  void run(){
        numRef.testMethod();
    }
}
public class ThreadB extends Thread{
    private  HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }
    @Override
    public  void run(){
        numRef.testMethod();
    }
}

        最后创建main函数类

public class Run1 {
    public static void main(String[] args) {
        HasSelfPrivateNum num1 = new HasSelfPrivateNum();
        HasSelfPrivateNum num2 = new HasSelfPrivateNum();
        ThreadA a = new ThreadA(num1);
        a.start();
        ThreadB b = new ThreadB(num2);
        b.start();
    }
}

        运行结果:

        代码分析:首先创建了两个 HasSelfPrivateNum.java类的对象,即产生了两把锁。两个线程分别访问同一个类的两个不同示例的相同名称的同步方法(testMethod()),控制台输出了两个begin和end,且begin和end不是成对的输出,呈现了两个线程交叉输出的效果,说明两个线程以异步方式同时运行。

        本示例创建了两个业务对象,在系统中产生了两把锁,线程和业务对象属于一对一的关系,每个线程执行自己所属业务对象中的方法,不存在锁的争抢关系,所以运行结果是异步的。另外,在这种情况下synchronzied可以不需要,因为不会出现非线程安全的问题。

        只有多个线程执行统一个业务对象中的同步方法时,线程和业务对象属于多对一的关系,为了避免出现非线程安全问题,所以使用了synchronzied。

        总结:多个线程对共享的资源有写操作,则必须同步,如果只是读操作,则不需要同步。

6 synchronzied方法将对象作为锁

        为了证明上面说的将对象作为锁,开发以下代码:

public class MyObject {
    public  void methodA(){
        try{
            System.out.println("开始执行 methodA方法,线程名为:"+Thread.currentThread().getName());
            Thread.sleep(3000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class ThreadADemo215 extends Thread{
    private MyObject myObject;

    public ThreadADemo215(MyObject myObject) {
        this.myObject = myObject;
    }
    @Override
    public void  run(){
        myObject.methodA();
    }
}
public class ThreadBDemo215 extends Thread{
    private MyObject myObject;

    public ThreadBDemo215(MyObject myObject) {
        this.myObject = myObject;
    }
    @Override
    public void  run(){
        myObject.methodA();
    }
}
public class Run1 {
    public static void main(String[] args) {
        MyObject object = new MyObject();
        ThreadADemo215 a = new ThreadADemo215(object);
        a.setName("A");
        ThreadBDemo215 b = new ThreadBDemo215(object);
        b.setName("B");
        a.start();
        b.start();
    }
}

运行结果

        两个线程一同进入methodA方法,因为该方法没有同步。更改MyObject类,加上synchronzied关键字。

public class MyObject {
    synchronized public  void methodA(){
        try{
            System.out.println("开始执行 methodA方法,线程名为:"+Thread.currentThread().getName());
            Thread.sleep(3000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

 运行结果:

        通过上面的示例得到结论,调用关键字synchronzied声明的方法一定是排队运行。另外,需要记住“共享”这两个字,只有共享资源的写操作才需要同步,如果不是共享资源,是没有同步的必要的。 那其他方法被调用时会有什么效果呢?如何查看将对象作为锁的效果呢?重新修改MyObject.java类,新增一个没用synchronzied修改的方法methodB()

public class MyObject {
    synchronized public  void methodA(){
        try{
            System.out.println("开始执行 methodA方法,线程名为:"+Thread.currentThread().getName());
            Thread.sleep(3000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void methodB(){
        try{
            System.out.println("开始执行 methodB方法,线程名为:"+Thread.currentThread().getName());
            Thread.sleep(3000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

同时修改 ThreadBDemo215.java,run方法调用methodB()。运行结果:

        通过这个示例得知,虽然线程A先持有了object对象的锁,但线程B完全可以异步调用非synchronzied类型的方法。

        在把MyObject.java中的methodB方法加上synchronzied关键字,执行结果如下:

        两个线程访问同一个对象的两个同步方法。结论如下:

        1、A现成先持有object对象的锁,B现成可以以异步的方式调用object对象中的非synchronzied类型的方法。       

        2、A现成先持有object对象的锁,B现成如果在这时调用object对象中的synchronzied类型的方法,则需要等待A线程释放锁。

        3、在方法声明处添加sync并不是锁方法,而是锁当前类的对象。

        4、在java中,只有将对象作为锁,并没有锁方法这种说法。

        5、在Java中,锁就是对象,对象可以映射成锁,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronzied同步方法。

        6、如果在X对象中使用了synchronzied关键词声明非静态方法,则X对象就被当成锁。

7 脏读与解决

        在多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronzied关键字进行同步。虽然在赋值时进行了同步,但是在取值时有可能出现脏读。发生脏读的原因是在读取实例变量时,此值已经被其他线程更改过了。

【示例7.1】 

public class PublicVar {
    public String username = "A";
    public String password = "B";
    synchronized public void setValue(String username,String password){
        try{
            this.username = username;
            Thread.sleep(2000);
            this.password = password;
            System.out.println("执行setValue()方法,线程名是: "+Thread.currentThread().getName() + " username = " + username + ";password = " + password);
            
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public void getValue(){
        System.out.println("getValue()方法,线程名是: "+Thread.currentThread().getName() + " username = " + username + ";password = " + password);
    }
}
public class ThreadADemo216 extends Thread{
    private PublicVar publicVar;

    public ThreadADemo216(PublicVar publicVar) {
        this.publicVar = publicVar;
    }
    @Override
    public void run(){
        publicVar.setValue("B","BB");
    }
}
public class Test1 {
    public static void main(String[] args) {
        try {
            PublicVar publicVar = new PublicVar();
            ThreadADemo216 a = new ThreadADemo216(publicVar);
            a.start();
            Thread.sleep(2000);
            publicVar.getValue();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

运行结果:

出现脏读是因为getValue方法并不是同步的,所以可以在任意时候进行调用,解决办法是加上synchronzied。

8 synchronzied锁重入

        关键词synchronzied拥有重入锁的功能,即在使用synchronzied时,当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁。这也证明在一个synchronzied方法/块的内部调用本类的其他synchronzied方法/this块时,是永远可以得到锁的。 

public class Service {
    synchronized public void service1(){
        System.out.println("service1");
        service2();
    }

    synchronized private void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized private void service3() {
        System.out.println("service3");
    }
}
public class MyThread extends Thread{
    
    @Override
    public void run(){
        Service service = new Service();
        service.service1();
    }
}
public class Run1 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

        可重入锁是指自己可以再次获取自己的内部锁。例如,有1个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象锁时还可以获取。如果是不可重入锁,方法service2和service3都不会被调用。

9 出现异常,锁自动释放

        当一个线程执行的代码出现异常时,其锁持有的锁会自动释放。

        【示例】

public class Service {
    synchronized public void testMethod(){
        if(Thread.currentThread().getName().equals("a")){
            System.out.println("线程名="+Thread.currentThread().getName()
            +"开始执行时间 = " + System.currentTimeMillis());
            int i = 1;
            while(i == 1){
                if(("" + Math.random()).substring(0,8).equals("0.123456")){
                    System.out.println("线程名="+Thread.currentThread().getName()
                            +"执行异常时间 = " + System.currentTimeMillis());
                    Integer.parseInt("a");
                }
            }
        }else{
            System.out.println("线程B运行时间 = " +System.currentTimeMillis());
        }
    }
    
}

        创建两个自定义线程。

public class ThreadA extends Thread{
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }
    @Override
    public void run(){
        service.testMethod();
    }
}
public class ThreadB extends Thread{
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

    @Override
    public void run(){
        service.testMethod();
    }

}
public class Run1 {
    public static void main(String[] args) {
        try {
            Service service = new Service();
            ThreadA a = new ThreadA(service);
            a.setName("a");
            a.start();
            Thread.sleep(500);
            ThreadB b = new ThreadB(service);
            b.setName("b");
            b.start();
        }catch (InterruptedException e){
            e.printStackTrace();
        }


    }
}

        线程a出现异常并释放锁,线程b进入方法正常输出,说明出现异常时,锁被自动释放了。

        【注意】Thread.java中的suspend()和sleep()方法被调用后不会释放所。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/195508.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蚂蚁庄园小课堂答题今日答案最新

蚂蚁庄园小课堂答题今日答案最新 温馨提醒&#xff1a;由于本文章会停留在一个固定的更新时间上&#xff0c;包含当前日期最新的支付宝蚂蚁庄园小课堂答题今日答案。如果您看到这篇文章已成为过去时&#xff0c;请按下面的方法进入查看天天保持更新的最新今日答案&#xff1b; …

Martin Fowler:数字化时代,远程与本地协同工作孰优孰劣?(2)| IDCF

作者&#xff1a;Martin Fowler 译者&#xff1a;冬哥 原文&#xff1a;https://martinfowler.com/articles/remote-or-co-located.html &#xff08;接上篇 &#xff09; 二、大多数人在同地办公时工作效率更高 与软件开发中的许多主题一样&#xff0c;我不能拿 100 个软…

NX二次开发UF_CURVE_convert_conic_to_std 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_convert_conic_to_std Defined in: uf_curve.h int UF_CURVE_convert_conic_to_std(UF_CURVE_genconic_p_t gen_conic_data, UF_CURVE_conic_t * conic_data, logical * se…

15:00面试,15:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到8月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…

自动化测试|我为什么从Cypress转到了Playwright?

以下为作者观点&#xff1a; 早在2019年&#xff0c;我就开始使用Cypress &#xff0c;当时我所在的公司决定在新项目中放弃Protractor 。当时&#xff0c;我使用的框架是Angular&#xff0c;并且有机会实施Cypress PoC。最近&#xff0c;我换了工作&#xff0c;现在正在使用R…

MEFLUT: Unsupervised 1D Lookup Tables for Multi-exposure Image Fusion

Abstract 在本文中&#xff0c;我们介绍了一种高质量多重曝光图像融合&#xff08;MEF&#xff09;的新方法。我们表明&#xff0c;曝光的融合权重可以编码到一维查找表&#xff08;LUT&#xff09;中&#xff0c;该表将像素强度值作为输入并产生融合权重作为输出。我们为每次…

涵盖多种功能,龙讯旷腾Module第一期:物质结构

Module是什么 在PWmat的基础功能上&#xff0c;我们针对用户的使用需求开发了一些顶层模块&#xff08;Module&#xff09;。这些Module中的一部分是与已有的优秀工具的接口&#xff0c;一部分是以PWmat的计算结果为基础得到实际需要的物理量&#xff0c;一部分则是为特定的计…

预算削减与经济动荡:2024 年明智且经济地创新

如何在经济衰退周期中保持创新&#xff1f;这篇创新研究提供了实用建议。在经济下行压力下领导者往往会试图降低成本和维持生存。然而&#xff0c;这种二元对立的压力往往会导致领导者做出不够理想的决策&#xff0c;更多地关注生存而不是未来投资。本文提供了一系列实用的建议…

PC行内编辑

点击编辑&#xff0c;行内编辑输入框出现&#xff0c;给列表的每条数据定义编辑标记&#xff0c;最后一定记得 v-model双向绑定&#xff0c;使数据回显。 步骤&#xff1a; 1、给行数据定义编辑标记 2、点击行编辑标记&#xff08;isedit&#xff09; 3、插槽根据标记渲染表单 …

Redis大key与热Key

什么是 bigkey&#xff1f; 简单来说&#xff0c;如果一个 key 对应的 value 所占用的内存比较大&#xff0c;那这个 key 就可以看作是 bigkey。具体多大才算大呢&#xff1f;有一个不是特别精确的参考标准&#xff1a; bigkey 是怎么产生的&#xff1f;有什么危害&#xff1f;…

408—电子笔记分享

一、笔记下载 链接&#xff1a;https://pan.baidu.com/s/1bFz8IX6EkFMWTfY9ozvVpg?pwddeng 提取码&#xff1a;deng b站视频&#xff1a;408-计算机网络-笔记分享_哔哩哔哩_bilibili 包含了408四门科目&#xff08;数据结构、操作系统、计算机组成原理、计算机网络&#xff09…

Python交互式解释器及用法

为了让开发者能快速学习、测试 Python 的各种功能&#xff0c;Python 提供的“python”命令不仅能用于运行 Python 程序&#xff0c;也可作为一个交互式解释器一一开发者逐行输入 Python 代码&#xff0c;它逐行解释执行。 当输入“python”命令时&#xff0c;可以看到如下输出…

如何生成唯一ID:探讨常用方法与技术应用

文章目录 1. UUID&#xff08;Universally Unique Identifier&#xff09;2. 数据库自增ID3. Twitter的Snowflake算法4. 数据库全局唯一ID&#xff08;Global Unique Identifier&#xff0c;GUID&#xff09;结语 &#x1f389;如何生成唯一ID&#xff1a;探讨常用方法与技术应…

steam搬砖如何选品?选品软件和教程靠谱吗?

说到steam搬砖项目&#xff0c;目前平台最火的就是CSGO游戏搬砖。在steam搬砖项目中&#xff0c;选品是一个至关重要的环节&#xff0c;直接影响到利润。而选品软件可以帮助我们更快地了解市场变化、计算成本利润等关键信息&#xff0c;提高选品的效率和准确性。可靠的选品软件…

MySQL学习day03

一、SQL图形化界面工具 常用比较常用的图形化界面有sqlyog、mavicat、datagrip datagrip工具使用相当方便&#xff0c;功能比前面两种都要强大。 DataGrip工具的安装和使用请查看这篇文档&#xff1a;DataGrip 安装教程 DML-介绍 DML全称是Data Manipulation Language(数据…

硬质金属件去毛刺技术,机械臂去毛刺主轴是核心

作为一种先进且高效的自动化去毛刺技术&#xff0c;机械臂去毛刺主轴在制造业中&#xff0c;特别是金属加工和汽车零部件加工中得到了广泛的应用&#xff0c;通过高速旋转的主轴和精确控制的机械臂实现高精度、高效率、高质量的自动化去毛刺作业。机械臂去毛刺技术是通过主轴的…

40.0/jdbc/Java数据连接/jar包运用增删改

目录 40.1. 回顾 40.2. 正文 40.1 为什么需要jdbc 40.2 如何连接mysql数据库 40 .3 jdbc容易出现的错误 40.4 完成删除 40.5 完成修改 40.1. 回顾 1. 自联查询: 自己连接自己的表。注意:一定要为表起别名。 2. 嵌套查询: 把一个查询的结果作为另一个查询的条件值。 3. 组…

基于C#实现十字链表

上一篇我们看了矩阵的顺序存储&#xff0c;这篇我们再看看一种链式存储方法“十字链表”&#xff0c;当然目的都是一样&#xff0c;压缩空间。 一、概念 既然要用链表节点来模拟矩阵中的非零元素&#xff0c;肯定需要如下 5 个元素(row,col,val,down,right)&#xff0c;其中&…

Unity之NetCode多人网络游戏联机对战教程(10)--玩家动画同步

文章目录 前言NetworkAnimation服务端权威客户端权威 前言 这次的动画同步与位置同步&#xff0c;可以说实现思路是一样的&#xff0c;代码相似度也非常高 NetworkAnimation 如果直接挂载这个脚本只有Host&#xff08;服务端&#xff09;才可以同步&#xff0c;Client是没有…

视频封面:视频图片提取技巧,从指定时长中捕捉需求的图片

在当今的数字时代&#xff0c;视频已成为日常生活中不可或缺的一部分。无论是社交媒体、博客&#xff0c;视频都发挥着重要的作用。而一个吸引的视频封面往往能吸引更多的观众点击观看&#xff0c;选择清晰度高、色彩鲜艳且能吸引人的图片。同时&#xff0c;确保图片与视频内容…