04.JAVAEE之线程2

1.线程的状态

1.1 观察线程的所有状态

线程的状态是一个枚举类型 Thread.State

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}

 

NEW:Thread 对象已经有了.start 方法还没调用.

TÉRMINATED: Thread 对象还在,内核中的线程已经没了.

RUNNABLE: 就绪状态 (线程已经在 cpu 上执行了/线程正在排队等待上 cpu 执行)

TIMED WAITING: 阻塞. 由于 sleep 这种固定时间的方式产生的阻塞.

WAITING: 阻塞. 由于 wait 这种不固定时间的方式产生的阻塞

BLOCKED: 阻塞. 由于锁竞争导致的阻塞,

2. 多线程带来的的风险-线程安全 (重点)

2.1 观察线程不安全

static class Counter {
    public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

2.2 线程安全的概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。 

2.3 线程不安全的原因

1.线程的随机调度

操作系统中, 线程的调度顺序是随机的 (抢占式执行).罪魁祸首,万恶之源,

count++ 这个操作,本质上,是分成三步进行的~~

站在 cpu 的角度上,count++ 是由 cpu 通过三个指令来实现的~~

1)load 把数据从内存, 读到 cpu 寄存器中,

2)add 把寄存器中的数据进行 +1

3)save 把寄存器中的数据, 保存到内存中

如果是多个线程执行上述代码,由于线程之间的调度顺序,是“随机"的,就会导致在有些调度顺序下,上述的逻辑就会出现问题

在多线程程序中,最困难的一点:线程的随机调度,使两个线程执行逻辑的先后顺序,存在诸多可能,我们必须要保证在所有可能的情况下,代码都是正确的!

以下是正确的执行顺序

不是按照此顺序,最终结果一定有bug,且最终值小于10w。 

2.两个线程,针对同一个变量进行修改

1)一个线程针对一个变量修改.ok

2)两个线程针对不同变量修改.ok

3)两个线程针对一个变量读取.ok 

3.修改操作,不是原子的.

此处给定的 count++ 就属于是 非原子 的操作.(先读,再修改)类似的,如果一段逻辑中,需要根据一定的条件来决定是否修改,也是存在类似的问题
假设 count++ 是原子的(比如有一个 cpu 指令,一次完成上述的三步)

4.内存可见性问题.

5.指令重排序

要想解决线程安全问题,就是要从上述方面入手。

1.系统内核里实现的->最初搞多任务操作系统的人,制定了"抢占式执行大的基调.在这个基调下,想做出调整是非常困难的。

2.有些情况下,可以通过调整代码结构,规避上述问题但是也有很多情况,调整不了。

3.通过加锁!!!

通过加锁, 就能解决上述问题.
如何给 java 中的代码加锁呢?
其中最常用的办法, 就是使用 synchronized 关键字!

synchronized 在使用的时候,要搭配一个 代码块{}进入{就会 加锁.出了}就会解锁.

在已经加锁的状态中,另一个线程尝试同样加这个锁,就会产生"锁冲突/锁竞争",后一个线程就会阻塞等待一直等到前一个线程解锁为止.

【锁对象到底用哪个对象?无所谓!!!对象是谁,不重要: 重要的是俩线程加锁的对象,是否是同一个对象.】

synchronized (locker) {
       count++;
 } 

()中需要表示一个用来加锁的对象这个对象是啥不重要,重要的是通过这个对象来区分两个线程是否在竞争同一个锁.

t2 线程由于锁的竞争, 导致 lock 操作出现阻塞, 阻塞到 t1 线程 unlock 之后t2 的 lock 才算执行完,此时 t2 就处在 blocked 状态下 。

阻塞就避免了下列的 load add save 和第一个线程操作出现穿插形成这种"串行"执行的效果此时线程安全问题就迎刃而解。

// 线程安全
public class Demo13 {
    // 此处定义一个 int 类型的变量
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            // 对 count 变量进行自增 5w 次
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            // 对 count 变量进行自增 5w 次
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

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

        // 如果没有这俩 join, 肯定不行的. 线程还没自增完, 就开始打印了. 很可能打印出来的 count 就是个 0
        t1.join();
        t2.join();

        // 预期结果应该是 10w
        System.out.println("count: " + count);
    }
}
class Counter {
    public int count;

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

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

