一、基本概念

1.1 概念

串行、并行、并发
串行:多个程序按序执行。上一个做完,接着做下一个。
并行:多个程序同时执行,一般需要多核处理器的支持。
并发:多个程序交错执行,就像看上去是同时执行的一样。

同步异步、阻塞非阻塞
同步和异步:当前线程是否需要等待方法调用执行完毕。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 同步
isFinished = doTask();
// wait for finished
if isFinished{
// 任务完成
}

// 异步
doTask({
// 任务完成
// 执行回调
send a message
})

在异步调用下的代码逻辑相对而言不太直观,需要借助回调或事件通知,这在复杂逻辑下对编码能力的要求较高。而同步调用就是直来直去,等待执行完毕然后拿到结果紧接着执行下面的逻辑,对编码能力的要求较低,也更不容易出错。
所以你会发现有很多方法它是异步调用的方式,但是最终的使用还是异步转同步。
比如你向线程池提交一个任务,得到一个future,此时是异步的,然后你在紧接着在代码里调用 future.get(),那就变成等待这个任务执行完成,这就是所谓的异步转同步,像 Dubbo RPC 调用同步得到返回结果就是这样实现的。

阻塞和非阻塞:当前接口数据还未准备就绪时,线程是否被阻塞挂起。
何为阻塞挂起?就是当前线程还处于 CPU 时间片当中,调用了阻塞的方法,由于数据未准备就绪,则时间片还未到就让出 CPU。所以阻塞和同步看起来都是等,但是本质上它们不一样,同步的时候可没有让出 CPU。一旦同步结束,CPU可立马执行接下去的代码。

而非阻塞就是当前接口数据还未准备就绪时,线程不会被阻塞挂起,可以不断轮询请求接口,看看数据是否已经准备就绪。

1.2 线程的状态

  • NEW:Thread对象被创建出来了,但是还没有执行start方法。
  • RUNNABLE:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)
  • BLOCKED、WAITING、TIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
  • BLOCKED:synchronized没有拿到同步锁,被阻塞的情况
  • WAITING:调用wait方法就会处于WAITING状态,需要被手动唤醒(notify或notifyAll)
  • TIME_WAITING:调用sleep方法、join方法、wait(long timeout),会被自动唤醒,无需手动唤醒
  • TERMINATED:run方法执行完毕,线程生命周期到头了

二、线程的创建

2.1 继承Thread类

java
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
package com.leggasai.concurrent;  

public class ThreadDemo {

public static void main(String[] args) {
// thread 1
MyJob worker1 = new MyJob();
worker1.start();
// thread 2
MyJob worker2 = new MyJob();
worker2.start();

// error!
// 调用run方法只会在当前线程执行执行run方法内部代码
// 调用start方法才会新建一个线程,去执行
// worker1.run();
}
}

class MyJob extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+":"+"I am working!");
}
}

2.2 实现Runnable

java
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
package com.leggasai.concurrent;  

public class ThreadDemo {

public static void main(String[] args) {
Thread worker1 = new Thread(new MyRun());

Thread worker2 = new Thread(() -> {
System.out.println(Thread.currentThread().getId() + ":" + "I am working!");
});

Thread worker3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + ":" + "I am working!");
}
});
worker1.start();
worker2.start();
worker3.start();
}
}

class MyRun implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+":"+"I am working!");
}
}

2.3 实现Callable

实现Callable接口,可以创建具有返回值的线程。需要配合FutureTask使用。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.Callable;  
import java.util.concurrent.FutureTask;

public class ThreadDemo {
public static void main(String[] args) throws Exception {
FutureTask futureTask = new FutureTask<>(new MyCall());
Thread worker1 = new Thread(futureTask);
worker1.start();
String res = (String)futureTask.get();
System.out.println(res);
}
}

class MyCall implements Callable{
@Override
public Object call() throws Exception {
return Thread.currentThread().getId()+":"+"I am working!";
}
}

2.4 线程池

在大型应用中,一般都是使用线程池去并发执行多个任务,因为线程池实际上已经创建好了若干个活跃的线程,省去了创建线程的时间,具有更高的效率。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void threadPoolDemo(){  
// 创建大小为3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交多个任务给线程池执行
for (int i = 1; i <= 5; i++) {
MyRun myRun = new MyRun();
executorService.execute(myRun);
}
// 关闭线程池
executorService.shutdown();
}
class MyRun implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+":"+"I am working!");
}
}

三、线程的使用

3.1 获取线程信息

