【多线程】 synchronized关键字 | 可重入锁 | 死锁 | volatile关键字 | 内存可见性问题 |waitnotify方法|

news2024/11/25 7:19:57

文章目录

  • synchronized和volatile关键字
    • 一、加锁互斥
    • 二、synchronized的使用
        • 1.修饰实例方法
        • 2.修饰类方法
    • 三、可重入锁
        • 1.死锁
          • 关于死锁
          • 哲学家就餐问题
          • 如何避免死锁
            • 死锁成因的四个必要条件
    • 四、volatile关键字
      • 1.保证内存可见性
          • 什么是内存可见性问题
          • 如何解决
          • Java内存模型JMM
          • volatile不能保证原子性,保證的是内存可见性
    • 五、wait 和 notify
      • 1.wait
      • 2.notify
      • ”线程饿死“

synchronized和volatile关键字

synchronized 监视器锁 monitor lock

一、加锁互斥

​ 前文提到了线程安全问题,为了解决线程安全问题,最常用的方法就是使用synchronized关键字进行加锁处理。

synchronized在使用时,要搭配代码块{ },进入{ 就会加锁,出了 } 就会解锁。

  • 在已经加锁的状态中,另一个线程尝试同样加这个锁。就会产生“锁冲突/锁竞争”。此时,后一个线程就会阻塞,一直等到前一个线程解锁为止。本质上把“并行”变成了“串行”。

  • synchronized ()中需要表示一个用来加锁的对象,这个对象是谁不重要。重要的是根据这个对象来区分两个进程是否在竞争同一个锁。

  • 如果两个线程在针对同一个对象加锁,就会有锁竞争。如果不是针对同一个对象加锁,就没有锁竞争,仍然是并发执行。

    public static int count = 0;


    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            //对变量自增50000次
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    //()中需要表示一个用来加锁的对象,这个对象是谁不重要
                    //主要是根据这个对象来区分两个进程是否在竞争同一个锁
                    count++;
                }

            }

        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    count++;
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();//没有join的话,线程还没自增完,就会打印count

        System.out.println("count: " + count);
    }

count: 100000
  • 引入锁之后,保证了线程安全

synchronized用的锁是存在Java对象头里面的。在对象头中,就有属性来表示当前这个对象是否加锁,来进行区分。

Java的一个对象对应的空间中,除了自己定义的一些属性外,还会有一些自带的属性,这些自带的属性就叫对象头。

二、synchronized的使用

​ synchronized除了修饰代码块外,还可以修饰一个实例方法,或者修饰一个类方法。

class Counter {
    public int count;
    public static int num;

    synchronized public void increase() {
        count++;
    }

    public void increase2() {
        synchronized (this) {
            count++;
        }
    }

    synchronized public static void increase3() {
        num++;
    }

    public static void increase4() {
        synchronized (Counter.class) {
            num++;
        }
    }
}

public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase2();
                Counter.increase3();

            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase2();
                Counter.increase4();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("count: " + counter.count);
        System.out.println("num: " + Counter.num);

    }
1.修饰实例方法
  • 用synchronized来修饰一个方法,此时就是使用this作为锁对象
    synchronized public void increase() {
        count++;
    }
    public void increase2(){
        synchronized (this){
            count++;
        }
    }

两种写法等价

2.修饰类方法
  • 如果用synchronized来修饰类方法,就是针对类对象加锁。

​ 类对象:.java代码会编译成.class文件。.class文件会被JVM加载到内存中。加载到内存中的数据就是类对象。

类对象中包含类的属性、方法、继承、接口(名字、类型、权限)等信息。同时,类对象在一个Java进程中,是唯一的。

    public static int num;
   synchronized public static void increase3() {
        num++;
    }

    public static void increase4() {
        synchronized (Counter.class) {
            num++;
        }
    }

两种写法等价

锁对象是谁不重要,重要的是两个线程中锁对象是否是同一个对象。同一个对象才能进行锁冲突。

三、可重入锁

可重入锁,指的是一个线程。连续针对一把锁加锁两次,不会出现死锁。