    synchronized public static void increase3() {

    }

    public static void increase4() {
        synchronized (Counter.class) {

        }
    }
}

// synchronized 使用方法
public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

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

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

        System.out.println(counter.count);
    }
}

 

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

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

二者等价

 

 synchronized public static void increase3() {

    }

    public static void increase4() {
        synchronized (Counter.class) {

        }
    }

二者等价

 3.synchronized 关键字-监视器锁monitor lock

3.1 synchronized 的特性

1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待. 
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁 

synchronized 用的锁是存在 Java 对象头里的。

Java 的一个对象,对应的内存空间中,除了你自己定义的一些属性之外,还有一些自带的属性(在对象头中,其中就有属性表示当前对象是否已经加锁)

2) 刷新内存
synchronized 的工作过程: 

1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁

3) 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
所谓的可重入锁,指的是,一个线程,连续针对一把锁,加锁两次,不会出现死锁.满足这个要求,就是"可重入'不满足, 就是"不可重入"
上面把 synchronized 设计成" 可重入锁"就可以有效的解决上述死锁问题(让锁记录一下,是哪个线程给它锁住的后续再加锁的时候,如果加锁线程就是持有锁的线程就直接加锁成功!!!)
如何判断是不是最外层?
引用计数
锁对象中,不光要记录谁拿到了锁,还要记录,锁被加了几次,
每加锁一次,计数器就 + 1.
每解锁一次,计数器就 -1.

 关于死锁

1.一个线程,针对一把锁,连续加锁两次,如果是不可重入锁,就死锁了.(synchronized 不会出现.)
2.两个线程, 两把锁.(此时无论是不是可重入锁, 都会死锁).

1)t1 获取锁 A, t2 获取锁 B
2)t1 尝试获取 B, t2 尝试获取 A

public class Demo16 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // 此处的 sleep 很重要. 要确保 t1 和 t2 都分别拿到一把锁之后, 再进行后续动作.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

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

                synchronized (locker1) {
                    System.out.println("t2 加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

死锁代码中两个 synchronized 是嵌套关系,

不是并列关系,嵌套关系说明,

是在占用一把锁的前提下,获取另一把锁.(并列关系,则是先释放前面的锁,再获取下一把锁,(不会死锁的)

3.N 个线程, M 把锁.(相当于 2 的扩充),此时,是更容易出现死锁的情况了~~
1个经典的描述 N 个线程 M 把锁死锁的模型,哲学家就餐问题

死锁,是属于比较严重的 bug(会直接导致线程卡住,也就无法执行后续工作了)

如何解决/避免死锁呢??

死锁的成因, 涉及到 四个 必要条件, 

1.互斥使用.(锁的基本特性).当一个线程持有一把锁之后,另一个线程也想获取到锁, 就要阻塞等待.
2.不可抢占.(锁的基本特性).当锁已经被线程1拿到之后,线程2 只能等线程1 主动释放,不能强行抢过来~

3.请求保持.(代码结构).一个线程尝试获取多把锁.(先拿到锁1 之后,再尝试获取锁2,获取的时候,锁1 不会释放).(吃着碗里的, 看着锅里的)

4.循环等待/环路等待. 等待的依赖关系,形成环了(钥匙锁车里了,车钥匙锁家里了)

1和2是锁的基本特性,不能破坏,

只要满足3和4就会出现死锁,

解决死锁,破坏3和4条件即可。

对于 3 来说, 调整代码结构,避免编写"锁嵌套" 逻辑(这个方案不一定好使,有的需求可能就是需要进行这种获取多个锁再操作)

对于 4 来说, 可以约定加锁的顺序, 就可以避免循环等待(针对锁,进行编号.比如约定,加多把锁的时候,先加编号小的锁,后加编号大的锁.)

public class Demo16 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // 此处的 sleep 很重要. 要确保 t1 和 t2 都分别拿到一把锁之后, 再进行后续动作.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

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

                synchronized (locker2) {
                    System.out.println("t2 加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

4.volatile 关键字  

1)volatile 能保证内存可见性

volatile 修饰的变量 , 能够保证 " 内存可见性 ".
计算机运行的程序/代码,经常要访问数据
这些依赖的数据,往往会存储在 内存 中.(定义一个变量,变量就是在内存中.)
cpu 使用这个变量的时候,就会把这个内存中的数据,先读出来, 放到 cpu 的寄存器中再参与运算.(load)
cpu 读取内存的这个操作,其实非常慢!!!(快,慢,都是相对的)cpu 进行大部分操作,都很快.一旦操作到读/写内存,此时速度一下就降下来了
  • 读内存 相比于 读硬盘, 快几千倍,上万倍,
  • 读寄存器, 相比于读内存,又快了几干倍,上万倍

为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器减少读内存的次数,也就可以提高整体程序的效率. 

public class Demo17 {
    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 scanner = new Scanner(System.in);
            // 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
            isQuit = scanner.nextInt();
        });
        t2.start();
    }
}

 1)load 读取内存中 isQuit 的值到寄存器中2)通过 cmp 指令比较寄存器的值是否是 0,决定是否要继续循环
