Java核心基础

本文详细介绍了Java的核心基础知识,包括Java的三大体系、环境配置、类与对象、封装、继承、多态、异常处理、多线程、集合框架、泛型、IO流等方面。讲解了Java的运行机制、类与对象的创建、接口、异常的捕获和处理,以及多线程的概念、状态和使用。还探讨了字符串、日期类和输入输出流的使用,以及反射在Java中的重要性。
摘要由CSDN通过智能技术生成

Java优点

  1. 跨平台

  2. 面向对象编程语言

  3. 支持分布式计算

Java的运行机制

  • 程序员编写Java程序

  • 编译器编译Java文件

  • JVM读取字节码文件运行程序

  • 程序在JVM上运行

Java的三大体系

  • Java SE(J2SE)

    定义了Java的核心类库,包括常用的组件等,是Java开发的基础

  • Java ME(J2ME)

    基于Java SE衍生出来的,用于移动应用开发

  • Java EE(J2EE)

    基于Java SE扩展出的企业级开发组件,提供Java Web等相关的组件

配置Java环境

  • JRE、JDK
  • JRE:Java Runtime Environment Java 运行环境
  • JDK:Java Devlopment Kit Java 开发工具包

开发(类名要与文件名一致)

  • 编译

    javac 类名.java
    
  • 运行

    java 类名
    

Java IDE(Java 集成开发环境)

NetBeans、Eclipse、IDEA

代码规范

  • 强制性代码规范
    1. Java 程序的文件名与类名必须一致,若不一致,无法通过编译。
    2. main 方法是程序的入口,方法的定义必须严格按照格式书写。
    3. 类是组织 Java 代码结构的,类中的方法是执行具体业务的。
  • 非强制性代码规范
    1. 一行只写一条语句。
    2. 在 1 的基础上,还要注意代码缩进。

面向对象

面向对象编程思想:将程序模块化的思想。

  • 什么是面向对象?

    面向对象的编程思想诞生之前,程序开发采用的是面向过程的结构化编程方式,是一种面向功能划分的软件结构。最小粒度细化到方法这一层。

    面向过程注重的是每一个步骤,面向对象关注点在于整件事情的模块化结构。

  • 类和对象

    类和对象的关系

    每个对象都有特定的特征:1、属性。2、方法

    属性:指对象的静态特征

    方法:用来描述对象的动态特征

    对象用来描述客观存在的一个实体,该实体是由一组属性和方法构成

    类是与对象紧密结合的另外一个概念,类是产生对象的模板,所有对象都是通过类来创建的。

    二者的关系:类是对象的抽象化描述。这些对象具有相同的特征和动作(属性和方法)。对象是类的具体实例。

    Java程序是以类为组织单元构成,程序运行时的主体是通过类创建的具体对象。

定义类

public class 类名{
   
	//定义属性,属性名符合驼峰式命名法
	如:private 数据类型 属性名
	//定义方法,方法名符合驼峰式命名法
	public 返回值类型 方法名(参数列表:数据类型 参数名){
   
		//方法体
	}
}

Java 关于返回值的定义分为两类:有返回值和无返回值,有返回值的方法在定义时指定返回时的数据类型,并在方法体用return将结果返回给外部调用者,如果一个方法不需要进行返回值操作,将返回值类型定义为void。

参数列表是指外部在调用该方法时需要传入到方法内部进行运算的数据。

构造函数、构造方法、构造器(同一个意思)

构造方法是一种特殊的方法,普通方法是用来描述某个动作的,构造方法是用来创建对象的。

  • 方法名必须与类名一致
  • 不需要定义返回值类型

构造方法可分为有参构造和无参构造,有参构造是指带参数的构造方法,无参构造是指不带参数的构造方法

任何一个类都默认自带的无参构造函数,如果手动在类中定义一个有参构造方法会覆盖默认的无参构造方法

this关键字

this用来指代当前类的实例化对象,通过this可以调用当前类的属性和方法,比如在有参构造中,通过this可以将外部传入的值赋给当前类的实例化对象

this除了可以在类中访问属性,也可以在类中调用方法,类中的方法可以分为两类:构造方法、普通方法

调用构造函数的语法:this(参数列表),不能再普通方法中调用构造方法,只能在构造函数中调用

调用普通函数的语法:this.普通函数名(参数列表),可以在构造函数使用,也可以在普通方法中使用

成员变量和局部变量

