Java 线程创建与常用方法

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

进程与线程的区别

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

并行与并发

单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的 。总结为一句话就是: 微观串行,宏观并行 。一般会将这种 线程轮流使用 CPU 的做法称为并发 (concurrent)

多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

Java 线程

创建和运行线程

  • 直接使用 Thread

    package create;   import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.ThreadCre") public class ThreadCre {     public static void main(String[] args) {          Thread t = new Thread(){             @Override             public void run() {                 log.debug("running");             }         };          t.start();          log.debug("running");      } } 
  • 使用 Runnable 配合 Thread

    package create;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.RunnableCre") public class RunnableCre {     public static void main(String[] args) {         Runnable r = new Runnable() {             @Override             public void run() {                 log.debug("running");             }         };          Thread t = new Thread(r,"t2");          t.start();     } } 

    使用 lambda 方式简化

    package create;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.RunnableCre") public class RunnableCre {     public static void main(String[] args) {         Runnable r = () -> { log.debug("running"); };          Thread t = new Thread(r,"t2");          t.start();     } } 
  • FutureTask 配合 Thread

    package create;  import lombok.extern.slf4j.Slf4j;  import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;  @Slf4j(topic = "c.FutureTaskCre") public class FutureTaskCre {     public static void main(String[] args) throws ExecutionException, InterruptedException {         FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {             @Override             public Integer call() throws Exception {                 log.debug("running...");                 Thread.sleep(1000);                 return 100;             }         });          Thread t = new Thread(task,"t1");         t.start();          log.debug("{}",task.get());     } } 

Thread 与 Runnable 的关系

  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

线程运行的原理

栈与栈帧

每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

常见方法

方法名 static 功能说明 注意
start() 启动一个新线程,在新的线程运行 run 方法中的代码 start 方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU的时间片还没有分给它)。每个线程对象的 start 方法只能调用一次,否则会出现异常
run() 新线程启动后会调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法。但可以创建 Thread 的子类对象来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结果,最多等待 n 毫秒
getId() 获取线程长整型的 id
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判断是否被打断 不会清除 打断标记
isAlive() 线程是否存活(还没有运行完毕)
interrupt() 打断线程 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
interrupted() static 判断当前线程是否被打断 会清除 打断标记
currentThread() static 获取当前正在执行的线程
sleep(long n) static 让当前执行的线程休眠 n 毫秒,休眠时让出 CPU 的时间片给其他程序
yield() static 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试

start 与 run

调用 run

public static void main(String[] args) {  	Thread t1 = new Thread("t1") {  		@Override  		public void run() {  			log.debug(Thread.currentThread().getName());  			FileReader.read(Constants.MP4_FULL_PATH);  		}  	};       	t1.run();  	log.debug("do other things ..."); } 

输出

19:39:14 [main] c.TestStart - main 19:39:14 [main] c.FileReader - read [1.mp4] start ... 19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms 19:39:18 [main] c.TestStart - do other things ... 

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

总结

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

sleep 与 yield

sleep

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行(抢占时间片)
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

join

等待一个线程执行结束

等待多个线程的结果

Java 线程创建与常用方法

情况一:

package testJoin;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.demo1") public class demo1 {      static int r = 0 , r1 = 0 , r2 = 0;      public static void main(String[] args) throws InterruptedException {         test2();     }      private static void test2() throws InterruptedException {         Thread t1 = new Thread(() -> {             try {                 Thread.sleep(1000);                 r1 = 10;             } catch (InterruptedException e) {                 e.printStackTrace();             }         });         Thread t2 = new Thread(() -> {             try {                 Thread.sleep(2000);                 r1 = 20;             } catch (InterruptedException e) {                 e.printStackTrace();             }         });         long start = System.currentTimeMillis();         t1.start();         t2.start();         log.debug("join begin");         t1.join();         log.debug("t1 join end");         t2.join();         log.debug("t2 join end");         long end = System.currentTimeMillis();         log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);     } } 

输出:

14:18:02 [main] c.demo1 - join begin 14:18:03 [main] c.demo1 - t1 join end 14:18:04 [main] c.demo1 - t2 join end 14:18:04 [main] c.demo1 - r1: 20 r2: 0 cost: 2008 

情况二:

package testJoin;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.demo1") public class demo1 {      static int r = 0 , r1 = 0 , r2 = 0;      public static void main(String[] args) throws InterruptedException {         test2();     }      private static void test2() throws InterruptedException {         Thread t1 = new Thread(() -> {             try {                 Thread.sleep(1000);                 r1 = 10;             } catch (InterruptedException e) {                 e.printStackTrace();             }         });         Thread t2 = new Thread(() -> {             try {                 Thread.sleep(2000);                 r1 = 20;             } catch (InterruptedException e) {                 e.printStackTrace();             }         });         long start = System.currentTimeMillis();         t1.start();         t2.start();         log.debug("join begin");         t2.join();         log.debug("t2 join end");         t1.join();         log.debug("t1 join end");         long end = System.currentTimeMillis();         log.debug("r1: {} r2: {} cost: {}",r1,r2,end-start);     } } 