由于这个循环,循环速度飞快.短时间内,就会进行大量的循环也就是进行大量的 load 和 cmp 操作.
此时,编译器/JVM 就发现了,虽然进行了这么多次 load,但是 load 出来的结果都一样的. 并且, load 操作又非常费时间,一次 load 花的时间相当于上万次cmp 了.
所以, 编译器就做了一个大胆的决定~~ 只是第一次循环的时候, 才读了内存后续都不再读内存了,而是直接从寄存器中,取出 isQuit 的值了

编译器优化:编译器的初心是好的,希望能够提高程序的效率.但是提高效率的前提是保证逻辑不变此时由于修改 isQuit 代码是另一个线程的操作, 编译器没有正确的判定所以编译器以为没人修改 isQuit, 就做出了上述优化. 也就进一步引起 bug 了】【这就是内存可见性问题】

volatile 就是解决方案
在多线程环境下,编译器对于是否要进行这样的优化, 判定不一定准,就需要程序猿通过 volatile 关键字,告诉编译器, 你不要优化!!!(优化,是算的快了,但是算的不准了)
编译器,也不是万能的.也会有一些自己短板的地方.此时就需要程序猿进行补充了只需要给isQuit 加上 volatile 关键字修饰,此时编译器自然就会禁止上述优化过程

 private static  volatile int isQuit = 0; 

此时没加 volatile,但是给循环里加了个 sleep此时,t1 线程是可以顺利退出的!!!
加了 sleep 之后, while 循环执行速度就慢了由于次数少了,load 操作的开销,就不大了,因此,优化也就没必要进行了.
没有触发 load 的优化,也就没有触发内存可见性问题了 

2) volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性. 

3)synchronized 也能保证内存可见性

synchronized 既能保证原子性, 也能保证内存可见性.

5.wait和notify 

多线程中一个比较重要的机制.
协调多个线程的执行顺序的
本身多个线程的执行顺序,是随机的(系统随机调度,抢占式执行的)很多时候,是希望能够通过一定的手段,协调的执行顺序的,join 是影响到线程结束的先后顺序相比之下,此处是希望线程不结束,也能够有先后顺序的控制。

  • wait 等待,让指定线程进入阻塞状态
  • notify 通知,唤醒对应的阻塞状态的线程,
public class Demo18 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1 结束!");
        });

        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t2 结束!");
        });
        t1.start();
        t2.start();

        System.out.println("主线程结束!");
    }
}

 join
等待的过程和"主线程"没有直接联系,哪个线程调用 join, 哪个线程就阻塞

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

        synchronized (object) {
            System.out.println("wait 之前");
            // 把 wait 要放到 synchronized 里面来调用. 保证确实是拿到锁了的.
            object.wait();
            System.out.println("wait 之后");
        }
    }
}

 wait和notify

//释放锁的前提,是加锁