变量的作用域是指在程序中可以通过该变量名来访问该变量的范围,变量的作用域是由变量被声明时所在的位置决定的,Java中根据不同的作用域可以将变量分为成员变量和局部变量

局部变量:在方法中声明

成员变量:在方法外,类中声明

public class Hello{
   
	int x;   //成员变量
	public void test(){
   
		int n;   //局部变量
	}
}

成员变量和局部变量的区别:

  1. 作用域不同,成员变量的作用域在整个类中,类中每个方法都可以访问该变量,局部变量的作用域只在定义变量的方法内
  2. 成员变量和局部变量的初始值也不同,局部变量不会赋初始值,成员变量会赋初始值,具体的值由数据类型决定。

封装

封装是指将类的属性隐藏在内部,外部不能直接访问和修改,通过修改成员变量的可见性,从公有改为私有

封装的核心思想:尽可能把属性都隐藏在内部,对外提供方法来访问,然后可以在这些方法中添加逻辑处理来实现过滤,以屏蔽错误数据的赋值。

封装的步骤:

  • 修改属性(成员变量)的访问权限为私有,使得外部不能直接访问
  • 提供外部可以直接调用的方法
  • 在该方法中加入属性的逻辑控制,避免出现逻辑上的错误

访问权限:指该属性可以被直接访问的范围,实在属性定义时设定的,访问权限的可选项一共有4种:public、private、默认(不写)和protected,这四种的区别在于作用域范围不同

static

static 表示静态或者全局,可以用来修饰成员变量和成员方法以及代码块。

使用static修饰的成员变量和成员方法可以独立于该类的任何一个实例化对象,访问时不依赖于该类的对象,而是可以直接通过该类去访问,可以理解为被该类的所有实例对象所共用,所以说是全局的。

继承

继承是用来描述类之间的关系,即一个类继承(拥有)另外一个类中的属性和方法,同时被继承的类叫做父类,继承父类的类叫做子类

继承的基本语法:

public class 类名 extends 父类名{
   
	
}

Java中的继承是单继承,一个子类只能有一个直接父类。

子类访问父类

在创建一个子类对象的时候,就会默认先创建一个父类对象,无论是通过有参构造或是无参构造来创建子类对象,都是通过无参构造来创建父类对象的。

可以通过super关键字让子类创建对象时调用父类的有参构造

public 子类名(){
   
	super(父类有参构造的参数列表);
}

子类可以访问父类的构造方法(只有在子类的构造函数中才能访问)、普通方法以及成员变量,都是通过super关键字来完成,具体语法:

构造方法:super()
普通方法:super.方法名
成员变量:super.成员变量名

在子类的构造方法中,可以通过super访问父类的构造方法和普通方法

在子类的普通方法中,只能通过super访问父类的普通方法

子类的访问权限

访问权限修饰符:public、protected、默认修饰符、private

包:package,用来管理Java文件,一个项目中不可避免会出现同名的Java类,为了防止产生冲突,可以把同名的Java类分别放入不同的包中。

包的命名规范:包名由小写字母组成,不能以点(.)开头或者结尾,可以包含数字但不能以数字开头,使用点(.)来分层,包的命名方式一般采用网络域名的反向输出,如com.company.test

同一个类中 不同包 同一个包中 在子类中(同一个包中) 在子类中(不同一个包中)
public 可以访问 可以访问 可以访问 可以访问 可以访问
protected 可以访问 不能访问 可以访问 可以访问 可以访问
默认修饰符 可以访问 不能访问 可以访问 可以访问 不能访问
private 可以访问 不能访问 不能访问 不能访问 不能访问

方法重写vs方法重载

位置:方法重写在子类中对父类方法进行重写,方法重载是在同一个类中。

方法名:方法重写相同,方法重载也相同

参数列表:方法重写相同,方法重载不同

返回值:方法重写相同或是其子类,方法重载没有要求

访问权限:方法重写的访问权限不能小于父类,方法重载没有要求

多态

一个事物具有多种表现形态,在Java程序中,定义一个方法,在具体的生产环境中根据不同的需求呈现不同的业务逻辑,多态的前提是继承

public class Member(){
   
	public void buyBook(){
   
        
    }
}
public class OrdinaryMember extends Member(){
   
	public void buyBook(){
   
		System.out.println("普通会员");
	}
}
public class SuperMember extends Member(){
   
	public void buyBook(){
   
		System.out.println("超级会员");
	}
}
public class Cashier{
   
