-
提交作品后,经历2小时后报错:internal_error,中间没有出现任何超时现象,而在本地跑是没有问题的。想问下,这种情况,是华为云平台本身的问题,还是我提交的作品的问题呀?如果是我作品的问题,这种情况,是什么原因导致的呢?
-
CodeArts的Java版本和C/C++和ShellRemote是一个安装包吗, 为啥我下载这三个的安装包,安装下来都是一个软件呢?
-
Java内存分析是诊断性能问题和内存泄漏的关键技能。下面我将从内存结构、分析工具、常见问题和实战案例等方面进行全面介绍。一、Java内存结构详解1. 堆内存(Heap)新生代(Young Generation)Eden区:新对象分配区域Survivor区(From/To):存活对象过渡区默认比例:Eden:From:To = 8:1:1老年代(Old Generation)存放长期存活对象大对象直接进入老年代元空间(Metaspace)存储类元数据取代永久代(PermGen)默认不限制大小(受物理内存限制)2. 非堆内存JVM栈:线程私有,存储栈帧本地方法栈:Native方法调用程序计数器:线程执行位置二、内存分析工具1. JDK自带工具工具命令用途jpsjps -lvm查看Java进程jstatjstat -gcutil <pid> 1000实时GC统计jmapjmap -heap <pid>堆内存快照jhatjmap -dump:format=b,file=heap.hprof <pid> + jhat heap.hprof堆转储分析jstackjstack -l <pid>线程堆栈分析2. 图形化工具VisualVM:多功能监控/分析JConsole:基础监控Eclipse MAT:专业堆分析JProfiler:商业级分析工具Arthas:阿里开源的在线诊断工具3. 生产环境推荐Prometheus + Grafana:时序监控HeapHero:自动化堆分析GCeasy:GC日志分析三、常见内存问题分析1. 内存泄漏(Memory Leak)特征:堆内存持续增长Full GC后内存不释放OOM错误诊断步骤:获取堆转储:jmap -dump:live,format=b,file=leak.hprof <pid>使用MAT分析查找"Accumulation Point"(积累点)检查大对象和GC Roots引用链2. 内存溢出(OOM)常见类型:java.lang.OutOfMemoryError: Java heap space:堆内存不足java.lang.OutOfMemoryError: Metaspace:元空间不足java.lang.OutOfMemoryError: Unable to create new native thread:线程过多解决方案:增加对应区域内存(-Xmx, -XX:MaxMetaspaceSize)优化代码(减少对象创建)调整线程池配置3. GC相关问题症状:应用停顿(STW时间过长)CPU使用率高(GC线程占用)吞吐量下降分析方法:收集GC日志:-Xloggc:/path/to/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps使用GCeasy或GCEViewer分析四、实战分析案例案例1:内存泄漏分析场景:Web应用运行一段时间后OOM步骤:获取堆转储使用MAT打开,选择"Leak Suspects"报告发现HashMap占用了80%内存检查引用链发现静态Map缓存未清理解决方案:改用WeakHashMap或添加清理逻辑案例2:元空间溢出场景:动态生成类导致Metaspace不足解决:增加Metaspace大小:-XX:MaxMetaspaceSize=256m使用jcmd <pid> VM.metaspace监控优化动态类生成逻辑案例3:GC频繁症状:Young GC每分钟超过20次调优:增加新生代大小:-Xmn512m调整Survivor比例:-XX:SurvivorRatio=6优化短命对象创建五、高级分析技巧1. 堆外内存分析使用NMT(Native Memory Tracking):-XX:NativeMemoryTracking=detailjcmd <pid> VM.native_memory detail检查DirectByteBuffer使用2. 线程堆栈分析jstack <pid> > thread.txt查找死锁(deadlock)和阻塞(blocked)线程结合top -Hp <pid>查找CPU高的线程3. 内存分配分析使用JFR(Java Flight Recorder):-XX:StartFlightRecording=duration=60s,filename=alloc.jfr分析对象分配热点
-
CodeArts IDE最新只能选择 17 的特性,什么时候支持 21
-
有2个问题需要求助:1.压缩包内需要存放整个项目还是单独的文件就可以呀?2.上传作品后得分为-1,是因为编译没通过吗?还是其他什么原因呀?
-
Object类常见方法解析在Java编程中,Object类是所有类的根类,它包含了许多实用的方法,这些方法在不同的场景下发挥着重要作用。下面我们来详细了解一下Object类中的一些常见方法。1. toString方法toString方法是用于将对象转换为字符串表示形式的方法。在默认情况下,toString方法返回的结果是类名加上@符号,再跟上该对象对应哈希码的十六进制表示。例如,当我们打印一个对象时,如果没有重写toString方法,就会得到类似这样的结果:com.example.MyClass@12345678。然而,在实际开发中,我们通常需要根据对象的具体属性来定制它的字符串表示形式,以便更清晰地展示对象的信息。这时,我们就需要重写toString方法。比如,对于一个表示学生信息的类Student,我们可以这样重写toString方法:public class Student { private String name; private int age; // 构造方法和其他方法省略 @Override public String toString() { return "Student{name='" + name + "', age=" + age + "}"; }}这样,当我们打印一个Student对象时,就会得到包含学生姓名和年龄信息的字符串。2. equals和hashcode方法equals方法和hashcode方法在对象比较和集合操作中起着关键作用。equals方法:在进行对象比较的时候,我们通常会使用equals方法。如果不重写equals方法,默认情况下它会根据对象的地址进行比较,即只有两个引用指向同一个对象时,它们才被认为是相等的。但在很多实际场景中,我们需要根据对象的属性和方法来判断它们是否相等。例如,对于两个表示学生信息的对象,如果它们的姓名和年龄都相同,我们就认为这两个学生是相等的。这时,我们就需要重写equals方法,如下所示:@Overridepublic boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass()!= obj.getClass()) return false; Student student = (Student) obj; return age == student.age && Objects.equals(name, student.name);}hashcode方法:hashcode方法用于返回对象的哈希码值。哈希码是一个整数,它在许多数据结构中都有重要作用,比如HashMap和HashSet。当我们对对象进行存储和查找操作时,哈希码可以帮助我们快速定位对象的位置。需要注意的是,当我们重写equals方法时,通常也需要同时重写hashcode方法。这是因为如果两个对象根据equals方法判断是相等的,那么它们的哈希码也必须相等。如果不重写hashcode方法,可能会导致哈希冲突的概率增大。例如,在HashMap中,如果只重写了equals方法而没有重写hashcode方法,那么具有相同属性的两个对象可能会被存储在不同的哈希桶中,这会使得一些哈希桶中的对象个数过多,从而降低搜索效率。另外,如果只重写了hashcode方法而没有重写equals方法,当发生哈希冲突时,会导致对象的安全性受到威胁。因为在哈希冲突的情况下,不同的对象可能会被存储在同一个哈希桶中,如果不重写equals方法,就无法正确判断这些对象是否真的相等,从而可能导致一些错误的结果。3. wait、notify和notifyAll方法wait、notify和notifyAll方法是用于线程间通信的方法,它们只能在同步代码块或同步方法中使用。 wait方法:当一个线程调用某个对象的wait方法时,它会解除对该对象的锁,并进入等待状态,直到其他线程调用该对象的notify或notifyAll方法来唤醒它。例如,在生产者-消费者模型中,当消费者发现缓冲区为空时,它会调用缓冲区对象的wait方法进入等待状态,直到生产者生产了新的数据并调用notify或notifyAll方法来唤醒它。notify方法:notify方法用于唤醒陷入等待状态的某一进程。它会随机选择一个正在等待该对象锁的线程,并将其唤醒。需要注意的是,notify方法只会唤醒一个线程,如果有多个线程在等待,那么具体唤醒哪个线程是不确定的。notifyAll方法:notifyAll方法用于唤醒陷入等待状态的所有进程。它会唤醒所有正在等待该对象锁的线程,这些线程会竞争获取对象的锁,然后继续执行。在这个示例中,生产者线程先生产数据,然后通过notify方法唤醒消费者线程,消费者线程在收到通知后开始消费数据。Java为什么被称为平台无关性语言Java实现平台无关性的核心机制在于JVM(Java虚拟机)的中间层设计。当Java源代码通过javac编译器生成字节码文件(.class文件)后,这些包含平台中立中间代码的文件可以在任何安装有对应平台JVM的设备上运行。各个操作系统虽然使用不同的机器指令集,但通过针对特定平台实现的JVM,字节码会被实时转换为所在系统的本地机器指令执行。这种"一次编译,到处运行"的特性,使得开发者无需针对不同操作系统修改源代码,JVM作为抽象层有效隔离了底层硬件和操作系统的差异。= =和equals有什么区别?== 和 equals 的区别基本概念== 是一个操作符,用于比较两个变量的值或引用。equals 是 Object 类的一个方法,用于比较两个对象的内容。如果 equals 方法没有被重写,它的默认行为与 == 相同。比较规则对于基本数据类型(如 int、char 等):== 比较的是变量的值。例如:int a = 5;int b = 5;System.out.println(a == b); // 输出 true对于对象类型(如 String、Integer 等):== 比较的是对象的引用(即内存地址)。例如:String str1 = new String("hello");String str2 = new String("hello");System.out.println(str1 == str2); // 输出 falseequals 方法比较的是对象的内容。例如:System.out.println(str1.equals(str2)); // 输出 trueequals 方法的重写默认情况下,equals 方法比较的是对象的引用(与 == 相同)。如果类重写了 equals 方法,则可以根据自定义的逻辑比较对象的内容。例如,String 类重写了 equals 方法,用于比较字符串的内容。重写 equals 方法时,通常需要同时重写 hashCode 方法,以确保对象在哈希表等数据结构中的行为一致。equals()与hashcode()equals和hashCode属于Object的方法,equals默认情况下和==等效,hashCode()是根据一定的规则根据对象返回的值,比如对象的地址,经过处理,返回哈希值在进行对象的比较的时候,可以进行自定义比较方法,需要重写equals方法,为了使HashMap、HashSet等方法正常存储,还需要对HashCode()进行重写HashMap存储对象的时候,会先调用HashCode方法进行比较,当HashCode值相等的时候,再使用equals方法再进行比较Student对象,有name和height两个成员变量,使用HashSet存储两个name和height相等的对象,判断Student对象是否相等就看这两个变量,当没有重写HashCode的时候,存储到HashSet中也会当作两个不同的对象,调用HashCode方法,因为没有重写,默认哈希值不会相同,所以会被认为两个对象不相同解决方法就是重写HashCode(),最简单的就是返回两个变量哈希值的积equals() 与 hashCode() 的核心机制equals() 和 hashCode() 是定义在 java.lang.Object 类中的基础方法。equals() 方法:默认实现与 == 运算符等价,用于判断两个对象是否严格相等(即是否指向同一内存地址)。开发中需根据业务需求重写此方法,以实现自定义的相等性逻辑。hashCode() 方法:默认返回对象的内存地址经过哈希算法处理后的整数值。其作用是为对象生成一个紧凑的哈希标识,主要用于快速定位数据存储位置(如哈希表)。集合框架中的关键作用当使用 HashMap、HashSet 等基于哈希的集合存储对象时:哈希优先原则:集合会先调用对象的 hashCode() 方法计算哈希值,若哈希值冲突,则进一步通过 equals() 方法比较对象的实际内容。一致性要求:若重写了 equals(),必须同步重写 hashCode()。若二者逻辑不一致(例如两个对象通过 equals() 判断相等,但 hashCode() 返回不同值),会导致集合无法正确维护元素唯一性,引发潜在 bug。Student 对象的典型场景分析假设存在如下 Student 类:public class Student { private String name; private int height;}未重写 hashCode() 的问题向 HashSet<Student> 中添加两个属性完全相同的对象时:Student s1 = new Student("Alice", 160);Student s2 = new Student("Alice", 160);Set<Student> students = new HashSet<>();students.add(s1);students.add(s2); // 实际执行结果:s2 会被视为新元素加入集合原因:默认的 hashCode() 实现基于对象内存地址,两次 new 操作生成的 s1 和 s2 地址不同,导致哈希值冲突被判定为不同对象。解决方案:重写 hashCode()通过覆盖 hashCode() 方法,将关键字段纳入哈希值计算:@Overridepublic int hashCode() {return Objects.hash(name, height); // 或简化为 name.hashCode() * 31 + height}原理:将 name 和 height 的哈希值按规则组合,确保内容相同的对象生成相同的哈希码,从而解决集合存储异常问题。重载和重写重载(Overloading)重载指的是在同一个类中,允许存在多个方法名相同但参数列表不同的方法。这些方法的参数个数、顺序或类型必须有所区别。通过这种方式,可以为同一操作提供多种不同的实现方式,以适应不同的输入需求。特点:方法签名不同:方法名相同,但参数列表(参数的个数、顺序或类型)不同。编译时决定:重载的解析是在编译期间完成的,编译器根据调用方法时提供的参数类型来决定具体调用哪一个重载的方法。静态绑定:由于在编译时已经确定了调用的具体方法,因此重载属于静态绑定。重写(Overriding)重写是指子类对父类中已有的方法进行重新定义,方法名、参数列表以及返回类型都必须与父类中的方法完全一致。通过重写,子类可以改变父类方法的行为,以适应子类的特定需求。特点:方法签名相同:方法名、参数列表和返回类型必须与父类中的方法完全一致。运行时决定:重写的解析是在运行期间完成的,具体执行哪一个方法取决于对象的实际类型。动态绑定:由于在运行时才确定调用的具体方法,因此重写属于动态绑定。抽象和接口抽象类与接口的对比分析【核心概念】抽象类使用 abstract 关键字修饰,用于定义具有部分实现的基类;接口使用 interface 关键字声明,作为纯粹的行为契约。二者在代码组织层面存在本质区别:【抽象类的应用】当多个类存在代码复用需求时,通过抽象类进行逻辑抽取可提升代码简洁性与可维护性。其核心价值体现在:代码复用:将公共实现逻辑提取到抽象基类中,子类通过继承复用代码强制约束:通过抽象方法(public abstract 修饰)强制子类实现特定功能类型标识:表达严格的继承关系,体现"是"(is-a)的语义特征修改时只需调整抽象基类即可影响所有子类,但需注意普通类也能实现类似效果,抽象类的核心区别在于其无法实例化并具有强制约束力。【接口的演进】接口作为规范标准,体现"具有...能力"(like-a)的语义特征,其发展历经多个阶段:传统接口(Java 7-)仅包含隐式 public abstract 修饰的抽象方法只允许声明 public static final 常量增强接口(Java 8+)引入 default 方法(含具体实现)支持静态方法定义允许定义私有方法(Java 9+)【实现规范】类实现接口时需遵循:必须实现所有抽象方法(未实现则类需声明为 abstract)可选择性覆盖 default 方法常量字段自动继承且不可修改支持多接口实现,体现灵活的组合特性Finalfinal 是 Java 的关键字,用于限制类、方法或变量的可修改性,具体用法如下:修饰类当类被 final 修饰时,该类不能被继承。核心目的:防止通过子类继承修改原有类的行为,常用于保护核心 API 的稳定性。示例:public final class String { ... } // String 类不可被继承修饰方法当方法被 final 修饰时,该方法不能被子类重写。核心目的:确保关键方法的行为不被破坏,常用于模板方法模式中的固定步骤。示例:public class Parent { public final void lock() { ... } // 子类无法重写 lock()}修饰变量基本类型变量:变量值初始化后不可修改,成为常量。示例:final int MAX_VALUE = 100;// MAX_VALUE = 200; // 编译报错引用类型变量:引用类型变量变量指向的内存地址不可变,但对象内部状态可以修改。示例:final List list = new ArrayList<>();list.add("Java"); // 允许修改内容 // list = new LinkedList<>(); 编译报错(地址不可变) static final 联合使用 static final 修饰的变量为全局常量,在类加载时初始化。 优势: 编译时常量(如字符串字面量)会被 JVM 优化,直接存入常量池,提升访问效率。 示例: public static final String VERSION = "1.0";不可变设计的经典案例:String 类的不可变性: String 类被 final 修饰,禁止继承。 内部存储数据的 char[] value 数组被 private final 修饰,且不提供修改方法(如 setter)。 所有修改操作(如 substring、replace)均返回新对象,原对象不变。 优势: 线程安全(无需同步锁)。 哈希值可缓存,提升性能。多线程场景下的应用使用 final 修饰变量可确保该变量的值在线程间可见且不可变,避免数据竞争问题。示例: public class SafePublication { private final int safeValue; // 安全发布,防止指令重排序 public SafePublication(int value) { this.safeValue = value; }}异常的理解异常是指在程序运行过程中可能发生的非正常事件,这类事件会中断程序的正常执行流程。通过异常处理机制,开发者可以捕获并处理这些异常,在保障程序核心功能不受致命影响的前提下完成错误恢复操作。异常的种类Java 的异常类继承体系以 Throwable 为顶层父类,具体分为两类:Error(错误)由 JVM 或底层系统资源引发的严重问题(如 StackOverflowError 栈溢出、OutOfMemoryError 内存耗尽),属于不可恢复的致命错误。Exception(异常)可被捕获处理的非致命性问题,进一步细分为:编译时异常(Checked Exception)编译器强制要求处理的异常类型,如 FileNotFoundException(文件未找到)、IOException(输入输出异常)。运行时异常(Runtime Exception)由代码逻辑缺陷引发的异常,编译器不强制处理,如 NullPointerException(空指针异常)、IndexOutOfBoundsException(数组越界异常)。thow和throws的区别throw是在方法内部使用,进行抛出异常,是一个动作。throws是在方法声明的后面,表示可能会抛出的异常,是一种声明那么直接try-catch不就行了,为什么还要抛出异常呢?首先我们要分情况说明,如果该异常可以被try-catch处理的话,就可以直接try-catch,比如说虽然A方法抛出了一个异常,但是基于他异常处理的方法在其他地方,并不需要处理,不需要处理的情况下,就抛出异常给调用者。其次,当调用的几个方法中都存在相同的异常,如果在这几个方法内部进行try-catch的话,会使得整体变得冗余,可以让调用方统一进行捕获异常,并进行处理,简洁化。最后还有可能就是上层调用方处理String不可变类的核心优势线程安全:String的不可变性天然规避了多线程同步问题,无需额外锁机制即可保证线程安全。哈希缓存:首次计算哈希值后缓存,作为Map键时无需重复计算,显著提升查找效率(如HashMap的键比较场景)。字符串创建与常量池机制共享优化:常量池通过引用复用已存在的字符串,避免重复创建。例如:String s1 = "hello"; // 常量池新建对象(若不存在)String s2 = "hello"; // 直接引用常量池现有对象对象创建示例:String a = new String("aa") + "bb"; 的详细过程:常量池新建"aa"对象(若不存在)。new String("aa")在堆中创建新对象(非池引用)。常量池新建"bb"对象(若不存在)。拼接生成"aabb",堆中创建新对象并可能更新常量池(若未存在),共创建了四个对象。字符串存储与内存限制底层结构:使用char[]存储,理论最大长度为Integer.MAX_VALUE(2³¹-1)。实际限制:编译期:最大长度为2¹⁶-2(65534,受CONSTANT_Utf8_info限制)。运行时:受JVM可用内存限制(如堆大小)。字符串常量池的演进Java 7前:位于永久代(PermGen),易引发OOM。Java 8+:迁移至元空间(Metaspace),降低内存溢出风险。字符串拼接效率对比String的+操作:String result = "a" + "b"; // 编译后等效于:String result = new StringBuilder().append("a").append("b").toString();隐含创建StringBuilder和临时String对象,效率较低。StringBuffer:直接操作内部可变数组,避免频繁对象创建。线程安全(同步开销),适合多线程场景。StringBuilder(补充建议):单线程下更高效(非线程安全)。———————————————— 原文链接:https://blog.csdn.net/tulingtuling/article/details/147128918
-
前言上期我们介绍了类与对象的知识点,那么这期我先为大家带来关于抽象类与接口的具体的讲解,希望大家能更进一步理解Java中的特点,下节再为大家介绍两大特性的详解。一、抽象类1.抽象类的概念在面向对象的概念中,所有的对象都是通过类来进行描绘的,但是并不是所有的类来进行描绘对象的。如果一个类中没有足够的信息内容来回一个具体的对象,那么这种类就是抽象类。2.抽象类的语法1.抽象类都是被abstract类修饰的叫做抽象类public abstract class A{}2.抽象类中的成员方法用abstract修饰的方法,叫做抽象方法public abstract class A{public abstract void test();//抽象方法}3.抽象类的特点通过上述方法我们可以得知:一个类中有抽象方法,那么这个类必是抽象类抽象类中也可以有普通成员变量和普通成员方法比如:abstract class Shape{ public int a = 10;//普通成员变量 public void draw(){//普通成员方法 System.out.println("画图形"); } public abstract void draw2();//抽象方法}抽象类不能直接实例化public static void main(String[] args) { Shape shape = new Shape(); } 抽象类不能实例化,那么是用来干什么的呢?当然是用来被继承的了,所以抽象类也可以实现了向上转型抽象类中的方法是不能被private修饰的 private abstract void draw2();final 和 abstract 两者不能同时存在,被abstract修饰的方法就是要被继承的与重写的,而final修饰的不能被重写和继承,属于是密封类和密封方法public abstract final void draw2();抽象方法不能被static修饰,因为抽象方法要被子类重写,而static是属于类本身的方法public static final void draw2();当一个普通类继承了这个抽象类之后,这个普通类一定要重写这个抽象类的当中的所有的抽象方法abstract class Shape{ public int a = 10; public void draw(){ System.out.println("画图形"); } public abstract void draw2();//抽象方法 public void test(){ }}class React extends Shape{ @Override public void draw2() { System.out.println("矩形"); }}class Flower extends Shape{ @Override public void draw2() { System.out.println("花" ); }}子类如果不想重写抽象方法,那么就把子类也设为抽象类比如这有一个子类Aabstract class A extends Shape{ public abstract void testDemo();}但是我们会想到,如果一直设计为抽象子类,那么它们的抽象方法怎么办?class B extends A{ @Override public void draw2() { } @Override public void testDemo() { }}所以B继承了A,A继承了Shape,两个类都是抽象类,所以这个普通类B类就要重写两个方法当一个抽象类A不想被一个普通类B继承,此时可以把这个B类变成抽象类,那么在当一个普通类C继承这个抽象类B之后,C要重写B和A的里面的所有抽象方法4.抽象类的操作给出完整的代码,让我们分析一下:abstract class Shape{ public int a = 10; public void draw(){ System.out.println("画图形"); } public abstract void draw2(); public void test(){ } public Shape() { //既然它不能实例化,但是可以让子类调用帮助这个抽象类初始化它自己的成员 }}class React extends Shape{ @Override public void draw2() { System.out.println("矩形"); }}class Flower extends Shape{ @Override public void draw2() { System.out.println("花" ); }}public class Test { public static void drawMap(Shape shape) { shape.draw2(); } public static void main(String[] args) { Shape shape = new React(); drawMap(new React()); drawMap(new Flower()); }Shape shape1 = new React();Shape shape2 = new Flower();这个操作是向上转型,在我们调用了一些抽象方法的时候,由于我们将其抽象方法进行了重写,进而发生了动态绑定,从而发生多态。 public static void drawMap(Shape shape) { shape.draw2(); }这个方法我们来接收Shape类型,但是由于子类继承父类,构成了协变类型,也会发生向上转型,往里面传入了参数时,再调用抽象方法,这个时候发生了动态绑定,就会发生了多态new React() new Flower() 这种没有名字的对象 —> 匿名对象匿名对象的缺点:每次使用,都得去重新实例化所以我们可以使用向上转型的直接赋值,来去传进参数public static void main(String[] args) { Shape shape1 = new React(); Shape shape2 = new Flower(); drawMap(shape1); drawMap(shape2); }5.抽象类的作用我们都知道普通的类也能被继承,那还需要抽象类来做什么?但是我们正常都需要跟业务和实际情况来进行选择,使用抽象类相当于多了一重编译器的校验:使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成.。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的;但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。意义:很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.充分利用编译器的校验, 在实际开发中是非常有意义的.二、接口1.接口的概念比如:我们都知道电脑上面有USB接口,可以插些:U盘,鼠标等符合USB协议的设备;接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。在Java中,接口可以看成是多个类的公共规范,是一种引用数据类型。2.接口语法public interface 接口名称{//抽象方法//成员变量}一般的接口名称前加一个I字母。interface IShape{//一般情况下我们以I开头来表示接口的 int a = 10; void draw(); default void test(){ System.out.println("test()"); } public static void test2(){ System.out.println(" static test2()"); }}在这个接口中我没有看到修饰符,难道是默认的吗?并不是默认的!原因这是在接口中,成员变量默认是被public static final修饰的,成员方法是抽象方法,但是默认被public abstract修饰的但是这种我们一般不写接口中的方法一般不能实现,都是抽象方法,但是从JDK8之后,可以支持default修饰的成员方法和static修饰的成员方法中可以有具体的实现(即主体)3.接口的使用与特性接口的成员方法必须是默认为public abstract来修饰的。其他修饰符不可以;private 和 protected还有default均不可以,都会出现报错接口类型也是一种引用类型接口不能有普通成员方法,也不能有成员变量interface IShape{//一般情况下我们以I开头来表示接口的 private int a = 10; void draw(); default void test(){ System.out.println("test()"); } public void test2(){ System.out.println(" static test2()"); }}就算你写成了public的成员变量,这时候也是默认成public static final修饰的,所以不会出现错误接口不能被new关键字来进行实例化,但是也可以用向上转型IShape iShape = new IShape();类实现了接口,那么该子类就要重写接口中的所有抽象的方法,用implements关键字来实现class Rect implements IShape{ @Override public void draw() { System.out.println("矩形"); }}class Flower implements IShape{ @Override public void draw() { System.out.println("花"); }}如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类abstract class A implements IShape{ }当然了,还是要还的,因为他只能被用来继承,所以这种类被继承后还是需要重写接口中的方法当一个类实现了接口当中的方法之后,当前类中的方法不能不加public,因为原来接口中的方法是public修饰的,所以重写的方法要大于等于public的范围,所以必须是public修饰class Rect implements IShape{ @Override void draw() { System.out.println("矩形"); }}接口当中,不能使用构造方法和静态代码块interface IShape{//一般情况下我们以I开头来表示接口的 public IShape(){ } static{ }}4.实现多个接口在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。在父类中是放入一些共性的特点,而不是特有的,是共有的比如我们创建了一个动物类,再创建几个具体的动物的类,再分别创建几个接口://游泳接口public interface ISwimming { void swim();}//跑步接口public interface IRunning { void run();}//飞行接口public interface IFlying { void fly();}//动物类:public abstract class Animals { public String name; public int age; public Animals(String name, int age) { this.name = name; this.age = age; } public abstract void eat();}鸟可以进行飞行和跑步,所以实现这两接口,并且继承了动物类//鸟类public class Bird extends Animals implements IFlying, IRunning { public Bird(String name, int age) { super(name, age);//帮助父类进行构造,用super来进行访问父类 } @Override public void eat() { System.out.println(this.name + "正在吃饭!"); } @Override public void fly() { System.out.println(this.name + "正在用翅膀飞!"); } @Override public void run() { System.out.println(this.name + "正在用鸟腿跑!"); }}狗类是可以进行跑步和游泳,所以实现这两个接口,并且继承了父类//狗类public class Dog extends Animals implements IRunning, ISwimming { public Dog(String name, int age) { super(name, age);//帮助父类进行构造 } @Override public void eat() { System.out.println(this.name + "正在吃狗粮!"); } @Override public void run() { System.out.println(this.name + "正在用狗腿跑!"); } @Override public void swim() { System.out.println(this.name + "正在狗腿游泳!"); }}主类public class Test { public static void test1(Animals animals){ animals.eat(); } public static void test4(IFlying iFlying){ iFlying.fly(); } public static void test2(IRunning iRunning){ iRunning.run(); } public static void test3(ISwimming iSwimming){ iSwimming.swim(); } public static void main(String[] args) { Bird bird = new Bird("小鸟",1); Dog dog = new Dog("小狗",10); test1(dog); test1(bird); test2(bird); test2(dog); System.out.println("============"); test3(dog); //test3(bird);bird没有实现swimming的接口 System.out.println("============"); test4(bird); //test4(dog);dog没有实现了flying的接口 }}上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。语法上先继承后实现,否则会报错继承表示是其中的一种,接口表示的是它具有什么特性,能做什么。这时候我们在创建一个机器人类:public class Robot implements IRunning { @Override public void run() { System.out.println("机器人在跑!"); }}比如我们在主类中测试test2(new Robot());1那么这种操作的好处是什么?时刻牢记多态的好处, 让我们忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力我们创建了这个机器人类,实现了跑步的接口,所以直接去实现调用test2的方法,这个时候我们就可以发现了这个接口,回避了向上转型会使其更加灵活。5.接口之间的继承在Java中,类和类之间是单继承的,但是一个类可以实现多个接口,接口与接口之间可以多继承。用接口达到多继承的目的。接口的继承用extends关键字interface A{ void testA();}interface B{ void testB();}interface C{ void testC();}interface D extends B,C{ //D 这个接口具备了B和C接口的功能 //并且D还可以有自己的方法 void testD();}在接口中是进行抽象方法的声明,所以接口之间继承不用重写抽象方法当类来实现这个D接口的时候,我们要重写D接口中的方法和D所继承的接口中的方法public class Test implements D{ @Override public void testB() { } @Override public void testC() { } @Override public void testD() { } //就如同子孙类一样总结:接口间的继承相当于把多个接口合并在一起。在接口中是进行抽象方法的声明,所以接口之间继承不用重写抽象方法。6.接口的实例(1).对象大小的比较当我们先给出了几个对象,从而想要按照一定顺序比较其中内容的大小我们先创建了一个Student类class Student { public String name ; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}这个时候我们再创建一个主类public class Test {public static void main(String[] args) { Student student1 = new Student("zhangsan", 10); Student student2 = new Student("lisi", 15); System.out.println(student1 > student2); }} 这么运行会出现错误;(因为两者都是引用类型,无法直接进行比较)所以这个时候我们用接口来实现这个自定义类型的比较,所以分别是 Comparable和Comparator接口(1).Comparable接口这个是接口Comparable中的源码这个接口中涉及到后续的泛型,关于这个泛型到后期的数据结构我们在详细的讲解如果我们要实现比较的方法,就要用compareTo这个方法,当然如果直接使用:System.out.println(student1.compareTo(student2));因为compareTo是Comparable中的方法,所以要想使用这个方法,就要实现这个接口,并指定比较的类型用<>class Student implements Comparable<Student>{ public String name ; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) {//重写的CompareTo方法 return this.age - o.age; //大于0,则前者大于后者 //小于0,前者小于后者 //this.age表示的是student1,o.age表示的是student2}}public class Test {public static void main(String[] args) { Student student1 = new Student("zhangsan", 10); Student student2 = new Student("lisi", 15); System.out.println(student1.compareTo(student2)); }}如果我们给出的是student的对象数组,让其大小进行排序,不作对student类进行修改,只在主类中进行实例化import java.util.Arrays;public class Test { public static void main(String[] args) { //利用对象数组 Student[] students = new Student[3]; students[0] = new Student("zhangsan",10); students[1] = new Student("lisi",2); students[2] = new Student("liwu",18); Arrays.sort(students);//这个时候要用comparable来进行一定的顺序排序. System.out.println(Arrays.toString(students)); }}因为通过comparable这个接口来进行查找一定的内容,从而按一定的内容来进行比较大小当然,我们想要按其他内容的时候进行排序,需要改动某些代码,这时候就容易会导致某些错误。(2).Comparator接口Comparator接口中有compare的方法为默认权限,所以我们调用这个方法这种方式叫做比较器,在类的外部进行实现class Student2{ public String name; public int age; @Override public int compareTo(Student2 o) { return this.age - o.age; } public Student2(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}比如我们要按对象的年龄大小进行比较,这时候再创建一个age类,实现了Comparator这个接口class AgeComparator implements Comparator<Student2>{ @Override public int compare(Student2 o1, Student2 o2) { return o1.age - o2.age; }}这个时候我们先把在外部的类实例化,进而要比较需要两个对象的变量传递到compare方法中去,从去重写这个方法,并且发生向上转型(参数类型)import java.util.Comparator;public class Test2 { public static void main(String[] args) { Student2 student1 = new Student2("zhangsan", 10); Student2 student2 = new Student2("lisi", 15); AgeComparator ageComparator = new AgeComparator(); System.out.println(ageComparator.compare(student1, student2));}}当我们按照name进行比较的时候,这个时候在创建了Name类,再去实现了Comparator这个接口class NameComparator implements Comparator<Student2>{ @Override public int compare(Student2 o1, Student2 o2) { return o1.name.compareTo(o2.name); }}name是引用类型,不能进行相减来进行比较,但是我们发现了String类中也有去实现Comparable的方法所以我们只需要重写compareTo方法即可当然我们还是需要进行将Name类进行类的实例化 NameComparator nameComparator = new NameComparator(); System.out.println(nameComparator.compare(student1, student2));关于对于自定义类型比较的总结:Comparator这种方式的优势就是对于类的侵入性不强,会比较灵活Comparable这种方式对于类的侵入性比较强,所以做好不要去改动原有的逻辑,比如我按名字排序这个时候就会容易发生变化并且两者可以共存,互不干扰;因为comparable是对原有的类进行实现,comparator是对要比较内容的类而实现,所以互不干扰(2).实现类的克隆(1).类的克隆Java中有许多的内置接口,而Cloneable就是一个非常有用的接口Object类中有一个clone方法,调用这个对象可实现创建一个对象的“拷贝”,但是想要成功调用这个方法,就要实现这个其中的接口class Person { public int age; public Person(int age) { this.age = age; } @Override public String toString() { return "Person{" + " age=" + age + " }"; }}public class Test3 { public static void main(String[] args){ Person person1 = new Person(10); System.out.println(person1); Person person2 = person1.clone(); System.out.println(person1); }}例如:我们创建一个person类,存入了一个年龄的成员变量,并重写了toString方法;然后我们对Person类实例化,并存入了一个age为10的值让其构造方法为它自己进行初始化,这个时候我们直接打印了person1,这个时候我们再创建一个变量person2来接收person1的拷贝后的值。但是如果我们直接这么做,直接去调用:Person person1 = person.clone();这个时候就会出现这种错误;因为clone方法是在object类中的方法,所以我们点击查看clone的源码:protected native Object clone() throws CloneNotSupportedException;这个时候我们就会发现了这个clone方法是本地方法,是用C/C++来写的,所以我们也无法知道这个具体的过程实现,并且我们也看到了这个方法是被protected修饰的,它的访问权限是在不同包的子类可以访问不同包的父类被protected修饰的成员变量和方法, 但是也需要用super来访问,所以需要重写这个clone方法;class Person { public int age; public Person(int age) { this.age = age; } @Override public String toString() { return "Person{" + " age=" + age + " }"; }@Override protected Object clone() throws CloneNotSupportedException { return super.clone();//调用object类的方法来进行访问 }}这个时候我们会发现还是有错误啊,但是这时候提示我们类型不兼容,因为Object类是所有的类的父类,但是其中clone方法是比较特殊的,它返回的是object类型,但是它并没有与Person构成显示的父与子的协变类型,所以这也反映了不构成重写的条件之一,所以会出现这种错误,但是我们把这个类型强制转换为Person类,所以就可以了public class Test3 { public static void main(String[] args){ Person person1 = new Person(10); System.out.println(person1); Person person2 = (Person) person1.clone(); System.out.println(person1); }}这个时候在运行结果:这个时候还是报了错误,告诉我们还有异常这错误,但是异常处理我们后期会详细讲解,因为clone方法的异常是受查异常/编译时异常,所以必须在编译时处理:public class TestDemo { public static void main(String[] args) throws CloneNotSupportedException{//必须在编译时处理 Person person1 = new Person(10); System.out.println(person1); Person person2 = (Person) person1.clone(); System.out.println(person2);}但是我们运行完,还是会出现错误,这可是真愁人CloneNotSupportedException,不支持克隆,告诉我们Person类不支持克隆所以我们这时候就要提到前面所说的Cloneable接口了,这个时候我们就用Person实现这个接口,那么运行结果:这时候我们就完成了person2对person1的所指的对象的克隆这个时候我们来看一下Cloneable这个接口:Cloneable是一个空接口,我们可以在IDEA中去查看源码:它是一个空接口,也作为一种标记接口,因为它用来证明当前类是可以被克隆的在堆上,创建了一个对象,假设地址是0X98,那么在虚拟机栈上得到是person1所指这个对象的地址,通过clone的方法来进行克隆出堆中一个相同的一份的对象,但是两者(person1和person2)在堆上不是一个位置,所以person2这个克隆后的获得了一个新的地址0X88(假设的),所以我们通过 return super.clone() 来克隆。那么就来总结一下Person person1 = person.clone();这个其中的所有的错误:修饰符的错误,protected,这一步用super.clone()访问object类,来重写这个clone方法异常,clone方法的异常是受查异常/编译时异常,所以必须在编译时处理,(在main后面加入throws)向下转型,进行强制转换,因为我们用的是object类中的clone方法,所以返回类型是固定的object类,而不是构成了协变类型。所以我们要进行向下转型,将其转换为子类;进行了上述的操作,还是会报错:CloneNotSupportedException,不支持克隆。所以我们要实现Cloneable这个接口,从而才能使用我的理解:clone方法实现的具体操作,用super.clone来访问,Cloneable判断这个类是否支持克隆操作(2).浅拷贝这时候我们在Person类的基础上再重新创建了一个Money类,让它与其Person类构成组合class Money{ public double money = 19.9;}class Person1 implements Cloneable{ public int age; public Money m; public Person1(int age) { this.age = age; this.m = new Money();//在构造方法中实例化一个money } @Override public String toString() { return "Person{" + " age=" + age + " }"; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class TestDemo { public static void main(String[] args) throws CloneNotSupportedException { Person1 person1 = new Person1(10); Person1 person2 = (Person1) person1.clone(); System.out.println(person1); System.out.println(person2); System.out.println("==============="); System.out.println(person1.m.money); System.out.println(person2.m.money);//先调用的是person1的成员变量m, //由于在构造方法中进行了m的实例化,所以我还可以调用Money类中的成员变量 System.out.println("==============="); //将person2进行修改,正常我们所期望的是只有person2进行修改,person1没有变化 person2.m.money = 99.99; System.out.println(person1.m.money); System.out.println(person2.m.money); }}this.m = new Money();也就是在堆上的空余的空间再创建了一个实例化Money的对象,并且这个m在堆上那个原本的空间占有的一定内存空间。person1.m.money为什么会这么调用money?这个m是Peron1类的成员变量,但是我们也在Person1类中的构造方法去实例化Money这个类,所以这个m又是Money实例化的变量名,再用这个m来调用了Money类中的成员变量money将person2进行修改,正常我们所期望的是只有person2进行修改,person1没有变化,但是可惜的是两者都发生了变化。所以我们可以通过图例来演示一下这个过程:在虚拟机栈上,会创建两个空间分别是person1和person2,person1所实例化的对象会在堆上创建一个对象的空间(地址为0x98),存入两个成员变量,但是其中m是Money类型即是引用类型,并且也是实例化了,所以那么也会是创建money类型的对象的空间(地址为0x65),这时候在对第一个对象中的成员变量m也会得到它实例化的地址person1.clone()是把person1指向的对象进行克隆,但是没把这个对象的空间中成员变量m所指的对象进行克隆所以那也意味着成员变量m还是指的是0x65的它自己的对象空间Money,这个对象的空间的成员变量是moneyperson2还是指的是第一次的对象中的对象的成员变量money;所以这个时候堆money的修改还是一次就改变person1和person2所指的money的值,两者还是一样的此时这个现象被称之为浅拷贝:并没有将对象中的对象去进行克隆(3).深拷贝class Money1 implements Cloneable{ public double money = 19.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}class Person2 implements Cloneable{ public int age; public Money1 m; public Person2(int age) { this.age = age; this.m = new Money1();//在构造方法中实例化一个money } @Override public String toString() { return "Person{" + " age=" + age + " }"; } @Override protected Object clone() throws CloneNotSupportedException { Person2 tmp = (Person2) super.clone(); //tmp.m类的调用成员变量,this.m.clone()中的this是指这个对象的成员变量m的引用 tmp.m = (Money1) this.m.clone();//谁调用某个方法谁就是this,现在是person1来调用,那么person1就是this return tmp; }}public class Test5 { public static void main(String[] args) throws CloneNotSupportedException{ Person2 person1 = new Person2(10); Person2 person2 = (Person2) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("==============="); person2.m.money = 99.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }}这时候我们把Money1类也进行实现了Cloneable的接口,这时候我们再对clone方法的重写,但是我们会发现两者的重写的clone方法不同,在Person2类中的重写方法,创建一个临时变量tmp,通过将其向下转型变成了Person2类型,tmp.m是Person2类的调用成员变量m,this.m.clone()就是person1正在调用clone方法,这个时候先访问了Pesron2类中成员变量m,再去通过Money1类的实例化之后再调用clone方法,这个时候最终是Money1这个类型去调用这个方法;但是我们会发现tmp是局部变量,除了生命作用域就会销毁,但是最终返回的是给person2,又因为person2是引用类型,会得到了tmp的地址。所以我们可以通过图例来演示一下这个过程:在虚拟机栈上,会创建两个空间分别是person1和person2,还有一个在person类中创建一个临时变量tmp,因为是把对象的对象进行克隆,所以要再类中的另一个类(该类作为成员变量)中重写clone方法,但是在原有的类中的clone方法也需要进行修改person1所实例化的对象会在堆上创建一个对象的空间(地址为0x98),存入两个成员变量,但是其中m是Money类型即是引用类型,并且也是实例化了,所以那么也会是创建money类型的对象的空间(地址为0x65),这时候在对中的成员变量m也会得到它实例化的地址因为我们还在Person类中修改了重写方法;通过创建了一个临时变量tmp也是为Person类型,先把外面的对象先进行克隆完成,这时候再通过这个临时变量去调用Person类中的m(其实也就是类的引用),由于我将m这个变量也进行了Money类的实例化的操作,所以这时候再用m去调用正常的clone方法,从而通过去调用Money类中的clone方法这个就完成了克隆那么Money类的克隆的地址(0x888),但是tmp是临时的局部的变量,出了作用域就销毁,最终返回的是给的是person2[person2 = (Person2) person1.clone()]这个就是返回了person2么因为它是类类型(引用类型)所以这个时候的person2就会得到它的地址(0x91)最终person2和person1所指的两个值是不一样此时这个现象被称之为深拷贝:完全将对象中的对象去进行克隆。注意:深拷贝与浅拷贝就是看的是代码实现的过程,跟克隆方法没有关系三、抽象类和接口的区别(面试常考)(1).抽象类中可以有普通成员方法和成员变量,其中还可以有抽象方法,如果子类继承抽象类,必须要重写抽象类中的抽象方法,如果不想重写抽象方法,那么就要将子类设计为抽象类一个类可以实现多个接口(模拟多继承),但是类与类之间的继承必须是单继承的关系,继承用extends关键字(2).接口中不可以有普通的成员方法和成员变量,它的成员变量默认是public static final修饰,成员方法默认是public abstract修饰,如果类要实现接口,就要重写接口中的抽象方法实现用implements关键字,接口之间也可以继承多个接口(这种操作也是多继承),这种操作不用父接口的抽象方法;但是用类实现时,要注意重写接口与被继承接口的所有的抽象方法。这期内容就分享到这里了,希望大家可以获得新的知识,当然,如果有哪些细节和内容不足,欢迎大家在评论区中指出!———————————————— 原文链接:https://blog.csdn.net/2401_87022967/article/details/147046361
-
本文详细介绍了Java中多线程的创建方式,包括继承Thread类、实现Runnable接口和Callable接口,以及线程安全、线程同步的概念和实现方法,如同步代码块、同步方法和Lock锁。此外,还探讨了线程通信、线程池的使用,包括ExecutorService、ThreadPoolExecutor和Executors工具类,以及定时器ScheduledExecutorService的应用。最后,文章还涵盖了线程并发与并行以及线程的生命周期。摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >展开 文章目录多线程多线程的创建方式一:继承Thread类方式二:实现Runnable接口多线程的实现方案二:实现Runnable接口(匿名内部类形式)方式三:JDK5.0新增:实现Callable接口Thread的常用方法线程安全线程安全问题是什么、发生的原因线程安全问题案例模拟线程同步同步思想概述方式一:同步代码块方式二:同步方法Lock锁线程通信线程通信案例模拟线程池线程池概述线程池实现的API、参数说明**线程池常见面试题**线程池处理Runnable任务线程池处理Callable任务Executors工具类实现线程池定时器ScheduledExecutorService定时器线程并发与并行线程的生命周期多线程什么是线程?线程(Thread)是一个程序内部的一条执行路径。我们之前启动程序执行后,main方法执行其实就是一条单独的执行路径程序中如果只有一条执行路径,那么这个程序就是单线程的程序。多线程是什么?多线程是指从软硬件上实现多条执行流程的技术。多线程的创建Thread类Java是通过java.lang.Thread类来代表线程的。按照面向对象的思想,Thread类应该提供了实现多线程的方式。方式一:继承Thread类定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法创建MyThread类的对象调用线程对象的start()方法启动线程(启动后还是执行run方法的)public class ThreadDemo01 { public static void main(String[] args) { //3.new一个线程对象 Thread t = new MyThread(); t.start(); for (int i = 0; i < 5; i++) { System.out.println("主线程启动了:"+i); } }}/** 1.定义一个线程类继承Thread类 */class MyThread extends Thread{ /** 2.重写run方法,里面定义线程以后要干啥 */ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程启动了:"+i); } }}方式一优缺点:优点:编码简单缺点:线程类以及继承Thread,无法继承其他类,不利于拓展为什么不直接调用了run方法,而是调用start启动线程直接调用run方法会当成普通方法执行,此时相当于还是单线程执行只有调用start方法才是启动一个新的线程执行把主线程任务放在子线程之前了。这样主线程一直是先跑完的,相当于是一个单线程的效果了。方式二:实现Runnable接口定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法创建MyRunnable任务对象把MyRunnable任务对象交给Thread处理调用线程对象的start()方法启动线程public class ThreadDemo02 { public static void main(String[] args) { Runnable runnable = new MyRunable(); Thread t = new Thread(runnable); t.start(); for (int i = 0; i < 10; i++) { System.out.println("主线程开始执行:"+i); } }}class MyRunable implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("子线程在执行:"+i); } }}方式二优缺点:优点:线程任务类只是实现接口,可以继续继承类和实现接口,拓展性强。缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。多线程的实现方案二:实现Runnable接口(匿名内部类形式)可以创建Runnable的匿名内部类对象交给Thread处理调用线程对象的start() 启动线程public class ThreadDemoOther02 { public static void main(String[] args) { //创建一个任务对象 Runnable a = new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程1执行输出:" + i); } } }; // 把任务对象交给Thread处理 Thread t = new Thread(a); t.start(); Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程2执行输出:" + i); } } }); t1.start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程3执行输出:" + i); } } }).start(); new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("子线程4执行输出:" + i); } }).start(); for (int i = 0; i < 5; i++) { System.out.println("主线程执行输出:" + i); } } }output:子线程2执行输出:0子线程2执行输出:1子线程2执行输出:2子线程2执行输出:3子线程2执行输出:4子线程1执行输出:0子线程1执行输出:1子线程1执行输出:2子线程1执行输出:3子线程1执行输出:4主线程执行输出:0主线程执行输出:1主线程执行输出:2主线程执行输出:3主线程执行输出:4子线程4执行输出:0子线程4执行输出:1子线程4执行输出:2子线程4执行输出:3子线程4执行输出:4子线程3执行输出:0子线程3执行输出:1子线程3执行输出:2子线程3执行输出:3子线程3执行输出:4方式三:JDK5.0新增:实现Callable接口多线程的实现方案三:利用Callable、FutureTask接口实现得到任务对象定义类实现Callable接口,重写call方法,封装要做的事情创建Callable任务对象交给FutureTask对象用FutureTask 把Callable对象封装成线程任务对象把线程任务对象交给Thread处理调用Thread的start方法启动线程,执行任务线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果public class ThreadDemo03 { public static void main(String[] args) { Callable<String> call = new myCallable(100); //FutureTask对象的作用:是Runnable的对象(实现了Runnable接口),可以交给Thread了 FutureTask<String> f = new FutureTask<>(call); Thread t = new Thread(f); t.start(); Callable<String> call2 = new myCallable(200); FutureTask<String> f2 = new FutureTask<>(call2); Thread t2 = new Thread(f2); t2.start(); try { String rs1 = f.get(); System.out.println(rs1); } catch (Exception e) { e.printStackTrace(); } try { String rs2 = f2.get(); System.out.println(rs2); } catch (Exception e) { e.printStackTrace(); } }}class myCallable implements Callable<String> { private int n; public myCallable(int n) { this.n = n; } @Override public String call() throws Exception { int sum = 0; for (int i = 0; i <= n; i++) { sum += i; } return "子线程结果是:" + sum; }}output:子线程结果是:5050子线程结果是:20100FutureTask的API方法名称 说明public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。public V get() throws Exception 获取线程执行call方法返回的结果Thread的常用方法Thread常用API说明Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法stop、守护线程、线程优先级等线程的控制方法,在开发中很少使用。Thread类获得当前线程的对象注意:此方法是Thread类的静态方法,可以直接使用Thread类调用。这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。public class ThreadDemo01 { // main方法是由主线程负责调度的 public static void main(String[] args) { Thread t1 = new MyThread();// t1.setName("1号"); t1.start(); System.out.println(t1.getName()); Thread t2 = new MyThread();// t2.setName("2号"); t2.start(); System.out.println(t2.getName()); // 哪个线程执行它,它就得到哪个线程对象(当前线程对象) // 主线程的名称就叫main Thread m = Thread.currentThread(); System.out.println(m.getName()); for (int i = 0; i < 5; i++) { System.out.println(m.getName()+"输出:"+i); } } public class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+"输出:"+i); } }}output:Thread-0Thread-1mainmain输出:0Thread-1输出:0Thread-1输出:1Thread-1输出:2Thread-1输出:3Thread-1输出:4main输出:1main输出:2main输出:3main输出:4Thread-0输出:0Thread-0输出:1Thread-0输出:2Thread-0输出:3Thread-0输出:4Thread的构造器方法名称 说明public Thread(String name) 可以为当前线程指定名称public Thread(Runnable target) 封装Runnable对象为线程对象public Thread(Runnable target, String name) 封装Runnable对象为线程对象,并指定线程名称Thread类的线程休眠方法方法名称 说明public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒public class ThreadDemo02 { public static void main(String[] args)throws Exception { for (int i = 1; i <= 5; i++) { System.out.println("主线程输出:"+i); if (i==3){ //让线程进入3秒休眠状态 Thread.sleep(3000); } } }}线程安全线程安全问题是什么、发生的原因线程安全问题多个线程同事操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。出现的原因:存在多线程并发同时访问共享资源存在修改共享资源线程安全问题案例模拟案例:取钱业务需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万分析:需要提供一个账户类,创建一个账户代表2人的共享账户需要定义一个线程类,线程类可以处理账户对象创建两个线程对象,传入同一个账户对象启动两个线程,去同一个账户对象中取钱10万public class ThreadDemo { public static void main(String[] args) { //1.定义线程类,创建一个共享的账户对象 Account acc = new Account("ICBC-110",100000); //2.创建2个线程对象,代表小红和小明同时进来了 new DrawThread(acc,"小明").start(); new DrawThread(acc,"小红").start(); }}/**共享账户类*/public class Account { private String cardID;//卡号 private double money;//余额 public Account() { } /** * 小明 小红取钱 */ public void drawMoney(double money) { //1.先获取是谁来取钱,线程的名字就是人名 String name = Thread.currentThread().getName(); //1.判断账户是否有钱 if (this.money >= money) { //取钱 System.out.println(name+"成功取出了"+money+"元"); //更新余额 this.money -= money; System.out.println(name+"取款后剩余:"+this.money); }else { System.out.println(name+"来取钱,余额不足!"); } } public Account(String cardID, double money) { this.cardID = cardID; this.money = money; } public String getCardID() { return cardID; } public void setCardID(String cardID) { this.cardID = cardID; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; }}/** 取钱的线程类 */public class DrawThread extends Thread{ //接收处理的账户对象 private Account acc; public DrawThread(Account acc,String name) { super(name); this.acc = acc; } @Override public void run() { //小红 小红 取钱 acc.drawMoney(100000); }}output:小红成功取出了100000.0元小明成功取出了100000.0元小明取款后剩余:-100000.0小红取款后剩余:0.0线程同步同步思想概述线程同步为了解决线程安全问题取钱案例出现问题的原因?多个线程同时执行,发现钱都是够的。如何才能保证线程安全呢?让多个线程实现先后依次访问共享资源,这样就解决了安全问题线程同步的核心思想加锁,把共享资源进行上锁,每次只能一个线程进入,访问完毕后解锁,然后其他线程才能进来。方式一:同步代码块作用:把出现线程安全问题的核心代码给上锁。原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。synchronized(同步锁对象){ 操作共享资源的代码(核心代码)}锁对象要求理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。/** 小红小明在此取钱 */public void drawMoney(double money) { //看谁来这里取钱 String name = Thread.currentThread().getName(); // 同步代码块 // 小明 小红 // this == acc 共享账户 synchronized (this) { //判断余额是否足够 if (this.money>=money){ //取钱 System.out.println(name+"取钱成功,本次取款:"+money); //更新余额 this.money-=money; System.out.println(name+"取款后,剩余:"+this.money); }else { System.out.println(name+"取款余额不足!"); } }}锁对象用任意唯一的对象好不好呢?不好,会影响其他无关线程的执行。锁对象的规范要求规范上:建议使用共享资源作为锁对象对于实例方法建议使用this作为锁对象。对于静态方法,建议使用字节码(类名.class)对象作为锁对象/** 假设100个人来调用这个方法, */public static void run(){ synchronized (Account.class){ //这样每次就只能有一个人来调用 }}方式二:同步方法作用:把出现线程安全问题的核心方法给上锁原理:每次只能进入一个线程,执行完毕后自动解锁,其他线程才可以进来执行。格式:修饰符 synchronized 返回值类型 方法名称(形参列表){ 操作共享资源}public synchronized void drawMoney(double money) { String name = Thread.currentThread().getName(); if (this.money>=money){ System.out.println(name+"取钱成功!取了"+money+"元"); //更新余额 this.money-=money; System.out.println(name+"取款后剩余"+this.money); }else { System.out.println(name+"余额不足!"); }}同步方法底层原理同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码如果方法是实例方法:同步方法默认用this作为锁对象,但是代码要高度面向对象如果方法是静态方法:同步方法默认用类名.class作为锁的对象。是同步代码块好还是同步方法好一点?同步代码块锁的范围更小,同步方法锁的范围更大Lock锁为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象方法名称 说明public ReentrantLock() 获得Lock锁的实现类对象Lock的API方法名称 说明void lock 获得锁void unlock 释放锁public class Account { private String cardId; private double money;//余额 // final修饰后:锁对象是唯一和不可替换的 private final Lock lock = new ReentrantLock(); public Account() { } public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } public void drawMoney(double money) { String name = Thread.currentThread().getName(); lock.lock();//上锁 try { if (this.money >= money) { System.out.println(name + "取钱成功!取了" + money + "元"); //更新余额 this.money -= money; System.out.println(name + "取款后剩余" + this.money); } else { System.out.println(name + "余额不足!"); } } finally { lock.unlock();//释放锁 } }}线程通信什么是线程通信?如何实现?所谓线程通信就是线程间相互发送数据。线程通信常见形式通过共享一个数据的方式实现。根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。线程通信实际应用模型生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据一般要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完数据后唤醒生产者,然后等待自己。线程通信案例模拟模拟手机接电话系统,有电话就接听,没有电话就等待、线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,切要保证线程安全。/** 模拟线程通信,来电提醒线程, 接电话线程 */public class Phone { private boolean flag = false; public void run(){ //a,负责来电提醒的线程 new Thread(new Runnable() { @Override public void run() { try { while (true) { synchronized (Phone.this){ if (!flag){ //代表有来电提醒 System.out.println("有电话正在呼入!!"); flag = true;//代表继续等待呼入电话 Phone.this.notify(); Phone.this.wait(); } } } } catch (Exception e) { e.printStackTrace(); } } }).start(); //b,负责接电话线程,正式接听了 new Thread(new Runnable() { @Override public void run() { try { //不断的接听电话 while (true) { synchronized (Phone.this){ if (flag){ //可以接听电话了 System.out.println("电话接听中,通话了5分钟结束了!"); flag = false;//代表继续等待呼入电话 Thread.sleep(5000); //唤醒别人,等待自己 Phone.this.notify(); Phone.this.wait(); } } } } catch (Exception e) { e.printStackTrace(); } } }).start(); } public static void main(String[] args) { //1,创建一部手机对象 Phone huawei = new Phone(); huawei.run(); }}output:有电话正在呼入!!电话接听中,通话了5分钟结束了!有电话正在呼入!!电话接听中,通话了5分钟结束了!Object类的等待和唤醒方法:方法名称 说明void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法void notify() 唤醒正在等待的单个线程void notifyAll() 唤醒正在等待的所有线程注意:上述方法应该使用当前同步锁对象进行调用线程池线程池概述什么是线程池?线程池就是一个可以复用线程的技术。不使用线程池的问题如果用户每发起一个请求,后台就创建一个新的线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。线程池实现的API、参数说明谁代表线程池?JDK5.0起提供了线程池的接口:ExecutorService如何得到线程池对象方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象ThreadPoolExecutor构造器的参数说明线程池常见面试题临时线程什么时候创建?新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。什么时候会开始拒绝任务?核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。小结谁代表线程池?ExecutorService接口ThreadPoolExecutor实现线程池对象的七个参数是什么意思?线程池处理Runnable任务ThreadPoolExecutor创建线程池对象示例ExecutorService1ExecutorService的常用方法方法名称 说明void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable任务Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务void shutdown() 等任务执行完毕后关闭线程池List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务新任务拒绝策略策略 详解ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常 这是不推荐的做法ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行public class MyRunnable implements Runnable { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld ==>" + i); } try { System.out.println(Thread.currentThread().getName()+"本任务与线程绑定了,线程进入休眠了"); Thread.sleep(5000000); } catch (Exception e) { e.printStackTrace(); } }}/** 自定义一个线程池对象并测试其特性 */public class ThreadPoolDemo1 { public static void main(String[] args) { //1.创建线程池对象 /** public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { */ ExecutorService pool = new ThreadPoolExecutor(3,5, 6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); //2.给任务线程池处理 Runnable target = new MyRunnable(); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); //创建临时线程 pool.execute(target); pool.execute(target); //不创建,拒绝策略被触发 pool.execute(target); //关闭线程池(开发中一般不会使用) pool.shutdownNow();//立即关闭,即使任务没有完成,会丢失任务 pool.shutdown();// 会等待全部任务执行完毕之后再关闭 }}output:pool-1-thread-2输出了:HelloWorld ==>1pool-1-thread-2输出了:HelloWorld ==>2pool-1-thread-2输出了:HelloWorld ==>3pool-1-thread-2输出了:HelloWorld ==>4pool-1-thread-3输出了:HelloWorld ==>1pool-1-thread-2输出了:HelloWorld ==>5pool-1-thread-1输出了:HelloWorld ==>1pool-1-thread-1输出了:HelloWorld ==>2pool-1-thread-1输出了:HelloWorld ==>3pool-1-thread-3输出了:HelloWorld ==>2pool-1-thread-3输出了:HelloWorld ==>3pool-1-thread-3输出了:HelloWorld ==>4pool-1-thread-3输出了:HelloWorld ==>5pool-1-thread-1输出了:HelloWorld ==>4pool-1-thread-1输出了:HelloWorld ==>5pool-1-thread-2本任务与线程绑定了,线程进入休眠了pool-1-thread-3本任务与线程绑定了,线程进入休眠了pool-1-thread-1本任务与线程绑定了,线程进入休眠了pool-1-thread-4输出了:HelloWorld ==>1pool-1-thread-4输出了:HelloWorld ==>2pool-1-thread-4输出了:HelloWorld ==>3pool-1-thread-4输出了:HelloWorld ==>4pool-1-thread-4输出了:HelloWorld ==>5pool-1-thread-4本任务与线程绑定了,线程进入休眠了pool-1-thread-5输出了:HelloWorld ==>1pool-1-thread-5输出了:HelloWorld ==>2pool-1-thread-5输出了:HelloWorld ==>3pool-1-thread-5输出了:HelloWorld ==>4pool-1-thread-5输出了:HelloWorld ==>5pool-1-thread-5本任务与线程绑定了,线程进入休眠了Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread-0,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@378bf509[Running, pool size = 5, active threads = 5, queued tasks = 5, completed tasks = 0]线程池处理Callable任务public class MyCallable implements Callable<String> { private int n; public MyCallable(int n) { this.n = n; } @Override public String call() throws Exception { int sum = 0; for (int i = 0; i < n; i++) { sum += i; } return Thread.currentThread().getName() + "执行1-" + n + "的结果是:" + sum; }}public class ThreadPoolDemo2 { public static void main(String[] args)throws Exception { //1.创建线程池对象 /** public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { */ ExecutorService pool = new ThreadPoolExecutor(3,5, 6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); //2.给任务线程池处理 Future<String> f1 = pool.submit(new MyCallable(100)); Future<String> f2 = pool.submit(new MyCallable(200)); Future<String> f3 = pool.submit(new MyCallable(300)); Future<String> f4 = pool.submit(new MyCallable(400)); Future<String> f5 = pool.submit(new MyCallable(500));// String rs = f1.get();// System.out.println(rs); System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); System.out.println(f5.get()); }}output:pool-1-thread-1执行1-100的结果是:4950pool-1-thread-2执行1-200的结果是:19900pool-1-thread-3执行1-300的结果是:44850pool-1-thread-2执行1-400的结果是:79800pool-1-thread-3执行1-500的结果是:124750线程池如何处理Callable任务,并得到任务执行完后返回的结果使用ExecutorService的方法:Future submit(Callable command)Executors工具类实现线程池Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。方法名称 说明public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间会被回收掉public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为它执行异常而结束,那么线程池会补充一个新线程替代它public static ExecutorService newSingleThreadExecutor ( ) 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池就会补充一个新线程public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的public class ThreadPoolDemo3 { public static void main(String[] args) { //1.创建固定线程数的线程池 ExecutorService pool = Executors.newFixedThreadPool(3); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); }}output:pool-1-thread-1输出了:HelloWorld ==>1pool-1-thread-1输出了:HelloWorld ==>2pool-1-thread-3输出了:HelloWorld ==>1pool-1-thread-3输出了:HelloWorld ==>2pool-1-thread-3输出了:HelloWorld ==>3pool-1-thread-3输出了:HelloWorld ==>4pool-1-thread-3输出了:HelloWorld ==>5pool-1-thread-1输出了:HelloWorld ==>3pool-1-thread-1输出了:HelloWorld ==>4pool-1-thread-1输出了:HelloWorld ==>5pool-1-thread-2输出了:HelloWorld ==>1pool-1-thread-2输出了:HelloWorld ==>2pool-1-thread-2输出了:HelloWorld ==>3pool-1-thread-2输出了:HelloWorld ==>4pool-1-thread-2输出了:HelloWorld ==>5pool-1-thread-3本任务与线程绑定了,线程进入休眠了pool-1-thread-1本任务与线程绑定了,线程进入休眠了pool-1-thread-2本任务与线程绑定了,线程进入休眠了Executors使用可能存在的陷阱大型并发系统环境中使用Executors如果不注意可能会出现系统风险Executors工具类底层是基于什么方式实现的线程池对象?线程池ExecutorService的实现类:ThreadPoolExecutorExecutors是否适合做大型互联网场景的线程池方案?不合适建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险定时器定时器是一种控制任务延时调用,或者周期调用的技术作用:闹钟、定时邮件发送、定时弹送广告定时器的实现方法方式一:Timer方式二:ScheduledExecutorServiceTimer定时器构造器 说明public Timer() 创建Timer定时器对象方法 说明public void schedule(TimerTask task, long delay , long period) 开启一个定时器,按照计划处理TimerTask任务Timer定时器的特点和存在的问题Timer是单线程,处理多个任务按照顺序进行,存在延时与设置定时器的时间有出入。可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行public class TimerDemo01 { public static void main(String[] args) { //1.创建Timer定时器 Timer timer = new Timer();//定时器本身就是一个单线程 //2.调用方法,处理定时任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "定时器AAA开始执行一次" + new Date()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }, 0, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "定时器BBB开始执行一次" + new Date()); System.out.println(10 / 0); } }, 0, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "定时器CCC开始执行一次" + new Date()); } }, 0, 2000); }}output:Timer-0定时器AAA开始执行一次Sat Jan 22 23:26:42 CST 2022Timer-0定时器BBB开始执行一次Sat Jan 22 23:26:47 CST 2022Exception in thread "Timer-0" java.lang.ArithmeticException: / by zeroat com.csl.d8_timer.TimerDemo01$2.run(TimerDemo01.java:28)at java.base/java.util.TimerThread.mainLoop(Timer.java:556)at java.base/java.util.TimerThread.run(Timer.java:506)ScheduledExecutorService定时器ScheduledExecutorService是jdk 1.5中引入了并发包,目的是为了弥补Timer的缺陷,ScheduledExecutorService内部为线程池Executors方法 说明public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象ScheduledExecutorService的方法 说明public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period , TimeuUnit unit) 周期调度方法ScheduledExecutorService的优点基于线程池,某个任务的执行情况不会影响其他定时任务的执行。public class TimerDemo2 { public static void main(String[] args) { //1.创建ScheduledExecutorService线程池,做定时器 ScheduledExecutorService pool = Executors.newScheduledThreadPool(3); //2.开启定时任务 pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出:AAA"); try { Thread.sleep(100000); } catch (Exception e) { e.printStackTrace(); } } }, 0, 2, TimeUnit.SECONDS); pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出:BBB"); System.out.println(10 / 0); } }, 0, 2, TimeUnit.SECONDS); pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出:CCC"); } }, 0, 2, TimeUnit.SECONDS); }}output:pool-1-thread-2执行输出:BBBpool-1-thread-3执行输出:CCCpool-1-thread-1执行输出:AAApool-1-thread-3执行输出:CCCpool-1-thread-3执行输出:CCCpool-1-thread-2执行输出:CCC线程并发与并行正在运行的程序(软件) 就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的并发的理解CPU同时处理线程的数量有限CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。并行的理解在同一个时刻上,同时有多个线程在被CPU处理并执行————————————————原文链接:https://blog.csdn.net/qq_46002941/article/details/122646052
-
1,网络发展史1)独立模式我们刚开始使用计算机呢,客户端的数据不是共享的,如果有一个人想要办理业务,而这个业务所需的资源是在三台电脑上,那么这个人就需要在这三个电脑上不断的办理任务,而其他人想要办理业务,还需要等到前一个人办理完,效率非常低,那怎么办,我们就改进;2)网络互联接下来我们就使用网络进行计算机资源的共享,让多个计算机可以一起办理业务,达成数据共享,即网络通信,我们可以根据网络互联的规模分为局域网和广域网;3)局域网LAN局域网是本地,局部构建的一种私有网络,又被称为内网,局域网内的主机能够实现网络通信,局域网和局域网在没有连接的情况是不能进行通信的;组件局域网等待方式也有很多种,可以通过网线直连,也可以通过交换机相连,还可以通过集线器相连,还可以通过路由器连接交换机在与多个主机相连;4)广域网WAN广域网就是多个局域网完成了连接,很多很多的局域网都能进行网络通信,我们其实可以把咱们中国的网络看成一个巨大的广域网,我们管内部叫做内网,外面的就是我们常说的外网,有很多人可能对此不满,但这也是保护我们的一种方式,起码我们活的挺快乐的;不说了,再说被封了,哈哈哈哈哈哈;2,网络通信基础1)IP地址那么,广域网这么大,我们怎么能准确找到每个主机的所在呢,我们就使用IP地址来标识每个网络主机和网络设备的网络地址,我们可以通过CMD看自己主机的地址,输入这个命令ipconfig,就能看到了,那个IPv4地址就是我们的地址啦;2)端口号端口是啥玩意,我们有了地址,那么电脑发送或者我们接收了一个数据,难道我们只是通过地址就能知道吗,我们知道了地址,但不知道是哪个软件发送或者接收这个数据,比如发来一个QQ的数据报,那我们去给CSDN吗,不,我们应该是找到QQ的端口号,之后把这个数据给到QQ,让QQ来做相应的操作;我们可以把网络通信可以看成送快递,我们把IP地址看作收货地址,把端口号看作收件人;3)认识协议我们现在能找到地址和端口号了,我们网络传输的是二进制的数据,那么我们传入一段二级制指令,对方是怎么知道我们传的是什么东西呢,之前说过,图片,音频和视频都是二进制的指令,我们到一个数据报,我们怎么知道这是啥文件呢,去使用什么编码方式呢,所以就需要大家都统一一下,我们就约定网络传输的格式,我们就管它叫协议;协议的最终体现呢,就是网络传输数据报的格式;4)五元组在TCP/IP协议中,我们使用五元组来标识网络通信:1,源IP:标识源主机2,源端口号:标识源主机中该次通信发送的进程3,目的IP:标识目的主机4,目的端口号:标识源主机中该次通信接收的进程5,协议号:标识发送进程和接收进程双方约定的格式5)协议分层啥事协议分层呢,我们的协议很多,很复杂,我们把它分为不同层次的协议,让每个协议尽可能有自己的功能,OSI七层模型和TCP/IP五层模型,都把每层划分了很多不同的功能;OSI七层调用模型:层数 名称 功能 功能概览7 应用层 针对特定应用的协议 比如我们发送邮件,就用电子邮件协议,实现登录,就要使用登录协议6 表示层 数据固有格式和网络标准格式的转换 我们将接收的信息会根据网络标准格式转换为标准的信息5 会话层 通讯管理,负责建立和断开通讯 何时建立连接,何时断开连接和建立多久的连接;4 传输层 管理两个节点之间的数据传输,负责可靠传输 检查是否有数据丢失3 网络层 地址管理与路由选择 会考虑经过哪些路由到达地址2 数据链路层 互联数据的传送和识别数据帧 .....数据帧和比特流之间的转换1 物理层 以‘0’,‘1’代表电压高低,灯光闪灭,界定连接器和网线的规格 比特流与电子信号的转换这个我们大概了解即可,我们也是从网上扒下来的,我们会重点去学习应用层; TCP/IP通讯协议:TCP/IP模型其实就是OSI七层协议模型,只不过把OSI重新划分了一下,TCP/IP通讯协议采用五层的层级结构,每一层都可以呼叫下一层来给自己提供网络需求;5层,应用层:负责应用程序间沟通,如简单电⼦邮件传输(SMTP)、文件传输协议(FTP)、网络远 程访问协议(Telnet)等。我们的⽹络编程主要就是针对应⽤层。4层,传输层:负责两台主机之间的数据传输。如传输控制协议(TCP),能够确保数据可靠的从源主机发 送到⽬标主机,还有UDP。3层,网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识⼀台主机,并通过路由表 的⽅式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)⼯作在⽹路层。2层,数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从⽹线上 检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就⾃动重发)、数据差错校验等⼯作。 有以太⽹、令牌环网,⽆线LAN等标准。交换机(Switch)工作在数据链路层。1层,物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的的同 轴电缆(现在主要⽤于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理 层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。这也是扒来的,下面我用自己的理解讲讲:——————5,应用层:就是我们拿到了包裹(数据包)后怎么样~4,传输层:任意两个设备的通信,不考虑传输过程,只考虑起点和终点;3,网络层:任意两个设备的通信,考虑中间的过程,传输过程可能有很多的交换机啥的;2,数据链路层:完成相邻两个设备之间的通信;1,物理层:规定网络通信中一些硬件设施的符合要求:我们拿送快递来举一个例子,比如我们网购一个手机,我们拿到手机之后怎么使用,就是应用层;商家发货的寄件方后收件方的地址,就是传输层;物流公司关心包裹是咋传输的,就是网络层;大车司机关心今天送到哪个地方,一个一个节点之间,就是数据链路层;TCP/IP协议栈其实里面包含了很多协议,但是最重要的就是TCP/IP协议了,我们再来谈谈主机,交换机和路由器都涉及到哪些层次:1,主机 工作涉及到 物理层到应用层(通过应用层满足数网络通信的要求);2,路由器 工作涉及 物理层到网路层(组件局域网,进行网络数据报的转发);3,交换机 工作涉及到 物理层到数据链路层(对路由器接口的扩展,不需要考虑组网问题);3,网络通信基本流程不同的协议层对数据包有不同的叫法,在传输层叫段,在网络层叫数据报,在数据链路层叫数据帧;应用层数据包,往往是结构化数据:我们发送数据的时候,会把结构化数据变成二进制比特流或者字符串,我们叫做序列化;我们接收数据的时候,会把二进制比特流或者字符串变成结构化数据,我们叫做反序列化;流程:我们使用QQ,发送Hello给对方;1,应用程序获取用户输入,构造一个应用层数据包,会遵守应用层协议(往往是程序员自己定制的)我们假设约定的格式为(发送者QQ,接收着QQ,消息时间,消息正文);2,应用层调用传输层API(socket api)把数据交给传输层,把数据拿到后,构造出传输层数据包,传输层的协议主要就是TCP和UDP;我们拿TCP数据包举例,TCP数据包 = TCP报头(TCP功能的相关属性) + TCP载荷(就是应用层的数据包);数据包就变成这样的了; 3,传输层数据包构造好之后,就会调用网络层的API,把传输层的数据包交给网络层,网络层来处理数据包,网络最重要的协议,IP协议我们又会加一个IP报头,IP数据包 = IP报头(包含很多信息,包括源IP和目的IP) + IP载荷(整个传输层的数据包);在这些报头中还包含了上一层所用协议的内容,4,IP协议继续调用数据链路层的API,把IP协议交给数据链路层,数据链路层的核心协议,以太网,根据以太网这个协议会在网络层的基础上进一步加工以太网数据帧 = 帧头 + 载荷 + 帧尾5,以太网继续把数据帧给硬件设备(网卡)网卡会把二进制的比特流发送出去,这才成功的发送出去 。发送数据我们我们从上到下的过程我们称为封住,反过来接收数据的时候我们从下到下的过程我们称为复用;———————————————— 原文链接:https://blog.csdn.net/2301_79083481/article/details/146917352
-
引言在开发中,尤其是需要处理大量数据或者进行任务调度的场景下,如何高效地管理数据的顺序和优先级是一个至关重要的问题。Java 提供了优先级队列(PriorityQueue),它基于堆(Heap)实现,能够以高效的方式管理数据的优先级。在本文中,我们将深入探讨优先级队列的工作原理,特别是堆的作用,并通过示例代码帮助你更好地理解其应用。一、什么是优先级队列?优先级队列(Priority Queue)是一种队列数据结构,其中每个元素都包含一个优先级,队列总是按元素的优先级顺序进行排序。与普通队列(先进先出 FIFO)不同,优先级队列确保每次从队列中移除的元素是具有最高优先级的元素。有些场景下,使⽤队列显然不合适,⽐如:在⼿机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在 Java 中,PriorityQueue 是基于堆的实现。堆是一种特殊的二叉树结构,满足特定的顺序性质:最大堆保证每个父节点的值大于等于其子节点的值,而最小堆则相反。二、堆的基本原理JDK1.8中的PriorityQueue底层使⽤了堆这种数据结构,⽽堆实际就是在完全⼆叉树的基础上进⾏了⼀些调整。具有以下特点:对于最大堆,父节点的值始终大于或等于子节点的值;对于最小堆,父节点的值始终小于或等于子节点的值。2.1 堆的概念如果有⼀个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全⼆叉树的顺序存储⽅式存储在⼀个⼀维数组中,并满⾜:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为⼩堆(或⼤堆)。Java 中的 PriorityQueue 默认是最小堆,也就是说队列中最小的元素将具有最高的优先级。堆的性质:• 堆中某个节点的值总是不⼤于或不⼩于其⽗节点的值;• 堆总是⼀棵完全⼆叉树。2.2 堆的存储⽅式从堆的概念可知,堆是⼀棵完全⼆叉树,因此可以层序的规则采⽤顺序的⽅式来⾼效存储,注意:对于⾮完全⼆叉树,则不适合使⽤顺序⽅式进⾏存储,因为为了能够还原⼆叉树,空间中必须 要存储空节点,就会导致空间利⽤率⽐较低。将元素存储到数组中后,可以根据⼆叉树章节的性质5对树进⾏还原。假设i为节点在数组中的下标,则有:• 如果i为0,则i表⽰的节点为根节点,否则i节点的双亲节点为 (i - 1)/2• 如果2 * i + 1 ⼩于节点个数,则节点i的左孩⼦下标为2 * i + 1,否则没有左孩⼦• 如果2 * i + 2 ⼩于节点个数,则节点i的右孩⼦下标为2 * i + 2,否则没有右孩⼦三、堆操作时间复杂度操作类型 描述 时间复杂度插入元素 使用 add() 或 offer() 方法插入元素 O(log n)删除最小元素 使用 poll() 方法移除并返回最小元素 O(log n)查看最小元素 使用 peek() 方法返回堆顶元素而不移除 O(1)获取堆大小 使用 size() 方法返回当前堆的元素数量 O(1)3.1 建堆的时间复杂度因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个节点不影响最终结果):因此:建堆的时间复杂度为O(N)。四、PriorityQueue 的基本操作1. PriorityQueue中放置的元素必须要能够⽐较⼤⼩,不能插⼊⽆法⽐较⼤⼩的对象,否则会抛出 ClassCastException异常2. 不能插⼊null对象,否则会抛出NullPointerException3. 没有容量限制,可以插⼊任意多个元素,其内部可以⾃动扩容4. 插⼊和删除元素的时间复杂度为5. PriorityQueue底层使⽤了堆数据结构6. PriorityQueue默认情况下是⼩堆—即每次获取到的元素都是最⼩的元素4.1 插⼊/删除/获取优先级最⾼的元素注意:优先级队列的扩容说明:• 如果容量⼩于64时,是按照oldCapacity的2倍⽅式扩容的• 如果容量⼤于等于64,是按照oldCapacity的1.5倍⽅式扩容的•如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进⾏扩容五、 构造一个最小堆的优先级队列import java.util.PriorityQueue;public class PriorityQueueExample { public static void main(String[] args) { // 创建一个最小堆 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 添加元素 pq.add(10); pq.add(5); pq.add(15); pq.add(7); // 打印并移除元素 while (!pq.isEmpty()) { System.out.println(pq.poll()); // 依次输出 5, 7, 10, 15 } }}输出:5710151234在这个示例中,PriorityQueue 自动按照最小堆的规则对元素进行排序。每次调用 poll() 方法时,队列中优先级最高的元素(即最小的元素)会被移除。六、 自定义优先级假设我们有一个包含多个任务的列表,每个任务有一个优先级,我们希望按优先级顺序处理这些任务。我们可以通过实现 Comparator 接口来自定义优先级。import java.util.PriorityQueue;import java.util.Comparator;class Task { String name; int priority; public Task(String name, int priority) { this.name = name; this.priority = priority; } @Override public String toString() { return name + " (Priority: " + priority + ")"; }}public class CustomPriorityQueueExample { public static void main(String[] args) { // 自定义Comparator,按优先级降序排列 PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() { @Override public int compare(Task t1, Task t2) { return Integer.compare(t2.priority, t1.priority); // 优先级高的排前面 } }); // 添加任务 pq.add(new Task("Task 1", 3)); pq.add(new Task("Task 2", 5)); pq.add(new Task("Task 3", 1)); pq.add(new Task("Task 4", 4)); // 打印并移除任务 while (!pq.isEmpty()) { System.out.println(pq.poll()); } }}输出:Task 2 (Priority: 5)Task 4 (Priority: 4)Task 1 (Priority: 3)Task 3 (Priority: 1)在这个例子中,PriorityQueue 被用来管理多个任务,并按照任务的优先级(从高到低)排序。. 自定义优先级示例代码解释步骤 代码示例 说明创建优先级队列 PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() {...}); 创建一个带有自定义排序规则的优先级队列,按优先级降序排序添加任务 pq.add(new Task("Task 1", 3)); 向队列中添加一个新任务打印任务 System.out.println(pq.poll()); 输出并移除队列中的优先级最高(优先级最大)的任务七、常见堆的应用场景应用场景 说明 示例任务调度 根据任务的优先级执行任务,堆帮助管理和调度任务顺序 操作系统的调度程序,网络请求调度器合并多个有序数据流 使用堆合并多个已排序的数据流,维持整体有序性 合并 k 个有序链表、流式数据处理实时数据处理 动态地从数据流中获取最小/最大值 获取最近的数据流中的最大值/最小值,实时计算排名前N的元素最短路径算法 在图算法(如 Dijkstra 算法)中,用堆优化路径的计算 Dijkstra 算法,最短路径计算中的优先级队列K 个最大元素问题 找出数组中最大的 K 个元素 求数组中前 K 大的元素,堆排序方法拓展:TOP-K问题:即求数据集合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:1. ⽤数据集合中前K个元素来建堆◦ 前k个最⼤的元素,则建⼩堆◦ 前k个最⼩的元素,则建⼤堆2. ⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素八、总结通过本文的介绍,我们了解了 Java 中优先级队列(PriorityQueue)的基本概念和实现原理。利用堆结构,优先级队列能够高效地管理数据并根据优先级进行处理。无论是任务调度、数据流合并,还是实时数据处理,堆都能发挥其强大的性能优势。8.1 堆的优点优点 说明高效的优先级管理 通过堆结构,可以快速处理数据的优先级。插入和删除操作的时间复杂度为 O(log n),适合动态数据处理。无序输入,高效排序 堆无需输入数据有序,只需通过堆的结构来维护顺序。适用于合并已排序数据流。内存占用少 堆是完全二叉树结构,相比于其他数据结构(如 AVL 树、红黑树)占用的内存较少。8.2 优先级队列的优势与局限性优势/局限性 说明优势 - 对于频繁插入和删除操作非常高效。- 适合任务调度、流式数据处理、最短路径问题等场景。局限性 - 不支持按优先级范围查询或批量删除。- 不是完全通用的排序工具,通常只适用于频繁访问最大或最小元素的场景。8.3 堆与其他数据结构对比数据结构 操作 时间复杂度 优势 局限性堆 插入、删除、查看顶元素 O(log n) 高效管理优先级,适合动态数据处理。 不支持按特定条件的排序,无法直接获取中间元素。数组 排序、查找 O(n log n) 方便查找和排序,简单易用。 插入、删除操作较慢,尤其是在无序数据中。链表 插入、删除 O(1) 插入和删除效率高,尤其适合频繁变动的场景。 查找元素需要 O(n) 的时间,无法高效管理优先级。红黑树 插入、删除、查找 O(log n) 支持高效的查找、插入和删除操作。 相较于堆,内存占用更大,且需要更多的平衡操作。哈希表 查找、插入、删除 O(1) 查找操作极快,适合无序数据的快速检索。 不支持排序,不适合优先级管理。前景:随着大数据和实时计算的不断发展,堆结构和优先级队列将在更多的算法优化和数据流处理中扮演重要角色,尤其是在机器学习、数据挖掘、搜索引擎优化等领域。———————————————— 原文链接:https://blog.csdn.net/2301_80350265/article/details/145959834
-
文章介绍了记忆集作为解决对象跨代引用问题的数据结构,特别是在新生代和老年代之间的引用。记忆集可以通过不同的精度来记录,如字长、对象或卡精度,而卡精度的实现方式是卡表。卡表是一个字节数组,标记内存区域中可能存在跨代指针的卡页。当卡页有跨代指针时,对应的卡表元素设为脏,垃圾收集时只需扫描这些脏元素对应的内存块。摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >记忆集 : 是一种用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构 。如果我们不考虑效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构 记忆集作用 :解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集( Remembered Set )的数据结构,用以避免把整个老年代加进 GC Roots 扫描范围。事实上并不只是新生代、老年代之间才有 跨代引用的问题 。所有涉及部分区域收集(Partial GC )行为的 垃圾收集器,典型的如 G1 、 ZGC 和 Shenandoah 收集器,都会面临相同的问题,因此我们有必要进一步 理清记忆集的原理和实现方式 这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾 收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针 就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为 粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范 围以外的)的记录精度: · 字长精度: 每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的 32 位或 64 位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。 · 对象精度: 每个记录精确到一个对象,该对象里有字段含有跨代指针。 · 卡精度: 每个记录精确到一块内存区域,该区域内有对象含有跨代指针。 “ 卡精度 ” 所指的是用一种称为 “ 卡表 ” ( Card Table)的方式去实现记忆集, 就是 记忆集的一种具体实现 ,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,读者不妨按照 Java 语言中 HashMap 与 Map 的关系来类比理解 卡表最简单的形式可以只是一个字节数组 ,而 HotSpot 虚拟机确实也是这样做的。以下这行代 码是 HotSpot 默认的卡表标记逻辑 : CARD_TABLE [this address >> 9] = 0; 字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作 “ 卡页 ” ( Card Page )。一般来说,卡页大小都是以 2的N次幂 的字节数,通过上面代码可 以看出 HotSpot 中使用的卡页是 2 的 9 次幂,即 512 字节(地址右移 9 位,相当于用地址除以 512 )。那如 果卡表标识内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0 、 1 、 2 号元素,分别对应了 地址范围为 0x0000 ~ 0x01FF 、 0x0200 ~ 0x03FF 、 0x0400 ~ 0x05FF 的卡页内存块 ,如图 所示。 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为 1 ,称为这个 元素变脏 ( Dirty ),没有则标识为 0 。在垃圾收集发生时,只要 筛选出卡表中变脏的元素 ,就能轻易得出哪些卡页内存块中包含跨代指针,把它 们加入 GC Roots 中一并扫描———————————————— 原文链接:https://blog.csdn.net/qq_33919114/article/details/131554268
-
前言在当今互联网时代,Web开发已成为一个炙手可热的领域。Java作为一种成熟的编程语言,以其稳定性和跨平台性,成为了Web开发的热门选择。本文将带您从基础知识入手,逐步深入Java Web开发的各个方面,帮助您构建自己的Web应用。项目实战:构建一个简单的Web应用结语一、Java Web开发基础1. Java Servlet与JSPJava Servlet是Java Web开发的核心组件之一,它允许开发者在服务器端处理请求并生成动态内容。Servlet可以接收HTTP请求,处理请求逻辑,并返回HTTP响应。JSP(JavaServer Pages)则是一种简化的Servlet,适合于创建动态网页。JSP允许嵌入Java代码到HTML中,便于快速开发。示例:简单的Servlet@WebServlet("/hello")public class HelloServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<h1>Hello, World!</h1>"); }}示例:简单的JSP<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>Hello JSP</title></head><body> <h1>Hello, World!</h1></body></html>2. MVC架构MVC(Model-View-Controller)是一种设计模式,用于分离应用的业务逻辑、用户界面和输入控制。通过MVC架构,您可以更清晰地组织代码,提高可维护性。模型(Model)用于处理数据和业务逻辑,视图(View)用于展示数据,控制器(Controller)用于接收用户输入并更新模型和视图。二、开发环境的搭建1. 安装JDK与IDE首先,您需要安装Java Development Kit(JDK),确保您使用的是最新版本。接下来,选择一个集成开发环境(IDE),如IntelliJ IDEA或Eclipse。这些工具将大大简化您的开发过程,提供代码补全、调试和项目管理等功能。JDK安装步骤:前往Oracle官网或OpenJDK下载页面。下载适合您操作系统的JDK安装包。按照安装向导进行安装,并配置环境变量。IDE安装步骤:访问IntelliJ IDEA或Eclipse的官方网站。下载相应的安装文件。按照安装向导完成安装。2. 配置Web服务器常用的Web服务器包括Apache Tomcat和Jetty。您可以下载并安装Tomcat,然后将您的Web应用部署到Tomcat中进行测试。Tomcat安装步骤:下载Tomcat的最新版本。解压缩下载的文件到指定目录。配置环境变量,设置CATALINA_HOME为Tomcat的安装目录。启动Tomcat:在命令行中进入Tomcat的bin目录,执行startup.bat(Windows)或startup.sh(Linux/Mac)。三、数据库连接与操作1. JDBC基础Java数据库连接(JDBC)是Java与数据库交互的标准API。通过JDBC,您可以执行SQL语句,处理结果集。JDBC支持多种数据库,如MySQL、Oracle和PostgreSQL等。JDBC连接示例:try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); while (rs.next()) { System.out.println("User: " + rs.getString("username")); } rs.close(); stmt.close(); conn.close();} catch (SQLException | ClassNotFoundException e) { e.printStackTrace();}2. 使用ORM框架为了提高开发效率,您可以使用ORM(对象关系映射)框架,如Hibernate或JPA。这些框架可以简化数据库操作,使您专注于业务逻辑。Hibernate示例:@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; // Getters and Setters}使用Hibernate时,您只需编写简单的HQL(Hibernate Query Language)查询,而不需要编写复杂的SQL。四、前端与后端的交互1. AJAX与JSONAJAX(Asynchronous JavaScript and XML)允许您在不重新加载页面的情况下与服务器进行交互。结合JSON格式,您可以实现高效的数据传输。AJAX可以显著提高用户体验,使应用更加流畅。AJAX示例:$.ajax({ url: "/api/users", method: "GET", dataType: "json", success: function(data) { console.log(data); // 处理返回的数据 }, error: function(xhr, status, error) { console.error("AJAX Error: " + status + error); }});2. RESTful API设计RESTful API是一种设计风格,用于创建可扩展的Web服务。通过使用HTTP动词(GET、POST、PUT、DELETE),您可以实现资源的增删改查。设计RESTful API时,确保使用清晰的URL和HTTP状态码。RESTful API示例:@Path("/users")public class UserResource { @GET @Produces(MediaType.APPLICATION_JSON) public List<User> getUsers() { return userService.findAll(); } @POST @Consumes(MediaType.APPLICATION_JSON) public Response createUser(User user) { userService.save(user); return Response.status(Response.Status.CREATED).build(); }}五、安全与性能优化1. 常见安全问题在Web开发中,安全问题不容忽视。常见的安全漏洞包括SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)。确保使用PreparedStatement来防止SQL注入,并对用户输入进行严格验证。防止SQL注入示例:PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE username = ?");pstmt.setString(1, username);ResultSet rs = pstmt.executeQuery();2. 性能优化技巧缓存:使用缓存机制(如Redis)来减少数据库访问次数,提高响应速度。异步处理:通过异步任务处理提高响应速度,避免阻塞用户请求。负载均衡:在多台服务器之间分配请求以提高可用性,确保系统的高可用性和可扩展性。六、项目实战:构建一个简单的Web应用为了巩固所学知识,您可以尝试构建一个简单的用户管理系统。该系统应具备用户注册、登录、信息查看等基本功能。通过实践,您将更深入地理解Java Web开发的各个环节。项目步骤:设计数据库:创建用户表,包含用户名、密码、邮箱等字段。实现后端逻辑:使用Servlet处理用户请求,连接数据库进行操作。前端页面:使用HTML/CSS/JavaScript构建用户界面,支持AJAX请求。数据库设计示例:CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, email VARCHAR(100) NOT NULL);后端逻辑示例:@WebServlet("/register")public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); String email = request.getParameter("email"); // 进行用户注册逻辑 userService.register(new User(username, password, email)); response.sendRedirect("success.jsp"); }}前端页面示例:<form id="registerForm"> <input type="text" name="username" placeholder="Username" required> <input type="password" name="password" placeholder="Password" required> <input type="email" name="email" placeholder="Email" required> <button type="submit">Register</button></form><script> document.getElementById("registerForm").onsubmit = function(event) { event.preventDefault(); var formData = new FormData(this); fetch('/register', { method: 'POST', body: formData }).then(response => { if (response.ok) { window.location.href = 'success.jsp'; } }); };</script>七、结语Java Web开发是一个充满挑战和机遇的领域。通过本文的学习,您应已掌握Java Web开发的基础知识和实践技巧。不断实践、深入学习,将使您在这个领域越走越远。希望您能在未来的开发旅程中创造出更多精彩的Web应用!如有任何问题或想法,欢迎在评论区留言讨论!期待您的参与与分享!———————————————— 原文链接:https://blog.csdn.net/m0_70474954/article/details/143245316
-
在 Java 中,方法参数的传递方式通常被讨论为 值传递(Pass by Value) 和 引用传递(Pass by Reference)。但需要明确的是,Java 只有值传递,并不存在真正的引用传递。1. 什么是值传递(Pass by Value)? 值传递是指方法调用时,实际参数的值会被复制一份传递给方法的形参,方法内部对形参的修改不会影响原来的变量。示例:值传递(基本数据类型)public class Test { public static void main(String[] args) { int num = 10; modify(num); System.out.println("方法调用后 num = " + num); // 10 } public static void modify(int x) { x = 20; // 修改的是 x,而不是 num }}输出:方法调用后 num = 10解释: num 的值被复制到 x,方法内部修改 x 不会影响外部变量 num。2. 什么是引用传递(Pass by Reference)?引用传递是指方法调用时,传递的是变量的引用(即地址),方法内部修改该对象的内容会影响原始对象。Java 中的对象变量并不存储对象本身,而是存储对象的引用(内存地址)。因此,当我们将对象作为参数传递给方法时,传递的是对象的引用的拷贝(仍然是值传递),但可以通过这个引用来修改对象的内容。示例:引用传递?(对象)class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; modify(person); System.out.println("方法调用后 name = " + person.name); // Bob } public static void modify(Person p) { p.name = "Bob"; // 修改的是 p 指向的对象,而不是 p 本身 }}输出:方法调用后 name = Bob 解释: p 是 person 的引用拷贝,它们指向同一个对象,因此 modify() 方法内修改 p.name,也影响了原来的 person.name。3. 为什么说 Java 只有值传递?尽管 Java 方法参数在传递对象时能够修改对象的内容,但它仍然是值传递,因为:对于基本数据类型,传递的是值的副本,方法内部修改不会影响原变量。对于对象,传递的是引用的副本(内存地址的值),方法内部修改对象的内容可以影响原对象,但如果修改引用本身,不会影响外部变量。示例:尝试修改对象的引用class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; modifyReference(person); System.out.println("方法调用后 name = " + person.name); // Alice } public static void modifyReference(Person p) { p = new Person(); // 这里 p 重新指向一个新对象 p.name = "Bob"; }}输出:方法调用后 name = Alice 解释: p 重新指向了一个新对象 new Person(),但这个新对象只在 modifyReference() 方法内生效,原来的 person 并未受到影响。4. 结论Java 只有值传递,没有引用传递。基本数据类型的参数传递是值的复制,方法内部修改不会影响原变量。对象类型的参数传递是对象引用的复制,可以修改对象的内容,但不能改变原引用的指向。如果想要方法修改对象的引用本身(类似引用传递的效果),需要使用返回值进行赋值。示例:让方法真正修改对象引用class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; person = modifyAndReturnNew(person); System.out.println("方法调用后 name = " + person.name); // Bob } public static Person modifyAndReturnNew(Person p) { p = new Person(); // 创建新对象 p.name = "Bob"; return p; // 返回新对象 }}输出:方法调用后 name = Bob解释: 由于 modifyAndReturnNew() 方法返回了新的 Person 对象,我们用 person 变量接收了这个返回值,从而真正改变了 person 的指向。5. 面试回答建议在面试中,若被问到 Java 是值传递还是引用传递,可以回答:Java 只有值传递。基本数据类型传递的是值的副本,方法内部修改不会影响原值。对象类型传递的是对象引用的副本,可以修改对象内容,但不能修改原引用的指向。如果面试官进一步追问如何在方法内真正修改对象引用,则可以提及:使用返回值赋值(如 person = modifyAndReturnNew(person))。使用数组或封装对象存储引用(因为数组和对象的内容可修改)。———————————————— 原文链接:https://blog.csdn.net/weixin_45277068/article/details/146419400
-
一.声明Java中没有引用传递二.值传递和引用传递值传递:就是在方法调用的时候,实参是将自己的一份拷贝赋给形参,在方法内,对该参数值的修改不影响原来的实参。引用传递:是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。三.举证3.1 做运算private static int baseValue= 30; public static void updateBaseValue(int value){ value = 2 * value; } public static void main(String[] args) { System.out.println("调用前baseValue的值:"+baseValue); updateBaseValue(baseValue); System.out.println("调用后baseValue的值:"+baseValue); }结果:调用前baseValue的值:30调用后baseValue的值:30可以看到,baseValue的值并没有发生变化。结果分析:1)value被初始化为baseValue值的一个拷贝(30)2)value被乘以2后等于60,但baseValue的值仍为303)这个方法结束后,参数变量value不再使用,被回收。3.2 基本数据类型的交换public static void main(String[] args) { int A = 2; int B = 3; swap(A, B); System.out.println("swap后A的结果为:"+A); System.out.println("swap后B的结果为:"+B); } public static void swap(int a, int b){ int tmp = a; a = b; b = tmp; }结果:swap后A的结果为:2swap后B的结果为:3结果分析:1)ab被初始化为AB值的一个拷贝(a=2;b=3)2)ab的值被交换后,但AB的值没有变化3)这个方法结束后,参数变量ab不再使用,被回收。3.3 引用数据类型的交换public class User { private String name; private int age; public User(String name, int age) { this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return "name:"+name+ " age:"+age; }}public static void main(String[] args) { User user1 = new User("zhangsan",20); User user2 = new User("lisi",22); System.out.println("交换前user1:" + user1 + "-》 user2:" + user2); swap(user1,user2); System.out.println("交换后user1:" + user1 + "-》 user2:" + user2); } private static void swap(User user1, User user2) { User tmp = v; user1 = user2; user2 = tmp; }结果:交换前user1:name:zhangsan age:20-》 user2:name:lisi age:22交换后user1:name:zhangsan age:20-》 user2:name:lisi age:22结果分析:执行swap方法:swap方法结束后,临时副本user1和user2被回收原user1和user2变量仍然指向之前的对象,没有任何改变3.4 你以为是引用传递,实际并不是public static void updateUserInfo(User student){ student.setName("erniu"); student.setAge(20); } public static void main(String[] args) { user = new User("cuihua",22); System.out.println("调用前user的值:"+user.toString()); updateUserInfo(user); System.out.println("调用后user的值:"+user.toString()); }结果:调用前user的值:name:cuihua age:22调用后user的值:name:erniu age:20结果分析:当updateUserInfo方法执行完后,参数变量student不再使用,被回收。———————————————— 原文链接:https://blog.csdn.net/JavaMonsterr/article/details/125595838
-
在 Java 中,垃圾回收(GC)的频率和触发条件取决于 GC算法、堆内存分配、对象生命周期 以及 JVM参数 的配置。GC 触发条件年轻代 GC(Minor GC / Young GC)Eden 区满了:当新对象分配到 Eden 区,如果 Eden 区没有足够的空间分配新对象,就会触发 Minor GC。Survivor 空间不足:当存活对象从 Eden 复制到 Survivor,但 Survivor 空间不够时,也可能导致 Minor GC。仅回收 年轻代(Young Generation),不会影响老年代(Old Generation)。采用复制算法(如 Serial、Parallel、G1 的 YGC)。停顿时间短,但回收频率较高。Minor GC 之后,存活对象可能晋升到老年代。老年代 GC(Major GC / Old GC)老年代空间不足:当对象从 Survivor 晋升到老年代,或者大对象直接进入老年代,导致老年代空间不够时,会触发 Major GC。CMS GC 的 concurrent mode failure:CMS GC 在并发回收过程中如果老年代空间不足,会触发 STW 的 Full GC。G1 GC 触发 Mixed GC:G1 在一定条件下会触发回收老年代的 Mixed GC。主要清理 老年代(Old Generation),回收存活时间较长的对象。相比 Minor GC,Major GC 的停顿时间更长,但一般回收频率较低。某些 GC(如 CMS)不会 STW,而是并发执行(Concurrent Mark-Sweep)。Full GC显式调用 System.gc()(不推荐,因为 JVM 可能会忽略)。老年代空间不足:当老年代没有足够空间存放新对象时,Major GC 可能变成 Full GC。Metaspace/元空间溢出(如类加载过多,导致 java.lang.OutOfMemoryError: Metaspace)。CMS GC 失败:如果 CMS GC 过程中发生 concurrent mode failure,会触发 Full GC。G1 GC 触发 Full GC:当 G1 发现回收无法跟上对象分配速度时,会进行 STW 的 Full GC。回收整个堆(包括年轻代 + 老年代 + 元空间)。停顿时间长,影响系统吞吐量和响应时间。一般不希望频繁发生 Full GC,需要调优。GC 频率的影响因素对象分配速率短生命周期对象多(临时变量、业务请求数据) → Minor GC 频繁。大量大对象(如 byte[]) → 可能直接进入老年代,加速 Major/Full GC。GC 算法不同 GC 算法对 GC 频率的影响不同:Serial GC(单线程、适用于小内存) → GC 频率高,暂停时间长。Parallel GC(多线程 GC,吞吐量优先) → GC 频率较低,适用于高吞吐场景。G1 GC(区域化分代、回收预测) → 控制 GC 停顿时间,适用于大内存。ZGC、Shenandoah GC(低延迟 GC) → 减少 GC 影响,适用于大内存应用。JVM 参数JVM 相关参数直接影响 GC 频率:-Xms / -Xmx(堆内存大小):较小的堆内存 → GC 触发更频繁。较大的堆内存 → GC 触发较少,但可能增加 Full GC 停顿时间。-XX:NewRatio(年轻代与老年代的比例):较大年轻代 → Minor GC 频率降低,但可能加速老年代填满导致 Major GC 。较小年轻代 → Minor GC 频率上升,但老年代增长较慢。-XX:SurvivorRatio(Eden 和 Survivor 的比例):Survivor 较小 → 对象更容易晋升老年代,加快 Major GC 触发。Survivor 较大 → Minor GC 次数可能减少,但 Survivor 可能浪费空间。-XX:MaxTenuringThreshold(晋升老年代的阈值):较低阈值 → 对象更快晋升老年代,可能增加 Major GC 频率。较高阈值 → 对象更长时间停留在 Survivor,可能增加 Minor GC 频率。GC 负担对象回收速率低 → GC 触发频率更高。对象生命周期较长(长生命周期的缓存对象等) → 老年代更容易被填满,增加 Major/Full GC 频率。如何优化 GC 频率调整堆内存大小增大 -Xmx(最大堆内存),减少 GC 触发频率。增大 -Xms(初始堆内存),减少动态扩展导致的 Full GC。调整 GC 参数增加年轻代大小(-XX:NewRatio=1):减少 Minor GC 触发频率,但可能影响老年代回收。调整 Survivor 空间(-XX:SurvivorRatio=6):减少对象晋升到老年代,降低 Major GC 频率。调高 -XX:MaxTenuringThreshold(如 10),避免短生命周期对象过早进入老年代。选择合适的 GC 算法吞吐量优先(如并发任务多、批量计算) → Parallel GC(-XX:+UseParallelGC)。低延迟场景(如微服务、高并发请求) → G1 GC(-XX:+UseG1GC)。极低延迟需求(如金融系统) → ZGC/Shenandoah GC(-XX:+UseZGC 或 -XX:+UseShenandoahGC)。监控 GC开启 GC 日志(-Xlog:gc* 或 -XX:+PrintGCDetails)观察 GC 频率。使用 jstat 分析 GC:jstat -gcutil <pid> 10001使用 VisualVM、Arthas 监控 GC 状态。总结GC 类型 触发条件 影响Minor GC (Young GC) Eden 区满,Survivor 区空间不足 频率高,暂停时间短,对业务影响小Major GC (Old GC) 老年代空间不足 频率较低,暂停时间长,对吞吐量影响较大Full GC 老年代不足、Metaspace 溢出、CMS 失败等 影响最大,应尽量避免优化 GC 频率的核心 是合理分配堆内存、调整 GC 策略,并监控 GC 运行情况。———————————————— 原文链接:https://blog.csdn.net/qq_41893505/article/details/146226878
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签