Java并发

前言:

    根据狂神说的视频以及《深入浅出Java多线程》整理出该笔记

JUC

概念

JUC并发编程就是多线程的进阶版,主要包含三个类java.util.concurrent、 java.util.concurrent.automic、 java.util.concurrent.locks

回顾

进程和线程

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

**一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。**总之,进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

当然区别还是有的,他们本质的区别就是是否单独占有内存地址空间及其它系统资源(比如I/O),另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

线程的几个状态

  • new 新建

  • runnable 运行

  • blocked 阻塞

  • waiting 等待

  • timed_waiting 超时等待

  • terminated终止

wait/sleep区别

  • wait 会释放锁 sleep 不会释放锁

  • 使用的范围不同 wait必须在同步代码块中 sleep可以任何地方使用

  • wait 不需要捕获异常 sleep则需要

Synchronzied 和Lock区别

  • Synchronized 内置的Java关键字 Lock是一个Java类

  • S无法判断锁的状态 Lock可以判断是否获取到了锁

  • S会自动释放锁 Lock必须手动释放 如果不释放就会死锁

  • S 线程1 (获得锁)线程2 (等待),lock锁下的线程就不一定一直等

  • S可重入锁 不可以中断,非公平,Lock 可重入锁

  • S适合锁少量的代码同步问题 Lock适合锁大量的代码同步问题

虚假唤醒问题

先来看看synchronized下的生产者和消费者问题

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
public class A {
public static void main(String[] args) {
data data = new data();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}


}