	private Member member;
	public Member getMember(){
   
		return member;
	}
	public void setMember(Member member){
   
		this.member = member;
	}
	public void settlement(){
   
		this.member.buyBook();
	}
}
public class Test(){
   
	public static void main(String[] args){
   
		OrdinaryMember ordinaryMember  =  new OrdinaryMember();
		SuperMember superMember = new SuperMember();
		Cashier cashier = new Cashier();
		cashier.setMember(superMember);
		cashier.settlement();
	}
}

多态的具体使用由两种形式:

  1. 定义方法时形参类型为父类,实际调用方法时传入子类类型的参数;
  2. 定义方法时返回值类型为父类,实际调用方法时返回子类对象

以上两种形式的基本原理都是父类引用可以指向子类对象

面向对象

三大特征:封装、继承、多态

Object

Object是Java官方提供的类,该类存放在java.lang包中,该类是所有类的直接父类或者间接父类,无论是Java提供的类还是开发者自定义的类,都是Object的直接子类或者间接子类。Java中的任何一个类都会继承Object中的public和protected方法。

hashCode();
getClass();
equals(null);
clone()toString();
notify();
notifyAll();
wait();
wait(1000L);
wait(1000L,100);

Object类中经常被子类重写的方法:

  1. public String toString() 以字符串的形式返回对象的信息
  2. public boolean equals(Object object) 判断两个对象是否相等
  3. public native int hashCode() 返回对象的散列码

包装类

  • 什么是包装类?包装类是Java提供的一组类,专门用来创建八种基本数据类型对应的对象,一共有8个包装类,存放在java.lang包中
  • 基本数据类型对应的包装类:
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

包装类的体系结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVVrVusU-1625626824414)(D:\S\暑假\Java核心基础\image-20210705104230276.png)]

装箱和拆箱

装箱和拆箱是包装类的特有名词,装箱是指将基本数据类型转为对应的包装类对象,拆箱是将包装类对象转为对应的基本数据类型

  1. 装箱

    public Type(type value)

    每个包装类都提供了一个有参构造函数:public Type(type value),用来实例化包装类对象:

    byte b = 1;
    Byte byt = new Byte(b); //通过构造函数来装箱
    
  2. public Type(String value)/public Type(char value)

    每个包装类还有一个重载构造函数,Character类的重载构造函数:public Type(char value),其他包装类的重载构造函数是:public Type(String value)

    Byte byt = new Byte("1");   //public  Type(String value)
    Short shor = new Short("1");    //public  Type(String value)
    Character characte = new Character('1')     //public Type(char value)
    Boolean bool = new Boolean("true")   //public  Type(String value)    ,Boolean中若传非true字符串则全返回false
    

    需要注意的是,Boolean类的构造函数中,当参数为“true”时,Boolean值为true,当参数不为“true”则Boolean值为false

接口

接口是由抽象类衍生出来的一个概念,并由此产生了一种编程方式:面向接口编程。

面向接口编程就是将程序中的业务模块进行分离,以接口的形式对接不同业务模块,目的是为了解耦合。

好处:

  • 当用户需求变更时只需要切换不同的实现类,而不需要修改串联模块的接口以减少对系统的影响

  • 能够最大限度实现解耦合,降低程序的耦合性

  • 使程序易于扩展,有利于后期维护

接口的使用:接口在Java中式独立存在的一种结构,和类相似,需要创建一个接口文件,在Java中用class标识类,用interface来标识接口,基本语法:

public interface 接口名{
   
	public 返回值 方法名(参数列表);
}

只有方法声明,没有方法实现,所以接口其实就是一个抽象类(一个类中一旦存在没有具体实现的方法时,该类就要定义为抽象类,抽象类中允许存在非抽象方法)

接口可以说是一个极度抽象的抽象类,因为接口中不能存在非抽象方法,必须全部为抽象方法

因为接口中必须全部为抽象方法,所以修饰抽象方法的关键字abstract可以省略

接口中允许定义成员变量,但有要求:

  • 不能定义private和protected的成员变量,只能定义public和默认权限的成员变量
  • 接口中的成员变量在定义时必须完成初始化
  • 接口中的成员变量都是静态常量,即可以直接通过接口访问,同时值不能被修改

使用接口时,不能直接实例化接口对象,而必须实例化其实现类对象,实现类本身就是一个普通的Java类,创建实现类的例子:

public class xxxImpl implements xxxInterface{
   
	@Override
	public 返回值类型 xxx(){
   
		
	}
}