获取当前线程Thread.currentThread()

java
1
2
System.out.println(Thread.currentThread());
// Thread[Thread-0,5,main]

获取线程相关信息

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
System.out.println(thread.getId());  
System.out.println(thread.getName());
System.out.println(thread.getState());
System.out.println(thread.getPriority());
System.out.println(thread.getUncaughtExceptionHandler());
System.out.println(thread.getThreadGroup());

20
Thread-0
RUNNABLE
5
java.lang.ThreadGroup[name=main,maxpri=10]
java.lang.ThreadGroup[name=main,maxpri=10]

3.2 设置线程相关信息

java
1
2
3
4
5
6
7
thread.setName("my thread");  
thread.setPriority(10);
System.out.println(thread.getName());
System.out.println(thread.getPriority());

t1.setDaemon(true);
System.out.println(t1.isDaemon());

线程的优先级:范围1-10。数值越高意味着优先级越高

java
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
public static void main(String[] args) throws Exception {  

Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(1);
});

Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
});
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}

// 基本先执行t2,后执行t1

线程让步Thread.yield():当前线程向处理器表明自己可以让步,放弃CPU使用权让给其他相同优先级线程。这并不是一个强制性的指令,而是一个建议,具体实现依赖于底层操作系统的线程调度器。该方法一般很少使用。

java
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
public static void main(String[] args) throws Exception {  
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if(i == 50){
Thread.yield();
}
System.out.println("t1:" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("t2:" + i);
}
});
t2.start();
t1.start();
}

// t1大概率输出到49时,会让t2执行一段时间,比如输出大致如下
...
t1:47
t1:48
t1:49
t2:0
t2:1
t2:2
...

下面这种情况,yield无法生效,因为yield()不会释放锁

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {  
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
for (int i = 0; i < 100; i++) {
if(i == 50){
Thread.yield();
}
System.out.println("t1:" + i);
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj){
for (int i = 0; i < 100; i++) {
System.out.println("t2:" + i);
}
}
});
t1.start();
t2.start();
}

线程休眠Thread.sleep():使得线程暂停执行一段时间进入时间等待状态,会让出CPU,但不会让出锁。

java
1
2
3
4
5
public static void main(String[] args) throws Exception {  
System.out.println(System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(System.currentTimeMillis());
}

线程join()方法:使得当前线程等待被调用join()方法的线程执行结束。
下面代码中,由于设置t1为守护线程,main线程不会等待t1执行结束才结束,因此会直接结束运行。

java
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {  
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.setDaemon(true);
t1.start();
}

执行t1.join()方法后,main线程会等待t1执行结束后,在继续运行。因此控制台可输出1

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {  
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.setDaemon(true);
t1.start();
// 等待t1执行结束
t1.join();
}

守护线程 vs 非守护线程

  • 默认情况下,线程都是非守护线程。
  • 守护线程是一种在程序运行时,在后台提供服务的线程,随着非守护线程的结束而自动退出。
  • 主线程默认是非守护线程,如果主线程执行结束,需要查看当前JVM内是否还有非守护线程,如果没有JVM直接停止
  • 可以通过setDaemon和isDaemon来设置守护线程和查看是否是守护线程
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {  
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("非守护线程执行结束:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
System.out.println("等待非守护线程执行结束:"+System.currentTimeMillis());
}

// 等待非守护线程执行结束:1708408152713
// 非守护线程执行结束:1708408153719

3.3 线程的等待和唤醒(重点)

wait()/wait(timeout):使得当前线程释放锁资源,进入等待状态,并加入等待队列
notify():唤醒在同一个对象上调用wait()而进入等待队列中的某个线程,进入到锁队列
notifyAll():唤醒在同一个对象上调用wait()而进入等待队列中的所有线程,进入到锁队列
上述关键词必须在synchronized代码块内或方法体内才能调用,因为他们是基于某个对象锁的。

  1. 线程A调用 wait() 方法,释放对象的锁,进入等待队列。
  2. 线程B执行一些操作,然后调用 notify() 方法,唤醒等待队列中的线程A。
  3. 线程A被唤醒后,它将进入锁队列,等待获取对象的锁。
  4. 当线程B释放对象的锁(例如,退出同步块或同步方法)时,线程A将有机会竞争锁,成功获取锁后继续执行。

3.4 线程结束

方法一:stop()方法,但由于stop()方法会产生不可预测行为使得程序进入不一致状态,已被弃用。
方法二:自然结束
方法三:修改共享变量。务必使用volatile修饰共享变量,如下面的flag。原因见4.2可见性

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception{  
Thread t1 = new Thread(() -> {
while(flag){

}
});

t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
flag = false;
// be sure the Thread t1 has terminated.
Thread.sleep(100);
System.out.println(t1.getState());
}

方法四:interrupt()方法(推荐使用)。
通过打断WAITING或者TIMED_WAITING状态的线程,从而抛出异常自行处理
这种停止线程方式是最常用的一种,在框架和JUC中也是最常见的

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception{  
Thread t1 = new Thread(() -> {
while(true){
// 获取任务
// 拿到任务,执行任务
// 没有任务了,让线程休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("基于打断形式结束当前线程");
return; }
}
});
t1.start();
Thread.sleep(500);
t1.interrupt();
}

标记法

java
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception{  
Thread t1 = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()){
// 获取任务
// 拿到任务,执行任务
// 没有任务了,让线程休眠
}
System.out.println("当前线程被打断");
});
t1.start();
Thread.sleep(500);
t1.interrupt();
}

