多线程基础
一、实现多线程的方法
方法一、实现Runable
接口
public class RunnableStyle implements Runnable {
public static void main(String[] args) {
Thread thread = new Thread(new RunnableStyle());
thread.start();
}
@Override
public void run() {
System.out.println("用Runnable实现线程");
}
}
方法二、继承Thread
类
public class ThreadStyle extends Thread{
@Override
public void run() {
System.out.println("用Thread类实现线程1");
}
public static void main(String[] args) {
new ThreadStyle().start();
new Thread() {
@Override
public void run() {
System.out.println("用Thread类实现线程2");
}
}.start();
}
}
面试题:
有多少种实现多线程的方式:
官方给出的答案是两种,分别是实现`Runnable`接口传入Runnable对象和继承`Thread`类重写类中run方法,
但是从不同的角度看可能会有不同的答案,从原理的角度来看`Thread`类实现Runnable接口,
并且Thread类中的run方法,会发现其中的本质都是一样的,主要是run方法内容的来源
/* What will be run. */
private Runnable target;
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
方法一最终会调用target.run();方法二会重写run()方法,最终都会调用start()方法来新建线程
当然还有其它实现线程的形式,例如线程池,但是仔细看源码本质也是相同的
实现Runnable
接口和继承Thread
类哪种方式更好
实现Runnable接口更好:
-
Java不支持多继承,继承Thread类后就不能再继承其他的类,也就会限制了扩展性
-
从代码的角度:用Runnable方法可以实现解耦,可以让一个单独的任务类实现Runnable接口,然后将对应的实例传入到Thread类就可以。这样同样一个任务类,可以传给不同的Thread,并且任务类也不负责创建线程的工作
-
使用继承Thread类,每次想要新建一个任务,只能新建一个独立的线程,损耗比较大.例:从头开始新建线程,执行完毕再销毁,如果这个线程实际上工作的内容就只是run方法中打印行语句,那么线程实际的工作内容还不如损耗的的大。如果使用Runnable和线程池就能大大减少这种损耗
二、正确的线程启动方式
1、调用start()
方法启动线程
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
2、流程:
-
检查线程状态,只有NEW状态下的线程才能继续,否则会抛出异常IllegalThreadStateException
-
被加入线程组
-
调用本地方法
start0()
;
注意:
start()方法是synchronized
修饰的方法,可以保证线程安全
由JVM创建的main方法栈和System主线程,并不会通过start()方法启动
总结:start() 方法在执行时会先检查当前线程的状态,只有NEW状态下的线程才能够继续,否则会抛出异常
3、面试题:
一个线程调用两次调用start()
方法会发生什么情况?为什么
会抛出`IllegalThreadStateException`异常,因为start()方法在执行会首先对当前线程的状态进行检查,
只有NEW状态下的线程才能继续,如果执行过start方法就会抛出异常
三、如何正确停止线程
原理: 使用interrupt
来通知线程停止运行,而不是强制停止,Java设计的想法是将要停止的线程更加清楚自己应该什么时候停止,interrupt只是通知这个线程停止
想要真正停止一个线程需要要请求方,被停止方,子方法被调用方相互配合才能停止线程
被停止方:每次循环都会或适当时间检查中断信号
请求方:发出中断信号
子方法调用方(被线程调用的方法):优先在方法层面抛出InterruptedException,然后在被停止方处理异常
public class RightWayStopThreadInProd implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
// 保存日志、停止程序
System.out.println("保存日志");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
System.out.println("线程下子程序开始");
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
错误的停止线程的方法:stop(), suspend()方法已经被废弃, volatile的boolean无法处理长时间阻塞情况
四、线程的生命周期
线程具有六种状态:NEW
,RUNNABLE
,BLOCKED
,WAITING
, TERMINATED
,TIMED_WAITING
个个状态之间的转换:
示例代码
/**
* 展示线程的New、Runnable、Terminated状态
* 即使正在运行也是Runnable,而不是Running
*/
public class NewRunnableTerminated implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
Thread thread = new Thread(new NewRunnableTerminated());
// 打印出new的状态
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印出Runnable,即使是正在运行也是Runnable,而不是Running
System.out.println(thread.getState());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getState());
}
}
/**
* 展示线程另外三种状态Blocked, Waiting, TimedWaiting
*/
public class BlockedWaitingTimedWaiting implements Runnable {
@Override
public void run() {
syn();
}
private synchronized void syn() {
try {
Thread.sleep(1000);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
BlockedWaitingTimedWaiting runnable = new BlockedWaitingTimedWaiting();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
Thread.sleep(50);
thread2.start();
// TIMED_WAITING,因为正在执行Thread.sleep(1000);
System.out.println(thread1.getState());
// BLOCKED.thread2想拿到syn锁却拿不到
System.out.println(thread2.getState());
Thread.sleep(1000);
//WAITING, 因为wait();未被唤醒
System.out.println(thread1.getState());
}
}
五、Thread 和 Object 中重要方法
wait()
方法会使线程进入阻塞状态,使用这个方法之后会释放锁
一般习惯而言,把Blocked(被阻塞), Waiting(等待)、TimedWaiting(计时等待)都称为阻塞状态,而非只有Blocked
一个线程调用wait() 方法后进入阻塞状态,在什么情况下会被唤醒
1. 另一个线程调用了这个线程对象的notify()方法
2. 另一个线程调用了这个线程对象的notifyAll()方法
3. 该线程过了wait(long timeout), 规定的时间(传入0表示永久等待)
4. 该等待的过程中线程自身调用了interrupt()方法
唤醒线程
1. notify()
: 唤醒单个线程,如果有多个线程处于等待转态,那么选择唤醒线程是任意的
2. notifyAll()
: 将所有线程唤醒,如果要访问加锁的资源,那么谁获得这把锁是有操作系统调度
sleep()
方法,不占用CPU资源,但不释放自己所持有的锁,会让线程进入waiting状态,所以不会占用CPU资源,直到规定时间之后执行,如果在休眠期被中断,则会抛出InterruptedException异常并清除中断状态
// 在这个例子中线程在休眠期被中断会抛出异常,但是会清除中断继续指向线程
public class SleepInterrupted implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了");
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
join()
:
作用:新的线程加入了我们,所以我们要等到新的线程执行完在后再继续执行
原理:底层相当于当前线程调用wait()方法,又因为JVM在设计每个类在执行完线程run()方法之后会执行类似notify()的类似操作,也就是唤醒当前线程
yield()
:释放当前线程获得的CPU时间片(资源)让给其他线程,但当前线程并不是出于阻塞等待状态,而是处于Runnable状态
yield()
和 sleep()
方法的主要区别在于是否能随时可能再次被调度
面试题:
为什么线程通信的方法wait()
, notify()
, notifyAll()
方法定义在了Object类中,而不是定义在了Thread类中
锁属于对象,每个对象都可以上锁, wait(),notify(),notifyAll()是锁级别的操作,是属于某一个对象的,一个线程可以有多把锁,如果将这些方法定义在Thread类中不灵活
wait
方法是属于Object对象的,如果调用Thread.wait()
方法会怎样
线程退出的时候会默认会执行notify()
方法,这可能会影响自己写的唤醒操作
六、线程的个个属性
-
线程ID:每个线程都有自己的id,用于标识不同的线程,不允许被修改
-
线程名字:让用户或程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题
-
守护线程:true代表该线程线程【守护线程】,false代表线程是非守护线程线程,也就是【用户线程】,给用户线程提供服务
用户线程和守护线程的区别
用户线程退出了,守护线程也会退出,典型的守护线程例如垃圾回收线程
用户线程主要用于处理用户的业务逻辑,守护线程是服务于用户线程的,只有任何非守护线程还在运行,守护线程就不会终止 -
线程优先级:十个级别,默认是5, 1-10;优先级这个属性的存在目的是告诉线程调度
器用户希望哪些线程相对多运行、哪些少运行。 但是编写程序不应该依赖优先级因为不同操
作系统对优先级理解是不一样的优先级会被操作系统改变, 而且线程调度器不能保证优先级高的线程一定先运行,这也就会导致先承认饥饿(优先级高的线程始终得不到运行)的现象
七、多线程会导致的问题
什么是线程安全
无论遇到怎样的多线程访问某对象或某方法的情况,在编写业务逻辑时候都不需要进行额外的处理程序也能正常运行,这种情况下就是线程安全的
有哪些线程安全方面的问题
运行结果错误
还未完成初始化(构造函数为完全执行)就将对象提供给外界
八、死锁
发放生在并发中,互不相让,当两个(或更多)的线程(或进程)相互持有对方所需要的资源,又不主动释放,但是所有的都无法继续执行,也就会导致无尽的阻塞
必然死锁的例子:
public class MustDeadLock implements Runnable {
static Object o1 = new Object();
static Object o2 = new Object();
int flag = 0;
@Override
public void run() {
if (flag == 1) {
System.out.println("flag = " + flag);
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "获得两把锁");
}
}
}
if (flag == 0) {
System.out.println("flag = " + flag);
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "获得两把锁");
}
}
}
}
public static void main(String[] args) {
MustDeadLock m1 = new MustDeadLock();
MustDeadLock m2 = new MustDeadLock();
m1.flag = 1;
m2.flag = 0;
Thread t1 = new Thread(m1);
Thread t2 = new Thread(m2);
t1.start();
t2.start();
}
}
死锁的影响:
死锁的影响在不同的系统中是不一样的,这取决于系统的处理能力
数据库中:检测并放弃事务
JVM:中无法自动处理,有检测的能力,而且压力测试无法检测出所有潜在的死锁
死锁发生的必要条件:
1. 互斥条件
2. 请求与保持条件
3. 不剥夺条件
4. 循环等待条件
如何定位死锁:
1. jstack 运行程序的pid
2. ThreadMXBean检测死锁
线上发生死锁怎么办
1. 保存现场(堆栈信息)然后重启服务器
2. 保证线上服务的安全,利用保存的信息排查死锁,修改代码
常见修复策略
1. 避免策略:哲学家换手,转账换序方案
2. 检测与恢复策略:一段时间检测是否死锁,如果有就剥夺某一资源,来打开死锁
3. 鸵鸟策略:不推荐
实际工程中如何有效避免死锁
-
设置超时时间
-
多使用并发类而不是自己设计锁
-
尽量降低锁的使用粒度:用不同的锁
-
如果能使用同步代码快,就不使用同步方法
-
避免使用锁的嵌套
-
分配资源前先看看能不能回收:银行家算法
-
尽量不要几个功能用同一把锁:专锁专用
活锁:
虽然线程没有阻塞,也始终运行(活),但是程序却得不到进展,因为线程始终重复做同样的事(消耗CPU资源)
死锁和活锁的区别:
死锁,那么两个线程始终不动,一直处于阻塞状态
死锁和活锁的结果是一样的
工程中活锁示例:消息队列
饥饿:
当线程需要某些资源(如CPU),但是却始终得不到。
或线程的优先级设置过低,或者某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件写锁
饥饿可能导致响应性差
大佬 超级赞👍
大佬见笑了,还是感谢您之前给我给我的帮助呢👍
大佬,C++可以处理多线程吗
可以,我对C++不熟悉,你可以上网查一下相关的用法,还有我是小白,距离大佬还很遥远。。。。
好的,谢谢