输出:

14:19:19 [main] c.demo1 - join begin 14:19:21 [main] c.demo1 - t2 join end 14:19:21 [main] c.demo1 - t1 join end 14:19:21 [main] c.demo1 - r1: 20 r2: 0 cost: 2006 

另外 join 也可以带参数,是有时效的等待。当到设定时间线程还未给出结果,直接向下运行,不再等待。如果设定时间还没到但是线程已经执行完毕,则直接向下执行,不再等待。

interrupt

打断 sleep,wait,join 的线程

这几个方法都会让线程进入阻塞状态

打断 sleep 的线程, 会清空打断状态,以 sleep 为例

package testInterrupt;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.demo1") public class demo1 {     public static void main(String[] args) throws InterruptedException {         Thread t1 = new Thread(() -> {             log.debug("sleep...");             try {                 Thread.sleep(5000);                 //注意:sleep,wait,join等被打断并以异常形式表现出来后                 // 会把打断标记重新置为 false(未打断状态)             } catch (InterruptedException e) {                 e.printStackTrace();             }         },"t1");          t1.start();         Thread.sleep(1000);         log.debug("interrupt");         t1.interrupt();         log.debug("打断标记:{}",t1.isInterrupted());     } } 

输出:

15:08:12 [t1] c.demo1 - sleep... 15:08:13 [main] c.demo1 - interrupt 15:08:13 [main] c.demo1 - 打断标记:false java.lang.InterruptedException: sleep interrupted 	at java.lang.Thread.sleep(Native Method) 	at testInterrupt.demo1.lambda$main$0(demo1.java:11) 	at java.lang.Thread.run(Thread.java:748)  Process finished with exit code 0 

打断正常运行的线程打断标记置为:true

package testInterrupt;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.demo2") public class demo2 {     public static void main(String[] args) throws InterruptedException {         Thread t1 = new Thread(() -> {             while (true){                 boolean interrupted = Thread.currentThread().isInterrupted();                 if(interrupted){                     log.debug("被打断了,退出循环");                     break;                 }             }         },"t1");         t1.start();          Thread.sleep(1000);         log.debug("interrupt");         t1.interrupt();     } } 

输出:

15:17:40 [main] c.demo2 - interrupt 15:17:40 [t1] c.demo2 - 被打断了,退出循环 

打断 park 线程

package testInterrupt;  import lombok.extern.slf4j.Slf4j;  import java.util.concurrent.locks.LockSupport;  @Slf4j(topic = "c.demo4") public class demo4 {     public static void main(String[] args) throws InterruptedException {         Thread t1 = new Thread(() -> {             log.debug("park...");             LockSupport.park();             log.debug("unpark...");             log.debug("打断状态:{}",Thread.currentThread().isInterrupted());         },"t1");          t1.start();          Thread.sleep(1000);         t1.interrupt();     } } 

输出:

14:16:21 [t1] c.demo4 - park... 14:16:22 [t1] c.demo4 - unpark... 14:16:22 [t1] c.demo4 - 打断状态:true 

两阶段终止模式

Java 线程创建与常用方法

package testInterrupt;  import lombok.extern.slf4j.Slf4j;  @Slf4j(topic = "c.demo3") public class demo3 {     public static void main(String[] args) throws InterruptedException {         TwoPhaseTermination tpt = new TwoPhaseTermination();         tpt.start();         Thread.sleep(3500);         tpt.stop();     } }  @Slf4j(topic = "c.TwoPhaseTermination") class TwoPhaseTermination{     private Thread monitor;      //启动监控线程     public void start(){         monitor = new Thread(() -> {             while (true){                 Thread current = Thread.currentThread();                 if(current.isInterrupted()){                     log.debug("料理后事");                     break;                 }                 try {                     Thread.sleep(1000);//情况1                     log.debug("执行监控记录");//情况2                 } catch (InterruptedException e) {                     e.printStackTrace();                     //重新设置打断标记                     current.interrupt();                 }             }         });          monitor.start();     }      //终止监控线程     public void stop(){          monitor.interrupt();     } } 

输出:

15:33:02 [Thread-0] c.TwoPhaseTermination - 执行监控记录 15:33:03 [Thread-0] c.TwoPhaseTermination - 执行监控记录 15:33:04 [Thread-0] c.TwoPhaseTermination - 执行监控记录 java.lang.InterruptedException: sleep interrupted 	at java.lang.Thread.sleep(Native Method) 	at testInterrupt.TwoPhaseTermination.lambda$start$0(demo3.java:29) 	at java.lang.Thread.run(Thread.java:748) 15:33:04 [Thread-0] c.TwoPhaseTermination - 料理后事  Process finished with exit code 0 

不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行
发表评论

相关文章