Java多线程基础

线程

线程是程序执行流的最小单元,可以理解为进程中独立运行的子任务,多线程优点是最大限度的利用 CPU 的空闲时间来处理其他任务。

创建线程

线程的创建方式:

1.继承Thread类

package com.isyxf.testThread; /** * @author xiaofei.yan * @Create 2020-04-01 16:29 * @Descript 我的测试线程 */ class MyThread extends Thread{ @Override public void run() { super.run(); System.out.println("hellow_world"); } }
public class ThreadDemo1 { public static void main(String[] args) { MyThread thread = new MyThread(); // 该方法调用多次,出现 java.lang.IllegalThreadStateException thread.start(); } }

2.实现Runnable接口

package com.isyxf.testThread; /** * @author xiaofei.yan * @Create 2020-04-01 16:37 * @Descript Runnable接口 */ public class MyRunnable implements Runnable { @Override public void run() { System.out.println("通过 Runnable 创建的线程"); } }
package com.isyxf.testThread; /** * @author xiaofei.yan * @Create 2020-04-01 16:39 * @Descript 测试demo2 */ public class ThreadDemo2 { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); } }

上面两种创建方式工作时性质是一样的,但是建议使用实现Runable接口方式,解决单继承的局限性。

线程 run() 和 start() 区别

  • start(): 会创建一个新的子线程并启动,它的内部有调用 run() 方法。(查看线程生命周期)
  • run(): 只是Thread的一个普通方法调用,没有新的线程启动,运行在原来的线程中

线程调用.png

线程运行结果与执行顺序无关

线程的调度是由 CPU 决定,CPU 执行子任务时间就有不确定性。

public class MyThread extends Thread { MyThread(String name) { super(name); System.out.println(name); } @Override public void run() { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } }
public class Main { public static void main(String[] args) { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new MyThread("RandomThread:" + i); } for (Thread thread : threads) { thread.run(); } } }

运行后会得到以下结果,PS: 顺序都是会变的

Connected to the target VM, address: ‘127.0.0.1:3143’, transport: ‘socket’
RandomThread:1
RandomThread:8
RandomThread:2
RandomThread:9
RandomThread:6
RandomThread:5
RandomThread:0
RandomThread:3
RandomThread:4
RandomThread:7
Disconnected from the target VM, address: ‘127.0.0.1:3143’, transport: ‘socket’

如何实现处理线程的返回值

  • 主线程等待方法: 该方式主要是通过 while 循环来阻塞往下执行。
  • 使用Thread类的join(): 把指定的线程加入到当前线程,将两个交替执行的线程合并为顺序执行的线程,比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
  • 通过 Callable 接口实现: 通过FutureTask or 线程池获取

想想通过 join 方法实现等待返回值会有什么缺点?join方法详情

线程实例变量与安全问题

线程之间变量有共享与不共享之分,共享理解为大家都使用同一份,不共享理解为每个单独持有一份。

共享数据情况

public class MyRunnable implements Runnable{ private int count = 5; @Override public void run() { System.out.println("" + Thread.currentThread().getName() + ", count:" + count--); } }
public class Main { public static void main(String[] args) { Runnable runnable = new MyRunnable(); Thread[] threads = new Thread[5]; for (int i = 0; i < 5; i++) { threads[i] = new Thread(runnable, "thread:" + (i+1)); } for (Thread thread : threads) { thread.start(); } } }

执行结果:

Connected to the target VM, address: ‘127.0.0.1:3548’, transport: ‘socket’
thread:1, count:5
thread:1, count:5
thread:2, count:3
thread:5, count:2
thread:3, count:1
Disconnected from the target VM, address: ‘127.0.0.1:3548’, transport: ‘socket’

从上面结果可以看出,count 变量时共享的,不然都会打印5,。但是也发现了一点 thread1 与 thread2 打印值一样,该现象就是我们通常称为的脏数据【多线程对同一变量进行读写操作不同步产生】。

解决方案

在访问变量方法中增加 synchronized 关键字:

public class MyRunnable implements Runnable{ private int count = 5; @Override public synchronized void run() { System.out.println("" + Thread.currentThread().getName() + ", count:" + count--); } }

Connected to the target VM, address: ‘127.0.0.1:3837’, transport: ‘socket’
thread:1, count:5
thread:2, count:4
thread:3, count:3
thread:5, count:2
thread:4, count:1
Disconnected from the target VM, address: ‘127.0.0.1:3837’, transport: ‘socket’