1.死锁
                synchronized (locker){
                    synchronized (locker){
                     //   
                    }
                }

​ 同一个对象加两次锁。第一次加锁,假设加锁成功,locker就属于“被锁定”状态。在进行第二次加锁的时候,原则上来说是要进行阻塞,等待到锁被释放之后才能进行第二次的加锁操作。此时由于第二次加锁被阻塞了,就出现了死锁。(线程卡死)第二次无法加锁导致第一个加锁无法执行完毕解锁。第一次无法解锁,第二次就不能加锁。

  • 把synchronized设计成“可重入锁”就可以解决死锁的问题。

让锁记录一下,是哪一个线程给它锁住的。后续再次加锁的时候,如果加锁线程是之前持有锁的线程,那么直接加锁成功。

​ 无论可重入锁有多少层,都需要在最外层才能释放锁。保证中间代码的线程安全。在锁对象中,不光要记录谁拿到了锁,还要记录这个锁被加了几次。每加锁1次,计数器+1,每解锁一次,计数器减一。出了最后一个大括号后减为0,此时进行锁的释放。

关于死锁

1.一个线程针对一把锁,连续加锁两次。如果是不可重入锁,就死锁了。

synchronized不会出现死锁,因为它是可重入锁。C++的 std::mutex是不可重入锁,就会出现死锁。

2.两个线程,两把锁(无论是不是可重入锁,都会死锁)

1.t1获取锁A,t2获取锁B

2.t1尝试获取B,t2尝试获取A

出现死锁,家钥匙锁车里,车钥匙锁家里

    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);//确保两个线程都先获取到一把锁
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1加锁成功");
                }
            }


        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1){
                    System.out.println("t2加锁成功");
                }
            }

        });
        t1.start();
        t2.start();
    }
  • 两个线程都没有获取到第二把锁
  • 两个synchronizid是嵌套关系,在占有一把锁的前提下,再获取另一把锁(可能出现死锁)
  • 而并列关系是先释放当前的锁,再获取下一把锁(不会死锁)

在这里插入图片描述

两个线程都是BLOCKED状态,因为等待锁而出现了阻塞。

3.N个线程,M把锁

更容易出现死锁的情况

哲学家就餐问题

哲学家就餐问题就是经典的N个线程M把锁的模型。

1.在一个圆桌上,每个哲学家左右两侧放了一根筷子,桌子的中间是一碗面。

2.每个哲学家要么进行思考,放下筷子,什么都不干。要么拿起左右两根筷子,开始吃面

3.哲学家吃面和思考是随机的。

4.哲学家什么时候吃完也是随机的

5.哲学家吃面时,会拿起左右两侧的筷子,相邻的哲学家就会阻塞等待。

哲学家=线程 ,哲学家行为的随机=线程的随机调度 ,筷子=锁

通常条件下,可以正常运转,但是在极端情况下。所有哲学家都想吃面条,同时拿起了左手边的筷子。所有人都只有一根筷子,都只能阻塞等待。

要解决问题,需要给筷子进行编号。同时规定每个哲学家先要拿起编号小的,再拿起编号大的筷子。

破除了循环等待,就不会出现死锁了。

如何避免死锁
死锁成因的四个必要条件

1.互斥使用(锁的基本特性)。当一个线程持有一把锁之后,另一个线程也想获取到锁,就要阻塞等待。

2.不可抢占(锁的基本特性)。当锁已经被线程1拿到后,线程2只能等线程1主动释放,不能强行抢过来。

3.请求保持(代码结构)。一个线程尝试获取多把锁(先拿到锁1后,再尝试获取锁2。获取时锁1不会释放)

4.循环等待/环路等待:等待的依赖关系形成环了(车、房锁、哲学家)

解决死锁的核心,就是破坏上面的这些必要条件:

对于3来说,需要调整代码结构,避免编写“嵌套”关系。

对于4来说,可以约定加锁的顺序,就可以避免循环等待。

针对锁进行编号,先加编号大/小的,再加编号小/大的(所有线程都要遵守规则)

