目录
第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
多线程概述
- 什么是进程?什么是线程?
进程:电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。
线程:线程是一个进程中的执行场景/执行单元。同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。一个进程可以启动多个线程。
- 多线程
那什么是多线程?提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多线程。
所谓串行,其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。
并行:下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。(单核cpu不支持)
多进程是指操作系统能同时运行多个任务(程序)。
并发:同一个微小的时间段(时间片)内,多个时间正在执行
多线程是指在同一程序中有多个顺序流在执行。多线程并发可以提高执行效率。
实现线程的两种方式
-
第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。
//定义线程类
public class MyThread extends Thread{
public void run(){
}
}
//创建线程对象
MyThread t = new MyThread();
//启动线程
t.start();
实现线程的第一种方式: 编写一个类,直接继承java.lang.thread,重写run方法 怎么创建线程对象? new 就行了 怎么启动线程呢? 调用线程对象的start()方法 注意: 亘古不变的原理:方法体当中的代码永远都是自上而下的顺序依次逐行执行的
public class ThreadTest02 {
public static void main(String[] args) {
//这里是main方法,这里的代码属于主线程,在主线中运行。
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
//start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后瞬间就结束了。
//这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
//启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)
//run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。
myThread.start();
// 这里的代码还是运行在主线程中。
for (int i = 0; i < 10000; i++) {
System.out.println("主线程--->"+ i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支线程(分支栈)。
for(int i = 0;i<10000;i++){
System.out.println("分支线程--->" + i);
}
}
}
-
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
- 这种方式使用较多。一个类实现了接口,还可以去继承其他的类,更灵活。
//定义一个可运行的类
public class MyRunnable implements Runnable {
public void run(){
}
}
//创建线程对象
Thread t = new Thread(new MyRunnable());
//启动线程
t.start();
public class ThreadTest03 {
public static void main(String[] args) {
/*创建一个可运行的对象
MyRunnable r = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);*/
Thread t = new Thread(new MyRunnable()); //合并代码
//启动线程
t.start();
for(int i = 0;i<10000;i++){
System.out.println("主线线程--->" + i);
}
}
}
//这并不是一个线程类,是一个可运行的类,它还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0;i<10000;i++){
System.out.println("分支线程--->" + i);
}
}
}
- 采用匿名内部类方式
//创建线程对象,采用匿名内部类方式
Thread t = new Thread(new Runnable(){
@Override
public void run() {
//for()....
}
});
//启动线程
t.start();
线程对象的生命周期
传统线程模型中把线程的生命周期描述为五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
线程调度(了解)
- 抢占式调度模型
哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些 。java采用的就是抢占式调度模型。- 均分式调度模型
平均分配CPU时间片,每个占有的CPU时间片长度一样,平均分配,一切平等。有一些编程语言,线程调度采用的这种方式。- 优先级最低为1,最高为10
- 一个新线程的默认优先级与创建此线程的优先级相同,main方法默认优先级为5
通常推荐设置Thread类的三个优先级常量
int
getPriority()
获取线程的优先级。void
setPriority(int newPriority)
设置线程的优先级。static void
yield() 礼让,抢到的时间片交给其他线程
暂停当前正在执行的线程对象,并执行其他线程。(运行状态---> 就绪状态)void
join()()
合并线程 当前线程进入阻塞,直到其他线程执行结束。
static int
MAX_PRIORITY 最高10
线程可以具有的最高优先级。static int
MIN_PRIORITY 最低1
线程可以具有的最低优先级。static int
NORM_PRIORITY 默认5
分配给线程的默认优先级。
在java.lang.Thread
类内部定义了一个枚举类用来描述线程的六种状态:
枚举中将就绪(New) 与 运行(Running) 状态定义为 Runnable,将阻塞(Blocked) 拆分为三种状态。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
Thread类常用方法
构造方法(Constructor)
|
Thread(Runnable target) 分配新的 Thread 对象。 |
Thread(Runnable target, String name) 分配新的 Thread 对象,并命名。 |
void | setName(String name) 改变线程名称,使之与参数 name 相同。 |
String | getName() 返回该线程的名称。 |
static Thread | currentThread() //this 返回对当前正在执行的线程对象的引用。 |
static void | sleep(long millis) //不释放锁 在指定的毫秒数内让当前正在执行的线程休眠(进入阻塞状态),此操作受到系统计时器和调度程序精度和准确性的影响。 |
void | interrupt() 中断线程睡眠。(依靠了java的异常处理机制) 中断线程,实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。 |
void | setDaemon(boolean on) //GC 垃圾回收器 将该线程标记为守护线程或用户线程。 |
void | setPriority(int newPriority) 更改线程的优先级。 |
static void | yield() 礼让,抢到的时间片交给其他线程 暂停当前正在执行的线程对象,并执行其他线程。(运行状态----> 就绪状态) |
boolean | isAlive() 测试线程是否处于活动状态。 |
void | interrupt() 中断线程。实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。 |
static boolean | interrupted() 检查线程的中断状态,调用此方法会清除中断状态(标记)。 |
boolean | isInterrupted() 检查线程中断状态,不会清除中断状态(标记) |
void | join() 加入线程,等待加入的线程终止后再继续执行当前线程。 |
void | join(long millis) 等待该线程终止的时间最长为 millis 毫秒。 |
部分Object方法
void | wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。 |
void | wait(long timeout, int nanos) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。 |
void | notify() 唤醒在此对象监视器上等待的单个线程。 |
void | notifyAll() 唤醒在此对象监视器上等待的所有线程。 |
获取当前线程对象
1、怎么获取当前线程对象? Thread t = Thread.currentThread(); 静态方法 2、修改、获取线程对象的名字 thread.setName("线程名字") thread.gatName(") 3、线程的默认名称 Thread-0; Thread-1; Thread-2;
public class ThreadTest05 {
public static void main(String[] args) {
//这个代码出现在main方法当中,所当前线程就是主线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()); //main
//创建线程对象
MyThread2 t = new MyThread2();
//启动线程
t.start();
//设置线程名字
t.setName("t1");
//获取线程的名字
System.out.println(t.getName());
MyThread2 t2 = new MyThread2();
t2.start();
t2.setName("t2");
System.out.println(t2.getName());
}
}
class MyThread2 extends Thread{
public void run(){
for (int i = 0; i < 100; i++) {
//currentThread就是当前线程对象 当t1线程执行run方法,当前线程就是t1
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()+"-->"+i);
}
}
}
sleep(睡眠)方法
关于线程的sleep方法 sleep(long millis) 1、静态方法:Thredd.sleep(1000); 2、参数是毫秒 3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用 4、Thredd.sleep()方法,可以做到这种效果: 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次
public class ThreadTest006 {
public static void main(String[] args) {
//让当前线程进入休眠,时间五秒 (当前线程是主线程)
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello World");
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 中断睡眠方法 interrupt()
/*
sleep睡眠太久了,如果希望中途停止,该如何做?
注意:不是中断线程的执行,是终止线程的睡眠。
*/
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnble2());
t.setName("t");
t.start();
//希望五秒之后,t线程醒来(5秒后主线程手里活儿干完了)
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断t线程的睡眠,(这种中断睡眠的方式依靠了java的异常处理机制)
t.interrupt();
}
}
class MyRunnble2 implements Runnable{
//重点:run()当中的异常不能throws,只能try catch
//因为run()方法在父类中没有抛出更多异常,子类不能比父类更多的异常。
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->begin" );
//睡眠一年
try {
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->end");
}
}
提前结束线程
/*
怎么合理的终止一个线程的执行。这种方式是很常用的
*/
public class ThreadTest10 {
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4(); //
Thread t = new Thread(r);
t.setName("t");
t.start();
//模拟五秒
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//想终止t的执行,将标记改为false即可
r.run = false;
}
}
class MyRunnable4 implements Runnable{
//标记
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(run) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//return就结束了,结束之前可以进行保存 save...
return; //终止当前线程
}
}
}
}
Java启动main方法,有几个线程?(扩展)
public static void main(String[] args) {
Map<Thread, StackTraceElement[]> maps = Thread.getAllStackTraces();
for (Thread thread : maps.keySet()) {
System.out.println(thread.getId() + "--" + thread.getName() + "---" + thread.getPriority());
}
}
Reference是java中的引用类,它用来给普通对像进行包装,从而在JVM在GC时,按照引用类型的不同,在回收时采用不同的逻辑。
Finalizer是垃圾回收线程
线程 | 所属 | 说明 |
Attach Listener | JVM | Attach Listener线程是负责接收到外部的命令,而对该命令进行执行的并且吧结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。 |
Signal Dispatcher | JVM | 前面我们提到第一个Attach Listener线程的职责是接收外部jvm命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部jvm命令时,进行初始化工作。 |
CompilerThread0 | JVM | 用来调用JITing,实时编译装卸class。通常,jvm会启动多个线程来处理这部分工作,线程名称后面的数字也会累加,例如:CompilerThread1 |
ConcurrentMark-SweepGCThread | JVM | 并发标记清除垃圾回收器(就是通常所说的CMS GC)线程,该线程主要针对于老年代垃圾回收。ps:启用该垃圾回收器,需要在jvm启动参数中加上:-XX:+UseConcMarkSweepGC |
DestroyJavaVM | JVM | 执行main()的线程在main执行完后调用JNI中的jni_DestroyJavaVM()方法唤起DestroyJavaVM线程。 JVM在Jboss服务器启动之后,就会唤起DestroyJavaVM线程,处于等待状态,等待其它线程(java线程和native线程)退出时通知它卸载JVM。线程退出时,都会判断自己当前是否是整个JVM中最后一个非deamon线程,如果是,则通知DestroyJavaVM线程卸载JVM。ps:扩展一下:1.如果线程退出时判断自己不为最后一个非deamon线程,那么调用thread->exit(false),并在其中抛出thread_end事件,jvm不退出。2.如果线程退出时判断自己为最后一个非deamon线程,那么调用before_exit()方法,抛出两个事件: 事件1:thread_end线程结束事件、事件2:VM的death事件。然后调用thread->exit(true)方法,接下来把线程从active list卸下,删除线程等等一系列工作执行完成后,则通知正在等待的DestroyJavaVM线程执行卸载JVM操作。 |
Container Background Processor | JBOSS | 它是一个守护线程,在jboss服务器在启动的时候就初始化了,主要工作是定期去检查有没有Session过期.过期则清除.参考:http://liudeh-009.iteye.com/blog/1584876 |
ConfigClientNotifier | ConfigServer | ConfigServer服务端当有配置变更时,就会将最新的配置推送到ConfigServer客户端的一个数据列队中,ConfigClientNotifier线程用于定期检查该数据列队中是否有数据,如果有数据,则将数据分发到订阅该数据的组件去做业务逻辑,比如:tair和hsf的数据都订阅了ConfigServer数据源,当ConfigClientNotifier线程发现数据有更新时,就触发做数据分发特定特定信号标识将数据分发到相应的订阅者。 |
ConfigClientWorker-Default | ConfigServer | 包括主动向服务器端发送数据(主要是订阅和发布的数据)和接收服务器推送过来的数据(主要是订阅数据的值)。 |
Dispatcher-Thread-3 | Log4j | Log4j具有异步打印日志的功能,需要异步打印日志的Appender都需要注册到AsyncAppender对象里面去,由AsyncAppender进行监听,决定何时触发日志打印操作。AsyncAppender如果监听到它管辖范围内的Appender有打印日志的操作,则给这个Appender生成一个相应的event,并将该event保存在一个buffuer区域内。 Dispatcher-Thread-3线程负责判断这个event缓存区是否已经满了,如果已经满了,则将缓存区内的所有event分发到Appender容器里面去,那些注册上来的Appender收到自己的event后,则开始处理自己的日志打印工作。Dispatcher-Thread-3线程是一个守护线程。 |
Finalizer线程 | JVM | 这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:1)只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;2)该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;3) JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;4) JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难; |
Gang worker#0 | JVM | JVM用于做新生代垃圾回收(monir gc)的一个线程。#号后面是线程编号,例如:Gang worker#1 |