class data{
private int number = 0;

//+1
public synchronized void increment() throws InterruptedException {
if(number!=0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程
this.notifyAll();
}

//-1
public synchronized void decrement() throws InterruptedException {
if(number==0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
this.notifyAll();
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0

若不是两个线程,而是多个线程,就会出现以下的问题

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
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
A=>1
B=>0
C=>1
A=>2
C=>3
B=>2
B=>1
B=>0
C=>1
A=>2
C=>3
A=>4
C=>5
D=>4
D=>3
C=>4
D=>3
D=>2
D=>1
D=>0
C=>1
D=>0
C=>1
D=>0
C=>1
D=>0
C=>1
D=>0

问题分析

结果的出现原因是资源类的if判断,可能导致两个加的方法同时进来,造成的虚假唤醒问题

1
2
3
4
5
6
7
8
9
10
11
12
public synchronized void increment() throws InterruptedException {
while(number!=0){
//这里改成while 像自旋锁
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程
this.notifyAll();
}

lock锁下的生产者消费者问题

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package PC;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @program: JUCDemo1
* @description:
* @author: Mr.Like
* @create: 2022-09-30 18:59
**/
public class B {
public static void main(String[] args) {
data2 data = new data2();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();

new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}

}



class data2{

private int number = 0;

//使用lock 锁
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();


//+1
public void increment() throws InterruptedException {
lock.lock();
//ctrl+alt+t 快速环绕
try {
while(number!=0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll();//通知全部线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放,防止死锁
}
}

//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while(number==0){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll();//通知全部线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放,防止死锁
}
}


}

八锁问题

资源类phone可以发短信和打电话

1.创建一个Phone实例多线程调用两个方法,问哪一个先执行?

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
public class Demo1 {
public static void main(String[] args) {
phone phone = new phone();

new Thread(()->{
phone.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone.call();
}).start();
}
}

class phone{
public synchronized void sendMessage(){
System.out.println("发短信");
}

public synchronized void call(){
System.out.println("打电话");
}
}

//输出
发短信
打电话

分析:synchronized关键字是对对象上锁,谁先拿到锁,谁就先执行

2.创建一个Phone实例多线程调用两个方法,其中第一个线程调用的方法中加延迟,问哪一个先执行?

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
public class Demo2 {
public static void main(String[] args) {
phone phone = new phone();

new Thread(()->{
phone.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone.call();
}).start();
}
}

class phone{
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized void call(){
System.out.println("打电话");
}
}

//输出
发短信
打电话

分析:原理同上,谁先拿到锁,谁先执行

3.创建一个Phone实例多线程调用两个方法,其中一个是普通方法,而且该线程位置靠后,问哪一个先执行?

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 Demo3 {
public static void main(String[] args) {
phone phone = new phone();

new Thread(()->{
phone.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone.watchvideo();
}).start();
}
}

class phone{
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized void call(){
System.out.println("打电话");
}

public void watchvideo(){
System.out.println("看视频");
}
}

//输出
看视频
发短信

分析:watchvideo()为普通方法,当然是不受锁的影响,因为 sendMessage()方法体 中有延迟语句,因此会后输出,其实 抛去延迟来说,两个输出结果为不一定

4.创建两个Phone实例多线程调用两个方法,问哪一个先执行?

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
public class Demo4 {
public static void main(String[] args) {
phone phone1 = new phone();
phone phone2 = new phone();

new Thread(()->{
phone1.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone2.call();
}).start();
}
}

class phone{
public synchronized void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized void call(){
System.out.println("打电话");
}

}

//输出
打电话
发短信

分析:由于是两个对象,因此锁对象之间没有干扰,因为延时,所以打电话先输出,而且可以证明锁住的是实例对象,多个之间并不干扰

5.创建一个Phone实例多线程调用两个方法,两个方法都有static修饰,问哪一个先执行?

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
public class Demo5 {
public static void main(String[] args) {
phone phone1 = new phone();
// phone phone2 = new phone();

new Thread(()->{
phone1.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone1.call();
}).start();
}
}

class phone{
public synchronized static void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized static void call(){
System.out.println("打电话");
}

}

//输出
发短信
打电话

分析:发短信 在前面的原因是 synchronized 加 静态方法 锁的是 Class ,Phone.Class只有单个。因此第二个线程需要 等待第一个线程释放Class锁才能执行。

6.创建两个Phone实例多线程调用两个方法,两个方法都有static修饰,问哪一个先执行?

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
public class Demo6 {
public static void main(String[] args) {
phone phone1 = new phone();
phone phone2 = new phone();

new Thread(()->{
phone1.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone2.call();
}).start();
}
}

class phone{
public synchronized static void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized static void call(){
System.out.println("打电话");
}

}

//输出
发短信
打电话

分析:虽然是两个实例对象,但是锁住的是同一个phone.class,原理和5是一样的

7.创建一个Phone实例多线程调用两个方法,其中一个有static修饰,而且调用线程在前面,问哪一个先执行?

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
public class Demo7 {
public static void main(String[] args) {
phone phone1 = new phone();
//phone phone2 = new phone();

new Thread(()->{
phone1.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone1.call();
}).start();
}
}

class phone{
public synchronized static void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized void call(){
System.out.println("打电话");
}

}

//输出
打电话
发短信

分析:打电话 先输出的原因是 Phone7 实例 和 Phone7.Class 分别被锁,两个线程之间并无影响,因为线程延迟的原因。再次 证明 synchronized 锁的是 类实例即对象 、synchronized 加 静态方法 锁的是 Class

8.创建两个Phone实例多线程调用两个方法,其中一个有static修饰,而且调用线程在前面,问哪一个先执行?

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
public class Demo8 {
public static void main(String[] args) {
phone phone1 = new phone();
phone phone2 = new phone();

new Thread(()->{
phone1.sendMessage();
}).start();


//Java的延时方法
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

new Thread(()->{
phone2.call();
}).start();
}
}

class phone{
public synchronized static void sendMessage(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}

public synchronized void call(){
System.out.println("打电话");
}

}

//输出
打电话
发短信

分析:原理同7两个线程分别锁的是 Phone8.Class 和 Phone8实例,因为线程延迟,打电话 才会优先输出。

集合的线程安全

概述

  • 线程安全集合:多线程并发的基础上修改一个集合,不会发生ConcurrentModificationException并发修改异常
  • CopyOnWriteArrayList是线程安全的集合,ArrayList是线程不安全的集合,Vector是线程安全的集合

ArrayList的线程安全

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
public class demo1_arraylist {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();

for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}

//输出
[null, 0c293, 7509b]
[null, 0c293, 7509b, 50105, a4b9f, cd3b1, 4d7bb, 89969, dd07a, de64e]
[null, 0c293, 7509b, 50105, a4b9f, cd3b1, 4d7bb, 89969]
[null, 0c293, 7509b, 50105, a4b9f, cd3b1]
[null, 0c293, 7509b, 50105, a4b9f, cd3b1, 4d7bb]
[null, 0c293, 7509b, 50105, a4b9f]
[null, 0c293, 7509b, 50105]
[null, 0c293, 7509b]
[null, 0c293, 7509b]
Exception in thread "8" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at listsecurity.demo1_arraylist.lambda$main$0(demo1_arraylist.java:20)
at java.lang.Thread.run(Thread.java:748)

如何解决呢?

  • 使用Vector:查看Vector的add()方法,发现相比于ArrayList的add()方法前面加了synchronized修饰,因此是线程安全的
  • 使用Collectiobns.synchronizedlist(new ArrayList):Collections集合工具类提供一些列线程安全的集合构造方法。
  • 使用 JUC包下的 CopyOnWriteArrayList集合:一种线程安全的集合,CopyOnWrite的意思是写入时复制,是一种计算机程序设计优化策略,解决多线程写入覆盖问题
1
2
3
4
List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
//这几个就是线程安全的

Set,Map集合的线程安全

对比ArrayList,Set同样线程并不安全,可以通过上面类似的解决

1
2
Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();

实际上HashSet的底层是HashMap方法

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

/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
//源码中的构造方法


/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//add方法也是直接调用的

因此Map也是不安全的,解决方法同样有两种

1
2
Map<String,String> map = Collections.synchronizedMap(new HashMap<>())
Map<String,String> map = new ConcurrentHashMap<>();

读写锁

ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个阅读器线程同时进行,只要没有作者。 write lock是独家的。简单理解就是 可以多个线程同时读,只能有一个线程同时写。

对于最初填充数据的集合,然后经常被修改的场合时使用读写锁的立项候选,但是对于数据的大部分时间被专门锁定,并且并发型增加很少,那么不建议使用读写锁,简单理解就是 经常被修改的读取的数据,建议使用读写锁,对于不长变动而且 并发很少的情况,不建议使用读写锁。

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
82
83
84
85
//使用读写锁,实现模拟缓存

public class demo1_rwl {
public static void main(String[] args) {
MycacheReadWriteLock cache = new MycacheReadWriteLock();

//四个线程写入缓存
for (int i = 0; i < 4; i++) {
final int temp = i;
new Thread(()->{
cache.save(temp + "",temp + "");
},String.valueOf(i)).start();
}

//四个线程写入缓存
for (int i = 0; i < 4; i++) {
final int temp = i;
new Thread(()->{
cache.get(temp + "");
},String.valueOf(i)).start();
}


}
}

//模拟缓存 加上读写锁
class MycacheReadWriteLock{
private volatile Map<String,String> cache = new HashMap<>();

//读写锁对象
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void save(String key,String value){

//加入写锁 保证只有一个线程来写
reentrantReadWriteLock.writeLock().lock();


try {
System.out.println(Thread.currentThread().getName() + "===>开始写入数据");
cache.put(key,value);
System.out.println(Thread.currentThread().getName() + "===>写入成功了");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.writeLock().unlock();
}


}

public void get(String key){
//加上读锁,保证读的时候没有其他的线程在写
reentrantReadWriteLock.readLock().lock();

try {
System.out.println(Thread.currentThread().getName() + "===>开始获取数据");
cache.get(key);
System.out.println(Thread.currentThread().getName() + "===>获取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放读锁
reentrantReadWriteLock.readLock().unlock();
}
}
}

//输出
0===>开始写入数据
0===>写入成功了
2===>开始写入数据
2===>写入成功了
3===>开始写入数据
3===>写入成功了
1===>开始写入数据
1===>写入成功了
1===>开始获取数据
1===>获取成功
0===>开始获取数据
2===>开始获取数据
3===>开始获取数据
3===>获取成功
2===>获取成功
0===>获取成功

阻塞队列

BlockingQueue是Java util.concurrent包下重要的数据结构,区别于普通的队列,BlockingQueue提供了线程安全的队列访问方式,并发包下很多高级同步类的实现都是基于BlockingQueue实现的。阻塞队列简单的来说就是你只管往里面存、取就行,而不用担心多线程环境下存、取共享变量的线程安全问题。

阻塞指两种状态

  • 入队:如果队列此时是满的,需要阻塞等待
  • 取出:如果队列是空的,需要阻塞等待生产

阻塞队列的操作方法

提供了四组不同的方法用于插入,移除,检查元素

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() - -
  • 抛出异常:如果试图的操作无法立即执行,抛异常。当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
  • 返回特殊值:如果试图的操作无法立即执行,返回一个特殊值,通常是true / false。
  • 一直阻塞:如果试图的操作无法立即执行,则一直阻塞或者响应中断。
  • 超时退出:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功,通常是 true / false。

注意之处

  • 不能往阻塞队列中插入null,会抛出空指针异常。
  • 可以访问阻塞队列中的任意元素,调用remove(o)可以将队列之中的特定对象移除,但并不高效,尽量避免使用。
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class demo1_blockingqueue {
public static void main(String[] args) throws InterruptedException {
//test1();
//test2();
//test3();
test4();
}


public static void test1(){
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

queue.add("william");
queue.add("will");
queue.add("iam");

System.out.println(queue);
//queue.add("队列已满,入队会抛异常"); IllegalStateException

System.out.println(queue.remove());
//获取队首元素
System.out.println(queue.element());
System.out.println(queue.remove());
System.out.println(queue.remove());
//System.out.println(queue.element()); 队列为空的时候,获取队首元素抛异常 NoSuchElementException


System.out.println(queue);

//System.out.println(queue.remove()); 队列已空,再次出队会抛异常 NoSuchElementException


}

public static void test2(){
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

queue.offer("william");
queue.offer("will");
queue.offer("iam");

System.out.println(queue.offer("队列已满,入队返回异常"));


System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());


System.out.println(queue.poll()); // 队列为空,出队返回null

}

public static void test3() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);


queue.put("william");
queue.put("will");
queue.put("iam");

queue.put("队列已满,一直阻塞等待");

System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());

System.out.println(queue.take());

}

public static void test4() throws InterruptedException {
ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

queue.put("william");
queue.put("will");
queue.put("iam");


queue.offer("队列已满,入队会超时等待",3, TimeUnit.SECONDS);


System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());


System.out.println(queue.poll(1, TimeUnit.SECONDS)); // 队列为空,超时等待,过时间自动结束
}
}
//test1输出
[william, will, iam]
william
will
will
iam
[]