通过implements关键字来指定实现类具体要实现的接口,在实现类的内部需要对接口的所以抽象方法进行实现,同时要求访问权限修饰符、返回值类型、方法名和参数列表必须完全一致

接口和继承的区别,Java只支持单继承(一个直接父类),但是接口可以多实现(一个实现类可以同时实现多个接口)

面向接口编程的实际应用

面向接口编程是一种常用的编程方式,可以有效地提高代码的复用性,增强程序的扩展性和维护性

当需求发生改变时,就要频繁修改类的内部结构的方式是需要避免的,因为这种结构的程序扩展性很差,应使用面向接口编程的方式来改进

异常

Java中的错误大致分为两类:一类是编译时错误,一般是语法错误;另一类是运行时错误。

try-catch-finally

try{
   
	//可能会抛出异常的代码
}catch(Exception e){
   
	//对异常进行处理
}finally{
   
	//一定会执行的代码
}

异常类

  • Error:系统错误,程序无法处理
  • Exception:程序运行时出现的错误,程序可以处理

Error和Exception都是Throwable的子类,Throwable、Error、Exception都是存放在java.lang包中

Error常见的子类:

  • VirtualMachineError
    • VirtualMachineError的子类:StackOverflowError、OutOfMemoryError,用来描述内存溢出等系统问题。
  • AWTError
  • IOError

Exception常见的子类:

  • IOException:IOException存放在java.io包中
    • IOException常见的子类:FileLockInterruptionException、FileNotFoundException、FilerException,这些异常都是通常在处理通过IO流进行文件传输的1时候发生的错误
  • RuntimeException:RuntimeException存放在java.lang包中
    • RuntimeException常见的子类:
      • ArithmeticException:表示数学运算异常
      • ClassNotFoundException:表示类未定义异常
      • IllelArgumentException:表示参数格式错误
      • ArrayIndexOutOfBounds:表示数组下标越界
      • NullPointException:表示空指针异常
      • NoSuchMethodException:表示方法未定义异常
      • NumberFormatException:表示将其他数据类型转为数值类型发生的类型不匹配异常

throw和throws

throw和throws是Java在处理异常时常用的两个关键字,都可以用来抛出异常,但是使用的方式和表示的含义完全不同。

Java中抛出异常有3种方式:

  • try-catch

  • throw:使用throw是开发者主动抛出异常,即throw代码就一定抛出异常,基本语法:throw new Exception(),是一种基于代码的逻辑判断而主动抛出异常的方式

    public class Test{
         
    	public static void main(String[] args){
         
    		int[] array = {
         1,2,3};
    		test(array,2);
    	}
    	public static void test(int[] array,int index){
         
    		if(index>3 || index<0){
         
    			try{
         
    				throw new Exception();
    			}catch(Exception e){
         
    				e.printStackTrace();
    			}
    		}else{
         
    			System.out.println(array[index]);
    		}
    	}
    }
    

    try-catch和throw都是作用于具体的逻辑代码,throws是作用于方法的,用来描述方法可能会抛出的异常

  • throws:如果方法throws的是RuntimeException异常或者其子类,外部调用时可以不处理,JVM会处理,如果方法throws的时Exception异常或者其子类,外部调用时一定要处理,否则会报错

异常捕获

  • 自动捕获try-catch
  • throw主动抛出异常
  • throws修饰可能抛出异常的方法

自定义异常

除了使用Java提供的异常外,也可以根据需求来自定义异常。

public class MyNumberException extends RuntimeException{
   
	public MyNumberException(String error){
   
		super(error);
	}
}
public class Test{
   
	public static void main(String[] args){
   
		Test test = new Test();
		test.add("a");
	}
	public int add(Object object){
   
		if(object instanceof Integer){
   
			int num = (int)object;
			return ++num;
		}else{
   
			String error = "传入的参数不是整数类型";
			MyNumberException myNumberException = new MyNumberException(error);
			throw myNumberException;
		}
	}
}

多线程

多线程是提升程序性能的非常重要的一种方式,必须掌握的技术。

使用多线程可以让程序充分利用CPU资源

优点:

  • 让系统资源得到更合理的利用
  • 程序设计更加简洁
  • 程序响应更快,运行效率更高

缺点:

  • 需要更多的内存空间来支持多线程
  • 多线程并发访问可能会影响数据的准确性
  • 数据被多线程共享可能会出现死锁情况

多线程并发—>数据不准确—>线程同步解决—>死锁

进程和线程