四、volatile关键字

作用:1.保证内存可见性 2.禁止指令重排序

1.保证内存可见性

什么是内存可见性问题

​ 计算机运行的程序/代码,经常要访问数据。这些依赖的数据,往往会存储在内存中(例如,当定义一个变量时,这个变量就存储在内存中)。当CPU使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu寄存器中,再参与运算(load)。但是CPU读取内存的操作,相对于读取寄存器来说要非常慢(CPU其他操作都很快,但是涉及到读写内存,速度就降下来了)。为了解决上述问题,提高效率。此时的编译器就会对代码做出优化,把原本要读内存的操作,优化成读取寄存器,减少读内存的次数,达到提高效率的目的。

    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (isQuit==0){
                //循环体内没有代码,意味着循环一秒就会执行很多次
            }
            System.out.println("t1线程退出");

        });
        t1.start();
        Thread t2 =new Thread(()->{
            System.out.println("请输入isQuit : ");
            Scanner sc = new Scanner(System.in);
            isQuit = sc.nextInt();
            //一旦用户输入的值不为0,就会使t1线程执行结束
        });
        t2.start();
    }
  • 预期结束: 只要用户输入一个非0的值,线程t1就会结束循环并退出。

  • 但是实际结果是:不管输入的是什么,线程1一直在循环,并没有结束。还是RUNNABLE状态。

这就是由于多线程引发的bug,同样是线程安全问题。

一个线程读、一个线程修改变量,也可能会出现问题。就是由于“内存可见性”情况引起的。

            while (isQuit==0){
                //循环体内没有代码,意味着循环一秒就会执行很多次
            }

在代码的这个while循环内主要做两件事:

1.要通过load读取isQuit的值到寄存器中

2.通过cmp指令比较寄存器的值是否是0,决定是否进行循环

​ 由于这个循环的速度非常快,短时间内会进行大量的循环。进行大量的load、cmp操作。此时,编译器/JVM发现:进行的这么多次load的结果是一样的,并且load非常耗费时间,一次load的时间可以做上万次cmp。因此,编译器自动做出来优化:在第一次循环的时候,才读了内存。后续的循环都不再读取内存了,直接从寄存器中读取isQuit的值。

​ 由于在线程1的循环中,编译器不会读取isQuit的值了,读的都是寄存器中的值。在线程2当中修改isQuit,线程1感知不到内存发生了变化,就无法对线程1产生影响了。

编译器本意是好的,可惜执行坏了。在优化之后,修改了多线程情况下的执行逻辑。

上述的这个问题就是内存可见性问题。内存已经修改了,但是无法进行感知。

如何解决

使用volatile关键字

在多线程环境下,编译器对于是否优化的判断并不准确。此时需要程序员通过volatile关键字来告诉编译器不用优化。

这时,给isQuit变量前加上volatile关键字,编译器就会禁止上述优化。

    private volatile static  int  isQuit = 0;

请输入isQuit : 
1
t1线程退出

当我们在循环内部加上sleep

    private  static  int  isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (isQuit==0){
                //循环体内没有代码,意味着循环一秒就会执行很多次
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
            System.out.println("t1线程退出");

        });
        t1.start();
        Thread t2 =new Thread(()->{
            System.out.println("请输入isQuit : ");
            Scanner sc = new Scanner(System.in);
            isQuit = sc.nextInt();
            //一旦用户输入的值不为0,就会使t1线程执行结束
        });
        t2.start();
    }
请输入isQuit : 
1
t1线程退出

​ 此时,没有加上volatile,t1线程却可以顺利退出。因为加了sleep后,while循环的速度变慢了,load操作的开销就不大了,就没有触发load的优化,也就没有触发内存可见性问题。

Java内存模型JMM

JMM(Java Memory Model) Java内存模型

是Java规范文档上的叫法

JMM把存储空间划分为了主内存和工作内存。t1线程对应的isQuit变量,本身是在主内存中的。由于此处的优化,就会把isQuit变量放到工作内存中,t2再修改主内存中的isQuit时,就不会影响到t1的工作内存。