//test2输出
false
william
will
iam
null

//test3一直阻塞


//test4输出
william
will
iam
null

同步队列

SynchronousQueue这个队列比较特殊,没有任何内部容量,甚至连一个队列的容量都没有。并且每个 put 必须等待一个 take,反之亦然。

需要区别容量为1的ArrayBlockingQueue、LinkedBlockingQueue。

以下方法的返回值,可以帮助理解这个队列:

  • iterator() 永远返回空,因为里面没有东西
  • peek() 永远返回null
  • put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
  • offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
  • take() 取出并且remove掉queue里的element,取不到东西他会一直等。
  • poll() 取出并且remove掉queue里的element,只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。
  • isEmpty() 永远返回true
  • remove()&removeAll() 永远返回false

线程池

池化技术就是事先准备好一些资源,有人需要用 久去那里拿,用完再还回去,

线程池的好处

  1. 线程复用,可以控制最大并发数,管理线程
  2. 降低资源的消耗
  3. 提高响应的速度
  4. 方便管理

使用Executors创建线程池

Executors创建的线程池实例常用的三个方法

  • newSingleThreadExecutor()创建一个使用从无界队列运行的单个工作线程的执行程序。
  • newFixedThreadPool(int nThreads)创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。
  • newCachedThreadPool()创建一个根据需要创建新线程的线程池核心线程数为0,全部线程可回收,在可用时将重新使用以前构造的线程。
  • newScheduThreadPool()创建一个定时任务线程池
  • 线程池关闭的方法pool.shutdown()
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
public class Demo1_ThreadPool {
public static void main(String[] args) {
ExecutorService pool1 = Executors.newSingleThreadExecutor();
ExecutorService pool2 = Executors.newFixedThreadPool(6);
ExecutorService pool3 = Executors.newCachedThreadPool();

try {
for (int i = 0; i < 10; i++) {
pool1.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
} finally {
//关闭线程池
pool1.shutdown();
}


try {
for (int i = 0; i < 10; i++) {
pool2.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
} finally {
//关闭线程池
pool1.shutdown();
}

try {
for (int i = 0; i < 10; i++) {
pool3.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
} finally {
//关闭线程池
pool1.shutdown();
}

}
}

//输出
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-2-thread-1
pool-2-thread-2
pool-2-thread-3
pool-2-thread-4
pool-2-thread-3
pool-2-thread-5
pool-2-thread-6
pool-2-thread-1
pool-2-thread-2
pool-2-thread-4
pool-3-thread-1
pool-3-thread-3
pool-3-thread-2
pool-3-thread-1
pool-3-thread-4
pool-3-thread-5
pool-3-thread-6
pool-3-thread-7
pool-3-thread-8
pool-3-thread-3

查看源码发现其实他们底层都是ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**
* Creates an Executor that uses a single worker thread operating
* off an unbounded queue, and uses the provided ThreadFactory to
* create a new thread when needed. Unlike the otherwise
* equivalent {@code newFixedThreadPool(1, threadFactory)} the
* returned executor is guaranteed not to be reconfigurable to use
* additional threads.
*
* @param threadFactory the factory to use when creating new
* threads
*
* @return the newly created single-threaded Executor
* @throws NullPointerException if threadFactory is null
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}

我们再来看一下ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
10
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}

七大参数

核心线程数 :@param corePoolSize 池中要保留的线程数,即使它们处于空闲状态,除非设置了 {@code allowCoreThreadTimeOut}

最大线程数:@param maximumPoolSize 池中允许的最大线程数

空闲存活时间:@param keepAliveTime 当线程数较大时与核心相比,这是多余空闲线程在终止之前等待新任务的最长时间。

超时单位:@param unit {@code keepAliveTime} 参数的时间单位

阻塞队列: @param workQueue 用于在执行任务之前保存任务的队列。该队列将仅保存由 {@code execute} 方法提交的 {@code Runnable} 任务。

线程工厂 :@param threadFactory 执行程序创建新线程时使用的工厂

拒接策略:@param handler 执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量 @throws IllegalArgumentException 如果以下情况之一成立:
{@code corePoolSize < 0}
{@code keepAliveTime < 0}
{@code maximumPoolSize <= 0}
{@code maximumPoolSize < corePoolSize}
@throws NullPointerException if {@code workQueue} 或 {@code threadFactory}或 {@code handler} 为空

四种拒绝策略

RejectedExecutionHandler 是拒绝策略的接口,有四个实现类