四、并发编程

三大特性:原子性、可见性、有序性。这三大特性是并发编程中需要特别关注和处理的问题,保证它们的正确性有助于避免并发引起的一系列问题,如竞态条件、死锁、活锁等。

4.1 原子性

原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。

JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。让Java的并发编程可以做到跨平台。

JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中。

java
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
public class CounterDemo {  
private static int count;

public static void increment(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}

}

上述程序按理应该输出200(两个线程各调用了increment()方法一百次),但实际最终结果往往小于200。其原因在于count++不满足原子性。

java
1
2
3
4
count++不满足原子性,其可分为三步
(1)从内存读取count的值到CPU寄存器
(2)ADD操作
(3)将寄存器的值再写回内存中

考虑多线程并发执行的情况:可能会发生如下情景
线程#1在执行count++但是在写回内存之前,有另外一个线程#2也开始执行count++。于是对于线程#2读取到的实际可以被认为脏值,因为线程#1还没将更新值写回内存。最后线程#2将count+1后的新值a+1写回内存。明显不符合预期count=a+2。因为丢失了更新。

如果加上synchronized关键词,则同一时间最多只能有一个线程访问increment()方法,这时流程图就变为了如下所示:这时最终值能够符合预期,因为锁的机制,使得count++变为了原子操作。

解决方法二:原子类

目前Java中提供的原子类大部分底层使用了CAS锁(CompareAndSet自旋锁),如AtomicInteger、AtomicLong等;也有使用了分段锁+CAS锁的原子类,如LongAdder等。

解决方法三:Lock锁
ReentrantLockReentrantReadWriteLock。详见锁章节

解决方法四:ThreadLocal类
这个类提供线程局部变量。这些变量与普通变量的不同之处在于,访问一个变量的每个线程都有自己的、独立初始化的变量副本。

ThreadThreadLocalMapThreadLocal的关系如上图所示。

  • 每个Thread实例对象都有其各自的ThreadLocalMap映射,该Map存放了ThreadLocalObject的映射
  • 可以创建多个ThreadLocal来为每个线程存储多个线程独立的变量。
  • 每个Thread的变量是独立的,因为ThreadLocalMap是每个线程独有的。而ThreadLocal只是作为Key
    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class ThreadLocalDemo {  
    static ThreadLocal tl1 = new ThreadLocal();
    static ThreadLocal tl2 = new ThreadLocal();
    public static void main(String[] args) {
    // belong to main thread
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
    System.out.println("t1:" + tl1.get());
    System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());

    }
    }
    main:123
    main:456
    t1:null
    t1:null

ThreadLocal

  • set():在当前线程上绑定一个变量。也就是在当前线程的ThreadLocalMap中添加了一个映射,ThreadLocal->Object
  • get():获取此ThreadLocal在当前线程上绑定的值。
  • remove():删除此ThreadLocal在当前线程上绑定的值,避免内存泄漏。

内存泄漏问题
内存泄漏:JVM创建的对象永远都无法访问到,但是GC又不能回收对象所占用的内存。
Java中的使用引用类型分别是强,软,弱,虚
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。强引用不会被JVM的GC回收。因此是造成内存泄漏的主要原因之一。

其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。

然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。

最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。

ThreadLocalMap中的Entrykey被设计成弱引用。

