Java基础增强-2 并发编程

1. 多线程基本知识

1.1 多线程运行的原理

原理:CPU 在线程中做时间片的切换。

一个(多核中的一个) CPU 在在运行程序的过程中某个时刻点上只能运行一个程序。而 CPU 可以在 多个程序之间进行高速的切换 (轮询制)。而切换频率和速度太快,导致人的肉眼看不到。

1.2 实现线程的两种方式

  1. 继承 Thread
  2. 声明实现Runnable接口
  3. 还可以实现Callable接口

1.3 线程的状态图解

Attachment.jpeg

  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。该状态 的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。
  3. 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。 直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    • 等待阻塞:运行的线程执行 wait()方法,JVM 会把该线程放入等待池中。(wait 会释 放持有的锁)
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把 该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕 时,线程重新转入就绪状态。(注意:sleep 是不会释放持有的锁)
  5. 死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

1.4 几个重要的方法的区别:

sleep(timeout):当前线程进入阻塞状态,暂停执行一定时间,不会释放锁标记

join():join()方法会使当前线程等待调用 join()方法的线程结束后才能继续执行

yield():调用该方法的线程重回可执行状态,不会释放锁标记,可以理解为交出 CPU 时间片, 但是不一定有效果,因为有可能又被马上执行。该方法的真正作用是使具有相同或者更高优 先级的方法得到执行机会。

wait(timeout):wait 方法通常和 notify()/notifyAll()搭配使用,当前线程暂停执行,会释放锁 标记。进入对象等待池。直到调用 notify()方法之后,线程被移动到锁标记等待池。只有锁 标记等待池的线程才能获得锁

1.5 Join的用法

联合线程:

线程的join方法表示一个线程等待另一个线程完成后才执行。有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程。join方法被调用之后,线程对象处于阻塞状态。

适用于A线程需要等到B线程执行完毕,再拿B线程的结果再继续运行A线程.

说人话: A线程需要拿到B线程的执行结果,才能继续往下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Join extends Thread{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("This is join: " + i);
}
}

public class JoinThread {
public static void main(String[] args) throws InterruptedException {

System.out.println("begin...");
Join joinThread = new Join();//创建join线程对象
for (int i = 0; i < 50; i++) {
System.out.println("main: " +i);
if (i == 10){
//启动join对象
joinThread.start();
}
if (i == 20){
System.out
.println("------------------------------------------");
joinThread.join();//在此处强制运行该线程
}
}
}
}

===================执行结果===================
begin...
main: 0
main: 1
main: 2
main: 3
main: 4
main: 5
main: 6
main: 7
main: 8
main: 9
main: 10
main: 11
main: 12
main: 13
This is join: 0
main: 14
This is join: 1
This is join: 2
This is join: 3
main: 15
This is join: 4
main: 16
main: 17
main: 18
main: 19
main: 20
------------------------------------------
This is join: 5
This is join: 6
This is join: 7
This is join: 8
This is join: 9
This is join: 10
...
...
This is join: 44
This is join: 45
This is join: 46
This is join: 47
This is join: 48
This is join: 49
main: 21
main: 22
main: 23
main: 24
main: 25
main: 26
main: 27
main: 28
main: 29
main: 30

2. Java同步关键词解释

2.1. synchronized

属于 JVM 级别加锁,底层实现是: 在编译过程中,在指令级别加入一些标识来实现的。

1. 锁对象注意点: 必须是锁的同一个对象

2. 锁获取和释放

  • 锁的获取是由JVM决定的, 用户无法操作
  • 锁的释放也是由JVM决定的
  • Synchronized 无法中断正在阻塞队列或者等待队列的线程。

3. 什么时候会释放

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  • 线程执行发生异常,此时 JVM 会让线程自动释放锁。

4.格式

1
2
3
4
// 加同步格式:
synchronized(需要一个任意的对象(锁)){
代码块中放操作共享数据的代码。
}

5.线程执行互斥代码的过程

  1. 获得互斥锁

  2. 清空工作内存

  3. 从主内存拷贝变量的最新副本到工作内存

  4. 执行代码

  5. 将更新后的共享变量的值刷新到主内存

  6. 释放互斥锁

    Lock -> 主内存 -> 工作内存 -> 主内存 -> unlock

2.2 Lock

手动获取或释放锁, 提供了比 synchronized 更多的功能

Lock 锁是 Java 代码级别来实现的,相对于 synchronized 在功能性上,有所加强,主要是,公平锁,轮 询锁,定时锁,可中断锁等,还增加了多路通知机制(Condition),可以用一个锁来管理多 个同步块。另外在使用的时候,必须手动的释放锁。Lock 锁的实现,主要是借助于队列同 步器(我们常常见到的 AQS)来实现。它包括一个 int 变量来表示状态;一个 FIFO 队列,来 存储获取资源的排队线程。

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
class X {
// 创建一把锁
private final ReentrantLock lock = new ReentrantLock();
// 需要做同步的方法
public void m() {
lock.lock(); //获取🔐, 加锁
try {
// 代码
} finally {
lock.unlock(); // 释放🔐
}
}
}