  • AbortPolicy : 默认拒绝策略,抛出异常
  • DiscardPolicy:不会抛出异常,会丢掉任务
  • DiscardOldestPolicy:不会抛出异常,会和最早的线程尝试竞争资源,竞争不一定会成功,失败就丢调任务
  • CallerRunsPolicy:从哪个线程来的,哪个线程处理,即main线程处理

最大线程数可以通过两种方式设置

  • CPU密集型 和本机核心数保持一致,可以保证CPU的效率最高

  • IO密集型 根据程序中大型IO耗时线程,保证大于等于

四大函数式接口

都可以使用lamda表达式简化,函数式接口 只有一个方法的接口

java.util.Function 包下的 有基本的四大函数式接口

  • Function 函数式接口

  • Predicate 断定型接口

  • Consumer 消费型接口 只有输入没有返回值

  • Supplier 供给型接口 只有输出没有参数

Function接口

apply方法接受一个参数t,返回一个参数R

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

/**
* Represents a function that accepts one argument and produces a result.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {

/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);


//接收T R返回的是R T

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class demo1_function {
public static void main(String[] args) {
Function<String, String> function = new Function<String, String>() {
@Override
public String apply(String s) {
return s;
}
};

Function function1 = (str)->{return str;};
Function function2 = str->{return str;};//当只有一个参数的时候 括号可以省略

System.out.println(function.apply("niubi"));
System.out.println(function1.apply("niubi"));
System.out.println(function2.apply("niubi"));
}
}

Predicate接口

断定型接口,根据传入的数据,只返回boolean值

can can 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**
* Represents a predicate (boolean-valued function) of one argument.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #test(Object)}.
*
* @param <T> the type of the input to the predicate
*
* @since 1.8
*/
@FunctionalInterface
public interface Predicate<T> {

/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);

示例:

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 Demo2_predicate {
public static void main(String[] args) {
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String s) {
if(!s.isEmpty()){
return true;
}
return false;
}
};

Predicate<String> predicate1 = s -> {
if(!s.isEmpty()){
return true;
}
return false;
};

System.out.println(predicate.test("william"));//true
System.out.println(predicate1.test("william"));//true
}
}