主内存 main memory: 平常说的内存

工作内存 work memory:cpu寄存器 和缓存

Java是跨平台的,要兼容各种硬件,所以采用统一的术语

volatile不能保证原子性,保證的是内存可见性

五、wait 和 notify

  • 用来协调多个线程的执行顺序

    线程是系统随机调度,抢占式执行的。很多时候,会通过一些手段来调整执行的顺序。

​ join影响的是线程结束的先后顺序,从而影响执行的顺序。而希望线程在不结束的情况下,进行执行顺序的控制,就可以用wait和notify

  • wait(等待):让指定的线程进入阻塞状态

  • notify(通知):唤醒对应的阻塞状态的线程

1.wait

wait和notify都是Object的方法。随便定义一个对象,都可以使用wait 和 notify.

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait之前");
        object.wait();
        System.out.println("wait之后");
    }
  • 此时如果进行执行,就会抛出异常

在这里插入图片描述

这个监视器就是synchronized (监视器锁),此时的含义就是非法的锁状态。

因为wait在执行的时候要做三件事。

1.释放当前的锁。

2.让线程进入阻塞。

3,当线程被唤醒的时候,重新获取到锁。

​ synchronized加锁就是把对象头的标记进行操作,而wait的第一步就是释放锁,如果之前就没加锁,就无法进行释放。所以会出现异常。所以要解决这个异常,就需要把wait放进 synchronized里面,确保已经拿到锁了,才能释放。

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object){
            System.out.println("wait之前");
            object.wait();
            System.out.println("wait之后");
        }
    }
}
  • 代码走到 object.wait()后线程就会一直阻塞等待下去,直到其他线程调用了对应的notify方法。

在这里插入图片描述

​ 此时主线程中的状态就是WAITING状态。

wait还有一个带参数的方法:可以指定一个超时时间。从而避免wait无休止的等待下去。

      object.wait(3000);
//最多等待三千毫秒

2.notify

    public static void main(String[] args) {
        Object object = new Object();

        Thread t1 = new Thread(()->{
            synchronized (object){
                System.out.println("wait之前");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait之后");
            }

        });
        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (object){
                System.out.println("进行通知");
                object.notify();
            }
        });
        t1.start();
        t2.start();
    }

wait之前
进行通知
wait之后

先进行阻塞,直到三秒后线程2进行了唤醒

这样就可以实现多线程执行顺序的调整。需要注意的是,这套操作,只能让线程执行的时间后移,不能向前。

”线程饿死“

使用wait notify 也可以避免”线程饿死“

​ 当1号线程加锁后,没有产生实际的操作,于是就进行了释放解锁。此时,后续的进程要想加锁,还需要一个系统的调度过程。但是由于1号进程已经在cpu上面执行了,所以1号线程更容易再次重新拿到锁,以此往复。这种情况叫做线程饿死

解决方法:让1号线程进行wait(wait本身就会对锁进行释放,并且对1号线程进行堵塞),1号线程就不会参与后续的锁竞争了,为后续的线程提供了机会。

notify:一次唤醒一个线程。相比之下notify更可控,用的更多。

notifyAll:一次唤醒全部线程

​ 当有多个线程调用wait,这些线程就都会进入阻塞,此时唤醒要么一次唤醒一个线程,要么一次唤醒全部线程。(实际上唤醒的时候,wait要涉及到一个重新获取锁的一个过程,就会产生锁竞争,此时就是一个挨一个串行执行的,没轮到执行的是BLOCKED状态)

点击移步博客主页,欢迎光临~

偷cyk的图

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝牙技术在智能硬件中应用火热,你的蓝牙适配测试如何解决?

蓝牙技术在物联网中的应用非常广泛&#xff0c;可以为人们的生活和工作带来更多的便利和智能化体验&#xff0c;主要五大核心应用场景&#xff0c;具体如下&#xff1a; 1、智能家居 通过蓝牙连接智能家居设备&#xff0c;如智能灯泡、智能插座、智能恒温器等&#xff0c;可以…

