Java基础(二)

进程与线程

  1. 进程:执行中的程序,一个进程至少包含一个线程;
  2. 线程:进程中负责程序执行的执行单元,线程本身依靠程序进行运行,线程是程序中的顺序控制流,只能使用分配给程序的资源和环境;
  3. 单线程:程序中只存在一个线程,实际上主方法就是一个主线程;
  4. 多线程:在一个程序中运行多个任务,目的是更好地使用CPU资源。

线程的状态

  1. 初始状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
    下面为线程中的 7 中非常重要的状态:(有的书上也只有认为前五种状态,而将“锁池”和“等待池”都看成是“阻塞”状态的特殊情况,这种认识也是正确的,但是将“锁池”和“等待池”单独分离出来有利于对程序的理解)。

创建线程常用的方法

  1. 继承Thread类
    • 在这子类中重写run方法,在run方法内写线程任务代码
    • 创建该子类实例,即是创建了一个线程实例
    • 调用该实例的start方法来启动该线程
  2. 实现Runnable接口
    • 该类去实现接口的run方法,run方法内写线程任务代码
    • 创建该类实例,把该实例当作一个标记target传给Thread类,如:Thread t = new Thread(该类实例);即创建一个线程对象
    • 调用线程的star方法来启用该线程

继承Thread程序示例

/**
 * 继承Thread类
 * 1.在这子类中重写run方法,在run方法内写线程任务代码
 * 2.创建该子类实例,即是创建了一个线程实例
 * 3.调用该实例的start方法来启动该线程
 */
public class ThreadTest extends Thread {

    @Override
    public void run() {
        System.out.printf("当前线程ID: %s %n", Thread.currentThread().getId());
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            ThreadTest threadTest = new ThreadTest();
            threadTest.start();
        }
    }

}

继承Thread示例输出结果

当前线程ID: 16 
当前线程ID: 13 
当前线程ID: 12 
当前线程ID: 14 
当前线程ID: 15 

实现Runnable程序示例

/**
 * 实现Runnable接口
 * 1.该类去实现接口的run方法,run方法内写线程任务代码
 * 2.创建该类实例,把该实例当作一个标记target传给Thread类,如:Thread t = new Thread(该类实例);即创建一个线程对象
 * 3.调用线程的start方法来启用该线程
 */
public class RunnableTest implements Runnable {

    @Override
    public void run() {
        System.out.printf("当前线程ID: %s %n", Thread.currentThread().getId());
    }

    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(runnableTest);
            thread.start();
        }
    }

}

实现Runnable示例输出结果

当前线程ID: 12 
当前线程ID: 14 
当前线程ID: 13 
当前线程ID: 16 
当前线程ID: 15 

堆(Heap)&栈(Stack)&静态区/方法区

  • 堆区(Heap):
    • 代表数据,只保存对象实体、数组,不保存对象的方法,每个对象实体都包含一个与之对应的class的信息(class的目的是得到操作指令);
    • JVM只有一个堆区,被所有线程共享;
  • 栈区(Stack):
    • 代表处理逻辑,保存对象的地址(引用)和基础数据类型,每个线程包含一个栈区;
    • 每个栈区的数据都是私有的,其他栈不能访问;
    • 栈区分3个部分:基本类型变量区、执行环境上下文、操作指令;
  • 静态区/方法区
    • 被所有线程共享;
    • 只存放整个程序中唯一、不变的元素,如class、static变量;
    • 静态常量放在常量区,初始化后不可更改。

例一

// 创建类
public class Person{
    // 编写属性
    String name;
    int age;
    doubel height;
    // 编写方法
    public void eat(){

    }
} 