Consumer接口

消费者型接口,只有输入参数,没有返回值

can can 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**
* Represents an operation that accepts a single input argument and returns no
* result. Unlike most other functional interfaces, {@code Consumer} is expected
* to operate via side-effects.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #accept(Object)}.
*
* @param <T> the type of the input to the operation
*
* @since 1.8
*/
@FunctionalInterface
public interface Consumer<T> {

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo3_consumer {
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("一共消费了" + s + "元");
}
};

Consumer<String> consumer1 = (s)->{
System.out.println("一共消费了" + s + "元");
};

consumer.accept("20");//20
consumer.accept("30");//30
}
}

Supplier接口

没有输入参数,只有 返回值

can can 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

/**
* Represents a supplier of results.
*
* <p>There is no requirement that a new or distinct result be returned each
* time the supplier is invoked.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #get()}.
*
* @param <T> the type of results supplied by this supplier
*
* @since 1.8
*/
@FunctionalInterface
public interface Supplier<T> {

/**
* Gets a result.
*
* @return a result
*/
T get();
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo4_supplier {
public static void main(String[] args) {
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "niuBi";
}
};

Supplier<String> supplier1 = ()->{return "niuBi";};

System.out.println(supplier.get());//niuBi
System.out.println(supplier1.get());//niuBi
}

}

Stream流式计算

什么是Stream流式计算

  • 常用的集合是为了存储数集,而对于集合数据的一些处理(像筛选集合数据等)可以使用Stream流来处理
  • java.util.tream包下的Stream接口 支持顺序和并行聚合操作的一系列元素
  • Stream流可以结合四大函数式接口进行数据处理(方法的参数支持函数式接口)
    使用

集合.stream() 可以将集合对象 转为 流对象,调用流对象的一些方法进行数据操作
常用方法

  • filter(Predicate<? super T> predicate) 返回由与此给定谓词匹配的此流的元素组成的流。
  • count() 返回此流中的元素数。
  • forEach(Consumer<? super T> action) 对此流的每个元素执行操作。
  • sorted(Comparator<? super T> comparator) 返回由该流的元素组成的流,根据提供的 Comparator进行排序。
  • map(Function<? super T,? extends R> mapper)返回由给定函数应用于此流的元素的结果组成的流。
  • sorted(Comparator<? super T> comparator)返回由该流的元素组成的流,根据提供的 Comparator进行排序。
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class Demo_stream {
public static void main(String[] args) {
User user1 = new User("william1", 18, 10000);
User user2 = new User("william2", 19, 11000);
User user3 = new User("william3", 20, 12000);
User user4 = new User("william4", 21, 13000);
User user5 = new User("william5", 22, 14000);
User user6 = new User("william6", 23, 15000);

List<User> users = Arrays.asList(user1, user2, user3, user4, user5, user6);

//1. forEach
users.stream().forEach((user)->{
System.out.println(user);
});

//记得关闭stream
users.stream().close();

System.out.println("---------我是分割线--------");

//2.filter predicate接口
users.stream()
.filter((user) -> {return user.getSalary() > 12000;})
.filter((user) -> {return user.getAge() > 20;})
.forEach((user)->{
System.out.println(user);
});

users.stream().close();

//3.map 和 排序
users.stream()
.filter((user)-> user.getSalary() < 20000)
.map((user)-> user.getSalary() + 2000)
.sorted((u1,u2)->{return u1.compareTo(u2);})
.forEach((user)->{
System.out.println(user);
});

users.stream().close();
}

}



class User{
private String name;
private int age;
private int salary;

public User(String name, int age, int salary) {
this.name = name;
this.age = age;
this.salary = salary;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}
}