1. lock 和 synchronized 的区别

  1. Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性Lock 是一个类,通过这个类可以实现同步访问;
  2. Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁, 当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占 用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现 象。

2. Lock 接口中方法的使用

ReentrantLock 类

ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法,ReentrantLock,意思是“可重入锁”。

lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用来获取锁的。

unLock()方法是用来释放锁的。

四个获取锁方法的区别

  1. lock(),阻塞方法,该方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被 其他线程获取,则进行等待。由于在前面讲到如果采用 Lock,必须主动去释放锁,并且在 发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并 且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生。
  2. tryLock(),非阻塞方法,该方法是有返回值的,它表示用来尝试获取锁,如果获取成功, 则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,也就说这个方法无论 如何都会立即返回。在拿不到锁时不会一直在那等待。

  3. tryLock(long time, TimeUnit unit),阻塞方法,阻塞给定时长,该方法和 tryLock()方法是 类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还 拿不到锁,就返回 false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。

  4. lockInterruptibly()这个方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待 获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通 过 lock.lockInterruptibly()想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等 待,那么对线程 B 调用 threadB.interrupt()方法能够中断线程 B 的等待过程。

2.3 Lock 与 synchronized 的选择

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生; 而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因 此使用 Lock 时需要在 finally 块中释放锁;
  3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等 待的线程会一直等待下去,不能够响应中断;
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  5. Lock 可以提高多个线程进行读操作的效率。

2.4 读写锁

  • 线程进入读锁的前提条件: 没有其他线程的写锁, 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
  • 线程进入写锁的前提条件: 没有其他线程的读锁, 没有其他线程的写锁

ReentrantReadWriteLock 与 ReentrantLock 都是单独的实现,彼此之间没有继承或实现的关系。

ReadWriteLock 类

1
2
3
4
5
6
// API
// 可以区别对待读、写的操作
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

ReentrantReadWriteLock 类

ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁。

注意:不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写 锁,则申请写锁的线程会一直等待释放读锁。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程 会一直等待释放写锁。

2.5 死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象, 若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起 无法完成任务。

死锁的发生必须满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规 定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁

2.6 Volatile 特殊域变量

多线程编程,我们要解决的问题集中在三个方面:

  1. 原子性。最简单的例子就是,i++,在多线程环境下,最终的结果是不确定的,为什么?就 是因为这么一个++操作,被编译为计算机底层指令后,是多个指令来完成的。那么遇到并发 的情况,就会导致彼此“覆盖”的情况。
  2. 可见性。通俗解释就是,在 A 线程对一个变量做了修改,在 B 线程中,能正确的读取到 修改后的结果。究其原理,是 cpu 不是直接和系统内存通信,而是把变量读取到 L1,L2 等 内部的缓存中,也叫作私有的数据工作栈。修改也是在内部缓存中,但是何时同步到系统内 存是不能确定的,有了这个时间差,在并发的时候,就可能会导致,读到的值,不是最新值。
  3. 指令重排。这里只说指令重排序,虚拟机在把代码编译为指令后执行,出于优化的目的, 在保证结果不变的情况下,可能会调整指令的执行顺序。

valotile,能满足上述的可见性和有序性。但是无法保证原子性。

可见性,是在修改后,强制把对变量的修改同步到系统内存。而其他 cpu 在读取自己的内部 缓存中的值的时候,发现是 valotile 修饰的,会把内部缓存中的值,置为无效,然后从系统 内存读取。

有序性,是通过内存屏障来实现的。所谓的内存屏障,可以理解为,在某些指令中,插入屏 障指令,用以确保,在向屏障指令后面继续执行的时候,其前面的所有指令已经执行完毕。

3. Java多线程中常见的面试题

1. sleep(),wait(),join(),yield()四个方法的区别

总结:

1):sleep(),Thread 类中的方法,表示当前线程进入阻塞状态,不释放锁

2):wait(),Object 类中的方法,表示线程进入等待状态,释放锁,所以一般能调用这个方 法的都是同步代码块,或者获取了锁的线程代码,通常和 notify()和 notifyAll()方法结合使用

3):join(),Thread 类中的方法,假如在 a 线程中调用 b 线程对象的 join()方法,表示当前 a 线程阻塞,直到 b 线程运行结束

4):yield(),Thread 类中的方法,表示线程回可执行状态。跟 sleep 方法一样,也不交出锁, 只不过不带时间参数,是指交出 cpu

2. ThreadRunnable 的区别

总结:

实现 Runnable 接口比继承 Thread 类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免 java 中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4):线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类

如果帮到你, 可以给我赞助杯咖啡☕️
0%