线程创建
多线程,也叫做并发编程 JUC
首先理解进程:通俗来讲运行中的程序就叫做进程。例如我们电脑中的APP、编写好的程序在未启动的时候是静态的,并未在运行。而一旦启动,就会开始运行,就可以称为是进程
进程的特征:
动态性:运行中的进程会动态的占有cpu以及内存资源等
独立性:进程与进程之间相互独立,不会产生相互影响
并发性:假设CPU是单核,那么在某一时刻CPU当中其实只有一个程序在运行,CPU会分时的切换为每个进程服务,只是切换的速度很快,给人的感觉就是很多进程在同时被执行,这就是并发性
并行是与并发相关的一个概念:四核CPU中同时有四个进程正在运行,这就被称为并行
线程:一个进程中会至少有一个顺序执行流,每个顺序执行流被称为线程,当有两个或以上的线程时就称为多线程。
理解如:WechatAPP:app运行时便会在内存中开辟空间占用CPU资源,而WeChat运行时不仅要支持发消息还要收消息、更新朋友圈等等,那么就牵扯到多线程,每个线程负责一个功能单元。所以多线程也是具有并发性的。
进程与线程:
- 一个进程可以有多个线程,但至少有一个线程。而一个线程只能在所属进程的内存中活动
- 资源分配给进程:进程中的所有线程共享资源
- CPU分享给线程:即真正在处理器中运行的是线程
- 线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的办法实现同步
线程创建
线程创建三种方式概述:
- 创建一个类并且继承Thread线程类,类中重写run方法(run方法就是此线程的线程主体),创建该类对象,调用start方法启动线程。
- 创建一个类实现Runnable接口,类中重写run方法,创建该类对象,然后将该类对象包装成线程对象,调用线程对象start方法启动线程
- 创建一个类实现Callable接口,将其实现类对象包装成FutureTask对象,再注入线程类对象
注意事项:
start方法不能连续调用,启动线程会失败
不能直接调用run方法,此方法调用不会创建新线程,只是普通方法调用
线程创建方式一
创建步骤:
-
编写一个类,此类继承Thread类
-
重写run方法,此方法是子线程的主体
-
调用star方法启动子线程
class MyThread extends Thread{
//此时创建的叫做线程类 并不是一个线程!
//重写run方法
@Override
public void run() {
System.out.println("子线程名称="+Thread.currentThread().getName()+"子线程ID="+Thread.currentThread().getId());
}
}
public class Demo02 {
//此时的Demo02可以看作是一个进程,而main方法是主线程
public static void main(String[] args) {
System.out.println("主线程名称="+Thread.currentThread().getName()+"主线程ID="+Thread.currentThread().getId());
//创建线程对象:
Thread t = new MyThread();//多态写法
//此时t线程创建完毕 但是并未启动 只有主线程(main线程)在运行,但是子线程并未启动运行
t.start();
//此时子线程启动, 子线程与主线程将会由于线程并发性相互争夺CPU资源,所以如果数据较多时将会发现主线程与子线程的运行是无序的
//或者直接利用对象调用: new MyThread().start();
}
}
/*
start方法的底层:
调用start方法后会在底层执行group.add(this),也就是将当前线程放入争夺CPU资源的组当中去
可以理解为start底层是在CPU中注册此线程并登录运行(触发run方法执行)
由此(方式一)可以得出:
线程类继承Thread,进而无法继承其他类。此处也体现了Java的单继承特性
启动线程必须调用start方法 直接调用run方法则当作普通类处理,则还是只有主线程在运行
多线程是并发执行,在执行过程中各个线程会争夺cpu,进而导致程序执行的随机性
*/
线程创建方式二
创建步骤:
- 创建一个类实现Runnable接口
- 重写run方法
- 创建实现类对象
- 创建线程类对象并将实现类对象注入线程类构造器
- 利用线程类对象调用start方法启动线程
//创建一个类实现Runnable接口
class TestThread implements Runnable{
//重写run方法
@Override
public void run() {
Thread.currentThread().setName("子线程");
System.out.println(Thread.currentThread().getName());
}
}
public class Demo04 {
public static void main(String[] args) {
//创建实现类对象
TestThread testThread = new TestThread();
//创建线程类对象并将实现类对象注入线程类构造器
Thread thread = new Thread(testThread,"子线程1");
//启用子线程
thread.start();
//继续使用同一个实现类对象包装成其他线程
Thread thread1 = new Thread(testThread,"子线程2");
thread1.start();
//方式二的匿名内部类写法:
//匿名内部类写法
new Thread(new TestThread(){
@Override
public void run() {
System.out.println("创建线程方式二写法-->匿名内部类");
}
}).start();//创建并直接启动该线程
}
}
/*
由此(方式二)可以得出:
-- 实现类可以继续实现其他接口,也可以继承其他类,避免了单继承的局限性
-- 线程代码和线程相互独立 线程代码可以被多个线程共享 同一个实现类对象可以被包装成多个线程
-- 适合多个线程共享同一个资源
缺点:
-- 不能直接获取线程返回的结果{
1. 不能抛出异常 因为其所实现的接口并未抛出异常 只能自己处理
2. 没有返回值 当需要获取线程执行后的返回值时需要在外部定义变量 最后在线程中存储并获取
}
*/
线程创建方式三
创建步骤:
- 创建类并实现Callable接口
- 重写call方法
- 创建Callable实现类对象
- 创建FutureTask对象并将Callable实现类对象注入FutureTask构造器
- 创建Thread线程类对象并将FutureTask对象注入线程类构造器
- 利用线程类对象调用start方法启动线程
- 接受Callable实现类的call方法返回值使用FutureTask的get方法即可
//创建Callable实现类
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
//返回值
return Thread.currentThread().getName()+"线程执行结果为="+sum;
}
}
public class Demo05 {
public static void main(String[] args) {
//创建Callable实现类对象
MyCallable myCallable = new MyCallable();
/**
* 为什么要创建FutureTask对象并注入Callable实现类对象?
* 打开FutureTask原码发现:
* public class FutureTask<V> implements RunnableFuture<V>
* Future实现了RunnableFuture接口
* 而RunnableFuture接口实现了Runnable接口
* public interface RunnableFuture<V> extends Runnable, Future<V>
* 综上:由于Callable实现类对象无法直接注入线程类构造器,所以使用了FutureTask类
* 最终将FutureTask对象包装成线程类对象
* FutureTask提供了get方法来获取Callable实现类重写call方法的结果
* 并且在线程执行完毕后可以获取线程的执行结果
*/
//创建FutureTask对象并注入Callable实现类对象
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//将FutureTask对象包装成线程类对象
Thread thread = new Thread(futureTask);
//利用线程类对象启动线程
thread.start();
//此处利用futureTask获取call方法返回的结果
try {
System.out.println(futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Thread类API
- 构造器
-------public Thread(){} //无参构造
-------public Thread(String name){} //注入线程名称
-------public Thread(Runnable target){} //注入Runnable实现类对象
-------public Thread(Runnable target,String name){} //注入Runnable实现类对象及线程名称
- getId():返回此线程的标识符 getName():返回此线程名称 setName():设置此线程名称
- 这三个都是非静态方法
- currentThread():返回对当前正在执行的线程对象的引用:
通俗点说就是通过此方法可以获取当前的线程对象,进而可以直接调用Thread的其他方法
- 可以通过currentThread()静态方法调用getName和getId
用法:
class MyThread1 extends Thread{
@Override
public void run() {
Thread.currentThread().getId();//获取子线程Id
Thread.currentThread().getName();//获取子线程Name
}
}
public class Demo03{
public static void main(String[] args) {
Thread.currentThread().setName("超级线程");
Thread.currentThread().getId();//获取主线程Id
Thread.currentThread().getName();//获取主线程Name 输出---> 超级线程
}
}
/*
Thread.currentThread().getName();放在哪个线程下就返回哪个线程的名称
*/
- 线程休眠静态方法sleep() :使当前线程休眠指定时间(以毫秒为单位)
- 用法:注意异常处理
public class Demo03{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(1000);//每次输出一个数据后休眠一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 通过有参构造器为线程命名,可以不使用setName()
- 用法:
class MyThread1 extends Thread{
//创建子类构造器
public MyThread1(String name) {
super(name);
}
//通过super将名字送上去到父类去接收
//父类的实现:private volatile String name;
@Override
public void run() {
}
}
public class Demo03{
public static void main(String[] args) {
Thread t1 = new MyThread1("子线程是也");
System.out.println(t1.getName());
}
}
线程生命周期
线程状态
- 当线程被创建并启动以后 ,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程
的生命周期中,它要经过新建(New)、就绪(Runnable)、运行 (Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
新建状态
- 当程序使用new关键字创建了 一个线程之后,该线程就处于新建状态,此时它和其他的 Java 对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体 。
就绪状态
-
当线程对象调用了 start()方法之后,该线程处于就绪状态, Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM线程调度器的调度。
-
注意问题
- 需要指出的是,调用了线程的run()方法之后,该线程己经不再处于新建状态,不要再次调用线程对象的 start()方法。
- 启动线程使用start()方法,而不是run()方法!永远不要调用 线程对象的run方法,调用 start()法来启动线程,系统会把该 run()方法当 成线程执行体来处理;但如果直接调用线程对象的 run()方法,则 run()方法立即就会被执行,而且在 run()方法返回之前其他线程无法并发执行一一也就是说,如果直接调用线程对象的 run()方法 ,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法 , 而不是线程执行体 。
public class InvokeRun extends Thread { private int i ; // 重写run方法,run方法的方法体就是线程执行体 public void run() { for ( ; i < 100 ; i++ ) { // 直接调用run方法时,Thread的this.getName返回的是该对象名字, // 而不是当前线程的名字。 // 使用Thread.currentThread().getName()总是获取当前线程名字 System.out.println(Thread.currentThread().getName() + " " + i); // ① } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 调用Thread的currentThread方法获取当前线程 System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20) { // 直接调用线程对象的run方法, // 系统会把线程对象当成普通对象,run方法当成普通方法, // 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法 new InvokeRun().run(); new InvokeRun().run(); } } } }
运行状态
- 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个 CPU , 那么在任何时刻只有一个线程处于运行状态 。 当然,在一个多处理器的机器上,将会有多个线程并行(注意是并行 : parallel ) 执行;当线程数大于处理器数时,依然会存在多个线程在同一个 CPU 上轮换的现象。
阻塞状态
-
在某一时刻某一个线程在运行一段代码的时候,这时候另一个线程也需要运行,但是在运行过程中的那个线程执行完成之前,另一个线程是无法获取到CPU执行权的(调用sleep方法是进入到睡眠暂停状态,但是CPU执行权并没有交出去,而调用wait方法则是将CPU执行权交给另一个线程),这个时候就会造成线程阻塞。
-
如何进入阻塞状态
- 线程调用sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程在等待某个通知 ( notify )。
- 程序调用了线程的 suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
-
如何解除阻塞状态
- 调用 sleep()方法的线程经过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获得了试图取得的同步监视器。
- 线程正在等待某个通知时,其他线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
-
阻塞状态转换图
- 阻塞注意问题
- 线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪状态和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态 的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用 yield()方法可以让运行状态的线程转入就绪状态。
线程死亡
-
线程会以如下三种方式结束,结束后就处于死亡状态 。
- run()或 call()方法执行完成,线程正常结束 。
- 线程抛出 一个未捕获的 Exception 或 Error
- 直接调用该线程的 stop()方法来结束该线程一一该方法容易导致死锁,通常不推荐使用 。
- 不要试图 对一个已经死亡的线程调用 start()方法使它重新启动, 死亡就是死亡 ,该线程将不可再次作为线程执行 。
-
不使用stop方法的原因
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
-
举例
package com.gec.创建线程.stop用法; class MyThread extends Thread { @Override public void run() { try { System.out.println("my thread name="+Thread.currentThread().getName()); //抛出 ThreadDeatherror 的错误 this.stop(); } catch (ThreadDeath e) { System.out.println("进入catch块了"); e.printStackTrace(); } } } public class StopThreadMainTest { public static void main(String[] args) { MyThread t=new MyThread(); t.start(); } }
-
-
使用退出标志退出线程
-
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (!exit){ //do something } } }
-
-
使用退出标志结束线程
public class Demo01 {
//利用退出标志终止子线程
public static void main(String[] args) throws Exception {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//主线程休眠退出抢夺cpu资源队列,资源提供给子线程执行
Thread.sleep(200);
myThread.setFlag(true);//子线程执行完毕后关闭线程
System.out.println("子线程已关闭");
}
}
class MyThread extends Thread{
private boolean flag = false;
public void run(){
while (!flag){
System.out.println("子线程执行体");
}
}
public void setFlag(boolean flag){
this.flag = flag;
}
}