进程:计算机正在运行的一个独立的应用程序,进程是一个动态的概念,当启动某一个应用时进程就产生了,当关闭该应用时,进程就结束了,进程的生命周期就是在使用这个应用的整个过程

线程:线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的

应用程序是静态的,进程和线程是动态的,有创建也有销毁,进程和线程的存在是暂时的

进程和线程的区别:

  • 进程在运行时拥有独立的内存空间,即每个进程所占用的内存空间是独立的,互不干扰
  • 线程是共享内存空间的,但每个线程的执行都是相互独立的,单独的线程是无法执行的,也就是说线程依赖于进程,由进程来控制多个线程的执行

多线程

多线程是指在一个进程中多个线程同时执行,这里的同时执行并不是真正意义上的同时执行,系统会给每个线程分配CPU资源,在某个时间片段内CPU会被一个线程占用,在不同的时间段内由不同线程来占用CPU资源,所以多个线程还是在交替执行,只是因为CPU运行速度较快让我们感觉是在同时执行(这里特指单核CPU)

例子:

package com.company;

public class Main {
   

    public static void main(String[] args) {
   
		//开启子线程
        new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 100; i++) {
   
                    System.out.println("张三炒菜!");
                }

            }
        }).start();
        for (int i = 0; i < 100; i++) {
   
            System.out.println("李四炒菜!");
        }

    }
}

Java程序由三部分组成:

  1. JDK系统类库

    JRE:Java Runtime Environment(Java 运行环境),用来运行程序

    JDK:Java Development Kit(Java 开发工具包),用来进行程序开发,也就是说JDK包含了JRE,JDK也包含了JVM

    String、Scanner、包装类……

  2. 第三方类库

    非Java官方的组织提供的成熟的工具,如C3P0数据库连接池、Spring框架、DBUtils、Dom4j等

  3. 开发者自定义代码

    根据具体业务需求编写的业务代码

Java中线程的使用

Java中使用线程有两种方式:

  • 继承Thread类

    1. 创建自定义类并继承Thread类

    2. 重写Thread类的run方法,并编写该线程的业务逻辑代码

    3. 使用:

      Thread1 thread1 = new Thread1();
      Thread2 thread2 = new Thread2();
      thread1.start();
      thread2.start();
      

      注意:不能通过run方法来调用线程的任务,因为run方法调用相当于普通对象的执行,并不会去抢占CPU资源。只有通过start方法才能开启线程,进而去抢占CPU资源,当线程抢占到CPU资源后会自动调用run方法

  • 实现Runnable接口

  1. 创建自定义类并实现Runnable接口

  2. 实现run方法,编写该线程的业务逻辑代码

  3. 使用:

    package com.company;
    
    public class MyRunnable implements Runnable{
         
    
        @Override
        public void run() {
         
            for (int i = 0; i < 1000; i++) {
         
                System.out.println("=============MyRunnable");
            }
        }
    }
    
    package com.company;
    
    public class Main {
         
    
        public static void main(String[] args) {
         
            //可以理解为Runnable为任务,使用Runnable可以将线程和任务分离开
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
        }
    }
    

线程和任务:

线程是抢占CPU资源的,任务是具体执行业务逻辑的,线程内部会包含一个任务,线程启动(start),当抢占到CPU资源后,任务就开始(run)

两种方式的区别:

  1. Thread1,继承Thread类的方式,是直接在类中重写run方法,使用时直接实例化然后start即可,因为Thread内部存在Runnable
  2. MyRunnable,实现Runnable接口的方式,在实现类中重写run方法,使用时需要先创建Thread对象,并将MyRunnable注入到Thread中,然后使用Thread中的start即可

实际开发中推荐第二种即Runnable方式,目的是为了降低耦合度(解耦合直白来说就是把东西分开)

线程的状态

线程共有5种状态,在特定的情况下,线程可以在不同的状态之间切换,5种状态如下所示:

  • 创建状态:实例化一个新的线程对象,还未启动时的状态

  • 就绪状态:创建好的线程对象调用start方法完成启动,这时进入线程池,变成就绪状态,等待抢占CPU资源

  • 运行状态:线程对象获取到CPU资源,在一定的时间内执行任务

  • 阻塞状态:正在运行状态的线程暂停执行任务,释放所占用的CPU资源,在解除阻塞状态后不能回到运行状态,而是重新回到就绪状态等待CPU资源

  • 终止状态:线程执行完毕或因为异常导致终止,线程变成终止状态