如果当我们不再使用ThreadLocal变量时,依旧有一条强引用路径指向entry
为了解决这个问题,ThreadLocal的作者就把entry中的key设计成弱引用的形式,一旦栈中的ref-threadlocal强引用消失,在下一次GC时就会清除堆中的threadlocal对象。此时,该entrykey值为null。此外,ThreadLocal还有些额外的逻辑清除整个entry的指针。其通过判断entrykey值是否为null来判断该entry是否过时。

4.2 可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。在多线程环境中,由于线程之间的缓存机制,每个线程的工作内存(CPU三级缓存)都是独立的,会导致每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestDemo {  
static boolean flag = true;
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
while (flag) {
// ....
}
System.out.println("t1线程结束");
});

t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
}
}

上述代码线程t1永远不会结束,因为它无法观察到flag已被修改。不同线程可能在各自的缓存中保存了相同变量的不同副本。如果一个线程修改了共享变量,其他线程可能不会立即看到这个修改。在上述代码中main线程修改了自己缓冲区中的flag = false,并写回主内存。但t1线程的while(flag)读取的依旧是自己缓冲区中的flag(true)因此无法退出循环。

解决方法一:(不推荐)在while循环中,使用某些语句读取一次flag

java
1
2
3
4
5
6
7
8
9
10
Thread t1 = new Thread(() -> {  
while (flag) {
// ....
System.out.println("flag[t1]:"+flag);
try {
Thread.sleep(100);
}catch (Exception e){}
}
System.out.println("t1线程结束");
});

在这个具体的语句中,由于涉及字符串拼接和 System.out.println 的操作,Java 的虚拟机可能会进行一些特殊处理,例如强制从主内存中读取 flag 的最新值,而不使用线程的本地缓存。因此可以通过重新读取flag的方式刷新缓存。

解决方法二:使用volatile关键词
volatile是一个关键字,用来修饰成员变量。
如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作。

  • volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量
    java
    1
    static volatile boolean flag = true;

解决方法三:使用synchronized
如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestDemo {  
static boolean flag = true;
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
while (flag) {
synchronized (TestDemo.class){}
}
System.out.println("t1线程结束");
});

t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
System.out.println("flag[main]:"+flag);
}
}

解决方法四:使用锁
Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestDemo {  
static boolean flag = true;
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
while (flag) {
lock.lock();
try {

}finally {
lock.unlock();
}
}
System.out.println("t1线程结束");
});

t1.start();
Thread.sleep(10);
flag = false;
System.out.println("主线程将flag改为false");
System.out.println("flag[main]:"+flag);
}
}

4.3 有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。在并发编程中,由于编译器的优化、指令重排等原因,程序的执行顺序可能被改变,导致程序的实际执行顺序与代码的顺序不一致。

一个指令重排的例子:

java
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
public class OrderDemo {  
static int a,b,x,y;
public static void main(String[] args) throws InterruptedException{
for (int i = 0; i < Integer.MAX_VALUE; i++) {
a = 0;
b = 0;
x = 0;
y = 0;

Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
if(x == 0 && y == 0){
System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
}
}
}
}
208802次,x = 0,y = 0

上述代码在正常情况下,xy不同时为0。只有发生指令重排,即先执行了x=b和y=a,才会导致xy同时为0。

happens-before 规则:在如下规则下不允许指令重排

  1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。
  4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

volatile修饰:被该关键词修饰的属性不会被指令重排。通过写前StoreStore 屏障和读后LoadLoad 屏障实现。

五、锁

重要!!!

5.1 锁的分类

可重入锁、不可重入锁

重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。
不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线程占用着,需要等待自己释放锁再获取锁。
Java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是可重入锁。

乐观锁、悲观锁

悲观锁:悲观锁的基本思想是在整个操作过程中,将数据处于锁定状态,以确保在这个期间数据不会被其他线程修改。获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核的太的切换,而这种切换是比较消耗资源的。
乐观锁:乐观锁的基本思想是假设在并发访问的情况下,数据不会发生冲突,因此不对数据加锁,而是在更新时检查数据是否被其他线程修改。获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。

Java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是悲观锁。
Java中提供的CAS操作,就是乐观锁的一种实现。Atomic原子性类中,也是基于CAS乐观锁实现的。

公平锁、非公平锁

公平锁是指多个线程按照请求的顺序依次获取锁,而非公平锁则允许线程插队。
Java中提供的synchronized是非公平锁。
Java中提供的ReentrantLockReentrantReadWriteLock可以实现公平锁和非公平锁

互斥锁、共享锁、读写锁