【PyQt5】环境配置

PyQt5 环境配置 一、前言1.1 PyQt5介绍1.2 PyCharm集成Pyqt5 二、pyqt5安装三、PyQt5-tools工具包安装四、常用工具环境配置4.1、环境变量配置4。2、验证是否安装成功 五、pycharm中设置Qt工具&#xff08;Qt Designer、PyUIC、PyRcc&#xff09;5.1、配置Qt Designer5.2、配置…

LangChain的RAG实践

1. 什么是RAG RAG的概念最先在2020年由Facebook的研究人员在论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》中提出来。在这篇论文中他们提出了两种记忆类型&#xff1a; 基于预训练模型&#xff08;当时LLM的概念不像现在这么如日中天&#xff0…

钉钉与金蝶云星空对接集成获取流程实例(宜搭)打通收款退款新增

钉钉与金蝶云星空对接集成获取流程实例&#xff08;宜搭&#xff09;打通收款退款新增 接入系统&#xff1a;钉钉 钉钉&#xff08;DingTalk&#xff09;是阿里巴巴集团专为中国企业打造的免费沟通和协同的多端平台&#xff0c;提供PC版&#xff0c;Web版和手机版&#xff0c;有…

GD32F470_微波多普勒无线雷达探测器探头传感器模块10.525GHz HB100带底板

2.20 微波多普勒无线雷达传感器 微波运动传感器是利用多普勒雷达原理设计的微波移动物体探测器。不同于一般的红外探测器&#xff0c;微波传感器通过通过检测物体反射的微波来探测物体的运动状况&#xff0c;检测对象将并不会局限于人体&#xff0c;还有很多其他的事物。微波传…

DB schema表中使用全局变量及在DB组件中查询

DB schema表中使用全局变量及在DB组件中查询 规则如下&#xff1a; 使用如下&#xff1a; 如果在unicloud-db组件上不加判断条件&#xff0c;就会报错&#xff0c;并进入到登录页。 那么就会进入到登录页&#xff0c;加上了判断条件&#xff0c;有数据了就不会了。 因为在sc…

java列表排序练习题

1、(模式识别方面:四个连续相等的数)编写下面的方法&#xff0c;测试某个数组是否有四个连续的值相同的数字。 public static boolean isConsecutiveFour(int[] values)编写测试程序&#xff0c;提示用户输入一个整数列表&#xff0c;如果这个列表中有四个连续的具有相同值的数…

Linux的环境搭建

目录 第一步&#xff1a;购买腾讯云轻量级云服务器 Step1&#xff1a;打开腾讯云 ​编辑 Step2&#xff1a;登录腾讯云并完成认证 Step3&#xff1a;选择服务器类型 Step4&#xff1a;选择服务器配置 第二部&#xff1a;下载XShell Step1&#xff1a;打开XShell Step2…

用于扩展Qt自身的插件(下)

扩展Qt自身的插件 引言必须满足项创建插件示例代码生成插件配置加载插件的环境创建使用插件的项目配置库和头文件依赖的步骤:应用程序代码运行结果总结引言 本文继上篇的扩展Qt自身的插件,接着记录Qt自身的插件,只不过本文提及的用于扩展Qt自身的插件是可以在QtCreator的设…

在 Leetcode 上使用 Javascript 查找数组中的所有重复项(使用 JS 的 DSA)

在本篇博客文章中&#xff0c;我们将探讨如何在数组中找出所有重复的元素&#xff0c;这个问题源自LeetCode上的一个问题。 问题描述&#xff1a; 我们有一个包含n个整数的数组&#xff0c;所有整数都在范围[1, n]内。每个整数要么出现一次&#xff0c;要么出现两次。任务是找…

如何借助AI高效完成写作提纲

AI变革力量&#xff1a;未来数据中心的智能化之旅&#xff01; 在当今这个信息爆炸的时代&#xff0c;人工智能&#xff08;AI&#xff09;在众多领域展现出了它的能力&#xff0c;特别是在写作领域。AI写作工具不仅能够帮助我们高效地生成内容&#xff0c;还能在一定程度上提升…