线程调度

  • 线程休眠

让当前的线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其他线程,通过sleep()方法来实现

sleep(long millis),调用时需要传入休眠时间,单位为毫秒

package com.company;

public class MyThread extends Thread{
   
    @Override
    public void run(){
   
        for (int i = 0; i < 100; i++) {
   
            if (i==10){
   
                try {
   
                    sleep(5000);
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
            }
            System.out.println(i);
        }
    }
}

package com.company;

public class Main {
   

    public static void main(String[] args) {
   
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

也可以在类的外部调用sleep方法,但需要注意休眠一定要放在启动之前,否则不生效

package com.company;

public class Main {
   

    public static void main(String[] args) throws InterruptedException {
   
        MyThread myThread = new MyThread();
        myThread.sleep(2000);
        myThread.start();

    }
}

要想让主线程休眠,直接通过静态方式调用sleep即可

package com.company;

public class Main {
   

    public static void main(String[] args) throws InterruptedException {
   
        Thread.sleep(2000);

    }
}

sleep方法源码:

public static native void sleep(long millis) throws InterruptedException;

sleep是静态的本地方法,可以通过类调用也可以通过对象调用,抛出InterruptedException,该异常继承自Exception,在外部调用时必须手动处理异常

  • 线程合并

合并是指将指定的某个线程加入到当前线程种,从而合并为一个线程,由两个线程交替执行变成一个线程中的两个子线程顺序执行,通过调用join方法来实现合并

线程A和线程B两个线程,线程A在执行到某个时间点时调用线程B的join方法,则表示从当前时间点开始CPU资源被线程B独占,线程A进入阻塞状态直到线程B执行完毕,线程A再进入就绪状态等待获取CPU资源进入运行状态

package com.company;

public class MyThread extends Thread{
   
    @Override
    public void run(){
   
        for (int i = 0; i < 100; i++) {
   
            System.out.println(i+"+++++++++++++++MyThread1");
        }
    }
}
package com.company;

public class Main {
   

    public static void main(String[] args) throws InterruptedException {
   
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 100; i++) {
   
            if (i==10){
   
                myThread.join();//调用该方法后,CPU资源交给myThread线程使用
            }
            System.out.println(i + "main========");
        }
    }
}

源码:

public final synchronized void join(long millis)
    throws InterruptedException {
   
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
   
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
   
            while (isAlive()) {
   
                wait(0);
            }
        } else {
   
            while (isAlive()) {
   
                long delay = millis - now;
                if (delay <= 0) {
   
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

public final void join() throws InterruptedException {
   
        join(0);
}

join方法重载,join()表示合并进来的线程执行完毕后才能执行其他线程,join(long millis)表示合并进来的进程执行millis毫秒后无论是否执行完毕其他线程都可来争夺CPU资源

线程礼让

线程礼让是指在某个特定的时间点让线程暂停抢占CPU资源的行为,从运行状态/就绪状态——>阻塞状态,将CPU资源让给其他线程来使用

Java中的线程礼让是通过yield来实现

package com.company.yield;

public class YieldThread1 extends Thread{
   
    @Override
    public void run() {
   
        for (int i = 0; i < 10; i++) {
   
            if (i==5||i==6){
   
                //当i==5或6时,线程1做一次礼让
                yield();
            }
            System.out.println(Thread.currentThread().getName()+"-------"+i+"------");
        }
    }
}
package com.company.yield;

public class YieldThread2 extends Thread{
   
    @Override
    public void run() {
   
        for (int i = 0; i < 10; i++) {
   
            System.out.println(Thread.currentThread().getName()+"-------"+i+"------");
        }
    }
}
package com.company.yield;

public class Test {
   
    public static void main(String[] args) {
   
        YieldThread1 yieldThread1 = new YieldThread1();
        YieldThread2 yieldThread2 = new YieldThread2();
        yieldThread2.setName("线程2");
        yieldThread1.setName("线程1");
        yieldThread1.start();
        yieldThread2.start();
    }
}

线程中断

线程中断也是线程礼让的一种方式,有很多种情况会造成线程停止运行:

  • 线程执行完毕自动停止

  • 线程执行过程中遇到错误抛出异常并停止

  • 线程在执行过程中根据需求手动停止(线程中断)

Java中的线程中断实现有如下方法:

  1. public void stop()
  2. public void interrupt()
  3. public boolean isInterrupted()

stop方法在新版本中已经不推荐使用,因为stop方法过于简单粗暴,重点关注后两个方法

interrupt方法是一个实例方法,当一个线程对象调用该方法时表示中断当前线程对象,

每个线程对象都是通过一个标志位来判断当前是否为中断状态

isInterrupted方法就是用来获取当前线程对象的标志位:true表示清除了标志位,当前线程已经中断;false表示没有清除标志位,当前对象没有中断

当一个线程对象处于不同的状态时,中断机制也是不同的

创建状态:实例化线程对象,不启动

package com.company.interrupted;

public class Test {
   
    public static void main(String[] args) {
   
        Thread thread = new Thread();
        System.out.println(thread.getState());
        thread.interrupt();
        System.out.println(thread.isInterrupted());
    }
}

运行结果:

NEW
false

NEW 表示当前线程对象为创建状态,false表示当前线程并未中断,因为当前线程根本没有启动,所以并不存在中断状态,不需要清除标志位

package com.company.interrupted;

public class Test {
   
    public static void main(String[] args) {
   
        Thread thread = new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < 10; i++) {
   
                    System.out.println(i);
                }
            }
        });
        thread.start();
        System.out.println(thread.getState());
        thread.interrupt();
        System.out.println(thread.isInterrupted());
        System.out.println(thread.getState());

    }
}

结果:

RUNNABLE
true
BLOCKED
0
1
2
3
4
5
6
7
8
9

线程同步

Java中允许多线程并行访问,同一时间段内多个线程同时完成各自的操作,多个线程同时操作同一个共享数据时,可能会导致数据不准确的问题。

使用线程同步可以解决上述问题

在Java中通过synchronized关键字(可以修饰实例方法也可以静态方法)修饰方法实现线程同步,原理是每个Java对象都有一个内置锁,内置锁会保护使用synchronized关键字修饰的方法,要调用该方法就必须先获得锁,否则就会处于阻塞状态

public class User implements Runnable {
   