互斥锁:同一时间点,只会有一个线程持有者当前互斥锁。
共享锁:同一时间点,当前共享锁可以被多个线程同时持有。
读写锁:读锁-读锁共享、写锁和其他锁互斥
Java中提供的synchronized、ReentrantLock是互斥锁。
Java中提供的ReentrantReadWriteLock,有互斥锁也有共享锁。

5.2 synchronized详解

同步代码块和同步方法

同步代码块:锁住{}之间的代码。锁可以使用某个对象或类

java
1
2
3
synchronized(xxx) { 
// todo some thing
}

同步方法:声明某个方法是同步方法,同一时间最多被一个线程访问。

  • 静态方法:此时使用的是当前类.class作为锁(类锁)
  • 非静态方法:此时使用的是当前对象做为锁(对象锁)
    类锁和其某个类实例的锁是相互独立的。下面代码中的两个线程可以同时执行。
    java
    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
    public class syncDemo {  
    public static void main(String[] args) {
    LockDemo lockDemo = new LockDemo();
    Thread thread1 = new Thread(() -> {
    lockDemo.staticMethod();
    });
    Thread thread2 = new Thread(() -> {
    lockDemo.instanceMethod();
    });

    thread1.start();
    thread2.start();
    }

    }

    class LockDemo{

    // 静态方法上的类锁
    public static synchronized void staticMethod() {
    // synchronized code
    String formattedTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    System.out.println("staticMethod() start Time: " + formattedTime);
    try {
    Thread.sleep(2000);
    }catch (Exception e){
    e.printStackTrace();
    }
    System.out.println("staticMethod() end Time: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }

    // 实例方法上的对象锁
    public synchronized void instanceMethod() {
    // synchronized code
    String formattedTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    System.out.println("instanceMethod() start Time: " + formattedTime);
    try {
    Thread.sleep(2000);
    }catch (Exception e){
    e.printStackTrace();
    }
    System.out.println("instanceMethod() end Time: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
    }

JIT优化方式

锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,此时synchronized无效。比如只有一些局部变量。

锁粗化:如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。(不一定会触发)

java
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
public void method(){
for(int i = 0;i < 999999;i++){
synchronized(对象){

}
}
// 这是上面的代码会触发锁膨胀
synchronized(对象){
for(int i = 0;i < 999999;i++){

}
}
}


public static void lockElimination(){
try {
Thread thread1 = new Thread(() -> {
method1();
});
Thread thread2 = new Thread(() -> {
method2();
});
thread1.start();
thread2.start();
}catch (Exception e){}

}

public static void method1() {
for (int i = 0; i < 1000; i++) {
synchronized (syncDemo.class){
System.out.println("method1:"+i);
}
}
}
public static void method2() {
for (int i = 0; i < 1000; i++) {
synchronized (syncDemo.class){
System.out.println("method2:"+i);
}
}
}
// 从控制台的输出看到
// thread1和thread2还是交错执行的,并没有触发锁膨胀

锁升级:锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。

  • 无锁、匿名偏向:当前对象没有作为锁存在。
  • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
    • 如果是,直接拿着锁资源走。
    • 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
  • 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
    • 如果成功获取到,拿着锁资源走
    • 如果自旋了一定次数,没拿到锁资源,锁升级。
  • 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)

synchronized是基于对象实现的,那这些锁的信息存储在哪?

实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
内存填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
Java头对象是是实现synchronized锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字节来存储对象头(数组是三个字节),其主要结构是由Mark Word和Class Metadata Address组成,其结构说明如下表:

头对象结构 说明
Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
Mark Word结构
锁状态转变流程

5.3 ReentrantLock详解

ReentrantLock和synchronized区别

  • ReentrantLock是类,而synchronized是关键词。
  • 底层都是基于JVM层面实现互斥锁
  • 实现原理:ReentrantLock是基于AQS实现,而synchronized是关基于ObjectMonitor
  • ReentrantLock功能更全面,支持公平锁和非公平锁,支持限制等待时间。
  • 效率区别:ReentranLock不存在锁升级概念;synchronized存在锁升级概念;

AQS

AbstractQueuedSynchronizer,即抽象的队列同步器,是一种用来构建锁和同步器的框架。JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,BlockingQueue,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。

首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象。

5.4 ReentrantReadWriteLock详解

读写锁,多个线程可以同时进行读操作,提高了并发效率。读-读不互斥,读-写互斥,写-写互斥。

实现原理

ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。

读锁操作:基于state的高16位进行操作。
写锁操作:基于state的低16位进行操作。

ReentrantReadWriteLock依然是可重入锁

六、阻塞队列

阻塞队列基于生产者消费者模式。
生产者存储方法:

  • add(E) : 添加数据到队列,如果队列满了,无法存储,抛出异常
  • offer(E) : 添加数据到队列,如果队列满了,返回false
  • offer(E,timeout,unit) : 添加数据到队列,如果队列满了,阻塞timeout时间,如果阻塞一段时间,依然没添加进入,返回false
  • put(E) : 添加数据到队列,如果队列满了,挂起线程,等到队列中有位置,再扔数据进去,死等!

消费者读取方法:

  • remove() : 从队列中移除数据,如果队列为空,抛出异常
  • poll() : 从队列中移除数据,如果队列为空,返回null
  • poll(timeout,unit) : 从队列中移除数据,如果队列为空,挂起线程timeout时间,等生产者存入数据,再获取
  • take() :从队列中移除数据,如果队列为空,线程挂起,一直等到生产者扔数据,再获取

6.1 ArrayBlockingQueue

ArrayBlockingQueue在初始化的时候,必须指定当前队列的长度。它是循环使用固定长度的数组实现,通过两个属性,putIndextakeIndex来控制放入和取出元素的下标。

6.2 LinkedBlockingQueue

LinkedBlockingQueue是基于链表的,通过头节点和尾节点控制放入和取出。可以指定最大容量,也可以不指定。

6.3 PriorityBlockingQueue

首先PriorityBlockingQueue是一个优先级队列,他不满足先进先出的概念。
会将查询的数据进行排序,排序的方式就是基于插入数据值的本身。
实现原理是基于数组实现的二叉堆。

6.4 DelayQueue

DelayQueue就是一个延迟队列,生产者写入一个消息,这个消息还有直接被消费的延迟时间。
DelayQueue会更具延迟时间进行排序,其本身也是一个PriorityBlockingQueue
其放入的元素需要实现Delayed接口。

java
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
// 外卖订单超时自动取消场景
public static void delayQ(){
DelayQueue<DelayTask> delayQueue = new DelayQueue<DelayTask>();
DelayTask task1 = new DelayTask("A", 2000l);
delayQueue.add(task1);
delayQueue.add(new DelayTask("B",1000l));
delayQueue.add(new DelayTask("C",3000l));
// 只有到延迟时间才会取出,应用场景,订单超时自动取消。
// 商家正常接单,从超时队列中移除,走正常消费逻辑
delayQueue.remove(task1);

// 不断移除超时订单
while (delayQueue.size()>0){
try {
System.out.println(delayQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}


class DelayTask implements Delayed{

private String name;
private Long delayTime;
private Long executeTime;

public DelayTask(String name, Long delayTime) {
this.name = name;
this.delayTime = delayTime;
this.executeTime = System.currentTimeMillis() + delayTime;
}

public Long getExecuteTime() {
return executeTime;
}

@Override
public long getDelay(TimeUnit unit) {
return unit.convert(executeTime - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}

@Override
public int compareTo(Delayed o) {
return (int) (this.executeTime - ((DelayTask)o).getExecuteTime());
}

@Override
public String toString() {
return "DelayTask{" +
"name='" + name + '\'' +
", delayTime=" + delayTime +
", executeTime=" + executeTime +
'}';
}
}

6.5 SynchronousQueue

这个队列比较特殊,它不存储元素。因为SynchronousQueue没有容量。与其他BlockingQueue(阻塞队列)不同,SynchronousQueue是一个不存储元素的BlockingQueue。只是它维护一组线程,这些线程在等待着把元素加入或移出队列。

可以理解为给线程“配对”。入和出必须是配对的,否则阻塞。
注意:有两种模式,分别为公平模式和非公平模式。公平模式按照先来先配对原则(TransferQueue),非公平按照后来先配对(TransferStack)。

java
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
public static void syncBlockQ() throws InterruptedException{  
SynchronousQueue<String> syncq = new SynchronousQueue<>(false);

new Thread(() -> {
try {
syncq.put("生1");
System.out.println("生1放入数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
syncq.put("生2");
System.out.println("生2放入数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
syncq.put("生3");
System.out.println("生3放入数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

Thread.sleep(100);
new Thread(() -> {
System.out.println("消1:" + syncq.poll());
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("消2:" + syncq.poll());
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("消3:" + syncq.poll());
}).start();


}
输出
1:生3
2:生2
3:生1

如果改为公平模式则输出
1:生3
2:生2
3:生1

参考:
Java多线程基础知识_waitset entrylist-CSDN博客
java并发编程(荣耀典藏版)-CSDN博客