在学习的时候,一定要注意学习顺序,使用3W1H的方式进行学习:
What:是什么? Who/When/Where : 应用场景 什么时候使用 在哪里使用 Why : 为什么要使用它 How: 内部原理
线程属性详解
| 属性名称 | 用途 |
|---|---|
| 编号(ID) | 每个线程有自己的ID,用于标识不同的线程.被后续创建的线程使用,唯一性不能保证,不允许被修改. |
| 名称(Name) | 作用让用户或程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等. |
| 是否是守护线程(isDaemon) | true代表该线程是[守护线程],false代表线程是非守护线程[用户线程].守护线程是服务用户线程的,而JVM退出是看用户线程的,如果用户线程都运行完毕了守护线程还有没有运行完毕,这时候JVM退出不会管守护线程是否运行完毕,直接强制退出. |
| 优先级(Priority) | 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对多运行、哪些少运行.默认和父线程的优先级相等,共有10个等级,默认值是5.在实际编码过程中,不推荐修改优先级. |
思考
- 什么时候需要设置守护线程
- 如何应用线程优先级来帮助程序运行?有哪些禁忌
- 不同的操作系统如何处理优先级问题?
线程id
每个线程都有自己的一个id,这个id是不能修改的.main函数就是第一个线程,id是自增的.直接上代码
/*** ID从1开始,JVM运行起来后,自己创建的线程的ID早已不是0*/public class Id {public static void main(String[] args) {Thread thread = new Thread();//主线程ID:1 为什么不是0呢 先++ 变成1了System.out.println("主线程ID"+Thread.currentThread().getId());//?? 子线程ID9 为什么到了ID9了呢?System.out.println("子线程ID"+thread.getId());}}
运行结果: 不知道大家有没有发现,主线的ID不是从0开始的,而且子线程的ID竟然变成了12 不是说好的ID是自增的吗?
不要慌,我们深入源码看一下,看到下面的代码,你肯定明白ID为什么不是从0开始的,因为它是先加1啊,所以主线程的ID就是1.
/* For generating thread ID */private static long threadSeqNumber;private static synchronized long nextThreadID() {return ++threadSeqNumber;}
子线程ID为什么不是2,而是变成了其他的值呢? 我们DEBUG来看一下, 可以看到在启动程序的时候JVM早已经有多个thread初始化了.
有兴趣的可以执行查询了解一下
- “Signal Dispatcher” 负责把操作系统的信号 发给适当的程序的
- “Reference Handler” 和GC引用相关的线程
- “Finalizer” 负责执行对象finalized的方法
也就是说当我们运行main()方法的时候,JVM不止创建了主线程还创建了很多辅助线程,这也就是说明我们创建的子线程的ID为什么变成了12,而不是从2开始.
线程名字
线程是如何命名的呢? 其实从Thread的构造方法中就可以看出来,我们打印出来的线程名称都是“Thread-XXX”对吧,其实还可以通过Thread的构造方法自定义线程的名称或者通过setName()方法自定义线程的名称.
public Thread() {init(null, null, "Thread-" + nextThreadNum(), 0);}/* For autonumbering anonymous threads. */private static int threadInitNumber;private static synchronized int nextThreadNum() {return threadInitNumber++;}public final synchronized void setName(String name) {checkAccess();if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;if (threadStatus != 0) {setNativeName(name);}}
守护线程
守护线程是干什么的? 守护线程就是给用户线程提供服务. 三个特性: 线程类型默认继承自父线程 被谁启动,通常所有的守护线程是由JVM启动 不影响JVM退出,JVM退出只看用户线程,不看守护线程.
守护线程和普通线程的区别
- 整体无区别
- 唯一区别在于JVM的离开
- 守护线程是服务普通线程的[用户线程]
用户线程会影响JVM的退出,而守护线程不会影响JVM退出.JVM退出的时候不会关心守护线程是否关闭了,JVM退出的时候会强制关闭守护线程
一般在开发中是不需要把自己的线程设置为守护线程.
线程优先级
10个级别,默认是5. :::tips 注意:程序设计不应依赖于优先级,因为不同的操作系统不一样,高度依赖操作系统.优先级会被操作系统改变.在实际编码过程中不推荐修改优先级. :::
/*** The minimum priority that a thread can have.*/public final static int MIN_PRIORITY = 1;/*** The default priority that is assigned to a thread.*/public final static int NORM_PRIORITY = 5;/*** The maximum priority that a thread can have.*/public final static int MAX_PRIORITY = 10;
未捕获异常如何处理
思考?
- Java异常体系图
- 在实际工作中,如何全局处理异常?为什么要全局处理?不处理会怎么样?
Java异常体系图 如下:
UncaugheException异常通过UncaughtExceptionHandler处理.
为什么需要UncaughtExceptionHandler?
- 主线程可以轻松发现异常,子线程却不行,即便子线程抛出异常,但是主线程还是在正常运行,无法发现子线程出现异常
通过下面代码演示:
/*** 单线程:抛出 处理 有异常堆栈* 多线程:子线程发生异常 会有什么不同*/public class ExceptionInChildThread implements Runnable{public static void main(String[] args) {new Thread(new ExceptionInChildThread()).start();//对主线程而言 没有任何影响 子线程可以正常打印出异常for (int i = 0; i < 1000; i++) {System.out.println(i);}}@Overridepublic void run() {//线程抛出异常throw new RuntimeException();}}

- 子线程异常无法用传统的方法捕获
如下代码,通过try/catch包裹线程代码,无法捕捉到异常.
package com.prim.threadcoreknowledge.uncaughtexception;/*** 1. 不加try catch抛出4个异常,都带线程名字* 2. 加了try catch 期望捕获到第一个线程的异常,线程234不应该运行,希望看到打印出Caught Exception* 3. 执行结果发现:根本没有Caught Exception 线程234依然运行并且抛出异常* <p>* 说明线程的异常不能用传统方法捕获*/public class CantCatchDirectly implements Runnable {public static void main(String[] args) throws InterruptedException {try {new Thread(new CantCatchDirectly(), "MyThread-1").start();Thread.sleep(300);new Thread(new CantCatchDirectly(), "MyThread-2").start();Thread.sleep(300);new Thread(new CantCatchDirectly(), "MyThread-3").start();Thread.sleep(300);new Thread(new CantCatchDirectly(), "MyThread-4").start();} catch (RuntimeException e) {System.out.println("Caught Exception");}}@Overridepublic void run() {throw new RuntimeException();}}

- 不能直接捕获的后果、提高健壮性
两种解决方案
- (不推荐):手动在每个run方法里进行try/catch
- (推荐):利用UncaughtExceptionHandler
如下UncaughtExceptionHandler是一个接口 通过uncaughtException() 方法可以拿到线程和异常.
@FunctionalInterfacepublic interface UncaughtExceptionHandler {/*** Method invoked when the given thread terminates due to the* given uncaught exception.* <p>Any exception thrown by this method will be ignored by the* Java Virtual Machine.* @param t the thread* @param e the exception*/void uncaughtException(Thread t, Throwable e);}
下面看线程异常处理的调用策略 ThreadGroup
publicclass ThreadGroup implements Thread.UncaughtExceptionHandler {public void uncaughtException(Thread t, Throwable e) {//默认情况下parent是nullif (parent != null) {parent.uncaughtException(t, e);} else {//调用Thread.setDefaultUncaughtExceptionHandler() 方法设置的//全局handler进行处理Thread.UncaughtExceptionHandler ueh =Thread.getDefaultUncaughtExceptionHandler();//如果取到的不为null 就是使用我们实现的接口方法调用if (ueh != null) {ueh.uncaughtException(t, e);} else if (!(e instanceof ThreadDeath)) {//全局 handler也不存在就输出异常栈System.err.print("Exception in thread \""+ t.getName() + "\" ");e.printStackTrace(System.err);}}}}
如何实现呢?
- 给程序统一设置
通过调用Thread的静态方法setDefaultUncaughtExceptionHandler 可以给所有的线程进行设置异常捕捉
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {private String name;public MyUncaughtExceptionHandler(String name) {this.name = name;}@Overridepublic void uncaughtException(Thread t, Throwable e) {Logger anonymousLogger = Logger.getAnonymousLogger();anonymousLogger.log(Level.WARNING, "线程异常,终止了" + t.getName(), e);System.out.println(name + "捕获了异常" + t.getName() + "异常:" + e);}}public class UseUncaughtExceptionHandler implements Runnable {public static void main(String[] args) throws InterruptedException {Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));new Thread(new UseUncaughtExceptionHandler(),"MyThread-1").start();Thread.sleep(300);new Thread(new UseUncaughtExceptionHandler(),"MyThread-2").start();Thread.sleep(300);new Thread(new UseUncaughtExceptionHandler(),"MyThread-3").start();Thread.sleep(300);new Thread(new UseUncaughtExceptionHandler(),"MyThread-4").start();}@Overridepublic void run() {throw new RuntimeException();}}
运行结果如下:捕获到了子线程的异常
UncaughtExceptionHandler 还可以给每个线程单独的设置以及给线程池设置.
- 给每个线程单独设置
-
多线程双刃剑:可能导致安全、性能问题
思考:
一共有哪几类线程安全问题(三种)?
- 哪些场景需要额外注意线程安全问题?
- 什么是多线程的上下文切换?
线程安全
线程安全是并发最重要的部分.什么是线程安全呢? 《Java 并发编程实战》Brian Goetz 对“线程安全”的一个描述: “当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”. 也可以这样理解:“不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全”
什么情况下会出现线程安全问题,怎么避免?
主要有两个问题:
- 数据争用:数据读写由于同时写,会造成错误数据
- 竞争条件 执行顺序:即使不是同时写造成的错误数据,由于顺序原因依然会造成错误,例如在写入前就读取了
- 运行结果错误:a++ 多线程下出现消失的请求现象
如下代码,两个线程同时执行a++的操作,那么理想得到的结果是20000.
public class MultiThreadError8 implements Runnable {int index = 0;static MultiThreadError8 instance = new MultiThreadError8();public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(instance);Thread thread2 = new Thread(instance);thread1.start();thread2.start();thread1.join();//等待线程1执行完毕thread2.join();//等待线程2执行完毕System.out.println(instance.index);}@Overridepublic void run() {for (int i = 0; i < 10000; i++) {index++;}}}
但是得到的实际结果如下: 远远要小于20000,并且每次运行的结果还不一样,这是为什么呢?
i++ 在多个线程下多执行情况如下图分析: 两个线程去执行a++,我们期望的结果是3. 但是实际情况线程1
执行i=1 -> i+1 但是这时候并没有将结果赋值给i,线程2再去执行的时候拿到的结果还是i=1 -> i+1,这时候🈶️切换到线程1,此时线程1不知道线程2的步骤,执行i=2 将i=2写进去了,一旦切换到线程2,而线程2也不知道线程1 的情况,线程2执行i=2.
线程1 线程2<br />
错误分析:
下面通过代码,来打印出错误的结果
/*** 线程安全问题* 第一种:运行结果出错* 演示计数不准确(减少),找出具体出错的位置*/public class MultiThreadsError implements Runnable {int index = 0;static MultiThreadsError instance = new MultiThreadsError();final boolean[] marked = new boolean[10000000];static AtomicInteger realIndex = new AtomicInteger();static AtomicInteger wrongCount = new AtomicInteger();//CyclicBarrier 让线程根据我们的需要在某个地方等待,直到所等待的人员都就绪了,在一起出发 parties :2 表示等待两个线程static volatile CyclicBarrier cyclicBarrier = new CyclicBarrier(2);static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(instance);Thread thread2 = new Thread(instance);thread1.start();thread2.start();thread1.join();//等待线程1执行完毕thread2.join();//等待线程2执行完毕System.out.println(instance.index);System.out.println("realIndex:" + realIndex.get());System.out.println("wrongCount:" + wrongCount.get());}@Overridepublic void run() {marked[0] = true;for (int i = 0; i < 10000; i++) {try {cyclicBarrier2.reset();//标记等待点 当两个线程都运行到这个点之后 一起执行cyclicBarrier.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}index++;try {//等待两个线程都执行完了 index++操作 然后放行cyclicBarrier.reset();cyclicBarrier2.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}realIndex.incrementAndGet();//但是存在问题 如果两个线程都执行完了index++ 操作这时候 synchronized只能有一个线程去执行//第一个线程执行 marked=true 而第二个线程index是一样的 执行的时候marked肯定为true 所以第二个线程肯定会打印//解决问题加入判断:marked[index - 1] 前一位是true//存在没有线程冲突的情况 很可能有没有被标记上。//比如 10 = true 11 = false 12 = true 正常的情况//错误情况 10 = true 11 = true 12synchronized (instance) {if (marked[index] && marked[index - 1]) {System.out.println("发生了错误:" + index);wrongCount.incrementAndGet();}marked[index] = true;}}}}
运行结果如下,可以揪出错误的位置,在哪里发生了错误,更有利于我们找到运行步骤和如何出错的.
- 活跃性问题:死锁 活锁 饥饿
如下代码,会造成死锁的问题synchronized中还有一个synchronized
package com.prim.background;import java.util.Map;/*** 第二个线程安全问题:死锁*/public class MultiThreadError implements Runnable {int flag = 1;static Object o1 = new Object();static Object o2 = new Object();public static void main(String[] args) {MultiThreadError r1 = new MultiThreadError();MultiThreadError r2 = new MultiThreadError();r1.flag = 1;r2.flag = 0;new Thread(r1).start();new Thread(r2).start();}@Overridepublic void run() {System.out.println("flag=" + flag);if (flag == 1) {synchronized (o1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("1");}}}if (flag == 0) {synchronized (o2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1) {System.out.println("0");}}}}}
- 对象发布和初始化的时候的安全问题
- 方法返回一个private对象(private本意是不然外部访问)
- 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界:
- 在构造函数中未初始化完毕就this赋值
- 隐式逸出-注册监听事件
- 构造函数中运行线程
- 方法返回一个private对象(private本意是不然外部访问)
/*** 发布:public return 等* 逸出:1. 方法返回一个private对象(private不然外部对象访问)* 2. 还未完成初始化(构造函数没有完全执行完毕)就把对象提供给外界* 在构造函数中未初始化完毕就this赋值* 隐式逸出 -- 注册监听事件* 构造函数中运行线程*/public class MultiThreadError3 {private Map<String, Object> states;public MultiThreadError3() {states = new HashMap<>();states.put("1", "周一");states.put("2", "周二");states.put("3", "周三");states.put("4", "周四");}public Map<String, Object> getStates() {return states;}public static void main(String[] args) {MultiThreadError3 multiThreadError3 = new MultiThreadError3();Map<String, Object> states = multiThreadError3.getStates();System.out.println(states.get("1"));//篡改了states states本意是private 这样逸出导致了问题的发生states.remove("1");//如果多个线程调用states.get("1") 就会导致其他线程获取到的是nullSystem.out.println(states.get("1"));//null}}
对象初始化导致的线程安全问题:
在构造函数中未初始化完毕就this赋值
public class MultisThreadError4 {static Point point;public static void main(String[] args) throws InterruptedException {new PointMarker().start();//如下 根据线程时间休眠的不同导致结果不同 这也是线程的安全问题// Thread.sleep(10);//1 0// Thread.sleep(105);// 1 1if (point != null) {System.out.println(point);}}}class Point {private final int x, y;public Point(int x, int y) throws InterruptedException {this.x = x;MultisThreadError4.point = this;Thread.sleep(100);this.y = y;}@Overridepublic String toString() {return "Point{" +"x=" + x +", y=" + y +'}';}}class PointMarker extends Thread {@Overridepublic void run() {try {new Point(1, 1);} catch (InterruptedException e) {e.printStackTrace();}}}p
隐式逸出-注册监听事件: 导致的线程安全问题,演示代码如下:
/*** 观察者模式*/public class MultiThreadError5 {int count;public MultiThreadError5(MySource mySource) {mySource.registerListener(new EventListener() {@Overridepublic void onEvent(Event event) {System.out.println("我得到的数字是:" + count);}});//对象还没有初始化完毕 就调用了EventListener 导致获得的count为0for (int i = 0; i < 10000; i++) {System.out.print(i);}count = 100;}public static void main(String[] args) {MySource mySource = new MySource();new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}mySource.eventCome(new Event() {});}}).start();MultiThreadError5 multiThreadError5 = new MultiThreadError5(mySource);}static class MySource {private EventListener eventListener;void registerListener(EventListener eventListener) {this.eventListener = eventListener;}void eventCome(Event e) {if (eventListener != null) {eventListener.onEvent(e);} else {System.out.println("还未初始化完毕");}}}interface EventListener {void onEvent(Event event);}interface Event {}}
如下结果,构造方法还没有初始化完毕,就调用了监听,导致得到的count是0.这也是线程安全问题
构造函数中运行线程
package com.prim.background;import java.util.HashMap;import java.util.Map;public class MultiThreadError6 {private Map<String, Object> states;public MultiThreadError6() {new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}states = new HashMap<>();states.put("1", "周一");states.put("2", "周二");states.put("3", "周三");states.put("4", "周四");}}).start();}public Map<String, Object> getStates() {return states;}public static void main(String[] args) {MultiThreadError6 multiThreadError6 = new MultiThreadError6();try {Thread.sleep(150);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(multiThreadError6.getStates().get("1"));}}
如何解决逸出?
返回”副本”
public class MultiThreadError3 {private Map<String, Object> states;public MultiThreadError3() {states = new HashMap<>();states.put("1", "周一");states.put("2", "周二");states.put("3", "周三");states.put("4", "周四");}public Map<String, Object> getStates() {return states;}/*** 通过副本的方法解决 发布逸出** @return*/public Map<String, Object> getStatesImproved() {return new HashMap<>(states);}public static void main(String[] args) {MultiThreadError3 multiThreadError3 = new MultiThreadError3();System.out.println(multiThreadError3.getStatesImproved().get("1"));multiThreadError3.getStatesImproved().remove("1");System.out.println(multiThreadError3.getStatesImproved().get("1"));}}
工厂模式 注册监听问题修复
将注册监听在初始化完毕后进行注册
public class MultiThreadError7 {int count;public EventListener listener;public MultiThreadError7(MySource mySource) {listener = new EventListener() {@Overridepublic void onEvent(Event event) {System.out.println();System.out.println("我得到的数字是:" + count);}};//对象还没有初始化完毕 就调用了EventListener 导致获得的count为0for (int i = 0; i < 10000; i++) {System.out.print(i);}count = 100;}public static MultiThreadError7 getInstance(MySource source) {//先进行初始化在注册监听MultiThreadError7 multiThreadError7 = new MultiThreadError7(source);source.registerListener(multiThreadError7.listener);return multiThreadError7;}public static void main(String[] args) {MySource mySource = new MySource();new Thread(new Runnable() {@Overridepublic void run() {// try {// Thread.sleep(10);// } catch (InterruptedException e) {// e.printStackTrace();// }mySource.eventCome(new Event() {});}}).start();MultiThreadError7.getInstance(mySource);}static class MySource {private EventListener eventListener;void registerListener(EventListener eventListener) {this.eventListener = eventListener;}void eventCome(Event e) {if (eventListener != null) {eventListener.onEvent(e);} else {System.out.println("还未初始化完毕");}}}interface EventListener {void onEvent(Event event);}interface Event {}}
结果如下,这样就可以在构造函数还没有初始化完毕的时候,不去执行业务逻辑,保证执行的顺序
需要考虑线程安全的情况:
- 访问共享的变量和资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有具有依赖时序性的操作,即使每一步操作都是线程安全的,还存在并发问题:read-modfiy-write(先读取再修改) check-then-act(先检查再执行)
- 不同的数据之间存在捆绑关系的时候(IP和端口号)
- 注意使用其他类的时候,如果对方没有声明是线程安全的,那么大概率会存在并发问题(比如HashMap它不是线程安全的)
性能问题
说到性能问题,首先我们要清楚性能问题有哪些体现、什么性能问题,以及为什么多线程会带来性能问题.
- 性能问题有哪些体现
- 为什么多线程会带来性能问题
- 调度上下文切换
- 什么是上下文? : 保存现场
- 缓存开销 : 缓存失效
- 何时会导致密集的上下文切换:抢锁 、 IO
- 协作:内存同步
- 调度上下文切换
