-
文章目录#面向对象的介绍:一、设计对象并使用1.类和对象2.类的几个补充注意事项3.开发中类的设计二、封装1.封装的介绍2.封装的好处3.private关键字三、this关键字1.成员变量和局部变量2.举例代码详细解释:1. 类的成员变量定义2. 构造方法中的 `this` 关键字使用3. `set` 方法中的 `this` 关键字使用4. `get` 方法中的 `this` 关键字使用5. `displayInfo` 方法中的 `this` 关键字使用四、构造方法1.构造方法的概述2.构造方法的格式3.构造方法的作用4.构造方法的分类5.构造方法的注意事项五、标准JavaBean1.标准的JavaBean类六、对象内存图1.一个对象的内存图内存执行顺序解析(基于Java内存模型)**1. 类加载阶段(方法区)****2. 栈内存操作(main方法启动)****3. 堆内存分配(对象实例化)****4. 对象初始化流程****5. 变量关联与操作****6. 方法调用(方法区与栈协作)****内存操作完整流程总结****关键现象解释**2.多个对象的内存图**2.1、执行顺序与内存操作分步解析****1. 加载class文件(方法区)****2. 声明局部变量(栈内存)****3. 堆内存分配(对象空间开辟)****4. 默认初始化(堆内存)****5. 显示初始化(堆内存)****6. 构造方法初始化(堆内存)****7. 地址赋值(栈内存)****2.2、内存模型与对象独立性的关键验证****1. 对象独立性的体现****2. 内存操作流程图解****2.3、执行流程总结(分阶段)****2.4、常见问题解答****1. 为什么`System.out.println(s)` 输出地址?****2. 显示初始化和构造方法初始化有何区别?****3. 如何优化内存使用?**3.两个变量指向同一个对象内存图3.1、类加载阶段(方法区)3.2、栈内存操作(main方法栈帧)3.3、堆内存操作(对象关联)3.4、最终内存结构3.5、输出结果分析4.this的内存原理4.1、类加载阶段(方法区核心操作)4.2、对象实例化流程(堆栈协同)4.3、方法调用时的内存隔离(栈帧作用域)4.4、关键差异对比表4.5、技术扩展:`this`的底层实现5.基本数据类型和引用数据类型的区别5.1基本数据类型5.2引用数据类型七、补充知识:成员变量、局部变量区别#面向对象的介绍:面向:拿、找对象:能干活的东西面向对象编程:拿东西过来做对应的事情面向对象编程的例子:import java.util.Random;import java.util.Scanner;public class mian { public static void main(String[] args) { //面向对象,导入一个随机数 Random r = new Random(); int data = r.nextInt(10)+1; //面向对象,输入一个随机数 System.out.println(data); Scanner sc = new Scanner(System.in); // 面向对象,输出一个数 System.out.println("请输入一个数:"); int a = sc.nextInt(); System.out.println(a); }}为什么java要采取这种方法来编程呢?我们在程序之中要干某种事,需要某种工具来完成,这样更符合人类的思维习惯,编程更简单,更好理解。面向对象的重点学习对象是什么?学习获取已有对象并使用,学习如何自己设计对象并使用。——面向对象的语法一、设计对象并使用1.类和对象类(设计图):是对象共同特征的描述如何定义类:public class 类名{ 1.成员变量(代表属性,一般是名词) 2.成员方法(代表行为,一般是动词) 3.构造器(后面学习) 4.代码块(后面学习) 5.内部类(后面学习)}public class Phone{ //属性(成员变量) String brand; double price; public void call(){ } public void playGame(){ }}如何得到对象?如何得到类的对象:类名 对象名= new 类名();Phone p = new Phone();对象:是真实存在的具体东西拿到对象后能做什么?对象.成员变量;对象.成员方法(...)在JAVA中,必须先设计类,才获得对象public class phone { //属性 String name; double price; public void call(){ System.out.println("打电话"); } public void send(){ System.out.println("发短信"); }}//测试public class phoneTest { public static void main(String[] args) { //创建手机对象 phone p = new phone(); //给手机对象赋值 p.name = "小米"; p.price = 1999; //获取手机对象的属性值 System.out.println(p.name); System.out.println(p.price); //调用手机对象的方法 p.call(); p.send(); }}2.类的几个补充注意事项用来描述一类事物的类,专业叫做:Javabean类。在javabean类中,是不写main方法的。在以前,编写main方法的类,叫做测试类。我们可以在测试中创建javabean类的对象并进行赋值调用。public class 类名 { 1.成员变量(代表属性) 2.成员方法(代表行为)}public class Student { //属性(成员变量) String name; int age; //行为方法 public void study(){ System.out.println("好好学习,天天向上"); } public void doHomework(){ System.out.println("键盘敲烂,月薪过万"); }}类名首字母建议大写,需要见名知意,驼峰模式。一个java文件中可以定义多个class类,且只能一个类是public修饰的类名必须成为代码文件名。实际开发中建议还是一个文件定义一个class类。成员变量的完整定义格式是:修饰符 数据类型 变量名称=初始化值;一般无需指定初始化值,存在默认值。int age;//这里不写初始化值是因为,这里学生的年龄是一个群体的值,没有一个固定的初始化值。//如果给age赋值,比如是18岁,那就代表者所有的学生年龄都是18岁。//类的赋值不是在类里面赋值,而是在创建了对象之后再赋值,这时赋值的时这个特定的对象。Student stu = new Student();Stu.name="张三";Stu.height=187;对象的成员变量的默认值规则数据类型 明细 默认值基本类型 byte,short,int,long 0基本类型 float,double 0.0基本类型 boolean false引用类型 类、接口、数组、String null//编写女朋友类,创建女朋友类的对象,给女朋友的属性赋值并调用女朋友类中的方法。自己思考女朋友有哪些属性,有哪些行为?public class girlFriend { public static void main(String[] args) { //创建女朋友对象 girl g = new girl(); //给女朋友对象赋值 g.name = "小红"; g.age = 20; g.hobby = "唱歌"; //获取女朋友对象的属性值 System.out.println(g.name); System.out.println(g.age); System.out.println(g.hobby); //调用女朋友对象的方法 g.eat(); g.sleep(); }}//这是一个类public class girl { //成员变量(代表属性) String name; int age; String hobby; //成员方法(代表行为) public void eat(){ System.out.println("吃饭"); } public void sleep(){ System.out.println("睡觉"); }}3.开发中类的设计先把需求拿过来,先要看这个需求当中有几类事物。每个事物,每类事务都要定义为单独的类,这类事物的名词都可以定义为属性,这类事物的功能,一般是动词,可以定义为行为。二、封装1.封装的介绍封装是面向对象的三大特征:封装、继承、多态封装的作用:告诉我们,如何正确设计对象的属性和方法。/**需求:定义一个类描述人属性:姓名、年龄行为:吃饭、睡觉*/public class Person{ String name; int age; public void eat(){ System.out.println("吃饭"); } public void sleep(){ System.out.println("睡觉"); }}原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为。public class Circle { double radius; public void draw(){ System.out.println("根据半径"+radius+"画圆"); }}//人画圆,我们通常人为行为主体是人,其实是圆//例如:人关门,这个门一定是门自己关的,人只是给了作用力,是门自己关上的。2.封装的好处对象代表什么,就得封装对应的数据,并提供数据对应的行为降低我们的学习成本,可以少学,少记,或者说压根不用学,不用记对象有哪些方法,有需要时去找就行3.private关键字是一个权限修饰符可以修饰成员(成员变量和成员方法)被private修饰的成员只能在本类中才能访问public class GirlFriend{ private String name; private int age; private String gender;}public class leiMing { private int age; //set(赋值) public void setAge(int a){ if(a<0||a>120){ System.out.println("你给的年龄有误"); return; } age = a; } //get(取值) public int getAge(){ return age; }}针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作提供“setXxx(参数)”方法,用于给成员变量复制,方法用public修饰提供“getXxx()”方法,用于获取成员变量的值,方法用public修饰为什么要调用set和get呢?封装是面向对象编程的四大特性之一,它将数据(成员变量)和操作数据的方法绑定在一起,并隐藏对象的内部实现细节。通过将成员变量声明为 private,外部类无法直接访问和修改这些变量,只能通过类提供的 set 和 get 方法来间接操作。这样可以防止外部代码对数据进行非法或不恰当的修改,保证数据的安全性和完整性。三、this关键字1.成员变量和局部变量public class GirlFriend{ private int age;//成员变量:方法的外面,类的里面 public void method(){ int age = 10;//局部变量:方法的里面 System.out.println(age); }}成员变量和局部变量一致时,采用就近原则谁离我近,我就用谁public class GirlFriend{ private int age;//成员变量:方法的外面,类的里面 public void method(){ int age = 10;//局部变量:方法的里面 System.out.println(age); }}//在这里中,最后1个age距离 age=10最近,所以最后一个age用的是10的值//假如我想用第一个int ,我们可以在System.out.println(this.age)age前加入:this. 这里就可以打破就近原则,选择另一个变量在 Java 中,当局部变量(比如方法的参数)和类的成员变量重名时,就会产生命名冲突。在这种情况下,如果直接使用变量名,Java 默认会使用局部变量。而 this 关键字的一个重要作用就是用来引用当前对象的成员变量,从而区分局部变量和成员变量。2.举例下面通过一个简单的示例来详细讲解从引用成员变量方向 this 关键字的用法:class Employee { // 定义成员变量 private String name; private int age; // 构造方法,用于初始化员工信息 public Employee(String name, int age) { // 这里参数名和成员变量名相同,使用 this 引用成员变量 this.name = name; this.age = age; } // 设置员工姓名的方法 public void setName(String name) { // 使用 this 引用成员变量 this.name = name; } // 获取员工姓名的方法 public String getName() { return this.name; } // 设置员工年龄的方法 public void setAge(int age) { // 使用 this 引用成员变量 this.age = age; } // 获取员工年龄的方法 public int getAge() { return this.age; } // 显示员工信息的方法 public void displayInfo() { System.out.println("姓名: " + this.name + ", 年龄: " + this.age); }}public class ThisKeywordVariableExample { public static void main(String[] args) { // 创建一个 Employee 对象 Employee employee = new Employee("李四", 25); // 调用 displayInfo 方法显示员工信息 employee.displayInfo(); // 调用 setName 和 setAge 方法修改员工信息 employee.setName("王五"); employee.setAge(30); // 再次调用 displayInfo 方法显示修改后的员工信息 employee.displayInfo(); }}代码详细解释:1. 类的成员变量定义private String name;private int age;12这里定义了两个私有成员变量 name 和 age,用于存储员工的姓名和年龄。2. 构造方法中的 this 关键字使用public Employee(String name, int age) { this.name = name; this.age = age;}在构造方法中,参数名 name 和 age 与类的成员变量名相同。此时,this.name 表示当前对象的成员变量 name,而直接使用的 name 则是构造方法的参数(局部变量)。通过 this.name = name; 语句,将局部变量 name 的值赋给了当前对象的成员变量 name。同理,this.age = age; 也是将局部变量 age 的值赋给了成员变量 age。3. set 方法中的 this 关键字使用public void setName(String name) { this.name = name;}public void setAge(int age) { this.age = age;}在 setName 和 setAge 方法中,同样存在参数名和成员变量名相同的情况。使用 this 关键字来明确指定要操作的是当前对象的成员变量,避免了与局部变量的混淆。4. get 方法中的 this 关键字使用public String getName() { return this.name;}public int getAge() { return this.age;}在 get 方法中,使用 this.name 和 this.age 来返回当前对象的成员变量的值。虽然在这种情况下,不使用 this 关键字也可以正常返回成员变量的值,因为这里没有局部变量与成员变量重名的问题,但使用 this 可以使代码的意图更加清晰,表明是在访问当前对象的成员变量。5. displayInfo 方法中的 this 关键字使用public void displayInfo() { System.out.println("姓名: " + this.name + ", 年龄: " + this.age);}在 displayInfo 方法中,使用 this.name 和 this.age 来获取当前对象的成员变量的值,并将其输出。四、构造方法1.构造方法的概述构造方法也叫做构造器、构造函数2.构造方法的格式public class Student{ 修饰符 类名(参数){ 方法体; }}public class Student { private String name; private int age; //如果我们自己没有写构造方法 // 那么编译器会自动生成一个无参构造方法 public Student() { System.out.println("无参构造方法"); } public Student(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 class StudentTest { public static void main(String[] args) { //创建类的对象 //调用的空参构造 //Student s1 = new Student(); Student s = new Student(name:"张三", age:20); System.out.println(s.getName()); System.out.println(s.getAge()); }}特点:方法名与类名相同,大小写也要一致没有返回值类型,连void都没有没有具体的返回值(不能由return带回结果数据)执行时机:创建对象的时候由虚拟机调用,不能手动调用构造方法每创建一次对象,就会调用过一次构造方法3.构造方法的作用在创建对象的时候,由虚拟机自动调用构造方法,作用是给成员变量进行初始化的4.构造方法的分类public class Student{ private String name; private int age; public Student(){ ...//空参构造方法 } public Student (String name, int age){ ....//带全部参数构造方法 }}无参构造方法:初始化的对象时,成员变量的数据均采用默认值有参构造方法:在初始化对象的时候,同时可以为对象进行5.构造方法的注意事项构造方法的定义如果没有定义构造方法,系统将给出一个默认的无参数构造方法如果定义了构造方法,系统将不再提供默认的构造方法构造方法的重载带参构造方法,和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载推荐的使用方式无论是否使用,都动手书写无参数构造方法,和带全部参数的构造方法五、标准JavaBean1.标准的JavaBean类类名需要见名知意成员变量使用private修饰提供至少两个构造方法无参构造方法带全部参数的构造方法成员方法提供每一个成员变量对应的setXxx()/getXxx()如果还有其他行为,也需要写上举例子:根据一个登录界面写一个JavaBean类public class User { //属性 private String username; private String password; private String email; private String gender; private int age; //构造方法 //无参构造 public User() { } //有参构造 public User(String username, String password, String email, String gender, int age) { this.username = username; this.password = password; this.email = email; this.gender = gender; this.age = age; } //方法 //set和get方法 public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }我们再写一个javabean中会遇到一个问题:这样写纯体力活啊!没事的没事的!我们有快捷键: 方法一: alt+insert 或 alt+insert+Fnalt+insert 第一个是构造函数,点击无选择生成的是空参 ,全选ok生成的是有参数的构造函数alt+insert 点击setter和geteer,全选生成的是set和get 方法二: 下载插件pdg,下载完成后点击空白处就会出现。然后点击Ptg To JavaBean六、对象内存图1.一个对象的内存图Student s = new Student();1加载class文件申明局部变量在堆中开辟一个空间默认初始化显示初始化构造方法初始化将堆中空间的地址值赋值给左边的局部变量举例:public class Student{ String name; int age; public void study(){ System.out.println("好好学习") }}public class TestStudent{ public static void main(String [] args){ Student s= new Student(); System.out.println(s); System.out.println(s.name+"...."+s.age); s.name = "阿强"; s.age = 23; System.out.println(s.name+"..."+s.age); s.study(); }}解析:内存执行顺序解析(基于Java内存模型)1. 类加载阶段(方法区)加载class文件JVM将Student.class和TestStudent.class加载到方法区,存储类结构信息(字段、方法签名、常量池等)。Student类包含字段name(String)、age(int)和方法study()。TestStudent类包含main()方法入口。2. 栈内存操作(main方法启动)声明局部变量执行main()时,在栈内存中创建main方法的栈帧,声明局部变量s(此时s未指向任何对象,值为null)。3. 堆内存分配(对象实例化)在堆中开辟空间执行new Student()时,在堆内存中为Student对象分配空间,内存大小由字段类型决定(String引用 + int值)。4. 对象初始化流程默认初始化对象字段赋默认值:name → null(引用类型默认值)age → 0(基本类型默认值)。显示初始化(本例中无)如果类中字段有显式赋值(如String name = "默认";),此时会执行。但示例代码未定义,此步骤跳过。构造方法初始化(本例中无)如果存在构造方法(如public Student() { age = 18; }),会通过构造器赋值。示例代码未定义构造方法,此步骤跳过。5. 变量关联与操作地址赋值给局部变量堆中对象地址赋值给栈帧中的s1变量,完成引用关联。执行Student s = new Student();后,s指向堆内存中的对象。对象字段修改后续代码通过s.name = "阿强";和s.age = 23;直接修改堆中对象的字段值,无需重新初始化。6. 方法调用(方法区与栈协作)执行s.study()从方法区加载study()的字节码指令。在栈中创建study()方法的栈帧,执行System.out.println(" 好好学习")(注:用户代码此处缺少分号,实际会编译报错)。内存操作完整流程总结步骤 操作内容 内存区域 示例代码体现1 加载类信息 方法区 Student和TestStudent类加载2 声明局部变量s 栈内存 Student s;3 堆中分配对象空间 堆内存 new Student()4 字段默认初始化(null/0) 堆内存 s.name 和s.age 初始值5 显式/构造初始化(无) - 代码未定义相关逻辑6 对象地址赋值给s 栈内存 s = new Student();7 修改字段值 堆内存 s.name = "阿强";等操作关键现象解释System.out.println(s) 输出哈希值因打印对象时默认调用toString(),而Student未重写该方法,输出格式为类名@哈希值。字段值修改的可见性直接通过引用s修改堆中对象字段,所有指向该对象的引用均会看到更新后的值。编译隐患study()方法中System.out.println(" 好好学习")缺少分号,实际运行前会因语法错误中断。2.多个对象的内存图举例:public class Student{ String name; int age; public void study(){ System.out.println("好好学习"); }}public class TestStudent{ public static void main(String [] args){ Student s= new Student(); System.out.println(s); s.name = "阿强"; s.age = 23; System.out.println(s.name+"..."+s.age); s.study(); Student s2= new Student(); System.out.println(s2); s2.name = "阿珍"; s2.age = 24; System.out.println(s2.name+"..."+s2.age); s2.study(); }}第二次在创建对象时。class文件不需要再加载一次解析:2.1、执行顺序与内存操作分步解析1. 加载class文件(方法区)触发条件:首次使用Student类时。操作内容:将Student.class 加载到方法区,存储类结构(字段name、age和方法study()的定义)。将TestStudent.class 加载到方法区,存储main()方法入口。2. 声明局部变量(栈内存)操作内容:执行main()方法时,在栈内存创建main方法的栈帧。声明局部变量s和s2(初始值均为null)。3. 堆内存分配(对象空间开辟)操作内容:new Student()触发堆内存分配,根据类结构计算对象大小(String引用 + int值)。示例:s = new Student() → 堆地址0x001。s2 = new Student() → 堆地址0x002(独立空间)。4. 默认初始化(堆内存)操作内容:对象字段赋默认值:name → null(引用类型默认值)。age → 0(基本类型默认值)。示例:s的初始状态:name=null, age=0。s2的初始状态:name=null, age=0。5. 显示初始化(堆内存)触发条件:类中显式赋值的字段(如String name = "默认")。当前代码:Student类未定义显式赋值字段,跳过此步骤。6. 构造方法初始化(堆内存)触发条件:存在自定义构造方法(如public Student() { ... })。当前代码:Student类未定义构造方法,使用默认无参构造器,跳过此步骤。7. 地址赋值(栈内存)操作内容:将堆内存地址赋值给栈中的局部变量。示例:s = 0x001(指向第一个对象)。s2 = 0x002(指向第二个对象)。2.2、内存模型与对象独立性的关键验证1. 对象独立性的体现对象 堆地址 字段修改后的值s 0x001 name="阿强", age=23s2 0x002 name="阿珍", age=24验证逻辑:s和s2指向不同堆地址,修改其中一个对象的字段不会影响另一个对象。System.out.println(s == s2) → 输出false。2. 内存操作流程图解2.3、执行流程总结(分阶段)阶段 操作内容 内存区域类加载 加载Student和TestStudent类信息 方法区栈帧创建 声明s和s2(初始null) 栈内存堆内存分配 为s和s2分配独立空间 堆内存对象初始化 默认初始化 → 显式赋值(用户代码修改) 堆内存方法调用 study()从方法区加载逻辑到栈执行 栈内存2.4、常见问题解答1. 为什么System.out.println(s) 输出地址?原因:未重写toString()方法,默认调用Object.toString() ,格式为类名@哈希值。2. 显示初始化和构造方法初始化有何区别?显示初始化:直接在类中赋值字段(如String name = "张三"),编译时自动插入到构造方法中。构造方法初始化:通过自定义构造器赋值(优先级高于显示初始化)。3. 如何优化内存使用?复用对象:避免频繁new对象(尤其循环中)。垃圾回收:main()结束后,s和s2成为垃圾对象,由GC自动回收。附:修正后的代码输出示例Student@1b6d3586 阿强...23 好好学习 Student@4554617c 阿珍...24 好好学习 3.两个变量指向同一个对象内存图举例:public class Student{ String name; int age; public void study(){ System.out.println("好好学习"); }}public class TestStudent{ public static void main(String [] args){ Student s= new Student(); s.name = "阿强"; Student s2= s; s2.name = "阿珍"; System.out.println(s.name+"..."+s2.name); }}3.1、类加载阶段(方法区)加载TestStudent.class当JVM启动时,首先将TestStudent.class 加载到方法区,存储类结构信息(成员方法、字段描述等)加载Student.class执行new Student()时触发类加载机制,将Student.class 加载到方法区,包含name、age字段和study()方法元数据3.2、栈内存操作(main方法栈帧)声明局部变量在main方法栈帧中创建引用变量s(地址未初始化)和s2(此时两者均为null)对象创建指令new Student()操作码触发堆内存分配,此时:在堆中生成对象内存空间(包含对象头 + String name + int age)默认初始化:name=null,age=0(基本类型和引用类型的零值初始化)显式初始化:由于Student类没有直接赋值的字段(如String name = "默认名"),此阶段跳过构造方法执行若存在构造方法(本案例未定义),会通过invokespecial指令调用<init>方法完成初始化3.3、堆内存操作(对象关联)地址赋值将堆中Student对象地址赋值给栈帧中的s变量(完成s = new Student())引用传递s2 = s操作使s2指向堆中同一个对象(此时两个引用共享对象数据)字段修改通过s2.name = "阿珍"修改堆内存对象数据,此时s.name 同步变化(引用指向同一实体)3.4、最终内存结构内存区域 存储内容方法区 TestStudent类字节码、Student类元数据(包含study()方法代码)堆内存 Student对象实例(name=“阿珍”, age=0)栈内存 main方法栈帧:s=0x100(指向堆对象), s2=0x100(与s同地址)3.5、输出结果分析System.out.println(s.name+"..."+s2.name)→ 输出阿珍...阿珍(s与s2引用同一对象,堆内数据修改对所有引用可见)关键理解点:引用类型变量的赋值操作传递的是对象地址值,而非创建新对象。这种特性是Java对象共享机制的核心体现。4.this的内存原理public class Student{ private int age; public void method(){ int age=10; System.out.println(age);//10 System.out.println(this.age);//成员变量的值 0 }}this的作用:区分局部变量和成员变量this的本质:所在方法调用者的地址值public class Student{ private int age; public void method(){ int age=10; System.out.println(age);//10 System.out.println(this.age);//成员变量的值 0 }}public class StudentTest{ public static void main (String[] args){ Student s = new Student(); s.method(); }}4.1、类加载阶段(方法区核心操作)加载StudentTest.classJVM启动时优先加载含main()的类到方法区存储类元数据:静态变量、方法表(含main()入口地址)触发Student.class 加载当执行new Student()时触发类加载方法区新增:字段描述表(private int age的访问权限和偏移量)method()的字节码指令集合隐式默认构造器<init>方法(因无自定义构造方法)4.2、对象实例化流程(堆栈协同)步骤 内存区域 具体行为 代码对应3 栈内存 在main方法栈帧声明局部变量s(初始值null) Student s;4 堆内存 分配对象空间:对象头(12字节)+ int age(4字节)= 16字节 new Student()5 堆内存 默认初始化:age=0(基本类型零值填充) 隐式执行6 堆内存 构造方法初始化:执行空参数的<init>方法(无实际操作) 隐式调用7 栈内存 将堆地址(如0x7a3f)赋值给s变量 s = new...4.3、方法调用时的内存隔离(栈帧作用域)执行s.method() 时发生:新建栈帧:在栈顶创建method()1的独立空间,包含:隐式参数this(指向堆地址0x7a3f)局部变量age=10(存储于栈帧变量表)变量访问规则:输出语句 内存访问路径 结果System.out.println(age) 访问栈帧局部变量表 10System.out.println(this.age) 通过this指针访问堆内存字段 04.4、关键差异对比表特征 成员变量this.age 局部变量age存储位置 堆内存对象内部 栈帧局部变量表生命周期 与对象共存亡 随方法栈帧销毁而消失初始化值 默认零值(int=0) 必须显式赋值访问方式 需通过对象引用 直接访问4.5、技术扩展:this的底层实现当调用method()时:字节码层面:java复制aload_0 // 将this引用压入操作数栈(对应堆地址0x7a3f)getfield #2 // 根据字段偏移量读取堆中age值(#2为字段符号引用)内存隔离机制:局部变量age会遮蔽同名的成员变量,必须通过this.显式穿透访问堆数据5.基本数据类型和引用数据类型的区别5.1基本数据类型public class Test{ public static void main (String [] args){ int a = 10; }}基本数据类型:在变量当中存储的是真实的数据值从内存角度:数据值是存储再自己的空间中特点:赋值给其他变量,也是赋值的真实的值。5.2引用数据类型public class TestStudent{ public static void main(String[] args){ Student s=new Student; }}引用数据类型:堆中存储的数据类型,也就是new出来的,变量中存储的是地址值。引用:就是使用其他空间中数据的意思。从内存的角度:数据值是存储在其他空间中,自己空间中存储的是地址值特点:赋值给其他变量,赋的地址值。七、补充知识:成员变量、局部变量区别成员变量:类中方法外的变量局部变量:方法中的变量 区别:区别 成员变量 局部变量类中位置不同 类中,方法外 方法内、方法申明上初始化值不同 有默认初始化值 没有,使用前需要完成赋值内存位置不同 堆内存 栈内存生命周期不同 随着对象的创建而存在,随着对象的消失而消失 随着方法的调用而存在,随着方法的运行结束而消失作用域 整个类中有效 当前方法中有效————————————————原文链接:https://blog.csdn.net/2401_87533975/article/details/145992557
-
简介:本文详细介绍了Java Development Kit (JDK) 1.8的安装过程,包括下载、安装、配置环境变量、卸载以及JDK 1.8的新特性。JDK 1.8是Oracle公司发布的重要Java开发工具包版本,适用于64位操作系统,并具备更好的资源利用率和性能。安装步骤包括接受许可协议、选择安装类型和位置、配置环境变量以及在必要时进行手动设置。文章还强调了正确安装和配置JDK对于Java编程的重要性,并对JDK 1.8新增的特性进行了总结,以帮助开发者充分掌握这一版本的JDK。 1. JDK 1.8概述与重要性简介Java Development Kit 8(JDK 1.8)是Java开发者必须熟悉的重要工具集。它在2014年发布,带来了许多新的语言特性和API,同时对JVM性能也进行了优化,是目前广泛使用的稳定版本之一。重要性JDK 1.8的重要性在于其引入的新特性极大地方便了Java开发者的编程工作,比如Lambda表达式简化了代码编写,Stream API增强了集合操作,新的日期时间API提供了更强大的日期时间处理能力。同时,对于企业级应用来说,稳定的JDK版本是降低风险和维护成本的首选。使用范围由于其广泛的支持和丰富的功能集,JDK 1.8适用于各种规模的项目,从简单的单机应用到大型分布式服务。同时,JDK 1.8为Java 9及更高版本的模块化特性做了铺垫,使开发者能更容易适应未来Java平台的变化。2. 64位JDK安装程序下载与介绍随着技术的发展,64位系统已成为主流,因此安装64位的JDK是大多数开发者的选择。64位JDK的安装不仅需要下载适合您系统的安装程序,还要确保它与您的操作系统兼容,同时理解安装文件夹中的各种组件。2.1 选择合适的JDK版本在开始安装之前,选择正确的JDK版本至关重要。由于JDK版本众多,选择一个适合自己操作系统和项目需求的版本是安装的第一步。2.1.1 确定系统类型和位数确定您的操作系统类型和位数是选择JDK的第一步。JDK有32位和64位之分,而操作系统也有Windows、macOS和Linux等不同版本。请务必确保您下载的JDK版本与您的操作系统兼容。2.1.2 访问Oracle官网下载页面选择版本后,进入Oracle官网,找到JDK下载页面。页面上会列出不同版本的JDK,您可以根据自己的需求选择相应版本进行下载。通常,页面会自动推荐最适合您系统类型的版本。2.2 安装程序的结构和组件安装程序安装包中包含了许多组件,其中一些是安装过程中的必需品,而另一些则提供了额外的功能。了解这些组件可以帮助您更好地利用JDK。2.2.1 JDK安装文件夹内容概览安装JDK后,会得到一个包含多个文件和子文件夹的目录。最重要的文件夹是 bin ,其中包含了JDK的主要可执行文件,如 java 和 javac 。 lib 文件夹包含了运行JDK所需的库文件。除了这些,还有 include 文件夹,它包含了一些平台特定的头文件。2.2.2 JDK与JRE的区别及其组件在安装JDK的同时,您会发现JRE(Java Runtime Environment)也会被安装。JRE是运行Java应用程序所必需的环境,而JDK则包含了JRE以及开发Java应用所需的编译器、调试器和其他工具。JDK的安装确保了开发者既有编译代码的工具也有运行代码的环境。2.3 安装前的系统要求与检查在安装JDK之前,检查系统是否满足安装JDK的要求是必要的。这不仅包括操作系统兼容性,还包括硬件的配置要求。2.3.1 操作系统兼容性要求JDK要求的操作系统类型和版本多种多样。例如,对于Windows系统,JDK 1.8支持从Windows XP到最新版本的Windows 10;对于Linux系统,则可能需要满足特定的包依赖关系。请确保您的系统符合要求。2.3.2 硬件配置和环境需求除了操作系统的兼容性外,您的系统也需要满足一定的硬件配置。JDK的安装和运行至少需要以下硬件资源:256MB的内存,40MB的硬盘空间。不过,为了更好的性能和开发体验,推荐的内存是1GB以上。为了确保系统满足这些要求,可以使用系统信息工具来检查您的硬件配置,并在必要时进行升级。当硬件和操作系统都符合要求后,您就可以开始安装JDK了。3. 安装步骤详解3.1 安装向导的启动与选项3.1.1 接受许可协议在开始安装JDK之前,需要首先接受Oracle的最终用户许可协议(EULA)。这一步骤是必须的,因为它规定了用户可以如何使用JDK。一般而言,安装向导会首先展示协议内容,并要求用户选择是否接受。用户必须选择“Accept”才能继续进行安装过程。3.1.2 选择安装路径和组件选择安装路径对于系统的环境变量配置和后续使用至关重要。在选择安装路径时,用户需要确保有相应的权限,并且安装路径不应包含空格或特殊字符,以避免潜在的路径解析错误。此外,用户需要明确安装JDK的各个组件,例如JRE(Java Runtime Environment)、JavaDoc工具和源代码等。通常情况下,建议安装所有组件以保证JDK功能的完整性。3.2 完成安装的确认步骤3.2.1 安装完成界面的解读在安装过程完成后,安装向导会显示一个完成界面。这一步骤提供了关于安装成功的确认,并通常会给出一些关于后续步骤的提示,比如配置环境变量或打开一个命令行窗口测试安装的JDK。确认安装成功后,即可关闭安装向导。3.2.2 安装过程中可能遇到的问题及解决方案安装JDK的过程中可能会遇到各种问题,比如权限不足、安装路径不正确、系统兼容性问题等。解决这些问题通常需要一些基本的故障排除步骤,比如重新选择安装路径、以管理员权限运行安装程序、确保系统满足最低要求等。在遇到安装问题时,建议首先查阅Oracle的官方文档或社区论坛获得帮助,因为可能已经有其他用户遇到并解决了相同的问题。另外,查看安装向导提供的错误日志或系统事件日志,也可能会提供问题的线索。下面是关于安装过程中遇到权限问题的代码示例及其解释:# Windows系统下运行安装程序时遇到权限不足的错误处理# 假设在以管理员权限运行安装程序时遇到如下错误信息:# "The setup cannot continue as the user does not have sufficient privileges. Please contact your system administrator." # 1. 首先,需要右键点击安装程序,选择“Run as administrator”。# 在Windows Vista及更新版本中,可能会收到UAC(用户帐户控制)提示,此时需确认。 # 2. 如果通过右键菜单直接运行安装程序仍然出现权限不足的问题,# 可以考虑手动以管理员身份打开命令提示符或PowerShell窗口,然后运行安装程序:start /b "" java -jar jdk-8uXXX-windows-x64.exe上述代码展示的是在Windows环境下处理权限问题的方式。首先解释了如何以管理员身份运行安装程序,然后解释了如果直接运行还是出现问题,该如何操作。表格展示:| 安装步骤 | 操作描述 | 注意事项 | |-----------------|--------------------------------------|--------------------------------------------| | 启动安装向导 | 双击JDK安装程序或运行命令行安装指令。 | 确保安装程序文件是完整的,以及未被病毒或恶意软件影响。 | | 接受许可协议 | 阅读协议条款,选择接受。 | 不接受协议将无法继续安装。 | | 选择安装路径 | 选择合适的目录,避免使用系统文件夹。 | 确保有访问权限,避免使用包含空格的路径。 | | 定制安装组件 | 根据需要选择要安装的组件。 | 通常建议安装所有组件以保证功能完整性。 | | 完成安装确认 | 完成安装后,确保检查安装成功提示。 | 如果有提示错误,根据信息进行相应的故障排除。 | | 解决安装问题 | 如遇问题,查看错误日志,参考官方文档或社区。 | 确保满足系统要求,以管理员权限运行安装程序。 |在完成以上安装步骤后,JDK就安装在指定的系统中了。接下来,要确保JDK可以被系统正确识别和使用,配置环境变量是下一步的关键工作。4. 环境变量配置方法4.1 环境变量的定义和作用4.1.1 PATH变量的作用PATH环境变量是一个重要的系统变量,它定义了操作系统搜索可执行文件的目录列表。当用户在命令行输入一个命令时,系统会在PATH变量指定的目录中查找对应的可执行文件。正确设置PATH变量能够让系统识别命令行中输入的命令,如 java 或 javac ,而不需要在每次调用时指定完整的路径。在JDK安装后配置PATH变量,是为了让系统能够找到JDK的 bin 目录,其中存放了Java的运行时环境(JRE)和编译器(JAVAC)等工具。这对于开发人员来说,确保命令行工具可以被系统识别,从而有效地运行Java程序。4.1.2 JAVA_HOME变量的重要性JAVA_HOME环境变量是一个高级设置,它为JDK安装位置提供了一个引用。将JDK的安装路径设置为JAVA_HOME变量,可以提高环境配置的灵活性。当需要在多个Java版本之间切换或者更新JDK时,只需修改JAVA_HOME变量的值,而不需要逐个修改系统环境变量或项目中引用JDK路径的设置。同时,许多Java应用服务器和开发工具依赖JAVA_HOME变量来确定JDK的安装位置。如果没有正确设置JAVA_HOME变量,可能会导致这些应用服务器或工具无法启动,从而影响开发和部署工作。4.2 配置环境变量的步骤4.2.1 Windows系统下的配置方法在Windows操作系统下,配置环境变量可以分为几个步骤:右击“我的电脑”或“此电脑”,选择“属性”。在弹出的系统窗口中,点击“高级系统设置”。在系统属性对话框中,点击“环境变量”按钮。在“系统变量”区域,点击“新建”按钮来设置JAVA_HOME变量。变量名填写 JAVA_HOME ,变量值填写JDK安装的完整路径,例如 C:\Program Files\Java\jdk1.8.0_291 。在“系统变量”中找到PATH变量,选择“编辑”,然后在变量值的末尾添加 ;C:\Program Files\Java\jdk1.8.0_291\bin (注意,前面的分号 ; 是分隔符,用于分隔不同的路径,如果PATH变量原本为空则不需要分号)。点击“确定”保存设置。配置完毕后,可以在命令行中输入 java -version 来检查JDK版本信息,确认配置是否成功。4.2.2 Linux系统下的配置方法在Linux系统下,环境变量的配置通常通过修改 ~/.bashrc 或 ~/.profile 文件来完成。以下是配置JAVA_HOME和PATH的具体步骤:打开终端。使用文本编辑器打开 ~/.bashrc 文件,例如使用命令 nano ~/.bashrc 。在文件的末尾添加以下内容:export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64export PATH=$JAVA_HOME/bin:$PATH请确保将 JAVA_HOME 的值替换为实际的JDK安装路径。保存并关闭文件。使配置生效,可以使用命令 source ~/.bashrc 或者重新启动终端。为了验证配置是否成功,同样可以在终端输入 java -version ,检查Java版本信息。4.3 验证环境变量配置是否成功4.3.1 使用命令行检查JDK版本在命令行界面,输入命令 java -version ,如果配置成功,将显示JDK的版本信息。例如:java version "1.8.0_291"Java(TM) SE Runtime Environment (build 1.8.0_291-b11)Java HotSpot(TM) 64-Bit Server VM (build 25.291-b11, mixed mode)此外,使用 javac -version 检查Java编译器的版本也是个好主意,确保编译环境同样配置正确。4.3.2 检查其他开发工具的配置情况除了确认Java环境变量之外,其他与Java相关的开发工具(如Maven、Gradle等)的配置也需要检查。可以通过在命令行输入这些工具的命令,如 mvn -v 或 gradle -v 来验证。这些工具通常会读取系统环境变量,并显示其版本信息及Java版本信息。例如,使用Maven的命令可能显示如下信息:Apache Maven 3.6.3Maven home: /usr/local/apache-maven-3.6.3Java version: 1.8.0_291, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-1.8.0-openjdk-amd64/jreDefault locale: en_US, platform encoding: UTF-8OS name: "linux", version: "5.4.0-65-generic", arch: "amd64", family: "unix"以上步骤能够确保JDK环境变量在系统中配置正确,并且可以为后续的开发工作打下坚实的基础。5. JDK 1.8的卸载过程5.1 卸载前的准备工作在开始卸载JDK之前,确保已经做好了适当的准备工作,以便在卸载后能够迅速恢复系统状态,并最小化对系统稳定性和开发工作的影响。5.1.1 关闭所有Java相关进程在卸载JDK前,需要确保系统中没有运行任何与Java相关的进程。可以通过任务管理器(Windows)或使用命令行工具(如 ps 命令在Linux中)来检查并终止这些进程。通常,这些进程包括Java虚拟机(JVM)实例、IDE或任何Java应用程序。5.1.2 备份必要的文件和设置在执行卸载操作之前,推荐备份任何重要的Java配置文件,例如 server.xml 、 context.xml 等,以及任何Java项目文件。虽然这些文件在卸载后通常不会被删除,但在进行系统操作时备份总是一种良好的习惯,以防万一需要恢复数据。5.2 系统卸载程序的使用接下来,我们将介绍如何在Windows和Linux系统上使用标准卸载程序来移除JDK。5.2.1 Windows控制面板中的卸载步骤打开“控制面板”。点击“程序和功能”或“卸载程序”。在列表中找到与Java相关的条目,通常是“Java”或“Java Platform (JDK)”。点击“卸载”按钮,然后按照屏幕上的指示完成卸载过程。5.2.2 Linux中的删除JDK包和文件在Linux系统中,卸载JDK通常需要使用包管理工具。对于基于Debian的系统(如Ubuntu),可以使用以下命令:sudo apt-get remove --purge oracle-java8-installer对于基于Red Hat的系统(如CentOS),可以使用:sudo yum erase java-1.8.0-openjdk请确保替换上述命令中的包名以匹配您系统上的JDK包名。5.3 手动清理残留文件即使使用了系统卸载程序,有时候一些文件仍会遗留在系统中。因此,在卸载程序完成后,手动检查并清理剩余的Java相关文件是一个好习惯。5.3.1 检查并删除Java相关目录在系统中可能遗留下一些Java相关的目录,如 /usr/lib/jvm 或其他类似路径。可以通过文件管理器或命令行进行检查:# Linux命令行检查ls /usr/lib/jvm如果发现有JDK相关的目录,请使用 rm -rf 命令将其删除:# Linux命令行删除sudo rm -rf /usr/lib/jvm/java-1.8.0请谨慎使用上述命令,因为 rm -rf 可以删除指定目录及其所有内容。5.3.2 清除环境变量残留配置如果在环境变量中设置了与Java相关的路径,那么在卸载JDK后,需要从环境变量中清除这些设置。在Windows中,这可以在“系统属性”->“高级”->“环境变量”中修改。在Linux中,需要编辑 ~/.bashrc 或 ~/.profile 文件,移除或注释掉Java相关的行。# Linux中移除PATH变量中的Java路径export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin完成这些步骤后,重新加载环境变量(在终端中输入 source ~/.bashrc 或重新登录)以确保更改生效。通过遵循上述步骤,可以确保JDK被彻底且安全地从您的系统中卸载。6. JDK 1.8新特性介绍Java Development Kit (JDK) 1.8引入了一系列新的特性和优化,旨在提升开发效率,增强语言表达能力,以及改善JVM性能。本章节我们将深入探讨这些新特性,并展示它们在实际应用中的优势。6.1 语言和API级别的改进6.1.1 Lambda表达式的深入应用Lambda表达式为Java带来了函数式编程的特性,允许开发者以更简洁的方式编写代码。Lambda表达式本质上是函数式接口的一个实例。// 示例代码:使用Lambda表达式排序字符串列表List<String> list = Arrays.asList("peter", "john", "maria", "bob");Collections.sort(list, (a, b) -> ***pareTo(b));在上述代码中, (a, b) -> ***pareTo(b) 是一个Lambda表达式,它等效于一个匿名内部类,实现了 Comparator 接口的 compare 方法。Lambda表达式的引入显著简化了代码,尤其是当需要传递行为作为参数时。6.1.2 Stream API的新增功能Stream API提供了一种高效且易于读写的处理数据的方式,适用于集合类的数据操作。Stream API通过其丰富的中间操作和终端操作,简化了集合操作的复杂度。// 示例代码:使用Stream API筛选和排序学生对象列表List<Student> students = // ... 获取学生列表List<Student> sortedStudents = students.stream() .filter(student -> student.getGrade() > 70) .sorted(***paring(Student::getGrade).reversed()) .collect(Collectors.toList());在上述代码中,我们通过Stream API对学生成绩进行筛选和排序,操作链式进行,逻辑清晰且易于理解。6.2 JVM的性能优化6.2.1 Graal编译器的引入Graal编译器是一个高性能的JIT(Just-In-Time)编译器,它可以将Java字节码编译成机器码。自JDK 1.8以来,Graal可以作为实验性特性使用,它旨在提高JVM的性能。6.2.2 垃圾收集器的改进JDK 1.8对垃圾收集器进行了一系列的改进,例如引入了并行的Full GC,以及改善了GC的停顿时间。这使得在高性能应用中,如实时系统,垃圾收集的影响大大降低。6.3 开发工具的增强6.3.1 JShell的介绍和使用JShell是JDK 1.9引入的一个交互式命令行工具,它允许开发者快速测试和执行Java代码片段,无需编写完整的类定义。# 示例JShell命令jshell> System.out.println("Hello, JShell!");| Hello, JShell!通过JShell,开发者可以立即看到代码执行结果,极大地方便了学习和测试新API。6.3.2 Java Mission Control的更新Java Mission Control是一个集成环境,用于监控和管理Java应用程序。在JDK 1.8中,Java Mission Control进行了更新,增加了对Java Flight Recorder的集成,提供了实时监控和分析运行中的Java应用程序的能力。通过以上讨论,我们看到JDK 1.8的每个新特性都旨在解决特定的开发痛点,并极大地提升开发效率。这些特性不仅改善了开发体验,也提高了应用性能和可靠性。对于那些有意采用JDK 1.8的开发者来说,了解和掌握这些新特性是至关重要的。7. JDK 1.8在实际开发中的应用随着企业软件系统的不断扩展和应用程序的日益复杂,选择合适的开发工具和版本对于软件的稳定性和开发效率有着至关重要的影响。JDK 1.8作为Java领域内的一个重要版本,其引入的新特性对现代应用程序开发产生了深远的影响。本章节将深入探讨JDK 1.8在实际开发环境中的应用、特性使用,以及如何应对兼容性和升级问题。7.1 开发环境的搭建与配置在进行任何实际的代码编写之前,构建一个高效且稳定的开发环境是至关重要的。JDK 1.8的引入,使得开发者可以利用其改进的性能和新特性,提升开发效率和代码质量。7.1.1 集成开发环境(IDE)的设置大多数现代IDE都对JDK 1.8有着良好的支持。以IntelliJ IDEA为例,配置JDK的过程非常简单:打开IntelliJ IDEA,选择 "File" > "Project Structure"。在弹出的窗口中选择 "SDKs"。点击 "+" 选择 "JDK",然后浏览至JDK 1.8的安装目录并选择。点击 "Apply" 和 "OK",以确认并保存设置。完成这些步骤后,JDK 1.8将被设置为项目的默认JDK版本,你可以开始使用该版本的所有新特性。7.1.2 项目构建工具Maven和Gradle的集成Maven和Gradle作为两个主流的Java项目构建工具,同样能够很好地支持JDK 1.8。以Maven为例,配置如下:<properties> <***piler.source>1.8</***piler.source> <***piler.target>1.8</***piler.target></properties>通过在项目的 pom.xml 文件中添加上述配置,即可指定使用JDK 1.8作为编译器的源代码和目标代码的版本。对于Gradle用户,可以通过以下配置:sourceCompatibility = 1.8targetCompatibility = 1.8在项目的 build.gradle 文件中添加这些配置,同样可以确保Gradle使用JDK 1.8进行构建。7.2 高效使用JDK 1.8特性JDK 1.8引入的新特性极大地提升了Java开发的效率和代码的可读性。本节将介绍如何利用Lambda表达式和Stream API来简化代码编写和数据处理。7.2.1 利用Lambda简化代码编写Lambda表达式提供了函数式编程的能力,允许我们以更简洁的方式编写代码。例如,创建一个简单的比较器可以不再需要匿名内部类:Comparator<String> comparator = (s1, s2) -> ***pareTo(s2);而使用Lambda表达式后,代码变得更加简洁:Comparator<String> comparator = String::compareTo;7.2.2 使用Stream进行高效数据处理Stream API的引入使得集合类的数据操作更加优雅和高效。在处理集合数据时,我们可以通过流的方式进行链式调用,例如对列表中的元素进行过滤和排序:List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");names.stream() .filter(name -> name.length() > 4) .sorted() .forEach(System.out::println);上面的代码首先创建了一个字符串列表的流,然后通过 filter 方法过滤长度大于4的元素,接着通过 sorted 方法排序,最后通过 forEach 方法输出。7.3 解决兼容性和升级问题随着技术的演进,应用程序需要不断地升级和改进。在从旧版本JDK迁移到JDK 1.8时,可能会遇到一些兼容性问题。7.3.1 兼容性考虑及解决方案JDK 1.8在引入新特性的同时,也注重了向后兼容性。但某些新特性可能需要对旧代码进行修改。当遇到特定的兼容性问题时,可以考虑以下解决方案:使用 @FunctionalInterface 注解 :在使用Lambda表达式之前,确保接口符合函数式接口的要求。检查第三方库的兼容性 :确保所有第三方库都支持JDK 1.8的特性。7.3.2 从旧版本JDK升级到JDK 1.8的步骤和注意事项当决定升级到JDK 1.8时,需要遵循以下步骤,并注意相应的问题:评估项目依赖 :确认项目依赖的库和框架都与JDK 1.8兼容。更新环境变量 :更新 PATH 和 JAVA_HOME 环境变量以指向新的JDK安装路径。修改项目配置 :如上所述,更新IDE和构建工具的配置。运行代码兼容性检查 :运行项目并测试所有功能以确保一切正常。升级过程中,可能会遇到需要代码重构的情况,比如对方法引用和Lambda表达式的支持。使用IDE提供的重构工具能够帮助简化这一过程。总结来说,JDK 1.8为Java开发带来了诸多新特性,能够有效地提升开发效率和代码质量。然而,升级到新版本的JDK同样伴随着对现有代码和环境的调整。通过本章的介绍,开发者能够更好地理解和应用JDK 1.8,同时能够更平滑地进行版本升级。本文还有配套的精品资源,点击获取 简介:本文详细介绍了Java Development Kit (JDK) 1.8的安装过程,包括下载、安装、配置环境变量、卸载以及JDK 1.8的新特性。JDK 1.8是Oracle公司发布的重要Java开发工具包版本,适用于64位操作系统,并具备更好的资源利用率和性能。安装步骤包括接受许可协议、选择安装类型和位置、配置环境变量以及在必要时进行手动设置。文章还强调了正确安装和配置JDK对于Java编程的重要性,并对JDK 1.8新增的特性进行了总结,以帮助开发者充分掌握这一版本的JDK。————————————————原文链接:https://blog.csdn.net/weixin_36001279/article/details/143217770
-
引言在开发中,尤其是需要处理大量数据或者进行任务调度的场景下,如何高效地管理数据的顺序和优先级是一个至关重要的问题。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
-
1. 问题背景:数据库与 Java 实体不同步1.1 常见场景数据库新增字段(如 ALTER TABLE ADD COLUMN),但 Java 实体类未更新。程序继续运行,调用 saveBatch()、insert()、查询等方法。是否会报错? 取决于字段约束和 ORM 框架行为。1.2 示例代码假设有一个 StatisticsData 实体类(使用 MyBatis-Plus):1234567@Data@TableName("statistics_data")public class StatisticsData extends BaseModel { private String agentId; private Long click; // 其他字段...}然后数据库新增一个字段:1ALTER TABLE statistics_data ADD COLUMN new_column INT NOT NULL;此时,如果 Java 代码未更新,会有什么影响?2. 不同操作的影响分析2.1 查询操作(SELECT)默认情况下,MyBatis-Plus 会忽略数据库中存在但实体类没有的字段,查询不会报错。但如果使用 SELECT * 或手动映射全部字段,可能会触发警告(取决于日志级别)。2.2 插入操作(INSERT)如果新增字段允许 NULL 或有默认值:1ALTER TABLE statistics_data ADD COLUMN new_column INT DEFAULT 0;✅ save() 或 saveBatch() 不会报错,插入时该字段会用 NULL 或默认值填充。如果新增字段是 NOT NULL 且无默认值:1ALTER TABLE statistics_data ADD COLUMN new_column INT NOT NULL;❌ saveBatch() 会报错:1ERROR 1364 (HY000): Field 'new_column' doesn't have a default value因为 MyBatis-Plus 生成的 SQL 不包含未定义的字段,导致 MySQL 拒绝插入。2.3 批量插入(saveBatch)saveBatch() 的底层逻辑:1234567// MyBatis-Plus 默认实现(简化版)public boolean saveBatch(Collection<T> entityList) { for (T entity : entityList) { baseMapper.insert(entity); // 生成 INSERT SQL,仅包含实体类定义的字段 } return true;}如果 new_column 是 NOT NULL,由于 SQL 不包含该字段,MySQL 会报错。如果允许 NULL 或设置默认值,则正常执行。3. 解决方案3.1 临时修复(不推荐长期使用)(1)修改数据库字段约束12345-- 允许 NULLALTER TABLE statistics_data MODIFY new_column INT NULL; -- 或设置默认值ALTER TABLE statistics_data MODIFY new_column INT DEFAULT 0; (2)避免自动映射,手动指定 SQL1234567// 使用 @TableField(exist = false) 忽略未知字段@TableField(exist = false)private String ignoredField; // 或自定义 SQL(明确指定插入字段)@Insert("INSERT INTO statistics_data (agent_id, click) VALUES (#{agentId}, #{click})")void customInsert(StatisticsData data); 3.2 长期最佳实践(推荐)(1)同步更新 Java 实体类1234567@Data@TableName("statistics_data")public class StatisticsData extends BaseModel { private String agentId; private Long click; private Integer newColumn; // 新增字段} (2)使用数据库迁移工具(如 Flyway/Liquibase)12345-- V1__init.sqlCREATE TABLE statistics_data (...); -- V2__add_new_column.sqlALTER TABLE statistics_data ADD COLUMN new_column INT DEFAULT 0; (3)自动化检查(可选)通过单元测试或 Schema 校验工具,确保数据库与实体类一致:123// 示例:使用 Hibernate Validator 检查(如果适用)@Column(nullable = false)private Integer newColumn; 4. 完整代码示例4.1 更新后的 Java 实体1234567891011@Data@TableName("statistics_data")public class StatisticsData extends BaseModel { private String agentId; private Long click; private Integer newColumn; // 新增字段 @TableField("`date`") private String date; // 其他字段...} 4.2 安全的批量插入方法12345678// 检查数据完整性后再插入public void safeBatchInsert(List<StatisticsData> dataList) { if (dataList == null || dataList.isEmpty()) { return; } // 可在此处做字段校验 statisticsDataService.saveBatch(dataList);} 4.3 数据库变更脚本(Flyway 示例)123-- V2__add_new_column.sqlALTER TABLE statistics_data ADD COLUMN new_column INT NOT NULL DEFAULT 0 COMMENT '新增字段'; 5. 总结场景是否报错解决方案新增字段允许 NULL 或 DEFAULT❌ 不报错可暂时不更新实体类新增字段 NOT NULL 且无默认值✅ 报错更新实体类 或 修改表结构使用 saveBatch()取决于约束同步实体类或调整 SQL
-
现在我们队是两个人,请问进复赛有没有机会再加新的队友?
-
Java RMI(Remote Method Invocation)是一种允许Java虚拟机之间进行通信和交互的技术。它使得远程Java对象能够像本地对象一样被访问和操作,从而简化了分布式应用程序的开发。一些应用依然会使用 RMI 来实现通信和交互,今天的内容我们来聊聊 RMI 的那些事儿。一、先来了解一下概念RMI原理RMI的基本思想是远程方法调用。客户端调用远程方法时,实际上是发送一个调用请求到服务器,由服务器执行该方法,并将结果返回给客户端。RMI通过存根(Stub)和骨架(Skeleton)类来实现远程调用,存根位于客户端,而骨架位于服务器端。RMI组件远程接口:必须继承自java.rmi.Remote接口,并声明抛出RemoteException。远程对象:实现了远程接口的类。RMI服务器:提供远程对象,并处理客户端的调用请求。RMI客户端:发起远程方法调用请求。注册服务(Registry):提供服务注册与获取,类似于目录服务。数据传递RMI使用Java序列化机制来传递数据。客户端将方法参数序列化后通过网络发送给服务器,服务器反序列化参数并执行远程方法,然后将结果序列化回传给客户端。RMI案例以下是一个简单的RMI案例,包括服务器和客户端的实现思路,下文V 将再用代码来解释:服务器端实现一个远程接口,例如PersonController,包含一个远程方法queryName。创建该接口的具体实现类PersonControllerImpl,并在其中实现远程方法。在服务器的main方法中,实例化远程对象,创建RMI注册表,并使用Naming.rebind将远程对象绑定到指定名称。客户端通过Naming.lookup方法,使用RMI注册表提供的名称获取远程对象的存根。调用存根上的方法,就像调用本地方法一样,实际上是在调用服务器上的远程方法。RMI的局限性语言限制:RMI是Java特有的技术,不能直接用于非Java应用程序。安全性问题:RMI的序列化机制可能带来安全风险,不建议将1099端口暴露在公网上。性能和扩展性:RMI的性能受网络延迟和带宽影响,且在高并发情况下可能面临扩展性限制。RMI的应用场景RMI适用于需要Java程序之间进行远程通信的场景,如分布式银行系统、游戏服务器、股票交易系统和网上商城等。接下来一起看一个简单的案例使用吧。二、案例使用先来搞一个简单的Java RMI服务器端和客户端的实现案例。这个案例中,服务器端将提供一个名为HelloWorld的远程服务,客户端将调用这个服务并打印返回的问候语。服务器端实现定义远程接口:服务器和客户端都需要这个接口。它必须继承自java.rmi.Remote接口,并且所有远程方法都要声明抛出RemoteException。import java.rmi.Remote; import java.rmi.RemoteException; public interface HelloWorld extends Remote { String sayHello() throws RemoteException; }实现远程接口:创建一个实现了上述接口的类,并实现远程方法。import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException; public class HelloWorldImpl extends UnicastRemoteObject implements HelloWorld { protected HelloWorldImpl() throws RemoteException { super(); } @Override public String sayHello() throws RemoteException { return "Hello, World!"; } }设置RMI服务器:创建一个主类来设置RMI服务器,绑定远程对象到RMI注册表。import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; public class HelloWorldServer { public static void main(String[] args) { try { // 创建远程对象 HelloWorld helloWorld = new HelloWorldImpl(); // 获取RMI注册表的引用,并在指定端口上创建或获取注册表实例 LocateRegistry.createRegistry(1099); // 将远程对象绑定到RMI注册表中,客户端可以通过这个名字访问远程对象 Naming.bind("rmi://localhost/HelloWorld", helloWorld); System.out.println("HelloWorld RMI object bound"); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }客户端实现调用远程服务:客户端使用RMI注册表的名字来查找远程对象,并调用其方法。import java.rmi.Naming; import java.rmi.RemoteException; public class HelloWorldClient { public static void main(String[] args) { try { // 使用RMI注册表的名字查找远程对象 HelloWorld helloWorld = (HelloWorld) Naming.lookup("rmi://localhost/HelloWorld"); // 调用远程方法 String response = helloWorld.sayHello(); System.out.println("Response: " + response); } catch (Exception e) { System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }来详细解释吧远程接口 (HelloWorld): 这是服务器和客户端之间通信的协议。它定义了可以被远程调用的方法。远程对象实现 (HelloWorldImpl): 这是远程接口的一个实现。RMI调用实际上会调用这个实现中的方法。服务器 (HelloWorldServer): 负责创建远程对象的实例,并将这个实例绑定到RMI注册表中。这样客户端就可以通过注册表的名字来访问这个对象。客户端 (HelloWorldClient): 使用RMI注册表的名字来查找服务器上的远程对象,并调用其方法。接下来就可以编译所有类文件,运行服务器端程序,确保RMI注册表已经启动(在某些Java版本中会自动启动),再运行客户端程序,搞定。注意一下哈,由于RMI使用Java序列化机制,因此客户端和服务器的类路径必须一致或兼容。三、RMI 在分布式银行系统中的应用接下来V哥要介绍业务场景下的应用了,拿在分布式银行系统中来说,我们可以使用RMI来实现不同银行分行之间的通信,例如,实现账户信息的查询、转账等操作。以下是一个简化的示例,其中包括两个基本操作:查询账户余额和执行转账,按步骤一步一步来吧。步骤1: 定义远程接口首先,定义一个远程接口BankService,它将被各个分行实现以提供银行服务。import java.rmi.Remote; import java.rmi.RemoteException; public interface BankService extends Remote { double getAccountBalance(String accountNumber) throws RemoteException; boolean transferFunds(String fromAccount, String toAccount, double amount) throws RemoteException; }步骤2: 实现远程接口接下来,实现这个接口来创建远程对象,这个对象将提供实际的银行服务。import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException; import java.util.HashMap; import java.util.Map; public class BankServiceImpl extends UnicastRemoteObject implements BankService { private Map<String, Double> accounts = new HashMap<>(); protected BankServiceImpl() throws RemoteException { super(); // 初始化一些账户信息 accounts.put("123456789", 5000.00); accounts.put("987654321", 1000.00); } @Override public double getAccountBalance(String accountNumber) throws RemoteException { return accounts.getOrDefault(accountNumber, 0.00); } @Override public boolean transferFunds(String fromAccount, String toAccount, double amount) throws RemoteException { if (accounts.containsKey(fromAccount) && accounts.get(fromAccount) >= amount) { accounts.put(fromAccount, accounts.get(fromAccount) - amount); accounts.merge(toAccount, amount, Double::sum); return true; } return false; } }步骤3: 设置RMI服务器服务器端将创建BankService的远程对象实例,并将其绑定到RMI注册表中。import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; public class BankServer { public static void main(String[] args) { try { LocateRegistry.createRegistry(1099); // 创建RMI注册表 BankService bankService = new BankServiceImpl(); Naming.rebind("//localhost/BankService", bankService); // 绑定远程对象 System.out.println("BankService is ready for use."); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }步骤4: 实现RMI客户端客户端将使用RMI注册表的名字来查找远程对象,并调用其方法。import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.RemoteException; public class BankClient { public static void main(String[] args) { try { BankService bankService = (BankService) Naming.lookup("//localhost/BankService"); System.out.println("Account balance: " + bankService.getAccountBalance("123456789")); // 执行转账操作 boolean isTransferSuccess = bankService.transferFunds("123456789", "987654321", 200.00); if (isTransferSuccess) { System.out.println("Transfer successful."); } else { System.out.println("Transfer failed."); } // 再次查询余额 System.out.println("New account balance: " + bankService.getAccountBalance("123456789")); } catch (RemoteException | NotBoundException e) { System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }来详细解释一下远程接口 (BankService): 定义了两个方法:getAccountBalance用于查询账户余额,transferFunds用于执行转账操作。远程对象实现 (BankServiceImpl): 实现了BankService接口。它使用一个HashMap来模拟账户和余额信息。服务器 (BankServer): 设置了RMI服务器,将BankService的实现绑定到RMI注册表中,供客户端访问。客户端 (BankClient): 查找RMI注册表中的BankService服务,并调用其方法来查询余额和执行转账。撸完代码后,编译所有类文件,运行服务器端程序BankServer,再运行客户端程序BankClient,测试效果吧。转载自https://www.cnblogs.com/wgjava/p/18343209
-
原子性的意义原子性特别是在并发编程领域,是一个极其重要的概念,原子性指的是一个操作或一组操作要么全部执行成功,要么全部不执行,不会出现部分执行的情况。这意味着原子性操作是不可分割的,它们在执行过程中不会被其他操作中断或干扰。原子性的意义在于它保证了数据的一致性和程序的正确性。在多线程或多进程的环境中,当多个操作同时访问和修改共享数据时,如果没有原子性保证,可能会导致数据不一致或不确定的结果。例如,如果一个线程在读取某个数据时,另一个线程同时修改了这个数据,那么第一个线程读取到的数据可能是不正确的。通过确保操作的原子性,可以避免这种情况,从而维护数据的完整性和程序的正确执行。了解了上面的原子性的重要概念后,接下来一起聊一聊 volatile 关键字。volatile 关键字在 Java 中用于确保变量的更新对所有线程都是可见的,但它并不保证复合操作的原子性。这意味着当多个线程同时访问一个 volatile 变量时,可能会遇到读取不一致的问题,尽管它们不会看到部分更新的值。Volatile 的限制不保证原子性:volatile 变量的单个读写操作是原子的,但复合操作(如自增或同步块)不是原子的。不保证顺序性:volatile 变量的读写操作不会与其他操作(如非 volatile 变量的读写)发生重排序。一个例子用一个示例来解释会更清楚点,假如我们有一段代码是这样的:class Counter { private volatile int count = 0; void increment() { count++; } int getCount() { return count; } } 尽管 count 是 volatile 变量,但 increment 方法中的复合操作 count++(读取-增加-写入)不是原子的。因此,在多线程环境中,多个线程可能会同时读取相同的初始值,然后增加它,导致最终值低于预期。volatile 不保证原子性的代码验证以下是一个简单的 Java 程序,演示了 volatile 变量在多线程环境中不保证复合操作原子性的问题:public class VolatileTest { private static volatile int counter = 0; public static void main(String[] args) throws InterruptedException { int numberOfThreads = 10000; Thread[] threads = new Thread[numberOfThreads]; for (int i = 0; i < numberOfThreads; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 100; j++) { counter++; } }); threads[i].start(); } for (int i = 0; i < numberOfThreads; i++) { threads[i].join(); } System.out.println("Expected count: " + (numberOfThreads * 100)); System.out.println("Actual count: " + counter); } }在这个例子中:counter 是一个 volatile 变量。每个线程都会对 counter 执行 100 次自增操作。理论上,如果 counter++ 是原子的,最终的 counter 值应该是 10000 * 100。然而,由于 counter++ 包含三个操作:读取 counter 的值、增加 1、写回 counter 的值,这些操作不是原子的。因此,在多线程环境中,最终的 counter 值通常会小于预期值,这证明了 volatile 变量不保证复合操作的原子性。解决方案1. 使用 synchronized 方法或块:将访问 volatile 变量的方法或代码块声明为 synchronized,确保原子性和可见性。class Counter { private volatile int count = 0; synchronized void increment() { count++; } synchronized int getCount() { return count; } }2. 使用 AtomicInteger 类:java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操作,可以替代 volatile 变量。import java.util.concurrent.atomic.AtomicInteger; class Counter { private AtomicInteger count = new AtomicInteger(0); void increment() { count.incrementAndGet(); } int getCount() { return count.get(); } }3. 使用锁(如 ReentrantLock):使用显式锁(如 ReentrantLock)来同步访问 volatile 变量的代码块。import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Counter { private volatile int count = 0; private final Lock lock = new ReentrantLock(); void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }使用volatile变量的正确使用场景如果操作是简单的读写,并且你只需要保证可见性,可以使用 volatile。但对于复合操作,可以使用上述其他方法来实现,通过这些方法,可以确保在多线程环境中对共享资源的正确同步和可见性。转载自https://www.cnblogs.com/wgjava/p/18311697
-
在Java中,Executors 类提供了多种静态工厂方法来创建不同类型的线程池。在学习线程池的过程中,一定避不开Executors类,掌握这个类的使用、原理、使用场景,对于实际项目开发时,运用自如,以下是一些常用的方法,V哥来一一细说:newCachedThreadPool(): 创建一个可缓存的线程池,如果线程池中的线程超过60秒没有被使用,它们将被终止并从缓存中移除。newFixedThreadPool(int nThreads): 创建一个固定大小的线程池,其中 nThreads 指定了线程池中线程的数量。newSingleThreadExecutor(): 创建一个单线程的执行器,它创建单个工作线程来执行任务。newScheduledThreadPool(int corePoolSize): 创建一个固定大小的线程池,它可以根据需要创建新线程,但会按照固定延迟执行具有给定初始延迟的任务。newWorkStealingPool(int parallelism): 创建一个工作窃取线程池,它使用多个队列,每个线程都从自己的队列中窃取任务。newSingleThreadScheduledExecutor(): 创建一个单线程的调度执行器,它可以根据需要创建新线程来执行任务。privilegedThreadFactory(): 创建一个线程工厂,用于创建具有特权访问的线程。defaultThreadFactory(): 创建一个默认的线程工厂,用于创建具有非特权访问的线程。unconfigurableExecutorService(ExecutorService executor): 将给定的 ExecutorService 转换为不可配置的版本,这样调用者就不能修改它的配置。这些方法提供了灵活的方式来创建和管理线程池,以满足不同的并发需求,下面 V 哥来一一介绍一下9个方法的实现以及使用场景。1. newCachedThreadPool()newCachedThreadPool 方法是 Java java.util.concurrent 包中的 Executors 类的一个静态工厂方法。这个方法用于创建一个可缓存的线程池,它能够根据需要创建新线程,并且当线程空闲超过一定时间后,线程会被终止并从线程池中移除。下面是 newCachedThreadPool 方法的大致实现原理和源代码分析:实现原理线程创建: 当提交任务到线程池时,如果线程池中的线程数少于核心线程数,会创建新的线程来执行任务。线程复用: 如果线程池中的线程数已经达到核心线程数,新提交的任务会被放入任务队列中等待执行。线程回收: 如果线程池中的线程在一定时间内(默认是60秒)没有任务执行,它们会被终止,从而减少资源消耗。源代码分析在 Java 的 java.util.concurrent 包中,Executors 类并没有直接提供 newCachedThreadPool 的实现,而是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }参数解释:corePoolSize: 核心线程数,这里设置为0,表示线程池不会保留任何核心线程。maximumPoolSize: 最大线程数,这里设置为 Integer.MAX_VALUE,表示理论上可以创建无限多的线程。keepAliveTime: 当线程数大于核心线程数时,多余的空闲线程能等待新任务的最长时间,这里设置为60秒。unit: keepAliveTime 参数的时间单位,这里是秒。workQueue: 一个任务队列,这里使用的是 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作必须等待一个相应的移除操作。实现过程初始化: 当调用 newCachedThreadPool 时,会创建一个 ThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,线程池会检查是否有空闲线程可以立即执行任务。线程创建: 如果没有空闲线程,并且当前线程数小于 maximumPoolSize,则创建新线程执行任务。任务队列: 如果当前线程数已经达到 maximumPoolSize,则将任务放入 SynchronousQueue 中等待。线程复用: 当一个线程执行完任务后,它不会立即终止,而是尝试从 SynchronousQueue 中获取新任务。线程回收: 如果线程在 keepAliveTime 时间内没有获取到新任务,它将被终止。这种设计使得 newCachedThreadPool 非常适合处理大量短生命周期的任务,因为它可以动态地调整线程数量以适应任务负载的变化。然而,由于它可以创建无限多的线程,如果没有适当的任务队列来控制任务的数量,可能会导致资源耗尽。因此,在使用 newCachedThreadPool 时,需要谨慎考虑任务的特性和系统的资源限制。使用场景:适用于执行大量短期异步任务,尤其是任务执行时间不确定的情况。例如,Web服务器处理大量并发请求,或者异步日志记录。2. newFixedThreadPool(int nThreads)newFixedThreadPool(int nThreads) 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个固定大小的线程池,它能够确保线程池中始终有固定数量的线程在工作。以下是 newFixedThreadPool 方法的实现原理、源代码分析以及实现过程:实现原理固定线程数: 线程池中的线程数量始终保持为 nThreads。任务队列: 提交的任务首先由核心线程执行,如果核心线程都在忙碌状态,新任务将被放入一个阻塞队列中等待执行。线程复用: 线程池中的线程会重复利用,执行完一个任务后,会立即尝试从队列中获取下一个任务执行。源代码分析newFixedThreadPool 方法是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor( nThreads, // 核心线程数 nThreads, // 最大线程数 0L, // 线程空闲时间,这里设置为0,表示线程不会空闲 TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() // 使用阻塞队列来存储任务 ); }参数解释:corePoolSize: 核心线程数,这里设置为 nThreads,表示线程池中始终有 nThreads 个线程。maximumPoolSize: 最大线程数,这里也设置为 nThreads,表示线程池的线程数量不会超过 nThreads。keepAliveTime: 当线程数大于核心线程数时,多余的空闲线程能等待新任务的最长时间,这里设置为0,表示如果线程池中的线程数超过核心线程数,这些线程将立即终止。unit: keepAliveTime 参数的时间单位,这里是毫秒。workQueue: 一个任务队列,这里使用的是 LinkedBlockingQueue,它是一个基于链表的阻塞队列,可以存储任意数量的任务。实现过程初始化: 当调用 newFixedThreadPool 时,会创建一个 ThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,线程池会检查是否有空闲的核心线程可以立即执行任务。任务队列: 如果所有核心线程都在忙碌状态,新提交的任务将被放入 LinkedBlockingQueue 中等待。线程复用: 核心线程执行完一个任务后,会尝试从 LinkedBlockingQueue 中获取新任务继续执行。线程数量控制: 由于 keepAliveTime 设置为0,当线程池中的线程数超过核心线程数时,这些线程会立即终止,从而保证线程池中的线程数量不会超过 nThreads。这种设计使得 newFixedThreadPool 非常适合处理大量且持续的任务,因为它可以保证任务以固定的线程数量并行执行,同时避免了线程数量的无限制增长。然而,由于线程池的大小是固定的,如果任务提交的速率超过了线程池的处理能力,可能会导致任务在队列中等待较长时间。因此,在使用 newFixedThreadPool 时,需要根据任务的特性和预期的负载来合理设置 nThreads 的值。使用场景:适用于执行大量长期运行的任务,其中线程数量需要固定。例如,同时运行多个数据加载或数据处理任务,且希望限制并发数以避免资源过载。3. newSingleThreadExecutor()newSingleThreadExecutor 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个单线程的执行器。这个执行器确保所有任务都按照任务提交的顺序,在一个线程中顺序执行。以下是 newSingleThreadExecutor 方法的实现原理、源代码分析以及实现过程:实现原理单线程执行: 线程池中只有一个线程,所有任务都由这个线程顺序执行。任务队列: 如果这个线程在执行任务时有新任务提交,新任务会被放入一个阻塞队列中等待执行。线程复用: 这个线程会重复利用,执行完一个任务后,会立即尝试从队列中获取下一个任务执行。源代码分析newSingleThreadExecutor 方法同样是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:public static ExecutorService newSingleThreadExecutor() { return new ThreadPoolExecutor( 1, // 核心线程数 1, // 最大线程数 0L, TimeUnit.MILLISECONDS, // 线程空闲时间,这里设置为0,表示线程不会空闲 new LinkedBlockingQueue<Runnable>() // 使用阻塞队列来存储任务 ); }参数解释:corePoolSize: 核心线程数,这里设置为1,表示线程池中始终有一个核心线程。maximumPoolSize: 最大线程数,这里也设置为1,表示线程池的线程数量不会超过1。keepAliveTime: 线程空闲时间,这里设置为0,表示如果线程空闲,它将立即终止。unit: keepAliveTime 参数的时间单位,这里是毫秒。workQueue: 一个任务队列,这里使用的是 LinkedBlockingQueue,它是一个无界队列,可以存储任意数量的任务。实现过程初始化: 当调用 newSingleThreadExecutor 时,会创建一个 ThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,如果核心线程空闲,则立即执行任务;如果核心线程忙碌,则将任务放入 LinkedBlockingQueue 中等待。顺序执行: 由于只有一个线程,所有任务都将按照提交的顺序被执行。任务队列: 如果核心线程在执行任务,新提交的任务将被放入 LinkedBlockingQueue 中排队等待。线程复用: 核心线程执行完一个任务后,会尝试从 LinkedBlockingQueue 中获取新任务继续执行。线程数量控制: 由于 keepAliveTime 设置为0,核心线程在没有任务执行时会立即终止。但由于 corePoolSize 和 maximumPoolSize 都为1,线程池会立即重新创建一个线程。这种设计使得 newSingleThreadExecutor 非常适合处理需要保证任务顺序的场景,例如,当任务之间有依赖关系或者需要按照特定顺序执行时。同时,由于只有一个线程,这也避免了多线程环境下的并发问题。然而,由于只有一个线程执行任务,这也限制了并行处理的能力,如果任务执行时间较长,可能会导致后续任务等待较长时间。因此,在使用 newSingleThreadExecutor 时,需要根据任务的特性和对顺序的要求来决定是否适用。使用场景:适用于需要保证任务顺序执行的场景,例如,顺序处理队列中的消息或事件。也适用于需要单个后台线程持续处理周期性任务的情况。4. newScheduledThreadPool(int corePoolSize)newScheduledThreadPool 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个固定大小的线程池,这个线程池支持定时以及周期性的任务执行。以下是 newScheduledThreadPool 方法的实现原理、源代码分析以及实现过程:实现原理定时任务: 线程池能够按照指定的延迟执行任务,或者以固定间隔周期性地执行任务。固定线程数: 线程池中的线程数量被限制为 corePoolSize 指定的大小。任务队列: 任务首先由核心线程执行,如果核心线程都在忙碌状态,新任务将被放入一个延迟任务队列中等待执行。源代码分析newScheduledThreadPool 方法是通过调用 ScheduledThreadPoolExecutor 类的构造函数来实现的。以下是 ScheduledThreadPoolExecutor 构造函数的调用示例:public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }这里的 ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的一个子类,专门为执行定时任务设计。ScheduledThreadPoolExecutor 构造函数的参数 corePoolSize 定义了线程池中核心线程的数量。ScheduledThreadPoolExecutor 内部使用了一个 DelayedWorkQueue 作为任务队列,这个队列能够按照任务的预定执行时间对任务进行排序。实现过程初始化: 当调用 newScheduledThreadPool 时,会创建一个 ScheduledThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,线程池会根据任务的预定执行时间,将任务放入 DelayedWorkQueue 中。任务调度: 线程池中的线程会从 DelayedWorkQueue 中获取任务,如果任务的执行时间已经到达,线程将执行该任务。线程复用: 执行完一个任务的线程会再次尝试从 DelayedWorkQueue 中获取下一个任务。线程数量控制: 如果任务队列中的任务数量超过了核心线程能够处理的范围,ScheduledThreadPoolExecutor 会创建新的线程来帮助处理任务,直到达到 corePoolSize 指定的最大线程数。特点ScheduledThreadPoolExecutor 允许设置一个线程工厂,用于创建具有特定属性的线程。它还允许设置一个 RejectedExecutionHandler,当任务无法被接受时(例如,线程池关闭或任务队列已满),这个处理器会被调用。与 ThreadPoolExecutor 不同,ScheduledThreadPoolExecutor 的 shutdown 和 shutdownNow 方法不会等待延迟任务执行完成。使用 newScheduledThreadPool 创建的线程池非常适合需要执行定时任务的场景,例如,定期执行的后台任务、定时检查等。然而,由于它是基于固定大小的线程池,所以在高负载情况下,任务可能会排队等待执行,这需要在设计时考虑适当的 corePoolSize 以满足性能要求。使用场景:适用于需要定期执行任务或在将来某个时间点执行任务的场景。例如,定时备份数据、定时发送提醒等。5. newWorkStealingPool(int parallelism)newWorkStealingPool 是 Java 8 中新增的 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个工作窃取(Work-Stealing)线程池,它能够提高并行任务的执行效率,特别是在多处理器系统上。实现原理工作窃取: 在工作窃取线程池中,每个线程都有自己的任务队列。当一个线程完成自己的任务后,它会尝试从其他线程的任务队列中“窃取”任务来执行。并行级别: 线程池的大小由 parallelism 参数决定,这个参数通常等于主机上的处理器核心数。动态调整: 工作窃取线程池可以动态地添加或移除线程,以适应任务的负载和线程的利用率。源代码分析newWorkStealingPool 方法是通过调用 ForkJoinPool 类的静态工厂方法 commonPoolFor 来实现的。以下是 ForkJoinPool 构造函数的调用示例:public static ExecutorService newWorkStealingPool(int parallelism) { return new ForkJoinPool( parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, // 没有未处理的异常处理器 false // 不是一个异步任务 ); }参数解释:parallelism: 线程池的并行级别,即线程池中的线程数量。ForkJoinPool.defaultForkJoinWorkerThreadFactory: 默认的线程工厂,用于创建线程。null: 未处理的异常处理器,这里没有指定,因此如果任务抛出未捕获的异常,它将被传播到 ForkJoinTask 的调用者。false: 表示这不是一个异步任务。ForkJoinPool 内部使用了 ForkJoinWorkerThread 来执行任务,并且每个线程都有一个 ForkJoinQueue 来存储任务。实现过程初始化: 当调用 newWorkStealingPool 时,会创建一个 ForkJoinPool 实例。任务提交: 当任务提交给线程池时,它们会被放入调用线程的本地队列中。任务执行: 每个线程首先尝试执行其本地队列中的任务。工作窃取: 如果本地队列为空,线程会尝试从其他线程的队列中窃取任务来执行。动态调整: 线程池可以根据需要动态地添加或移除线程。特点工作窃取线程池特别适合于工作量不均匀分布的任务,因为它可以减少空闲时间并提高资源利用率。它也适用于可分解为多个子任务的并行计算任务,因为可以将任务分解后,再将子任务提交给线程池。由于每个线程都有自己的队列,因此减少了锁的争用,提高了并发性能。使用 newWorkStealingPool 创建的线程池非常适合于需要高并发和高吞吐量的场景,尤其是在多处理器系统上。然而,由于工作窃取机制,它可能不适用于任务执行时间非常短或者任务数量非常少的场景,因为窃取任务本身可能会引入额外的开销。使用场景:适用于工作量不均匀或可分解为多个小任务的并行计算任务。例如,图像处理、数据分析等,可以在多核处理器上有效利用所有核心。6. newSingleThreadScheduledExecutor()newSingleThreadScheduledExecutor 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个单线程的调度执行器,它可以安排命令在给定的延迟后运行,或者定期地执行。以下是 newSingleThreadScheduledExecutor 方法的实现原理、源代码分析以及实现过程:实现原理单线程执行: 执行器确保所有任务都在单个线程中顺序执行,这保证了任务的执行顺序。定时任务: 支持延迟执行和周期性执行任务。任务队列: 所有任务首先被放入一个任务队列中,然后由单线程按顺序执行。源代码分析newSingleThreadScheduledExecutor 方法是通过调用 ScheduledThreadPoolExecutor 类的构造函数来实现的。以下是 ScheduledThreadPoolExecutor 构造函数的调用示例:public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new ScheduledThreadPoolExecutor(1); }这里,ScheduledThreadPoolExecutor 是 ExecutorService 的一个实现,专门为执行定时任务设计。构造函数只有一个参数,即核心线程数,这里设置为1,表示这是一个单线程的执行器。ScheduledThreadPoolExecutor 内部使用了一个 DelayedWorkQueue 作为任务队列,这个队列能够按照任务的预定执行时间对任务进行排序。实现过程初始化: 当调用 newSingleThreadScheduledExecutor 时,会创建一个 ScheduledThreadPoolExecutor 实例,其核心线程数为1。任务提交: 当任务提交给执行器时,任务会被封装成 ScheduledFutureTask 或者 RunnableScheduledFuture,然后放入 DelayedWorkQueue 中。任务调度: 单线程会不断地从 DelayedWorkQueue 中获取任务,并按照预定的时间执行。如果任务的执行时间已经到达,任务将被执行;如果还没有到达,线程会等待直到执行时间到来。顺序执行: 由于只有一个线程,所有任务都将按照它们被提交的顺序被执行。周期性任务: 对于需要周期性执行的任务,执行器会在每次任务执行完毕后,重新计算下一次执行的时间,并再次将任务放入队列。特点newSingleThreadScheduledExecutor 创建的执行器非常适合需要保证任务顺序的场景,例如,需要按照特定顺序执行的任务或者具有依赖关系的任务。它也适合执行定时任务,如定期执行的维护任务或者后台任务。由于只有一个线程,这也避免了多线程环境下的并发问题,简化了任务同步和状态管理。使用 newSingleThreadScheduledExecutor 创建的执行器可以提供强大的定时任务功能,同时保持任务执行的顺序性。然而,由于只有一个线程执行任务,这也限制了并行处理的能力,如果任务执行时间较长,可能会导致后续任务等待较长时间。因此,在使用 newSingleThreadScheduledExecutor 时,需要根据任务的特性和对顺序的要求来决定是否适用。使用场景:适用于需要单个后台线程按计划执行任务的场景。例如,定时检查系统状态、定时执行维护任务等。7. privilegedThreadFactory()privilegedThreadFactory 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个线程工厂,该工厂能够产生具有特权访问的线程。这意味着这些线程可以加载系统属性和库,并且可以访问文件系统。以下是 privilegedThreadFactory 方法的实现原理、源代码分析以及实现过程:实现原理特权访问: 创建的线程将具有访问系统资源的权限,例如,加载系统属性和库。线程创建: 线程工厂将创建新的线程实例,这些线程实例将继承创建它们的线程的上下文。源代码分析在 Java 的标准库中,privilegedThreadFactory 方法的实现细节并未公开,因为它是一个私有方法。然而,我们可以分析其大致工作原理。privilegedThreadFactory 方法的调用示例如下:public static ThreadFactory privilegedThreadFactory() { return new PrivilegedThreadFactory(); }这里,PrivilegedThreadFactory 是 Executors 类的一个私有静态内部类,它实现了 ThreadFactory 接口。ThreadFactory 接口定义了一个 newThread(Runnable r) 方法,用于创建新的线程。实现过程初始化: 当调用 privilegedThreadFactory 方法时,会返回一个新的 PrivilegedThreadFactory 实例。线程创建: 当使用这个工厂创建线程时,它会调用 newThread(Runnable r) 方法。特权访问: 在 newThread(Runnable r) 方法的实现中,会使用 AccessController.doPrivileged 方法来确保新创建的线程具有特权访问。上下文复制: 通常,新线程会复制创建它的线程的上下文,包括类加载器等。示例代码虽然我们不能查看 privilegedThreadFactory 的具体实现,但是我们可以提供一个示例实现,以展示如何创建具有特权访问的线程:public class PrivilegedThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { return AccessController.doPrivileged(new PrivilegedAction<>() { @Override public Thread run() { return new Thread(r); } }); } }在这个示例中,PrivilegedAction 是一个实现了 PrivilegedAction<T> 接口的匿名类,其 run 方法创建了一个新的线程。AccessController.doPrivileged 方法用于执行一个特权操作,这里是为了确保线程创建过程中具有必要的权限。特点使用 privilegedThreadFactory 创建的线程可以在需要访问敏感系统资源的情况下使用。这种线程工厂通常用于需要执行特权操作的应用程序,例如,访问系统属性或者执行文件 I/O 操作。使用 privilegedThreadFactory 可以确保线程在执行任务时具有适当的安全权限,从而避免安全异常。然而,需要注意的是,过度使用特权访问可能会带来安全风险,因此在设计应用程序时应谨慎使用。使用场景:适用于需要线程具有更高权限来访问系统资源的场景。例如,需要访问系统属性或执行文件I/O操作的应用程序。8. defaultThreadFactory()defaultThreadFactory 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个默认的线程工厂。这个线程工厂生成的线程没有特殊的权限,它们是普通的线程,具有标准的访问权限。以下是 defaultThreadFactory 方法的实现原理、源代码分析以及实现过程:实现原理标准线程创建: 创建的线程工厂将生成具有默认属性的线程。线程名称: 生成的线程具有默认的线程名称前缀,通常是 "pool-x-thread-y",其中 x 和 y 是数字。线程优先级: 线程的优先级设置为 Thread.NORM_PRIORITY,这是 Java 线程的默认优先级。非守护线程: 创建的线程不是守护线程(daemon threads),它们的存在不会阻止 JVM 退出。源代码分析Java 的 defaultThreadFactory 方法的具体实现细节并未完全公开,因为它是 Executors 类的一个私有静态方法。但是,我们可以根据 Java 的 ThreadFactory 接口和一些公开的源代码片段来分析其大致实现。以下是 defaultThreadFactory 方法的调用示例:public static ThreadFactory defaultThreadFactory() { return new DefaultThreadFactory(); }这里,DefaultThreadFactory 是 Executors 类的一个私有静态内部类,它实现了 ThreadFactory 接口。ThreadFactory 接口定义了一个 newThread(Runnable r) 方法,用于创建新的线程。实现过程初始化: 当调用 defaultThreadFactory 方法时,会返回一个新的 DefaultThreadFactory 实例。线程创建: 使用这个工厂创建线程时,它会调用 newThread(Runnable r) 方法。设置线程名称: 在 newThread(Runnable r) 方法的实现中,会创建一个新的 Thread 对象,并设置一个默认的线程名称。设置线程组: 新线程会被分配到一个默认的线程组中。线程优先级和守护状态: 线程的优先级设置为默认值,且线程不是守护线程。示例代码虽然我们不能查看 defaultThreadFactory 的具体实现,但是我们可以提供一个示例实现,以展示如何创建具有默认属性的线程:public class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }在这个示例中,DefaultThreadFactory 使用 AtomicInteger 来确保线程池和线程编号的唯一性。创建的线程名称具有前缀 "pool-x-thread-y",其中 x 和 y 是自增的数字。线程不是守护线程,且优先级设置为 Thread.NORM_PRIORITY。特点使用 defaultThreadFactory 创建的线程工厂生成的线程具有标准的 Java 线程属性。这种线程工厂通常用于不需要特殊权限的应用程序。由于线程不是守护线程,它们的存在可以维持 JVM 的运行,直到所有非守护线程执行完毕。使用 defaultThreadFactory 可以确保线程在执行任务时具有标准的安全和执行属性,适合大多数常规用途。然而,如果应用程序需要特殊的线程属性,如守护线程或不同的优先级,可能需要自定义线程工厂。使用场景:适用于大多数标准应用程序,需要创建具有默认属性的线程。这是大多数 ExecutorService 实现的默认选择。9. unconfigurableExecutorService(ExecutorService executor)unconfigurableExecutorService 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个不可配置的 ExecutorService 包装器,这意味着一旦包装后的 ExecutorService 被创建,就不能更改其配置,比如不能修改其线程池大小或任务队列等。以下是 unconfigurableExecutorService 方法的实现原理、源代码分析以及实现过程:实现原理封装: 将现有的 ExecutorService 封装在一个不可配置的代理中。不可修改: 所有修改配置的方法调用,如 shutdown, shutdownNow, setCorePoolSize 等,都将抛出 UnsupportedOperationException。转发: 除了配置修改的方法外,其他方法调用将被转发到原始的 ExecutorService。源代码分析unconfigurableExecutorService 方法的具体实现细节并未完全公开,因为它是 Executors 类的一个私有静态方法。但是,我们可以根据 Java 的 ExecutorService 接口和代理机制来分析其大致实现。以下是 unconfigurableExecutorService 方法的调用示例:public static ExecutorService unconfigurableExecutorService(ExecutorService executor) { return new FinalizableDelegatedExecutorService(executor); }这里,FinalizableDelegatedExecutorService 是 Executors 类的一个私有静态内部类,它实现了 ExecutorService 接口,并代理了对另一个 ExecutorService 的调用。实现过程初始化: 当调用 unconfigurableExecutorService 方法时,会返回一个新的 FinalizableDelegatedExecutorService 实例,它将原始的 ExecutorService 作为参数。方法调用拦截: 对 FinalizableDelegatedExecutorService 的方法调用将首先被拦截。配置修改拦截: 如果调用的方法是用于修改配置的,比如 shutdown 或 shutdownNow,将抛出 UnsupportedOperationException。转发其他调用: 对于其他不涉及配置修改的方法调用,比如 submit, execute, 将被转发到原始的 ExecutorService。示例代码下面V哥来模拟一个示例实现,以展示如何创建一个不可配置的 ExecutorService 代理:public class UnconfigurableExecutorService implements ExecutorService { private final ExecutorService executor; public UnconfigurableExecutorService(ExecutorService executor) { this.executor = executor; } @Override public void shutdown() { throw new UnsupportedOperationException("Shutdown not allowed"); } @Override public List<Runnable> shutdownNow() { throw new UnsupportedOperationException("Shutdown not allowed"); } @Override public boolean isShutdown() { return executor.isShutdown(); } @Override public boolean isTerminated() { return executor.isTerminated(); } @Override public void execute(Runnable command) { executor.execute(command); } // 其他 ExecutorService 方法的实现,遵循相同的模式 }在这个示例中,UnconfigurableExecutorService 拦截了 shutdown 和 shutdownNow 方法,并抛出了异常。其他方法则直接转发到原始的 ExecutorService。特点使用 unconfigurableExecutorService 创建的 ExecutorService 代理确保了线程池的配置不能被外部修改。这可以用于防止意外地更改线程池的状态,提高线程池使用的安全性。除了配置修改的方法外,其他所有方法都保持了原有 ExecutorService 的行为。使用 unconfigurableExecutorService 可以为现有的 ExecutorService 提供一个安全层,确保它们的状态不会被意外地更改。这对于在多线程环境中共享 ExecutorService 时特别有用。使用场景:适用于需要确保线程池配置在创建后不被更改的场景。例如,当多个组件共享同一个线程池时,可以防止一个组件意外修改配置转载自https://www.cnblogs.com/wgjava/p/18292258
-
1. 踩坑经历最近做了个需求,需要调用第三方接口获取数据,在联调时一直失败,代码抛出javax.net.ssl.SSLHandshakeException异常,具体错误信息如下所示:javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target2.原因分析因为调用第三方接口的代码是复用项目中原有的工具类(基于httpclient封装),所以在确认完传参没问题后,第一时间排除了编码问题。然后开始怀疑第三方提供的接口地址(因为竟然是IP+端口访问),在和第三方确认没有域名访问后,在浏览器里输入第三方的接口地址,发现证书有问题:又使用Postman调用第三方接口,也是失败,提示自签名证书:通过以上分析,可以发现出现该问题的根本原因是Java客户端不信任目标服务器的SSL证书,比如这个第三方使用的自签名证书。3.解决方案解决方案一般有2种,第1种方案是将服务器证书导入Java信任库,第2种方案是绕过SSL验证,这里采用第2种方案。首先,新建HttpClient工具类:import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; public class HttpClientUtils { public static CloseableHttpClient createIgnoreCertClient() throws NoSuchAlgorithmException, KeyManagementException { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new TrustManager[]{new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}, new java.security.SecureRandom()); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); return HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build(); } }然后将原来声明httpClient的代码改为如下所示:CloseableHttpClient httpClient = HttpClientUtils.createIgnoreCertClient();注意事项:确保项目中引入了httpclient依赖:<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>转载自https://www.cnblogs.com/zwwhnly/p/18795523
-
这种情况是不是因为代码内部逻辑卡死,偶尔崩溃偶尔超时的原因呀?
-
在本地判题器跑没有问题,提交代码后就出现runtime_error问题,好难排bug
-
本地的三个样例都没问题
-
我的代码在控制台手打样例可以正常跑,但本地编译卡在预编译,连ok都出不来,甚至用样例代码跑了也是这个问题
-
建立websocket连接,进入到集群内部,通过下发自定义指令在集群内部执行
-
本地正常运行,提交后报错编译错误 compile_error
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签