主要包含以下内容:进程与线程的定义、线程的状态、创建线程常用的方法、堆(Heap)栈(Stack)静态区/方法区、值传递和引用传递的区别和联系、构造函数和常量的理解、List、Map、Set存取元素时的特点、String,StringBuffer,StringBuilder的区别、单例模式(Singleton)实现方式
进程与线程
- 进程:执行中的程序,一个进程至少包含一个线程;
- 线程:进程中负责程序执行的执行单元,线程本身依靠程序进行运行,线程是程序中的顺序控制流,只能使用分配给程序的资源和环境;
- 单线程:程序中只存在一个线程,实际上主方法就是一个主线程;
- 多线程:在一个程序中运行多个任务,目的是更好地使用CPU资源。
线程的状态
- 初始状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
下面为线程中的 7 中非常重要的状态:(有的书上也只有认为前五种状态,而将“锁池”和“等待池”都看成是“阻塞”状态的特殊情况,这种认识也是正确的,但是将“锁池”和“等待池”单独分离出来有利于对程序的理解)。
创建线程常用的方法
- 继承Thread类
- 在这子类中重写run方法,在run方法内写线程任务代码
- 创建该子类实例,即是创建了一个线程实例
- 调用该实例的start方法来启动该线程
- 实现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';
}
- 在栈中运行main方法,当jvm看到Person时,会自动把Person类加载到方法区
- 当看到局部变量p1时,会在栈中开辟一块空间
- 当看到new phone()时,会在堆内存中开辟空间,并将堆内存中对应地址0x123赋值给p1
- 在main方法中运行到给对象p1的属性赋值时,通过地址去堆内存中找到相应的属性并赋值
- 当运行方法p1.eat()时,也是根据地址值去堆内存中找到相应的对象,再用对象去方法区中找到eat()方法,然后将eat()方法压到栈中(入栈),调用完毕eat()方法会出栈
- 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的区别
- 可变性
- String类中使用字符数组保存字符串,但因为有“final”修饰符,所以不可变;
- StringBuilder与StringBuffer都继承自AbstractStringBuilder类,AbstractStringBuilder类也是使用字符数组char[]保存字符串,char[]长度可变。
- 线程安全性
- String中的对象是不可变的,也就可以理解为常量,显然线程安全;
- StringBuffer对方法加了Sychronized同步锁或者对调用的方法加了Sychronized同步锁,所以是线程安全的;
- StringBuilder并没有对方法进行加Sychronized同步锁,所以是非线程安全的。
- 使用场景
- 在字符串不经常变化的场景中可以使用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;
}
}