在学习的时候,一定要注意学习顺序,使用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);
}
}
@Override
public 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");
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
- 不能直接捕获的后果、提高健壮性
两种解决方案
- (不推荐):手动在每个run方法里进行try/catch
- (推荐):利用UncaughtExceptionHandler
如下UncaughtExceptionHandler是一个接口 通过uncaughtException() 方法可以拿到线程和异常.
@FunctionalInterface
public 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
public
class ThreadGroup implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
//默认情况下parent是null
if (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;
}
@Override
public 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();
}
@Override
public 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);
}
@Override
public 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 />![image.png](https://cdn.nlark.com/yuque/0/2020/png/375694/1587304853276-63b7748c-5e10-4a2e-b962-cd77a59fb80b.png#align=left&display=inline&height=342&margin=%5Bobject%20Object%5D&name=image.png&originHeight=684&originWidth=1166&size=43497&status=done&style=none&width=583)
错误分析:
下面通过代码,来打印出错误的结果
/**
* 线程安全问题
* 第一种:运行结果出错
* 演示计数不准确(减少),找出具体出错的位置
*/
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());
}
@Override
public 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 12
synchronized (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();
}
@Override
public 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") 就会导致其他线程获取到的是null
System.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 1
if (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;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
class PointMarker extends Thread {
@Override
public 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() {
@Override
public void onEvent(Event event) {
System.out.println("我得到的数字是:" + count);
}
});
//对象还没有初始化完毕 就调用了EventListener 导致获得的count为0
for (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() {
@Override
public 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() {
@Override
public 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() {
@Override
public void onEvent(Event event) {
System.out.println();
System.out.println("我得到的数字是:" + count);
}
};
//对象还没有初始化完毕 就调用了EventListener 导致获得的count为0
for (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() {
@Override
public 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
- 协作:内存同步
- 调度上下文切换