//输出
User{name='william1', age=18, salary=10000}
User{name='william2', age=19, salary=11000}
User{name='william3', age=20, salary=12000}
User{name='william4', age=21, salary=13000}
User{name='william5', age=22, salary=14000}
User{name='william6', age=23, salary=15000}
---------我是分割线--------
User{name='william4', age=21, salary=13000}
User{name='william5', age=22, salary=14000}
User{name='william6', age=23, salary=15000}
12000
13000
14000
15000
16000
17000

进程已结束,退出代码为 0

Fork/Join分支合并

ork/Join框架是一个实现了ExecutorService接口的多线程处理器,它专为那些可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。

与其他ExecutorService相关的实现相同的是,Fork/Join框架会将任务分配给线程池中的线程。而与之不同的是,Fork/Join框架在执行任务时使用了工作窃取算法

fork在英文里有分叉的意思,join在英文里连接、结合的意思。顾名思义,fork就是要使一个大任务分解成若干个小任务,而join就是最后将各个小任务的结果结合起来得到大任务的结果。

image-20221017132505169

感觉就是分治算法的思想

工作窃取算法指的是在多线程执行不同任务队列的过程中,某个线程执行完自己队列的任务后从其他线程的任务队列里窃取任务来执行。

工作窃取流程如下图所示:

异步回调

  • 类似ajax技术,可以进行异步执行、成功回调、失败回调。

  • java.util.concurrent包下的 接口Future,有

    • 实现子类:CompletableFuture , CountedCompleter , ForkJoinTask , FutureTask , RecursiveAction , RecursiveTask , SwingWorker
  • CompletableFuture 有静态方法

    无返回结果

​ 有返回结果

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
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {

test2();

}

// 1. 无返回值的 异步
static void test1() throws ExecutionException, InterruptedException {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("异步任务执行了!!");
} catch (Exception e) {
e.printStackTrace();
} finally {
}
});

System.out.println("willaim1");
completableFuture.get();


}

// 2. 有返回值的 异步
static void test2() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
int num = 10 / 0;
try {


TimeUnit.SECONDS.sleep(3);
return 1;


} catch (Exception e) {
e.printStackTrace();
return -1;
}
});


System.out.println("william2");

completableFuture.whenCompleteAsync((t,u)->{
System.out.println(t+"=====> " + t); // 成功的时候返回值
System.out.println(u+"=====> " + u); // 失败的信息
System.out.println("有返回值的异步执行了!");

}).exceptionally((e)->{ // 异常捕获
e.printStackTrace();
return 404;
}).get();



}
}


JMM

JMM:Java内存模型,是一个概念,不是实际存在的东西

  • 线程解锁前,必须把 共享变量 立刻 刷新为主存(每个线程都有自己一块内存称为 工作内存,操作的变量是自己内存块的变量,但是实际存在的位置是主存,因此每次操作完之后需要 更新 主存)
  • 线程加锁前:必须读取主存中的最新值搭配线程工作内存中

JMM的八种操作

  • read、load
  • use、assign
  • write、store
  • lock、unlock
  1. lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  2. read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  3. load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  4. use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  5. assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  6. store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  7. write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  8. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

volatile

保证可见性

内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo1_volatile {
private static Boolean flag = true;

public static void main(String[] args) {
new Thread(()->{
while(flag){

}
},"其他线程").start();

try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("主线程修改了flag,但是另外一个线程工作空间的flag仍然是true.");
flag = true;
}
}

若不加volatile 该程序会一直死循环,因为内存不可见,可以通过修改flag为private static volatile Boolean flag = true; 即加上volatile保证内存可见性解决问题

不保证原子性

原子性即是不可分割性,通俗的讲就是线程A在执行的时候不可打扰 要么成功 要么失败

可以使用原子类来解决原子性的问题 比如原子性的integer类

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 class VolatileDemo2 {
private static volatile int num = 0;

public static void main(String[] args) {


for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}

// 确保上面的线程执行完,只剩下 main 和 gc 线程
while(Thread.activeCount() > 2){
Thread.yield();//当前主线程 暂时停止执行
}

System.out.println("正常结果为100,输出结果为===》" + num); // 正常结果为10000,输出结果为===》9991
}

public static void add(){
num++;
}
}

可以通过加上synchronzied关键字或者Lock锁来保证原子性

或者另外一种方式 使用原子类来解决

创建 可原子更新的int 值对象

private static volatile AtomicInteger num = new AtomicInteger(0);

该对象有方法+1,原理是调用Unsafe类的方法,底层使用的CAS
num.getAndIncrement();

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
public class AtomicDemo3 {

// 创建 可原子更新的int 值对象
private static volatile AtomicInteger num = new AtomicInteger(0);

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 1000; i1++) {
// 该对象有方法+1,底层使用的CAS
num.getAndIncrement();
}
}).start();
}

// 确保上面的线程执行完,只剩下 main 和 gc 线程
while(Thread.activeCount() > 2){
Thread.yield();//当前主线程 暂时停止执行
}

System.out.println("正常结果为10000,输出结果为===》" + num); // 正常结果为10000,输出结果为===》10000
}

}

禁止指令重排