如上面每次打印 count 都是正常递减,含有 synchronized 关键字的这个方法称为 “互斥区” 或 “临界区”,只有获得这个关键字对应的锁才能执行方法体,方法体执行完自动回释放锁。

停止线程

终止正在运行的线程方法有三种:

  • 1)使用退出标志,使线程正常的执行完 run 方法终止。
  • 2)使用 interrupt 方法,使线程异常,线程进行捕获或抛异常,正常执行完 run 方法终止。
  • 3)使用 stop 方法强制退出。

这里介绍前两种方法;

1. 使用退出标志方法

public class MyThread extends Thread { private boolean interrupt = true; public MyThread(String name) { super(name); } @Override public void run() { System.out.println(Thread.currentThread().getName() + ":线程开始运行!"); int i = 0; while (interrupt) { System.out.println("" + (i++)); } System.out.println("我停止了! timer:" + System.currentTimeMillis()); } /** * 停止 */ public void Stop() { System.out.println(Thread.currentThread().getName() + ":线程设置了停止!timer:" + System.currentTimeMillis()); this.interrupt = false; } }
public class Main { public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread("thread_1"); myThread.start(); Thread.sleep(1); myThread.Stop(); } }

运行得到的结果

Connected to the target VM, address: ‘127.0.0.1:4775’, transport: ‘socket’
thread_1:线程开始运行!
0
1
2
3
4
5
6
7
8
9
10
11
main:线程设置了停止!timer:1585757284282
12
我停止了! timer:1585757284283
Disconnected from the target VM, address: ‘127.0.0.1:4775’, transport: ‘socket’

Thread_1 中启动了一个 while 循环,一致打印 i 的累加值,main 线程在 sleep 1ms 后设置 Thread_1 停止标志,Thread_1 while 循环判断条件不符合正常执行完 run 方法结束,从上面可以看出设置完停止标志后 12 还是正常打印,原因是因为 while 方法体中是原子操作,不能直接打断。

2. 使用interrupt方法

