我们在开发过程中肯定会遇到多线程的情景,这篇文章主要来介绍一下关于多线程的知识点。
什么是进程?
操作系统调度的最基本单位,包含有操作系统分配的基本资源。
什么是线程?
CPU调度的最基本单位,不含有任何资源。
线程 VS 进程
- 线程处于进程中,一个进程中可以拥有多个线程,当最后一个线程死亡时,该进程也就死亡。
- 线程不拥有操作系统分配的资源,但在同一个进程中,多个线程可以共享内存。
- 线程不能离开进程单独存在。
并行 VS 并发
我们要记住,这两种情况都是存在多个CPU的情况下
- 并行:指的是在一个 时间点 允许多少个线程的同时执行任务;
- 并发:指的是在一个 时间段 允许多少个线程交替执行任务,通常与吞吐量联系起来
两者的区别,一个是时间点与时间段的区别,另外一个是,并行是真正的多个线程同时执行任务,而并发是交替执行任务。
电脑中核心数的概念?
我们经常可以听到电脑几核几核,指的就是CPU中具有几个线程数可以并行执行任务,近几年,Interl又引入了一个虚拟线程技术,指的就是将1个线程数虚拟成2个线程数,以提高CPU 的使用效率
![](https://img.haomeiwen.com/i11745324/90188c71cf893819.png)
Java中的多线程
开启一个线程的方式
相信很多人对新开一个线程肯定很熟悉,会回答以下 3 种
- 继承 Thread 类,然后调用该线程的 start 方法
class testThreadA extends Thread {
@Override
public void run() {
super.run();
}
}
----------------------------------------------------------------------------------------------
Thread threadA = new testThreadA();
threadA.start();
- 实现 Runnable 接口,在主线程调用 start 方法
class testThreadD implements Runnable{
@Override
public void run() {
System.out.println("this is thread-D");
}
}
----------------------------------------------------------------------------------------------------
Runnable runnable = new testThreadD();
Thread threadD = new Thread(runnable);
threadD.start();
- 实现 Callable 接口,通过 FutureTask 这个类,最后调用 start 方法。这种方式启动线程可以得到在Callable 中的返回值。
class CallThreadE implements Callable<String>{
@Override
public String call() throws Exception {
//在这里返回需要的值
return null;
}
}
----------------------------------------------------------------------------------------------------
CallThreadE callThreadE = new CallThreadE();
FutureTask<String> futureTask = new FutureTask<>(callThreadE);
Thread threadE = new Thread(futureTask);
threadE.start();
try {
//得到返回值
String result = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
而实际上第 3 种启动方式是属于第 2 种启动方式,原因如下:
- 从 Thread 的源码来看,并没有一个构造函数是接收 Callable接口的
Thread的构造函数 从上面可以看出Thread的构造函数只能接收一个Runnable接口。
- 我们刚才定义的这个 CallThreadE 的实例化对象被 FutureTask 接收,通过查看FutureTask的源码发现,FutureTask 实现了RunnableFuture这个接口,而RunnableFuture又继承了Runnable 接口,因此才可以被Thread所接收。
FutureTask
RunnableFuture 因此实际上第 3 种启动线程的方式实际上跟第2种启动方式是一样的。
线程的状态
所谓线程的状态指的就是线程在JVM中的状态,有以下 6 种状态:
![](https://img.haomeiwen.com/i11745324/1620d553ba126d90.png)
- new-初始状态:只是把创建了一个线程,还没有调用start方法
- runnable - 运行状态:其中又进行划分,分为 ready(就绪状态) 以及 running(运行中状态)。当新建完线程之后,调用了start()方法之后进入到 ready(就绪状态) ,处于可运行线程池中,等待分配CPU分配时间片。当被CPU分配到时间片后进入 running 状态。
- blocked-阻塞状态:表示线程阻塞,在这里要记住一点的是,只有使用了 synchronized 关键字的阻塞线程才是阻塞状态。
- waiting-等待状态:进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING-等待超时:与 waiting 不同,处于这个状态的线程会在指定时间后返回。
- TERMINATED-终止:表示该线程已经执行完毕。
在这里有几个问题需要注意一下:
- 只有使用了 synchronized 关键字的线程才有可能进入阻塞状态,其他线程在等待 CPU 分配时间片时只能是等待状态。
- 调用了了start 方法后不一定马上执行线程,只是开始进入就绪状态,什么时候开始线程是由CPU决定。
- 无法确定线程的优先级, 虽然Java中给线程定义了优先级,但在实际应用中有时并无法真正起作用,这是因为假如在在Java 中的线程的级别是1-9,而运行机器的级别只有1-2,这样就会发生级别映射,那么我们在代码中指定线程级别就显得没有意义了。
- CPU在一个时间里面只能有一个线程在跑,在单核cpu里面,也就是cpu给多个线程进行时间片分配,在多个线程间进行来回切换,可以理解为通过线程切换从而达到进程间切换的效果,而实际上进程切换是由操作系统决定的。
- 对于多核CPU,也就是多线程的机器中,并行指的就是多个线程在统一时间点同时工作,执行多个任务。
而并发指的是在一个时间段内执行任务数的多少。
因此,在单核CPU里面,只存在并发的情况,在一段时间段内由CPU 不断在线程之间来回切换。
在多核cpu中,允许并行于并发的情况发生
关于死锁
所谓死锁,指的就是在多线程下(N≥2),竞争多个同步资源(M≥2 且 M≤N)的情况下,而发生的情况。
死锁发生的具有以下 4 个条件:
1)互斥:指的是一个线程拿到资源后,其他资源没办法再继续获得资源,要想获得这个资源只能等待,等到拿到的资源线程释放资源后等待的线程才能拿到资源。
2)资源争夺的顺序不一致:多线程对同步资源的争夺顺序不一致,因为一个线程持有了一个同步资源后不释放,继续请求另一个同步资源时,再次请求的这个同步资源此时被另一个线程所持有。
3)不释放:指的是当某一个线程持有了同步资源后,必须执行完任务后才会释放这个同步资源。
4)请求和保持:指的是线程在已经持有一个同步资源的情况下,不释放此同步资源,同时又去请求新的同步资源。
以下是死锁的一个代码示例:
public static void main(String[] args) throws InterruptedException {
final Boo boo = new Boo();
//定义两把不同的锁(同步资源)
final Object obj13 = new Object();
final Object obj14 = new Object();
final Thread t1 = new Thread() {
public void run() {
//线程一:先请求 obj13这把锁,然后请求objobj14这把锁,释放顺序是反过来的,也就是要先释放obj14
//再释放obj13
synchronized (obj13){
boo.methodA();
synchronized (obj14){
boo.methodB();
}
}
}
};
Thread t2 = new Thread() {
public void run() {
//线程一:先请求 obj14这把锁,然后请求objobj13这把锁,释放顺序是反过来的,也就是要先释放obj13
//再释放obj14
synchronized (obj14){
boo.methodA();
synchronized (obj13){
boo.methodB();
}
}
}
};
t1.setName("thread-111");
t2.setName("thread-222");
t1.start();
t2.start();
}
------------------------------------------------------------------------------------------
class Boo {
public void methodA() {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName() + "正在执行A方法");
Thread.sleep(1000);
System.out.println(t.getName() + "执行A方法完毕");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
public void methodB() {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName() + "正在执行B方法");
Thread.sleep(1000);
System.out.println(t.getName() + "执行B方法完毕");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
发生死锁会带来以下 3 个危害:
- 线程是活着,但不工作了,一直在耗费资源。
- 发生死锁后,没有错误信息,无法找到问题。
- 发生死锁后,程序无法恢复正常,只能重启。
避免死锁的常见算法有:资源分配法,银行家算法。
而打破死锁的只要打破上面所说的任意一个条件就可以了。
- 比如指定同步资源的分配顺序,让同步资源按序分配给线程(让每个线程拿锁的顺序一致)
- 采用尝试拿锁的机制。
关于活锁
所谓活锁,指的就是两个线程在尝试拿锁的时候,每个线程每次拿到的都是同一把锁,而在尝试拿另一把锁的时候总是拿不到,而把自己原来已有的的锁释放的过程。
解决活锁的办法是:
每个线程休眠随机数,错开拿锁的时间。
关于ThreadLocal
当我们需要在线程中设置一个独自的变量时,就可以使用threadLocal,使用了threadLocal 的变量只受当前线程影响,不会被其他线程影响。
public static void main(String[] args) throws InterruptedException {
//ThreadLocal 初始化
final ThreadLocal<String> tl1 = new ThreadLocal<>();
final ThreadLocal<Integer> tl2 = new ThreadLocal<>();
tl1.set("这是:tl1");
tl2.set(0);
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
tl1.set("this is great");
String result = tl1.get();
System.out.println(result);
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
tl1.set("this is fanstastic");
String result = tl1.get();
System.out.println(result);
}
};
t1.start();
t2.start();
}
- 如果在线程外获取ThreadLocal的值,得到的就是初始化时的值。
- 在线程中 对 ThreadLocal set 值后 get 的就是 set的值,当每个线程不一致 set 时,就实现了线程隔离的作用。
- remove 方法:移除(删除)当前线程的局部变量,目的是加快内存的回收速度。这个方法是在 JDK5.0 新增的方法。需要指出的是,当线程结束后,会自动回收局部变量。这个方法不是必须调用的,只是调用了这个方法会加快内存回收。
ThreadLocal 的实现
**如果我们不去看 ThreadLocal 的源码,自己去实现一个 ThreadLocal 来达到变量线程隔离的目标。大多人会选择使用 HashMap 来实现:
public class MyThreadLocal<T> {
//使用一个map来存储每个线程数据
Map<Thread, T> threadMap = new HashMap<>();
public synchronized T get() {
return threadMap.get(Thread.currentThread());
}
public synchronized void setThreadMap(T t) {
threadMap.put(Thread.currentThread(), T);
}
}
使用这种方式看似可以达到线程隔离变量的作用,实际上有以下几个问题:
- 我们需要的是每个线程有各自的变量,而这种方式为了使变量达到同步而加了锁,而恰恰是因为加了锁,导致了每个线程是一种竞争的状态,每个线程在抢着拿锁,而不是每个线程内部持有自己一份变量。
- 同时还有一个问题就是,变量安全问题,指的是一个线程知道了其他线程的key值时,就可以对其变量进行修改。
那接下来我们来看一下JDK中是如何实现的,首先进入到 ThreadLocal 的 get方法:
public T get() {
Thread var1 = Thread.currentThread();
//注意这里也是跟我们刚才想的一样,用当前线程作key,从一个 ThreadLocalMap 里获取值
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
if (var2 != null) {
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
if (var3 != null) {
Object var4 = var3.value;
return var4;
}
}
return this.setInitialValue();
}
然后我们进入到 ThreadLocalMap 中看看是什么:
![](https://img.haomeiwen.com/i11745324/c0e0cadd6f25adb6.png)
因为可能需要隔离多个变量,因此在 ThreadLocalMap 内部使用一个数组来存储。整个 ThreadLocal 的结构:
![](https://img.haomeiwen.com/i11745324/ff721d2cd39f4928.png)
每天进步的你超酷的~
网友评论