第一章 快速认识线程
模板设计模式
Thread的run和start就是一个比较典型的模板设计模式,父类编写算法结构代码,子类实现逻辑细节,
示例:
public class TemplatePattern {
// 程序结构由父类控制,并且是final的方法,不允许被重写
public final void print(String message) {
System.out.println("####################");
wrapPrint(message);
System.out.println("####################");
}
/**
* 子类只需要实现想要的逻辑任务即可
*
* @param message
*/
protected void wrapPrint(String message) {
}
public static void main(String[] args) {
TemplatePattern templatePattern = new TemplatePattern() {
@Override
protected void wrapPrint(String message) {
System.out.println("*****" + message + "*****");
}
};
templatePattern.print("Hello");
}
}
策略设计模式
无论是Runnable的run方法,还是Thread类本身的run方法(事实上Thread类也是实现了Runnable接口)都是讲线程的控制本身和业务逻辑的运行分离开来,达到职责分明、功能单一的原则,例如:
下面做一个简单的查询操作,只不过是把查询出来的数据的封装过程抽取成一个策略接口
public interface RowHandler {
T handle(ResultSet resultSet);
}
RowHandler接口只代表一种对结果集的操作,具体要封装成什么样的对象,需要靠我们自己去实现.
public class RecordQuery {
private static String driver = "oracle.jdbc.driver.OracleDriver"; // 驱动
private static String url = "jdbc:oracle:thin:@//localhost:1521/tyrz";
private static String username = "lee";
private static String password = "123";
private Connection getConnection() {
try {
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public T query(RowHandler handler, String sql, String... params) {
Connection connection = getConnection();
try {
PreparedStatement statement = connection.prepareStatement(sql);
for (int index = 0; index < params.length; index++) {
statement.setObject(index, params[index]);
}
ResultSet resultSet = statement.executeQuery();
if (handler != null) {
return handler.handle(resultSet);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
RecordQuery类中的query方法是一个泛型方法,调用此方法需要我们自己传入RowHandler的具体实现类.
这样策略模式的优势就体现出来了,RecordQuery只负责查询,我们根据自己的需求传入具体的结果集(ResultSet)的解读类(RowHandler)就可以了.
纠正"创建线程有两种方式,第一个是构造一个Thread,第二个是实现一个Runnable接口"的错误说法
创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式,一种是重写Thread的run方法,第二种就是实现Runnable接口的run方法,并且将Runnable实例用作构造Thread的参数。
线程的声明周期
每一个线程都有自己的局部变量表、程序计数器,以及生命周期:
线程的生命周期的转换如下图所示:
由上图可见,线程的生命周期大致可分为5个阶段:
- NEW
- RUNNABLE
- RUNNING
- BLOCKED
- TERMINATED
NEW状态
当我们用关键字new创建一个Thread对象的时候,此时它并不处于执行状态,他就像通过 new Object()创建出一个普通的对象没有什么其他的区别;创建了一个普通的对象是一样的,只到调用了start()方法以后,它才进入到Runnable状态
RUNNABLE状态
线程对象进入到Runnable状态,就必须调用start()方法,此时才是真正的在JVM中创建了一个线程,线程进入到Runnable状态,并不是立即运行,线程的运行与否都必须要听令于CPU的调度,也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的调度。
由于存在Running状态,所以Runnable状态的线程不会直接进入到BLOCKED或者TERMINATED状态,即使是在线程的执行逻辑中调用wait,sleep或者取其他操作,也必须先获得CPU的调度执行权才可以,严格的来讲,RUNNABLE线程只能意外终止或者进入到Running状态
RUNNING状态
一旦cpu轮询或者其他方式从任务可执行队列中选中了线程,那么此时他才能真正的执行自己的逻辑代码。在该状态中线程的状态可以发生以下的状态转换:
注:线程执行了sleep或者wait方法就进入到了Blocked(阻塞)状态;调用了stop方法会使线程进入了Terminated状态
BLOCKED状态
线程在BLOCKED状态中可以切换至如下几个状态:
注:线程的状态不能直接从Terminated状态切花到Runnable状态,从阻塞状态先切换到Runnable状态,然后等待CPU调度,获得了执行权以后,才会进入到Running状态。
TERMINATED状态
Terminated状态是线程的最终状态,处于该状态的线程不能切换到其他的任何状态,线程进入到了Termidated状态以后,以为这线程的整个生命周期都结束了,下面这些情况会导致线程进入到Terminated状态:
总结
本章重点介绍了什么是线程,怎么构造一个线程,怎么启动一个线程.还有线程的生命周期,介绍了两个设计模式(见文末的附录).通过static虽然能够处理小范围的多线程数据共享的问题,但是当数据量很大的时候依然避免不了数据共享造成的数据错乱等不安全问题.
第二章 深入理解Thread的构造函数
线程的父子关系
- 一个线程的创建肯定是由另一个线程完成的
- 被创建的线程的父线程就是它的父线程
在没有执行start()方法之前,线程的状态是NEW状态,在构造线程的构造方法中 Thread parent = currentThread(),代表的就是创建它的那个线程.
Thread与ThreadGroup
如果构造线程的时候没有显示的指定ThreadGroup,那么子线程会加入父线程所在的线程组
- main线程所在的线程组为main
- 如果构造一个线程时没有显示的指定其所在的线程组,那么它将会和父线程同属于一个线程组.
Thread与Runnable
Thread负责线程本身相关的职责和控制,而Runnable则负责逻辑执行单元的部分.
jvm内存结构图
程序计数器
- 程序计数器的作用:用于存放当前线程接下来要将要执行的字节码指令、分支、循环、跳转、异常处理等信息
- 线程私有的(在任何时候,一个处理器只执行其中一个线程中的指令,为了能够在CPU时间片轮转切换上下文顺利的回到正确的执行位置,每条线程都需要具有一个独立的程序计数器,各个线程之间互不影响,因此JVM将此块内存设置成了线程私有的)
Java虚拟机栈
与程序计数器相类似,Java虚拟机栈也是线程私有的,他的生命周期与线程相同,是在JVM运行时创建的,在线程中,方法在执行的时候都会创建一个名为栈帧的数据结构,主要用于存贮局部变量表、操作栈、动态链接、方法出口等信息,方法的调用对应着栈帧在虚拟机中的压栈和弹栈过程
由上图可以看出:同等的虚拟机栈,如果局部变量表占用的内存越小,则可被压入的栈帧就会越多,一般将栈帧的内存大小称为宽度,而栈帧的数量则称为虚拟机栈的深度。
本地方法栈
线程私有的内存区域。提供了调用本地方法的接口(Java Native Interface),也就是C/C++程序,在程序执行的过程中,经常会碰到调用JNI方法的情况,比如网络通信,文件操作的底层,JVM为本地方法所划分的内存区域便是本地方法栈,这块内存区域其自由度非常高,完全靠不同的JVM厂商来实现.
堆内存
堆内存是JVM中最大的一块内存区域被所有线程所共享,Java在运行期间所创建的所有对象几乎都存在该内存区域,该内存区域也是垃圾回收器重点照顾的区域
方法区
方法区也是被多个线程所共享的内存区域,它主要用于存贮已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java8元空间
持久代内存被彻底删除,取而代之的是元空间(Meta Space)
总结
程序计数器是线程私有的,作用是:用于存放当前线程接下来将要执行的字节码指令、分支、循环、跳转、异常处理等信息。
Java虚拟机栈是线程私有的,它是一个帧栈的结构,主要用于存放局部变量表、操作栈、动态链接、方法出口等动态信息。
本地方法栈也是线程私有的内存区域,存储JNI方法。
堆内存是线程共享的,java运行期间创建的对象几乎都是存贮在该内存区域,它是垃圾回收器重点照顾的区域。
方法区也是多个线程共享的内存区域,主要用于存贮已经被虚拟机架子啊的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。
Java8以后的元空间是取代持久代内存区域。
堆(堆内存)溢出会出现:java.lang.OutOfMemoryError 错误;栈(Java虚拟机栈)溢出会出现:java.lang.StackOverflowError错误
扩展(Java内存管理机制)
在C++ 语言中,如果需要动态分配一块内存,程序员需要负责这块内存的整个生命周期。从申请分配、到使用、再到最后的释放。这样的过程非常灵活,但是却十分繁琐,程序员很容易由于疏忽而忘记释放内存,从而导致内存的泄露。 Java 语言对内存管理做了自己的优化,这就是垃圾回收机制。 Java 的几乎所有内存对象都是在堆内存上分配(基本数据类型除外),然后由 GC ( garbage collection)负责自动回收不再使用的内存。
上面是Java 内存管理机制的基本情况。但是如果仅仅理解到这里,我们在实际的项目开发中仍然会遇到内存泄漏的问题。也许有人表示怀疑,既然 Java 的垃圾回收机制能够自动的回收内存,怎么还会出现内存泄漏的情况呢?这个问题,我们需要知道 GC 在什么时候回收内存对象,什么样的内存对象会被 GC 认为是“不再使用”的。
Java中对内存对象的访问,使用的是引用的方式。在 Java 代码中我们维护一个内存对象的引用变量,通过这个引用变量的值,我们可以访问到对应的内存地址中的内存对象空间。在 Java 程序中,这个引用变量本身既可以存放堆内存中,又可以放在代码栈的内存中(与基本数据类型相同)。 GC 线程会从代码栈中的引用变量开始跟踪,从而判定哪些内存是正在使用的。如果 GC 线程通过这种方式,无法跟踪到某一块堆内存,那么 GC 就认为这块内存将不再使用了(因为代码中已经无法访问这块内存了)。
通过这种有向图的内存管理方式,当一个内存对象失去了所有的引用之后,GC 就可以将其回收。反过来说,如果这个对象还存在引用,那么它将不会被 GC 回收,哪怕是 Java 虚拟机抛出 OutOfMemoryError 。
扩展(Java内存泄漏)
一般来说,内存泄漏有两种情况:
- 一种情况如在C/C++ 语言中的,在堆中的分配的内存,在没有将其释放掉的时候,就将所有能访问这块内存的方式都删掉(如指针重新赋值)
- 另一种情况则是在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)
第一种情况,在 Java 中已经由于垃圾回收机制的引入,得到了很好的解决。所以, Java 中的内存泄漏,主要指的是第二种情况。
Vector v = new Vector( 10 );
for ( int i = 1 ;i < 100 ; i ++ ){
Object o = new Object();
v.add(o);
o = null ;
}
在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC ,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管 o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
尽管对于C/C++ 中的内存泄露情况来说, Java 内存泄露导致的破坏性小,除了少数情况会出现程序崩溃的情况外,大多数情况下程序仍然能正常运行。但是,在移动设备对于内存和 CPU都有较严格的限制的情况下, Java 的内存溢出会导致程序效率低下、占用大量不需要的内存等问题。这将导致整个机器性能变差,严重的也会引起抛出 OutOfMemoryError ,导致程序崩溃。
Thread与虚拟机栈
Java进程的内存大小 ≈ 堆内存 + 线程数量 * 栈内存
守护线程(Daemon)
守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,比如JDK的垃圾回收线程。
当JVM中只剩下守护线程(Daemon)的时候,则JVM的进程会退出。
守护线程的使用:
守护线程经常用作执行一些后台任务,例如简单的游戏程序中的一个线程不断与服务器进行交互以获取玩家最新的金币、武器等信息,若希望在退出游戏客户端的时候这个线程的工作也停止下来,那么此时就可以考虑使用守护线程。
本章总结:
Thread的构造
线程的父子关系,默认情况下子线程从父线程哪里继承是否守护线程、优先级、ThreadGroup等等的特性
什么是守护线程,以及守护线程的特性和其使用场景
附:
模板设计模式
参考本书第10页
其中Thread的run和start就是一个典型的模板设计模式,父类(Thread类)编写算法结构代码,子类实现逻辑细节
模板设计模式示例:
例如:
public interface RowHandler {
T handle(ResultSet resultSet);
}
该接口只负责对从数据库中查询出来的结果进行操作,至于最终要返回什么样的数据结构,需要自己来实现,这就类似于Runnable接口(线程究竟要执行什么样的内容,是靠自己来实现的)
public class RecordQuery {
private static String driver = "oracle.jdbc.driver.OracleDriver"; // 驱动
private static String url = "jdbc:oracle:thin:@//localhost:1521/tyrz";
private static String username = "lee";
private static String password = "123";
private Connection getConnection() {
try {
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public T query(RowHandler handler, String sql, String... params) {
Connection connection = getConnection();
try {
PreparedStatement statement = connection.prepareStatement(sql);
for (int index = 0; index < params.length; index++) {
statement.setObject(index, params[index]);
}
ResultSet resultSet = statement.executeQuery();
if (handler != null) {
return handler.handle(resultSet);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}
query
方法中传入了RowHandler接口,负责对从数据库中查询出来的数据进行操作,泛型T
决定了返回什么样的数据结构.