package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Descript 继承线程 */ public class InterruptThread extends Thread { public InterruptThread(String name) { super(name); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始!"); for(int i = 0; i < 1000; i++) { try { Thread.sleep(0); System.out.println("" + ( i + 1)); } catch (InterruptedException e) { e.printStackTrace(); System.out.println(Thread.currentThread().getName() + "线程捕获异常,退出循环"); break; } } System.out.println(Thread.currentThread().getName() + "线程结束!"); } }
package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Create 2020-04-02 10:31 * @Descript 测试 */ public class Main { public static void main(String[] args) throws InterruptedException { Thread thread = new InterruptThread("thread_1"); thread.start(); Thread.sleep(1); System.out.println(thread.getName() + "线程设置:interrupt"); thread.interrupt(); } }

运行得到的结果:

thread_1线程开始!
1
2
3
4
thread_1线程设置:interrupt
5
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.isyxf.thread.interrupt.InterruptThread.run(InterruptThread.java:18)
thread_1线程捕获异常,退出循环
thread_1线程结束!

从上面可以看出线程正常退出,但是发现一点循环结构体后面一句打印也打印了,解决这个问题的方法有两个:

1.异常法
package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Descript 继承线程 */ public class InterruptThread extends Thread { public InterruptThread(String name) { super(name); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始!"); try { for (int i = 0; i < 1000; i++) { if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + "线程停止状态"); throw new InterruptedException(); } Thread.sleep(0); System.out.println("" + (i + 1)); } System.out.println(Thread.currentThread().getName() + "线程结束!"); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "线程捕获异常,退出循环"); e.printStackTrace(); } } }

运行后结果:

thread_1线程开始!
1
2
3
4
5
6
thread_1线程设置:interrupt
7
thread_1线程停止状态
thread_1线程捕获异常,退出循环
java.lang.InterruptedException
at com.isyxf.thread.interrupt.InterruptThread.run(InterruptThread.java:20)

代码有两个关键点:

  • for循环捕获异常【这是关键点】
  • 判断设置了 interrupted 标志则抛出异常

2.return 法

package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Descript 继承线程 */ public class InterruptThread extends Thread { public InterruptThread(String name) { super(name); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始"); try { for (int i = 0; i < 1000; i++) { Thread.sleep(0); System.out.println("" + (i + 1)); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "线程捕获异常,退出循环"); e.printStackTrace(); return; } System.out.println(Thread.currentThread().getName() + "线程结束"); } }

这个方法相对简单,也比较常用。两种方法结果都一样直接退出不进行后续工作,两种方法依据功能需求选择。

线程优先级

线程优先级范围为 1-10, API 提供登记分为:低(MIN_PRIORITY = 1),中(NORM_PRIORITY = 5),高(MAX_PRIORITY = 10)

线程优先级有以下特点:

  • 继承特性【线程A中启动线程B,线程B继承了A的优先级】
  • 随机性【线程调度的顺序不一定是根据优先级,具有随机性】
package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Create 2020-04-02 13:54 * @Descript 优先级 */ public class ThreadPriority extends Thread { public ThreadPriority(String name) { super(name); } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("" + Thread.currentThread().getName() + ",number:" + i + ",Priority:" + Thread.currentThread().getPriority()); } } }
package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Create 2020-04-02 10:31 * @Descript 测试 */ public class Main { public static void main(String[] args) throws InterruptedException { Thread thread = new ThreadPriority("thread_1<<<<<"); Thread thread_1 = new ThreadPriority(">>>>thread_2"); thread_1.setPriority(Thread.MIN_PRIORITY); thread.setPriority(Thread.MAX_PRIORITY); thread_1.start(); thread.start(); } }

运行结果

Connected to the target VM, address: ‘127.0.0.1:49862’, transport: ‘socket’
thread_2,number:0,Priority:1
thread_2,number:1,Priority:1
thread_2,number:2,Priority:1
thread_2,number:3,Priority:1
thread_1<<<<<,number:0,Priority:10
thread_1<<<<<,number:1,Priority:10
thread_1<<<<<,number:2,Priority:10
thread_1<<<<<,number:3,Priority:10
Disconnected from the target VM, address: ‘127.0.0.1:49862’, transport: ‘socket’

以上体现了两个问题:

  • ①线程运行顺序与代码执行顺序无关
  • ②线程优先级具有随机性,不是优先级高的就先完成

守护线程

守护线程顾明思义是一个线程守护另一个线程【此线程为非守护线程】,故守护的线程称为守护线程,被守护的线程称为非守护线程,作用是为其线程运行提供遍历服务

package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Create 2020-04-02 14:18 * @Descript 守护线程 */ public class DaemonThread extends Thread { @Override public void run() { while (true) { System.out.println("DaemonThread 正在运行!"); } } }
package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Create 2020-04-02 10:31 * @Descript 测试 */ public class Main { public static void main(String[] args) throws InterruptedException { Thread thread = new DaemonThread(); thread.setDaemon(true); thread.start(); System.out.println("" + Thread.currentThread().getName() + "停止运行"); } }

线程让步

线程让步【yield方法】让当前线程释放CPU资源,让其他线程抢占

package com.isyxf.thread.interrupt; /** * @Descript 线程让步 */ public class YieldThread extends Thread { @Override public void run() { long time_start = System.currentTimeMillis(); for(int i = 0; i < 50000; i++) { Math.random(); Thread.yield(); } long time_end = System.currentTimeMillis(); System.out.println("用时:" + (time_end - time_start)); } }
package com.isyxf.thread.interrupt; /** * @author xiaofei.yan * @Create 2020-04-02 10:31 * @Descript 测试 */ public class Main { public static void main(String[] args) throws InterruptedException { Thread thread = new YieldThread(); thread.start(); } }

不让步

Connected to the target VM, address: ‘127.0.0.1:60154’, transport: ‘socket’
用时:6
Disconnected from the target VM, address: ‘127.0.0.1:60154’, transport: ‘socket’

线程让步

Connected to the target VM, address: ‘127.0.0.1:60603’, transport: ‘socket’
用时:9
Disconnected from the target VM, address: ‘127.0.0.1:60603’, transport: ‘socket’

从上面可以看出,线程让步比不让步耗时很多。

sleep、wait、yield、join区别?

  • sleep: 属于 Thread 类中的,sleep过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的世界到了又会自动恢复运行状态,可中断。sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。
  • yield: 和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程,不会释放资源锁。和 sleep 不同的是 yield 方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需等待重新获取CPU执行时间,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行。和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会。
  • wait: 属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程,wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,不然会报 IllegalMonitorStateException 的异常。
  • join: 等待调用 join() 方法的线程结束后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。

参考文章

使用多线程的几种方式以及对比
Java多线程基础-使用多线程
从源码的角度解析线程池运行原理