// 类的实例化
public static void main(String[] args) {
    Person p1 = new Person();
    p1.name = 'jim':
    Person p2 = new Person():
    p2.name = 'jack';
}

  1. 在栈中运行main方法,当jvm看到Person时,会自动把Person类加载到方法区
  2. 当看到局部变量p1时,会在栈中开辟一块空间
  3. 当看到new phone()时,会在堆内存中开辟空间,并将堆内存中对应地址0x123赋值给p1
  4. 在main方法中运行到给对象p1的属性赋值时,通过地址去堆内存中找到相应的属性并赋值
  5. 当运行方法p1.eat()时,也是根据地址值去堆内存中找到相应的对象,再用对象去方法区中找到eat()方法,然后将eat()方法压到栈中(入栈),调用完毕eat()方法会出栈
  6. main方法运行结束后也会出栈

例二

关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤: 
(1)先定义一个名为str的对String类的对象引用变量:String str; 
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,
接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁
边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。 
(3)将str指向对象o的地址。 
值得注意的是,一般String类中字符串值都是直接存值的。
但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 
            
int a = 3; 
int b = 3; 
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,
没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的
引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

值传递和引用传递的区别和联系

  • 值传递(形式参数类型是基本数据类型)
    • 只要是基本类型传递,都是值传递;
    • 方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参数值的改变不影响实际参数的值。
  • 引用传递(形式参数类型是引用数据类型)
    • 也称为地址传递,针对于基本类型进行封装(比如对象),对封装(对象)进行传递,是引用传递。
    • 方法调用时,实际参数是对象(或数组),这时实际参数与形式参数指向同一个地址,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,这个结果在方法结束后被保留了下来,所以方法执行中形式参数的改变将会影响实际参数。

构造函数&常量的理解

  • 构造函数:是类在实例化成对象时用来做一些事情的,而这些事情是该对象被创建时必须做的事。例如初始化属性,但不限于此。另外我们可以对构造函数进行重载,是让我们在类的实例化时能够更多元化。
  • 常量:简单地说,用final修饰过的变量就叫常量,常量一旦定义了就不允许被修改。往大的说,定义常量,是不想让某些固定的属性或方法被调用后改变了值,或者被继承后重写。往底层说,常量存放在常量池里,在类加载之前就已经被加载,且不会改变

List、Map、Set存取元素时的特点

  • List
    • 以特定次序来持有元素,可有重复元素。即,有序可重复。
    • 访问时可以使用for循环,foreach循环,iterator迭代器迭代。
  • Set
    • Set 无法拥有重复元素,内部排序。即,无序不可重复。
    • 访问时可以使用foreach循环,iterator迭代器迭代。
  • Map
    • Map保存 key-value 值,一一映射。key值 是无序,不重复的,value值可重复。
    • 访问时可以map中key值转为set存储,然后迭代这个set,用map.get(key)获取value。

String,StringBuffer,StringBuilder的区别

  1. 可变性
    • String类中使用字符数组保存字符串,但因为有“final”修饰符,所以不可变;
    • StringBuilder与StringBuffer都继承自AbstractStringBuilder类,AbstractStringBuilder类也是使用字符数组char[]保存字符串,char[]长度可变。
  2. 线程安全性
    • String中的对象是不可变的,也就可以理解为常量,显然线程安全;
    • StringBuffer对方法加了Sychronized同步锁或者对调用的方法加了Sychronized同步锁,所以是线程安全的;
    • StringBuilder并没有对方法进行加Sychronized同步锁,所以是非线程安全的。
  3. 使用场景
    • 在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算;
    • 在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装;
    • 在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼装、JSON封装等。

单例模式(Singleton):饿汉式、懒汉式

  • 饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变;
  • 懒汉式只有在调用getInstance方法时才会创建该类的对象。
// 饿汉
public class HungerSingleton {

    private static final HungerSingleton hungerSingleton = new HungerSingleton();

    HungerSingleton () {};

    public static HungerSingleton getInstance() {
        return hungerSingleton;
    }

}

// 懒汉
public class LazySingleton {

    private static LazySingleton instance = null;

    LazySingleton() {};

    public static synchronized LazySingleton getInstance() {
        return instance == null ? new LazySingleton() : instance;
    }

}
posted @ 2019-11-27 01:48  秋裤队长  阅读(200)  评论(0编辑  收藏  举报