    private static int num;
    @Override
    public synchronized void run() {
   
        //1.num++
        num++;
        //2.休眠1ms
        try {
   
            Thread.currentThread().sleep(1);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        //3.打印输出
        System.out.println(Thread.currentThread().getName() + "是第" + num + "个用户");
    }
}
public class Test {
   
    public static void main(String[] args) {
   
        User user = new User();
        Thread thread1 = new Thread(user,"张三");
        Thread thread2 = new Thread(user, "李四");
        thread1.start();
        thread2.start();
    }
}

synchronized关键字既可以修饰实例方法,也可以修饰静态方法,但两者的使用是有区别的:

synchronized修饰静态方法:

package com.dgut.test;

public class SynchronizedTest {
   
    public static void main(String[] args) {
   
        for (int i = 0; i < 5; i++) {
   
            Thread thread = new Thread(new Runnable() {
   
                @Override
                public void run() {
   
                    SynchronizedTest.test();
                }
            });
            thread.start();
        }
    }


    public static synchronized void test(){
   
        System.out.println("start……");
        try {
   
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        System.out.println("end……");
    }
}

synchronized修饰非静态方法:

package com.dgut.test;

public class SynchronizedTest2 {
   
    public static void main(String[] args) {
   
        for (int i = 0; i < 5; i++) {
   
            Thread thread = new Thread(new Runnable() {
   
                @Override
                public void run() {
   
                    SynchronizedTest2 synchronizedTest2 = new SynchronizedTest2();
                    synchronizedTest2.test();
                }
            });
            thread.start();
        }
    }


    public synchronized void test(){
   
        System.out.println("start……");
        try {
   
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        System.out.println("end……");
    }
}

给实例方法(非静态方法)添加synchronized关键字并不能实现线程同步

线程同步的本质是锁定多个线程所共享的资源,synchronized还可以修饰代码块,会为代码块加上内置锁从而实现同步

package com.dgut.test;

public class SynchronizedTest3 {
   
    public static void main(String[] args) {
   
        for (int i = 0; i < 5; i++) {
   
            Thread thread = new Thread(new Runnable() {
   
                @Override
                public void run() {
   
                    SynchronizedTest3.test();
                }
            });
            thread.start();
        }
    }