指令重排 ,简单的来说就是计算机将你的指令优化了一下,以最高效的顺序执行

源代码 ==> 编译器优化的重排 ==> 指令并行也可能重排 ==> 内存系统也会重排 执行

指令重排有个前提,他会考虑数据之间的依赖性,如果没有影响才会进行指令重排

加入volatile会避免指令重排,内存中有屏障,作用

  • 保证特定的执行顺序
  • 可以保证某些变量的内存可见性

单例模式

单例模式是Java中最简单的设计模式之一,这种模式涉及一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

注意

  • 单例类只能有一个实例
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他的对象提供这一实例

饿汉式单例

多线程下特点

  • 这种方式比较常用,但容易产生垃圾对象。私有构造器,开始直接创建实例被加载进内存。 容易造成内存空间的浪费
  • 优点:没有加锁,执行效率会提高。
  • 缺点:类加载时就初始化,浪费内存。
  • 它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo1_hungry {

// 因为是getInstance是静态方法,因此开始就被加载进内存,因此有可能会浪费内存空间
private byte[] data1 = new byte[1024* 1024];
private byte[] data2 = new byte[1024* 1024];
private byte[] data3 = new byte[1024* 1024];
private byte[] data4 = new byte[1024* 1024];

//注意 这里是私有的构造方法
private Demo1_hungry() {

}

private static final Demo1_hungry hungryMan = new Demo1_hungry();

public static Demo1_hungry getInstance(){
return hungryMan;
}
}

懒汉式单例

一般懒汉式

特点

  • 需要使用单例时,单例才初始化占用内存。

  • 单线程下没问题,但是线程下是不安全的,会出现多个实例。

  • 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
    这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

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
 public class Demo2_lazy {

private static Demo2_lazy lazyMan;

private Demo2_lazy(){
System.out.println(Thread.currentThread().getName() + "线程拿到了实例!");
}

public static Demo2_lazy getInstance(){
//需要使用单例的情况下 且实例没有被创建 才创建单例
if(lazyMan == null){
return new Demo2_lazy();
}
return lazyMan;
}

public static void main(String[] args) {
//模拟线程拿到实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
Demo2_lazy.getInstance();
},String.valueOf(i)).start();
}
}
}

//输出
0线程拿到了实例!
4线程拿到了实例!
5线程拿到了实例!
3线程拿到了实例!
2线程拿到了实例!
1线程拿到了实例!
7线程拿到了实例!
6线程拿到了实例!
8线程拿到了实例!
9线程拿到了实例!

//很明显不能保证一个单例

加锁懒汉式

特点

  • 相比一般的懒汉式,在获取实例的静态方法前加synchronized关键字,可以保证多线程之间的同步问题,保证单例模式
  • 这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
  • 优点:第一次调用才初始化,避免内存浪费。
  • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)
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
public class Demo3_sychronziedLazyMan {

private static Demo3_sychronziedLazyMan lazyMan;

private Demo3_sychronziedLazyMan(){
System.out.println(Thread.currentThread().getName() + "线程拿到了实例!");
}

public static synchronized Demo3_sychronziedLazyMan getInstance(){
//需要使用单例的情况下 且实例没有被创建 才创建单例
if(lazyMan == null){
lazyMan = new Demo3_sychronziedLazyMan();
}
return lazyMan;
}

public static void main(String[] args) {
//模拟线程拿到实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
Demo3_sychronziedLazyMan.getInstance();
},String.valueOf(i)).start();
}
}
}

//输出
0线程拿到了实例!

//只有一个线程拿到了实例

DCL双重校验锁

特点

  • DCL即 double-checked locking
  • 相比一般懒汉式,又加了一层 if(实例还不存在) { synchronized (单例类) { if(实力还不存在) 初始化单例} }, volatile关键字 保证线程之间的同步问题
  • 相比加锁的懒汉式,不是在方法前面加synchronized从而影响效率,这种方法效率更高。
    这种方式采用双锁机制,安全且在多线程情况下能保持高性能。 getInstance() 的性能对应用程序很关键。
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
public class Demo4_DCLLazyMan {

private static Demo4_DCLLazyMan lazyMan;

private Demo4_DCLLazyMan(){
System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
}

public static Demo4_DCLLazyMan getInstance(){
//第一次判断,没有实例的时候给类加锁
if(lazyMan == null){
synchronized (Demo4_DCLLazyMan.class){

//第二次判断,没有实例的时候 获得单例
if(lazyMan == null){
lazyMan = new Demo4_DCLLazyMan();
}
}
}
return lazyMan;
}

public static void main(String[] args) {
//模拟线程拿到实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
Demo3_sychronziedLazyMan.getInstance();
},String.valueOf(i)).start();
}
}
}

//输出
0线程拿到了实例!

但是上面的lazyMan = new Demo4_DCLLazyMan();依然有一定的缺陷,在JMM中并不是原子操作,创建实例的过程大致分为三个步骤

  • 分配内存空间
  • 执行构造方法
  • 将实例变量指向内存空间

而在多线程执行时候,最后两步是可以变的,而这就可能导致多线程同步异常,获取单例失败,因此可以通过使用volatile关键字禁止指令重排解决常见操作非原子性的问题