在vue和 js 、ts 数据中使用 vue-i18n,切换语言环境时,标签文本实时变化

我的项目需要显示两种语言(中文和英文)&#xff0c;并且我想要切换语言时&#xff0c;页面语言环境会随之改变&#xff0c;但是目前发现&#xff0c;只能在vue中使用$t(‘’)的方式使用&#xff0c;但是这种方式只能在vue中使用&#xff0c;但是我的菜单文件是定义在js中&#…

JAVAEE之事务和事务传播机制

1.事务 1.1 事务的概念 事务是⼀组操作的集合, 是⼀个不可分割的操作. 事务会把所有的操作作为⼀个整体, ⼀起向数据库提交或者是撤销操作请求. 所以这组操作要么同时成功, 要么同时失败. 1.2 需要事务的原因 转账的时候&#xff0c;要么同时成功&#xff0c;要么同时失败…

关闭笔记本自带的键盘

目录 一、问题 二、方法 【方法一】 【方法二】 一、问题 笔记本自带的键盘上的个别按键又坏了&#xff0c;可能是因为使用电脑时&#xff0c;最先坏的几个按键那里温度比较高&#xff0c;久而久之就烧坏了吧。距离上次更换新键盘才差不多一年&#xff0c;所以不打算再买新…

2024年思维100春季线上比赛倒计时8天,来做做官方样题

今天是2024年4月12日&#xff0c;距离2024年春季思维100活动第一阶段的线上比赛4月20日还有8天。今年思维100活动的考试重点是什么呢&#xff1f;虽然主办方未公布&#xff0c;我们可以从主办方发布的参考题目中来推测今年的考试重点&#xff0c;并且按照这个来举一反三&#x…

任推邦七款热门拉新项目,普通人逆袭路径,月入6个W!

任推邦 不扣量的项目拉新平台 1UC网盘 —网推 价格上涨行业置顶 &#xff0c;大厂项目 市场空白&#xff0c;预算充足&#xff0c;不限量 适合自媒体/抖快等渠道 上传下载不限速 2迅雷网盘—网推 官方核心服务商&#xff0c;大厂项目 群组内测&#xff08;新增转播收…

TiDB 数据库调度(PD)揭秘

目录 一、PD 简介 1.1 元数据管理 1.2 调度决策 1.3 全局服务 1.4 集群配置与管理 二、TiKV 管理 2.1 调度需求 2.2 信息收集 三、TiDB server 管理 四、PD 集群主节点选取 一、PD 简介 TiDB PD (Placement Driver) 是 TiDB 分布式数据库系统中的核心组件之一&#xff0c;负…

应用实战|从头开始开发记账本2:基于模板快速开始

上期视频我们创建好了BaaS服务的后端应用。从这期视频开始&#xff0c;我们将从头开发一个互联网记账本应用。本期视频我们介绍一下如何使用模板快速开启我们的应用开发之旅。 应用实战&#xff5c;从头开始开发记账本2&#xff1a;基于模板快速开始 相关代码 本期视频我们介绍…

LigaAI x 极狐GitLab,共探 AI 时代研发提效新范式

近日&#xff0c;LigaAI 和极狐GitLab 宣布合作&#xff0c;双方将一起探索 AI 时代的研发效能新范式&#xff0c;提供 AI 赋能的一站式研发效能解决方案&#xff0c;让 AI 成为中国程序员和企业发展的新质生产力。 软件研发是一个涉及人员多、流程多、系统多的复杂工程&#…

三小时使用鸿蒙OS模仿羊了个羊,附源码

学习鸿蒙arkTS语言&#xff0c;决定直接通过实践的方式上手&#xff0c;而不是一点点进行观看视频再来实现。 结合羊了个羊的开发思路&#xff0c;准备好相应的卡片素材后进行开发。遇到了需要arkTS进行解决的问题&#xff0c;再去查看相应的文档。 首先需要准备卡片对应的图片…