    public static void test(){
   
        synchronized (SynchronizedTest3.class) {
   
            System.out.println("start……");
            try {
   
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
   
                e.printStackTrace();
            }
            System.out.println("end……");
        }
    }
}

如何判断线程是否同步?

关键点:锁定的资源在内存中是一份还是多份,一份的话每个线程都要排队等待资源,这时是线程同步,如果是多份则不需要排队等,每个线程各自用自己的资源,这时线程不同步

无论是锁定方法还是对象、类,只需要分析这个方法、对象或者类在内存中有几份即可。

对象一般是多份,类一定是一份,方法要看是静态方法还是非静态方法,静态方法一定是一份,非静态方法一般是多份

线程安全的单例模式

单例模式是一种常见的软件设计模式,核心思想是一个类只有一个实例对象

多线程下的单例模式:

package com.dgut.test.singleton;

public class SingletonDemo {
   
    private static SingletonDemo singletonDemo;

    private SingletonDemo(){
   
        System.out.println("创建Singleton对象");
    }
	//使用synchronized确保线程安全,但由于锁定的是整个方法,所以效率不高
    public synchronized static SingletonDemo getSingletonDemo(){
   
        if (singletonDemo == null){
   
            singletonDemo = new SingletonDemo();
        }
        //其他业务逻辑
        return singletonDemo;
    }

}
package com.dgut.test.singleton;

public class Test2 {
   
    public static void main(String[] args) {
   
        new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                SingletonDemo singletonDemo = SingletonDemo.getSingletonDemo();
            }
        }).start();
        new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                SingletonDemo singletonDemo = SingletonDemo.getSingletonDemo();
            }
        }).start();
    }
}

使用双重检测的方式来优化多线程下的单例模式:

使用synchronized修饰代码块,通过同步代码块的方式来实现

package com.dgut.test.singleton;

public class SingletonDemo {
   
    private static SingletonDemo singletonDemo;

    private SingletonDemo(){
   
        System.out.println("创建Singleton对象");
    }

    public static SingletonDemo getSingletonDemo(){
   
        if (singletonDemo == null){
   
            synchronized (SingletonDemo.class){
   
                if (singletonDemo == null){
   
                    singletonDemo = new SingletonDemo();
                }
            }
        }
        //其他业务逻辑
        return singletonDemo;
    }

}
  1. 线程同步是为了实现线程安全,如果只创建一个对象,那么线程就是安全的
  2. 如果synchronized锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的

使用volatile关键字来优化多线程下的单例模式:

volatile关键字的作用:可以使内存中的数据对线程可见(不需要经过工作内存)

主内存中的数据对线程是不可见的,添加volatile关键字后主内存对线程可见

package com.dgut.test.singleton;

public class SingletonDemo {
   
    //volatile关键字的作用:可以使内存中的数据对线程可见
    private volatile static SingletonDemo singletonDemo;

    private SingletonDemo(){
   
        System.out.println("创建Singleton对象");
    }

    public static SingletonDemo getSingletonDemo(){
   
        if (singletonDemo == null){
   
            synchronized (SingletonDemo.class){
   
                if (singletonDemo == null){
   
                    singletonDemo = new SingletonDemo();
                }
            }
        }
        //其他业务逻辑
        return singletonDemo;
    }

}

死锁 DeadLock

前提:一个线程完成业务需要同时访问两个资源

死锁:多个线程同时在完成业务,这时会出现争抢资源的情况,假如此时一个线程占用了一个资源,而另一个线程占用了另一个资源,此时两个线程都不愿意放弃手中的资源,则会导致程序无法运行下去

package com.dgut.test.lock;

public class Chopsticks {
   

}
package com.dgut.test.lock;

public class DeadLock implements Runnable{
   

    ///编号
    public int num;
    //资源
    private static Chopsticks chopsticks1 = new Chopsticks();
    private static Chopsticks chopsticks2 = new Chopsticks();

    /**
     * num=1的拿到chopsticks1,等待chopsticks2
     * num=2的拿到chopsticks2,等待chopstick1
     */
    @Override
    public void run() {
   
        if (num == 1){
   
            System.out.println(Thread.currentThread().getName() + "拿到了chopsticks1,等待chopsticks2");
            synchronized (chopsticks1){
   
                try {
   
                    Thread.sleep(100);
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
                synchronized (chopsticks2){
   
                    System.out.println(Thread.currentThread().getName() + "用餐完成!");
                }
            }
        }
        if (num == 2){
   
            System.out.println(Thread.currentThread().getName() + "拿到了chopsticks2,等待chopsticks1");
            synchronized (chopsticks2){
   
                try {
   
                    Thread.sleep(100);
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
                synchronized (chopsticks1){
   
                    System.out.println(Thread.currentThread
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zjojk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值