1
2
//所以可以将上述代码改为
private static volatile Demo4_DCLLazyMan lazyMan;

CAS

概念

  • CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。
  • CAS也是现在面试经常问的问题
    之前学习的java.util.concurrent.automics下的AtomicsInteger等类提供简单的cas操作
1
2
3
4
5
6
7
8
9
10
public class Demo1_CAS {
public static void main(String[] args) {
AtomicInteger num = new AtomicInteger(100);
//期望 更新
//如果期望的值达到了就跟新 否则不更新
num.compareAndSet(100,101);
System.out.println(num.compareAndSet(101, 120));//true
System.out.println(num.compareAndSet(150, 130));//false
}
}

而Java实现CAS的功能使用的依然是我们熟悉的Unsafe类

ABA问题

ABA问题即是狸猫换太子,也就是一个线程速度过快,在正常线程1,2之间穿插了一个捣乱的线程,但是捣乱线程的操作会使 数据恢复为未执行之前的状态,故不会影响正常线程执行结果,一般情况下ABA并不会出现什么问题,但是设计引用的时候就会出现问题。

通过原子引用来解决ABA问题

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
public class ABA {

private static AtomicStampedReference<Integer> num = new AtomicStampedReference<>(1,1);

public static void main(String[] args) {

//线程2 期望的版本号
int currentStamp = num.getStamp();

new Thread(()->{
System.out.println("=============正常线程1===========");

//获得版本号
System.out.println("正常版本号1 拿到版本号" + num.getStamp());
},"正常线程").start();

//休眠2s
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}

//捣乱的线程不影响正常结果
new Thread(()->{
System.out.println("=========I AM 捣乱线程==========");
System.out.println(num.compareAndSet(1,2,num.getStamp(), num.getStamp() + 1));
System.out.println("捣乱线程第一次修改值后的 版本号===》" + num.getStamp());
System.out.println(num.compareAndSet(2, 1, num.getStamp(), num.getStamp() + 1));
System.out.println("捣乱线程第二次修改值后的 版本号===》" + num.getStamp());
},"捣乱线程").start();

//休眠2s
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}

//正常线程2
new Thread(()->{
System.out.println("=============正常线程2===========");

//获得版本号
System.out.println("正常版本号2执行前 当前版本号" + num.getStamp());
System.out.println("正常版本号2执行前 期望版本号" + currentStamp);
System.out.println(num.compareAndSet(1000, 3000, currentStamp, num.getStamp() + 1));
System.out.println("正常线程2执行后 版本号===》" + num.getStamp());
},"正常线程").start();
}
}

//输出
=============正常线程1===========
正常版本号1 拿到版本号1
=========I AM 捣乱线程==========
true
捣乱线程第一次修改值后的 版本号===》2
true
捣乱线程第二次修改值后的 版本号===》3
=============正常线程2===========
正常版本号2执行前 当前版本号3
正常版本号2执行前 期望版本号1
false
正常线程2执行后 版本号===》3

进程已结束,退出代码为 0

各种锁

公平,非公平锁

概念

  • 公平锁:非常公平,线程之间不可以插队
  • 非公平锁:非常不公平,线程之间可以插队(默认)

这里的“公平”,其实通俗意义来说就是“先来后到”,也就是FIFO。如果对一个锁来说,先对锁获取请求的线程一定会先被满足,后对锁获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。

一般情况下,非公平锁能提升一定的效率。但是非公平锁可能会发生线程饥饿(有一些线程长时间得不到锁)的情况。所以要根据实际的需求来选择非公平锁和公平锁。

ReentrantLock支持非公平锁和公平锁两种。

乐观锁与悲观锁

悲观锁:

悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁:

乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。

由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁

乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

可重入锁

所谓重入锁,顾名思义。就是支持重新进入的锁,也就是说这个锁支持一个线程对资源重复加锁

synchronized关键字就是使用的重入锁。比如说,你在一个synchronized实例方法里面调用另一个本实例的synchronized实例方法,它可以重新进入这个锁,不会出现任何异常。

如果我们自己在继承AQS实现同步器的时候,没有考虑到占有锁的线程再次获取锁的场景,可能就会导致线程阻塞,那这个就是一个“非可重入锁”。

ReentrantLock的中文意思就是可重入锁。

自旋锁

概述

  • AtomicXxx类下的CAS方法底层就是 自旋锁,不断循环直到成功为止
  • 自旋锁主要应用CAS操作,直到手动释放锁才停止

主要就是while啥的,不成功就一直循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Atomically updates the current value with the results of
* applying the given function, returning the previous value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the previous value
* @since 1.8
*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}

死锁

概念

  • 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
  • 两个线程互相抢夺资源,互相争用还未释放的锁

可以使用下面的方法排查死锁

  • 使用命令 jps -1 定位进程号
  • 使用jstack 进程号 查看堆栈信息

Java并发
http://example.com/2022/09/14/Java并发/
作者
Mercury
发布于
2022年9月14日
许可协议