//wait 会持续的阻塞等待下去,直到其他线程调用 notify 唤醒,

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

        Thread t1 = new Thread(() -> {
            synchronized (object) {
                System.out.println("wait 之前");
                try {
                    object.wait(3000);
                } 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和notify都要放在synchronized

使用 wait notify 也可以避免"线程饿死'

这个等的状态,是阻塞的,啥都不做,不会占据 cpu

5.1 notify和notifyAll

notify->一次唤醒一个线程
notifyAll->一次唤醒全部线程 (唤醒的时候,wait 要涉及到一个重新获取锁的过程也是需要串行执行的)

调用 wait 不一定就只有一个线程调用.

N 个线程都可以调用 wait此时,当有多个线程调用的时候,这些线程都会进入阻塞状态

唤醒的时候,也就有两种方式了

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

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

相关文章

AI大模型日报#0424:全球首个AI基因编辑器、出门问问上市、微软开源Phi-3 Mini、昆仑万维年收49亿

导读&#xff1a; 欢迎阅读《AI大模型日报》&#xff0c;内容基于Python爬虫和LLM自动生成。目前采用“文心一言”生成了每条资讯的摘要。标题: 爱诗科技完成A2轮超亿元融资&#xff0c;蚂蚁集团领投摘要: 爱诗科技完成A2轮超亿元融资&#xff0c;成为视频大模型领域融资规模最…

MySQL中的死锁预防和解决

MySQL中的死锁预防和解决 死锁是数据库管理系统中常见的问题&#xff0c;特别是在高并发的应用场景下。MySQL数据库中的死锁会导致事务处理速度减慢&#xff0c;甚至完全停止&#xff0c;因此理解并预防死锁至关重要。本文将详细介绍如何预防MySQL中的死锁&#xff0c;包括常用…

山海鲸电力看板:运维数据一目了然

在信息化高速发展的今天&#xff0c;电力行业的运维管理也迎来了前所未有的变革。山海鲸可视化智慧电力运维可视化看板&#xff0c;以其独特的数据整合能力和直观的可视化效果&#xff0c;成为了电力行业运维管理的得力助手&#xff0c;为电力的稳定运行提供了强大的技术支撑。…

李沐64_注意力机制——自学笔记

注意力机制 1.卷积、全连接和池化层都只考虑不随意线索 2.注意力机制则显示的考虑随意线索 &#xff08;1&#xff09;随意线索倍称之为查询(query) &#xff08;2&#xff09;每个输入是一个值value&#xff0c;和不随意线索key的对 &#xff08;3&#xff09;通过注意力池…

客服话术分享:客服如何挖掘需求?

电商客服主动挖掘询问顾客需求是非常重要的&#xff0c;这就需要我们具备一定的沟通技巧。今天这篇客服话术分享&#xff0c;很适合想提升业绩的你们哦&#xff01; 一、打招呼式询问需求&#xff1a; 1.欢迎光临&#xff0c;本店竭诚为您服务~请问您有什么具体想了解的问题吗&…

java-spring 06 图灵 getBean方法和 doGetBean方法

01.一般的流程是&#xff0c;这里是从上一章的preInstantiateSingleton方法顺序过来的。 getBean() -> doGetBean() -> createBean() -> doCreateBean() -> createBeanInstance() -> populateBean() -> initializeBean() 02.getBean方法&#xff0c;一般就…

C语言(1):初识C语言

0 安装vs2022 见 鹏哥视频即可 1 什么是C语言 c语言擅长的是底层开发&#xff01; 现在一般用的是C89和C90的标准 主要的编辑器&#xff1a; 2 第一个C语言项目 .c 源文件 .h头文件 .cpp c文件 c语言代码中一定要有main函数 标准主函数的写法&#xff1a; int main() { …

菜鸟Java面向对象 1. Java继承

1. Java继承 Java继承 1. Java继承1. 继承的概念_简单介绍继承的用处生活中的继承&#xff1a; 2. 类的继承格式类的继承格式 3. 为什么需要继承企鹅类&#xff1a;老鼠类&#xff1a;公共父类&#xff1a;企鹅类&#xff1a;老鼠类&#xff1a; 4. 继承类型_多重继承5. 继承的…

视频怎么批量压缩?5个好用的电脑软件和在线网站

视频怎么批量压缩&#xff1f;有时候我们需要批量压缩视频来节省存储空间&#xff0c;便于管理文件和空间&#xff0c;快速的传输发送给他人。有些快捷的视频压缩工具却只支持单个视频导入&#xff0c;非常影响压缩效率&#xff0c;那么今天就向大家从软件和在线网站2个角度介绍…

AI建模效果到底行不行?试用这些AI工具告诉你!

当前AI大模型技术浪潮正掀起一股颠覆性的变革浪潮。诸如Midjourney、Stable Diffusion等AI绘画生成工具变得日益成熟&#xff0c;赋能千行百业。在之前的文章中我给大家介绍了很多Midjourney、Stable Diffusion的使用方法和对应的功能&#xff1a; Midjourney vs Stable Diffu…

【连接管理,三次握手,拥塞控制原理】

文章目录 连接管理TCP连接管理同意建立连接TCP3次握手3次握手解决&#xff1a;半连接和接受老数据问题TCP&#xff1a;关闭连接 拥塞控制原理拥塞控制的方法 连接管理 TCP连接管理 TCP连接管理 在正式交换数据之前&#xff0c;发送方和接收方握手建立通信关系&#xff1a; 同…

ECharts海量数据渲染解决卡顿

file模块用来写文件 我们首先使用node来生成10万条数据; 借助node的fs模块就行; 如果不会的小伙伴;也不要担心;超级简单// 引入模块 let fs = require(fs); // 数据内容 let fileCont=我是文件内容 /*** 第一个参数是文件名* 第二个参数是文件内容,这个文件的内容必须是字…

内容平台加码旅游:谁是下一个网红城市

“姐妹们&#xff0c;你们五一啥安排&#xff1f;”早在3月中旬&#xff0c;小威就在询问两个好朋友的行程&#xff0c;“不早早问&#xff0c;怕约不上你们。” 去年以来&#xff0c;国人的旅游需求快速复苏&#xff0c;像小威的朋友一样&#xff0c;之前爱玩的、不爱玩的似乎…

使用Unity扫描场景内的二维码,使用插件ZXing

使用Unity扫描场景内的二维码&#xff0c;使用插件ZXing 使用Unity扫描场景内的二维码&#xff0c;ZXing可能没有提供场景内扫描的方法&#xff0c;只有调用真实摄像机扫描二维码的方法。 实现的原理是&#xff1a;在摄像机上添加脚本&#xff0c;发射射线&#xff0c;当射线打…

世界首台能探测单个原子的量子模拟器,诞生!

量子物理学依赖于高精度的传感技术&#xff0c;以便深入研究材料的微观特性。近期开发的模拟量子处理器显示出量子气体显微镜在原子层面理解量子系统方面的强大潜力。这种显微镜可以生成极高分辨率的量子气体图像&#xff0c;甚至能够检测到单个原子。 在西班牙巴塞罗那的ICFO&…

XxlJob外网访问

Xxl-Job使用外网访问 服务注册中心配置 ### web server.port8088 server.servlet.context-path/xxl-job-admin### actuator management.server.base-path/actuator management.health.mail.enabledfalse### resources spring.mvc.servlet.load-on-startup0 spring.mvc.static…

Java练习题

打印9*9乘法口诀表 解析&#xff1a;利用for循环解决 代码如图所示&#xff1a; public class Cc {public static void main(String[] args) {for (int i 1; i < 10; i){ //从1遍历到9 for(int j 1; j < i; j){ System.out.print(j "*" i "&…

由于找不到steam_api64.dll,无法继续执行代码的解决方法

当用户在尝试启动某款基于Steam平台的游戏时&#xff0c;遇到了“游戏显示找不到steam_api64.dll”的错误提示&#xff0c;这会导致无法正常启动游戏。这究竟是什么原因导致的呢&#xff1f;本文将介绍五种解决方法&#xff0c;帮助大家解决这一问题。 一&#xff0c;了解steam…

实现ALV页眉页脚

1、文档介绍 在ALV中&#xff0c;可以通过增加页眉和页脚&#xff0c;丰富ALV的展示。除了基本的页眉和页脚&#xff0c;还可以通过插入HTML代码的方式展示更加丰富的页眉和页脚&#xff0c;本篇文章将介绍ALV和OOALV中页眉页脚的使用。 2、ALV页眉页脚 效果如下 2.1、显示内…

对于地理空间数据,PostGIS扩展如何在PostgreSQL中存储和查询地理信息?

文章目录 一、PostGIS扩展简介二、PostGIS存储地理空间数据1. 创建空间数据表2. 插入空间数据 三、PostGIS查询地理空间数据1. 查询指定范围内的地理空间数据2. 计算地理空间数据之间的距离3. 对地理空间数据进行缓冲区分析 四、总结 地理空间数据是指描述地球表面物体位置、形…