共计 7605 个字符,预计需要花费 20 分钟才能阅读完成。
多线程可能晋升程序性能,也属于高薪必能核心技术栈,本篇会全面详解 Java 多线程。@mikechen
次要蕴含如下几点:
基本概念
很多人都对其中的一些概念不够明确,如同步、并发等等,让咱们先建设一个数据字典,免得产生误会。
过程
在操作系统中运行的程序就是过程,比方你的 QQ、播放器、游戏、IDE 等等
线程
一个过程能够有多个线程,如视频中同时听声音,看图像,看弹幕,等等。
多线程
多线程:多个线程并发执行。
同步
Java 中的同步指的是通过人为的管制和调度,保障共享资源的多线程拜访成为线程平安,来保障后果的精确。
比方:synchronized 关键字, 在保障后果精确的同时,进步性能,线程平安的优先级高于性能。
并行
\
多个 cpu 实例或者多台机器同时执行一段解决逻辑,是真正的同时。
并发
通过 cpu 调度算法,让用户看上去同时执行,实际上从 cpu 操作层面不是真正的同时。
并发往往在场景中有专用的资源,那么针对这个专用的资源往往产生瓶颈,咱们会用 TPS 或者 QPS 来反馈这个零碎的解决能力。
线程的生命周期
在线程的生命周期中,它要通过新建 (New)、就绪(Runnable)、运行(Running)、阻塞(Blocked) 和死亡(Dead)5 种状态
- 新建状态:当程序应用 new 关键字创立了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
- 就绪状态 :当线程对象调用了 start() 办法之后,该线程处于就绪状态。Java 虚构机会为其创立办法调用栈和程序计数器,期待调度运行
- 运行状态 :如果处于就绪状态的线程取得了 CPU,开始执行 run() 办法的线程执行体,则该线程处于运行状态
- 阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
- 死亡状态 :线程在 run() 办法执行完结后进入死亡状态。此外,如果线程执行了 interrupt()或 stop()办法,那么它也会以异样退出的形式进入死亡状态。
线程状态的管制
\
能够 对照下面的线程状态流转图 来看具体的办法,这样更分明具体作用:
1.start()
启动以后线程, 调用以后线程的 run()办法
2.run()
通常须要重写 Thread 类中的此办法, 将创立的线程要执行的操作申明在此办法中
3.yield()
开释以后 CPU 的执行权
4.join()
在线程 a 中调用线程 b 的 join(), 此时线程 a 进入阻塞状态, 晓得线程 b 齐全执行完当前, 线程 a 才完结阻塞状态
5.sleep(long militime)
让线程睡眠指定的毫秒数,在指定工夫内,线程是阻塞状态
6.wait()
一旦执行此办法,以后线程就会进入阻塞,一旦执行 wait()会开释同步监视器。
7.sleep()和 wait()的异同
相同点:两个办法一旦执行,都能够让线程进入阻塞状态。
不同点:
1) 两个办法申明的地位不同:Thread 类中申明 sleep(),Object 类中申明 wait()
2) 调用要求不同:sleep()能够在任何须要的场景下调用。wait()必须在同步代码块中调用。
2) 对于是否开释同步监视器:如果两个办法都应用在同步代码块呵呵同步办法中,sleep 不会开释锁,wait 会开释锁。
8.notify()
一旦执行此办法,将会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先度最高的。
9.notifyAll()
一旦执行此办法,就会唤醒所有被 wait 的线程。
10.LockSupport
LockSupport.park()和 LockSupport.unpark()实现线程的阻塞和唤醒的。
多线程的 5 种创立形式
1. 继承 Thread 类
package com.mikechen.java.multithread;
/**
* 多线程创立:继承 Thread
*
* @author mikechen
*/
class MyThread extends Thread {
private int i = 0;
@Override
public void run() {for (i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {MyThread myThread=new MyThread();
myThread.start();}
}
2. 实现 Runnable 接口
package com.mikechen.java.multithread;
/**
* 多线程创立:实现 Runnable 接口
*
* @author mikechen
*/
public class MyRunnable implements Runnable {
private int i = 0;
@Override
public void run() {for (i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {Runnable myRunnable = new MyRunnable(); // 创立一个 Runnable 实现类的对象
Thread thread = new Thread(myRunnable); // 将 myRunnable 作为 Thread target 创立新的线程
thread.start();}
}
3. 线程池创立
线程池:其实就是一个能够包容多个线程的容器,其中的线程能够重复的应用,省去了频繁的创立线程对象的操作,无需重复创立线程而耗费过多的系统资源。
package com.mikechen.java.multithread;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* 多线程创立:线程池
*
* @author mikechen
*/
public class MyThreadPool {public static void main(String[] args) {
// 创立带有 5 个线程的线程池
// 返回的实际上是 ExecutorService, 而 ExecutorService 是 Executor 的子接口
Executor threadPool = Executors.newFixedThreadPool(5);
for(int i = 0 ;i < 10 ; i++) {threadPool.execute(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName()+"is running");
}
});
}
}
}
外围参数
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
{....}
线程池工作执行流程
从上图能够看出,提交工作之后,首先会尝试着交给外围线程池中的线程来执行,然而必然外围线程池中的线程数无限,所以必须要由工作队列来做一个缓存,先将工作放队列中缓存,而后期待线程去执行。
最初,因为工作太多,队列也满了,这个时候线程池中剩下的线程就会启动来帮忙外围线程池执行工作。
如果还是没有方法失常解决新到的工作,则线程池只能将新提交的工作交给饱和策略来解决了。
4. 匿名外部类
实用于创立启动线程次数较少的环境,书写更加简便
package com.mikechen.java.multithread;
/**
* 多线程创立:匿名外部类
*
* @author mikechen
*/
public class MyThreadAnonymous {public static void main(String[] args) {// 形式 1:相当于继承了 Thread 类,作为子类重写 run()实现
new Thread() {public void run() {System.out.println("匿名外部类创立线程形式 1...");
};
}.start();
// 形式 2: 实现 Runnable,Runnable 作为匿名外部类
new Thread(new Runnable() {public void run() {System.out.println("匿名外部类创立线程形式 2...");
}
} ).start();}
}
5.Lambda 表达式创立
package com.mikechen.java.multithread;
/**
* 多线程创立:lambda 表达式
*
* @author mikechen
*/
public class MyThreadLambda {public static void main(String[] args) {
// 匿名外部类创立多线程
new Thread(){
@Override
public void run() {System.out.println(Thread.currentThread().getName()+"mikchen 的互联网架构创立新线程 1");
}
}.start();
// 应用 Lambda 表达式,实现多线程
new Thread(()->{System.out.println(Thread.currentThread().getName()+"mikchen 的互联网架构创立新线程 2");
}).start();
// 优化 Lambda
new Thread(()-> System.out.println(Thread.currentThread().getName()+"mikchen 的互联网架构创立新线程 3")).start();}
}
线程的同步
线程的同步是为了避免多个线程拜访一个数据对象时,对数据造成的毁坏,线程的同步是保障多线程平安拜访竞争资源的一种伎俩。
1. 一般同步办法
锁是以后实例对象,进入同步代码前要取得以后实例的锁。
/**
* 用在一般办法
*/
private synchronized void synchronizedMethod() {System.out.println("--synchronizedMethod start--");
try {Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("--synchronizedMethod end--");
}
2. 动态同步办法
锁是以后类的 class 对象,进入同步代码前要取得以后类对象的锁。
/**
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {System.out.println("synchronizedStaticMethod start");
try {Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("synchronizedStaticMethod end");
}
3. 同步办法块
锁是括号外面的对象,对给定对象加锁,进入同步代码库前要取得给定对象的锁。
/**
* 用在类
*/
private void synchronizedClass() {synchronized (SynchronizedTest.class) {System.out.println("synchronizedClass start");
try {Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("synchronizedClass end");
}
}
4.synchronized 底层实现
synchronized 的底层实现是齐全依赖 JVM 虚拟机的, 所以谈 synchronized 的底层实现,就不得不谈数据在 JVM 内存的存储:Java 对象头,以及 Monitor 对象监视器。
1.Java 对象头
在 JVM 虚拟机中,对象在内存中的存储布局,能够分为三个区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
Java 对象头次要包含两局部数据:
1)类型指针(Klass Pointer)
是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
2)标记字段(Mark Word)
用于存储对象本身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标记、线程持有的锁、偏差线程 ID、偏差工夫戳等等, 它是实现轻量级锁和偏差锁的要害.
所以,很显著 synchronized 应用的锁对象是存储在 Java 对象头里的标记字段 里。
2.Monitor
monitor 形容为 对象监视器, 能够类比为一个非凡的房间,这个房间中有一些被爱护的数据,monitor 保障每次只能有一个线程能进入这个房间进行拜访被爱护的数据,进入房间即为持有 monitor,退出房间即为开释 monitor。
下图是 synchronized 同步代码块反编译后的截图,能够很分明的看见 monitor 的调用。
应用 syncrhoized 加锁的同步代码块在字节码引擎中执行时,次要就是通过锁对象的 monitor 的取用 (monitorenter) 与开释 (monitorexit) 来实现的。
多线程引入问题
多线程的长处很显著,然而多线程的毛病也同样显著,线程的应用(滥用)会给零碎带来上下文切换的额外负担, 并且线程间的共享变量可能造成死锁的呈现。
1. 线程平安问题
1)原子性
在并发编程中很多的操作都不是原子操作,比方:
i++; // 操作 2
i = j; // 操作 3
i = i + 1; // 操作 4
在单线程环境中这 3 个操作都不会呈现问题,然而在多线程环境中,如果不通过加锁操作,往往很可能会呈现意料之外的值。
在 java 中能够通过 synchronized 或者 ReentrantLock 来保障原子性。
2)可见性
可见性:指当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻失去这个批改的值。
如上图所示,每个线程都有本人的工作内存,工作内存和主存间要通过 store 和 load 进行交互。
为了解决多线程的可见性问题,java 提供了 volatile 关键字,当一个共享变量被 volatile 润饰时,他会保障批改的值会立刻更新到主存,当有其余线程须要读取时,他会去主存中读取新值,而一般共享变量不能保障其可见性,因为变量被批改后刷回到主存的工夫是不确定的。
2. 线程死锁
线程死锁是指因为两个或者多个线程相互持有对方所须要的资源,导致这些线程处于期待状态,无奈返回执行。
当线程相互持有对方所须要的资源时,会相互期待对方开释资源,如果线程都不被动开释所占有的资源,将产生死锁,如图所示:
举一个例子:
public void add(int m) {synchronized(lockA) { // 取得 lockA 的锁
this.value += m;
synchronized(lockB) { // 取得 lockB 的锁
this.another += m;
} // 开释 lockB 的锁
} // 开释 lockA 的锁
}
public void dec(int m) {synchronized(lockB) { // 取得 lockB 的锁
this.another -= m;
synchronized(lockA) { // 取得 lockA 的锁
this.value -= m;
} // 开释 lockA 的锁
} // 开释 lockB 的锁
}
两个线程各自持有不同的锁,而后各自试图获取对方手里的锁,造成了单方有限期待上来,这就是死锁。
3. 上下文切换
多线程并发肯定会快吗?其实不肯定,因为多线程有线程创立和线程上下文切换的开销。
\
CPU 是很贵重的资源,速度也十分快,为了保障平衡,通常会给不同的线程调配工夫片,当 CPU 从一个线程切换到另外一个线程的时候,CPU 须要保留以后线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等,这个切换称之为上下文切换。
个别缩小上下文切换的办法有:无锁并发编程,CAS 算法,应用协程等形式。
多线程用好了能够成倍的减少效率,用不好可能比单线程还慢。
以上
作者简介
陈睿 |mikechen,10 年 + 大厂架构教训,《BAT 架构技术 500 期》系列文章作者,分享十余年 BAT 架构教训以及面试心得!
浏览 mikechen 的互联网架构更多技术文章合集
Java 并发 |JVM|MySQL|Spring|Redis| 分布式 | 高并发 | 架构师
关注「mikechen 的互联网架构」公众号,回复 【架构】 支付我原创的《300 期 + BAT 架构技术系列与 1000 + 大厂面试题答案》