开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java 多线程及并发知识大集合 并发

发表于 2021-11-21

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」

并发

多线程

实现多线程的三种方式

Runnable

  1. class CountDown implements Runnable
    启动方式:new Thread(new CountDown(),"Runnable").start();//new Thread()第二个参数是线程名字
    方法定义:
1
2
csharp复制代码public void run() {
}

Callable

  1. class ThreadCall implements Callable<String>String是可以替换的,是回调的值
    启动方式:
    使用这个接口要使用FutrueTask类作为简单的适配类
1
2
arduino复制代码FutureTask<String> ft = new FutureTask<>(new ThreadCall());
new Thread(ft,"Callable").start()

方法定义:

1
2
3
4
java复制代码@Override
public String call() throws Exception {
return "";
}

特别注意
(1)Callable规定的方法是call(),而Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的,为了支持此功能,Java中提供了Callable接口。

(3)call()方法可抛出异常,而run()方法是不能抛出异常的。
(4)运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。
(5)FutureTask 的get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

Thread

  1. class MyThread extends Thread
    启动方式:
1
scss复制代码new MyThread().start();

方法定义:

1
2
csharp复制代码public void run() {
}
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
java复制代码import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class SynTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Thread(new CountDown(),"Runnable").start();
new MyThread().start();
int time = 5;
FutureTask<String> ft = new FutureTask<>(new ThreadCall());
new Thread(ft,"Callable").start();
String output=ft.get();
System.out.println("Callable output is:"+output);
System.out.println("以下是被堵塞的线程");
while(time>=0){
System.out.println(Thread.currentThread().getName() + ":" +time );
time--;
try {
Thread.sleep(1000); //睡眠时间为1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ThreadCall implements Callable<String> {
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
int time=5;
while (time>0) {
System.out.println(Thread.currentThread().getName() + ":" + time--);
try {
Thread.sleep(1000); //睡眠时间为1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return time+"";
}
}



class MyThread extends Thread{
int time=5;
public void run() {
Thread.currentThread().setName("Thread");
while (time>=0){
System.out.println(Thread.currentThread().getName() + ":" + time--);
try {
Thread.sleep(1000); //睡眠时间为1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class CountDown implements Runnable{
int time = 5;
public void run() {
while(time>=0){
System.out.println(Thread.currentThread().getName() + ":" + time--);
try {
Thread.sleep(1000); //睡眠时间为1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

线程同步

竞争状态

当多个线程访问同一公共资源并引发冲突造成线程不同步,我们称这种状态为竞争状态。

线程安全

是针对类来说的,如果一个类的对象在多线程程序中不会导致竞争状态,我们就称这类为线程安全的,如StringBuffer是线程安全的,而StringBuilder则是线程不安全的。

临界区

造成竞争状态的原因是多个线程同时进入了程序中某一特定部分,我们称这部分为程序中的临界区,我们要解决多线程不同步问题,就是要解决如何让多个线程有序的访问临界区。

java在处理线程同步时常用方法

1、synchronized关键字。

2、Lock显示加锁。

3、信号量Semaphore。

volatile关键字

volatile关键字经常在并发编程中使用,其特性是保证可见性以及有序性

Java内存模型

所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

image.png
基于此种内存模型,便产生了多线程编程中的数据“脏读”等问题。
例如两个线程分别读取10存入各自所在的工作内存当中,然后线程1进行加100操作,然后把i的最新值101写入到内存。此时线程2的工作内存当中i的值还是10,进行加1操作之后,i的值为11,然后线程2把i的值写入内存,为11,但是想要111。

并发编程的三大概念:原子性,有序性,可见性

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

1
2
3
4
5
ini复制代码//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;

当线程1执行 i =10这句时,会先把i的初始值加载到工作内存中,然后赋值为10,那么在线程1的工作内存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到线程2的工作内存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
Java提供了volatile关键字来保证可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性
有序性:即程序执行的顺序按照代码的先后顺序执行
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行

1
2
3
4
5
6
7
8
9
scss复制代码//线程1:
context = loadContext(); //语句1,加载配置环境
inited = true; //语句2,加载配置环境成功

//线程2:
while(!inited ){ //等待加载配置环境
sleep()
}
doSomethingwithconfig(context); //用配置环境工作

于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

volatile保证可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

1
2
3
4
5
6
7
8
arduino复制代码//如果stop不用volatile修饰,那么可能性很小当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

volatile不能确保原子性

Note:自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存三个操作步骤。

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了

线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10(可以这样认为,把inc赋值给变量a,变量a又赋值给变量b,那么inc的值会重新刷新(inc==10为false!!!),但是b的值不会刷新了),所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存

那么两个线程分别进行了一次自增操作后,inc只增加了1

volatile保证有序性

volatile关键字禁止指令重排序有两层意思:
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

synchronized关键字

不太清楚synchronized以及notify的用法,以及锁方法,锁类的实现原理,还有信号量,感觉都是不同的东西,但是具体的用法忘记的差不多了。
官方解释:
Synchronized同步方法可以支持使用一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

简单就是说Synchronized的作用就是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保同一个时刻最多只有一个线程执行同步代码,从而保证多线程环境下并发安全的效果。 如果有一段代码被Synchronized所修饰,那么这段代码就会以原子的方式执行,当多个线程在执行这段代码的时候,它们是互斥的,不会相互干扰,不会同时执行。

Synchronized工作机制是在多线程环境中使用一把锁,在第一个线程去执行的时候去获取这把锁才能执行,一旦获取就独占这把锁直到执行完毕或者在一定条件下才会去释放这把锁,在这把锁释放之前其他的线程只能阻塞等待。

synchronized是Java中的关键字,被Java原生支持,是一种最基本的同步锁。
它修饰的对象有以下几种:
  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  4. 修改一个类,其作用的范围是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
typescript复制代码可能会卖同一张票,没有票了可能也会继续卖
public static void main(String[] args) {
// write your code here
MyThread1 r1=new MyThread1();
Thread t1=new Thread(r1,"First_Thread1");
Thread t2=new Thread(r1,"Second_Thread2");
Thread t3=new Thread(r1,"Third_Thread2");
Thread t4=new Thread(r1,"Forth_Thread2");
t1.start();
t2.start();
t3.start();
t4.start();
}

static class MyThread1 implements Runnable{
public static Integer num=10;//通过静态变量,让他们卖的是同一张票
@Override
public void run() {
while (num>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName()+ " 正在卖 "+num--+" 张车票");
}
}
}



Forth_Thread2 正在卖 10 张车票
First_Thread1 正在卖 9 张车票
Third_Thread2 正在卖 8 张车票
Second_Thread2 正在卖 7 张车票
First_Thread1 正在卖 6 张车票
Forth_Thread2 正在卖 5 张车票
Second_Thread2 正在卖 6 张车票
Third_Thread2 正在卖 4 张车票
Second_Thread2 正在卖 3 张车票
Forth_Thread2 正在卖 1 张车票
First_Thread1 正在卖 2 张车票
Third_Thread2 正在卖 0 张车票
Second_Thread2 正在卖 -1 张车票

synchronized 修饰方法

//如果synchronized是锁起来整个方法的,synchronized修饰函数不需要传入字符串参数,相当于默认是this

对于此案例,是两个线程之间竞争售票,因此不适宜锁起来整个方法

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
java复制代码    static  class  MyThread1 implements Runnable{
public static Integer num=10;
@Override
public synchronized void run() {
while (num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName()+ " 正在卖 "+num--+" 张车票");
}
}
}
只有First_Thread在卖票了
First_Thread1 正在卖 10 张车票
First_Thread1 正在卖 9 张车票
First_Thread1 正在卖 8 张车票
First_Thread1 正在卖 7 张车票
First_Thread1 正在卖 6 张车票
First_Thread1 正在卖 5 张车票
First_Thread1 正在卖 4 张车票
First_Thread1 正在卖 3 张车票
First_Thread1 正在卖 2 张车票
First_Thread1 正在卖 1 张车票

synchronized()中锁的是Object对象

synchronized 修饰代码块

可以定义公共的str静态变量,若不定义为static静态,则两个线程的str是线程自己的,而不是公共的

也可以用synchronized (“ “) {
//在需要加锁保证完整运行的代码块旁边加上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
typescript复制代码    static  class  MyThread1 implements Runnable{
public static Integer num=10;
public static String str = new String("weimeig");
@Override
public void run() {
while (true){
synchronized (str){
if(num>0){
System.out.println( Thread.currentThread().getName()+ " 正在卖 "+num--+" 张车票");
}else {
break;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
}


Forth_Thread2 正在卖 10 张车票
Second_Thread2 正在卖 9 张车票
First_Thread1 正在卖 8 张车票
Third_Thread2 正在卖 7 张车票
Third_Thread2 正在卖 6 张车票
Forth_Thread2 正在卖 5 张车票
First_Thread1 正在卖 4 张车票
Second_Thread2 正在卖 3 张车票
Third_Thread2 正在卖 2 张车票
First_Thread1 正在卖 1 张车票

wait notify

  1. wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
  2. 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
    当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
    只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
    也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
  3. wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
  4. notify 和 notifyAll的区别
    notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
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
java复制代码import java.util.LinkedList;
import java.util.Queue;

public class ProducerAndConsumer {
private final int MAX_LEN = 10;
private Queue<Integer> queue = new LinkedList<Integer>();
class Producer extends Thread {
@Override
public void run() {
producer();
}
private void producer() {
while(true) {
synchronized (queue) {
while (queue.size() == MAX_LEN) {
queue.notify();
System.out.println("当前队列满");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(1);
queue.notify();
System.out.println("生产者生产一条任务,当前队列长度为" + queue.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Consumer extends Thread {
@Override
public void run() {
consumer();
}
private void consumer() {
while (true) {
synchronized (queue) {
while (queue.size() == 0) {
queue.notify();
System.out.println("当前队列为空");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
queue.notify();
System.out.println("消费者消费一条任务,当前队列长度为" + queue.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
ProducerAndConsumer pc = new ProducerAndConsumer();
Producer producer = pc.new Producer();
Consumer consumer = pc.new Consumer();
producer.start();
consumer.start();
}
}

Lock下的 Condition 的await和signal()

在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性

针对Object的notify/notifyAll方法
void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

针对Object的wait方法
void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

Condition优点

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持

。

Lock接口

lock 概念

  1. Lock是一个接口,类似于List
  2. Lock和ReadWriteLock接口是两大锁的根接口
  3. Lock代表实现类是ReentrantLock(可重入锁)
  4. ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
  5. Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

锁的名词

1. 可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。前提得是同一个对象或者class)
锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
记录拥有锁的线程id以及获得锁的次数。再次获取的时候,如果这个锁不是它的则要等待,如果是它的话,总数+1。当释放的时候,拥有锁的个数减一。
对于synchronized而言,有多少个都行,自动释放
对于lock.lock();ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样
例如

1
2
3
4
5
6
csharp复制代码lock.lock();
lock.lock();
lock.lock();
lock.unlock();
lock.unlock();
lock.unlock();

作用:可重入锁是为了避免死锁。

2. 可中断锁

可中断锁就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
  如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性。   

3. 公平锁

公平锁即 尽量 以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。而非公平锁则无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。

  在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁   

4.自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
优点:
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
缺点:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

基于自旋锁,可以实现具备公平性和可重入性质的锁。

TicketLock:采用类似银行排号叫好的方式实现自旋锁的公平性,但是由于不停的读取serviceNum,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

CLH也是一种基于单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

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
java复制代码public interface Lock {
void lock();

void unlock();
}

public class QNode {
volatile boolean locked;
}


import java.util.concurrent.atomic.AtomicReference;

public class CLHLock implements Lock {
// 尾巴,是所有线程共有的一个。所有线程进来后,把自己设置为tail
private final AtomicReference<QNode> tail;//被final修饰的变量,不可变的是变量的引用,而不是变量的内容
// 前驱节点,每个线程独有一个。
private final ThreadLocal<QNode> myPred;
// 当前节点,表示自己,每个线程独有一个。
private final ThreadLocal<QNode> myNode;

public CLHLock() {。//仅仅是初始化而已
this.tail = new AtomicReference<QNode>(new QNode());
this.myNode = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return new QNode();
}
};
this.myPred = new ThreadLocal<QNode>();
}

@Override
public void lock() {
// 获取当前线程的代表节点
QNode node = myNode.get();
// 将自己的状态设置为true表示获取锁。
node.locked = true;
// 将自己放在队列的尾巴,并且返回以前的值。第一次进将获取构造函数中的那个new QNode
QNode pred = tail.getAndSet(node);//相当于两步操作,QNode pred=tail.get(); tail.set(node);
// 把旧的节点放入前驱节点。
myPred.set(pred);
// 判断前驱节点的状态,然后走掉。
while (pred.locked) {
}
}

@Override
public void unlock() {
// unlock. 获取自己的node。把自己的locked设置为false。
QNode node = myNode.get();
node.locked = false;
myNode.set(myPred.get());//可能为了突然中断,插队之类功能
}
}

MCSLock则是对本地变量的节点进行循环。
MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

AtomicReferenceFieldUpdater
一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的)

这步很经典

1
2
3
vbscript复制代码 // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
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
java复制代码import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isWaiting = true; // 默认是在等待锁
}
volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// 获取队列末尾的值保存在predecessor,并将自己添加到队伍末尾
if (predecessor != null) {//判断之前队伍是不是空的
predecessor.next = currentThread;// 告诉前面哥们,我排在你后面,你搞完了记得告诉我一下
while (currentThread.isWaiting) {// 等待前面哥们给自己的通知
}
} else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
currentThread.isWaiting = false;
}
}

public void unlock(MCSNode currentThread) {
if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
return;
}

if (currentThread.next == null) {// 检查是否有人排在自己后面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者!!!!!
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isWaiting = false;
currentThread.next = null;// for GC
}
}

CLHLock和MCSLock通过链表的方式避免了减少了处理器缓存同步,极大的提高了性能,区别在于CLHLock是通过轮询其前驱节点的状态,而MCS则是查看当前节点的锁状态。

CLHLock在NUMA架构下使用会存在问题。在没有cache的NUMA系统架构中,由于CLHLock是在当前节点的前一个节点上自旋,NUMA架构中处理器访问本地内存的速度高于通过网络访问其他节点的内存,所以CLHLock在NUMA架构上不是最优的自旋锁。

     

Lock接口有6个方法

lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的
unLock()方法是用来释放锁的
newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 获取锁  
void lock()

// 如果当前线程未被中断,则获取锁,可以响应中断
void lockInterruptibly()

// 返回绑定到此 Lock 实例的新 Condition 实例
Condition newCondition()

// 仅在调用时锁为空闲状态才获取该锁,可以响应中断
boolean tryLock()

// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
boolean tryLock(long time, TimeUnit unit)

// 释放锁
void unlock()

用法
lock()

1
2
3
4
5
6
7
8
9
csharp复制代码Lock lock = ...;//=new ReentrantLock();
lock.lock();
try{
//处理任务
}catch(Exception ex){

}finally{
lock.unlock(); //释放锁!
}

tryLock()

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){

}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}

lockInterruptibly();
通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。
例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者
synchronized 中,当一个线程处于等待某个锁的状态,是无法被中断的

1
2
3
4
5
6
7
8
9
csharp复制代码public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}

Lock的实现类 ReentrantLock
Lock lock=new ReentrantLock(false); 通过参数可以设定是否为公平的锁

读写锁
ReadWriteLock lock=new ReentrantReadWriteLock();
lock.readLock().lock();

lock 与 synchronized的区别

  1. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  2. 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
  3. 占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待。Lock 可以通过:只等待一定的时间:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly()))解决 。
  4. synchronized当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,lock通过读写锁解决问题
  5. 可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

lock demo

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
csharp复制代码    static  class  MyThread1 implements Runnable{
public static Integer num=10;
Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();
if(num>0){
System.out.println( Thread.currentThread().getName()+ " 正在卖 "+num--+" 张车票");
}else {
break;
}
}finally {
lock.unlock();
}

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
}

Semaphore

Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。

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
java复制代码class TestSemaphore {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(3);
// 模拟10个客户端访问
for (int index = 0; index < 10; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
synchronized (this){
semp.acquire();
System.out.println(Thread.currentThread().getName()+ "Accessing: " + NO);
Thread.sleep((long) (Math.random() * 1000));
// 访问完后,释放
System.out.println("-----------------"+semp.availablePermits());
semp.release();//之前上句话和这句话位置互换就不对了,换成这样的到了想要的结果了

}

} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
}

运行结果基本为这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
lua复制代码pool-1-thread-1Accessing: 0
pool-1-thread-2Accessing: 1
pool-1-thread-3Accessing: 2
-----------------0
pool-1-thread-4Accessing: 3
-----------------0
pool-1-thread-5Accessing: 4
-----------------0
pool-1-thread-6Accessing: 5
-----------------0
pool-1-thread-7Accessing: 6
-----------------0
pool-1-thread-8Accessing: 7
-----------------0
pool-1-thread-9Accessing: 8
-----------------0
pool-1-thread-10Accessing: 9
-----------------0
-----------------1
-----------------2

Process finished with exit code 0

线程池

线程的生命周期

image.png
线程池概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
线程池的优点
在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了

如何创建一个线程池那?

Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor.

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
java复制代码package com.mianshi.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewFixedThreadPoolTest {

public static void main(String[] args) {
// 创建一个可重用固定个数的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);//创建空闲的线程
for (int i = 0; i < 10; i++) {
fixedThreadPool.execute(new Runnable() {//程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
public void run() {
try {
// 打印正在执行的缓存线程信息
System.out.println(Thread.currentThread().getName()
+ "正在被执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行
pool-1-thread-2正在被执行
pool-1-thread-3正在被执行
pool-1-thread-1正在被执行

因为线程池大小为3,每个任务输出打印结果后sleep 2秒,所以每两秒打印3个结果。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

线程池的任务

  1. Runnable对象可以直接扔给Thread创建线程实例,并且创建的线程实例与Runnable绑定,线程实例调用start()方法时,Runnable任务就开始真正在线程中执行。注意:如果直接调用run()方法而不调用start()方法,那么只是相当于普通的方法调用,并不会开启新的线程,程序只是在主线程中串行执行。
  2. Runnable对象也可以直接扔给线程池对象的execute方法和submit方法,让线程池为其绑定池中的线程来执行。
  3. Runnable对象也可以进一步封装成FutureTask对象之后再扔给线程池的execute方法和submit方法。
  4. Callable:功能相比Runnable来说少很多,不能用来创建线程,也不能直接扔给线程池的execute方法。但是其中的call方法有返回值。
  5. FutureTask:是对Runnable和Callable的进一步封装,并且这种任务是有返回值的,它的返回值存在FutureTask类的一个名叫outcome的数据成员中。(疑惑)那么为什么可以把没有返回值的Runnable也封装成FutureTask呢,马上我们会讨论这个问题。相比直接把Runnable和Callable扔给线程池,FutureTask的功能更多,它可以监视任务在池子中的状态。用Runnable和Callable创建FutureTask的方法稍有不同。
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
csharp复制代码class NewFixedThreadPoolTest {

public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建一个可重用固定个数的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);//创建空闲的线程
ArrayList<FutureTask<String>> list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
FutureTask<String> ft=new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "回掉结果";
}
});
list.add(ft);
fixedThreadPool.execute(ft);
}
fixedThreadPool.shutdown();//不关闭线程池,主线程也不会关闭
for (FutureTask<String> ft:list
) {
System.out.println(ft.get());
}
if(list.get(0).get().equals("回掉结果")){
System.out.println("开始进攻");
}
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码pool-1-thread-2
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
回掉结果
回掉结果
pool-1-thread-2
回掉结果
回掉结果
回掉结果
回掉结果
回掉结果
回掉结果
回掉结果
pool-1-thread-1
回掉结果
开始进攻

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mysql的binlog redolog undol

发表于 2021-11-21

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战。

日志是 mysql 数据库的重要组成部分,记录着数据库运行期间各种状态信息。mysql日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,我们重点需要关注的是二进制日志( binlog )和事务日志(包括

redo log 和 undo log ),本文接下来会详细介绍这三种日志。

一、日志定义

binlog二进制日志是server层的无论MySQL用什么引擎,都会有的,主要是左主从复制,时间点恢复使用;

redo log重做日志是InnoDB存储引擎层的,用来保证事务安全;

undo log回滚日志保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。undolog用来保证事务的一致性,在事务回滚的时候会查询对应的undolog,进行回滚。

1
2
lua复制代码undolog分为两种,一种是insert undolog,这种是插入语句对应的的undo log,他不会被作为mvcc,于是在提交之后这个log直接释放。
还有另外一种就是update undolog 这种是会以用于mvcc的,于是这种undolog是会用链表串起来的,知道没有开启的事务早于当前undolog的创建时间,这个时候这个undolog就可以删除了。
日志 物理日志 逻辑日志 定义 作用
redo log InnoDB存储引擎层的日志 N 重做日志 事务的原子性和持久性
undo log N Y 回滚日志 事务一致性,事务的回滚和实现mvcc
binlog N MySQL Server层记录的日志 归档日志 主从数据同步,数据恢复
  • 逻辑日志:可以简单理解为记录的就是sql语句 。
  • 物理日志:mysql 数据最终是保存在数据页中的,物理日志记录的就是数据页变更

二、日志作用

redo log日志模块

redo log 包括两部分:一个是内存中的日志缓冲( redo log buffer ),另一个是磁盘上的日志文件( redo log file )。mysql 每执行一条 DML 语句,先将记录写入 redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file 。这种 先写日志,再写磁盘 的技术就是 MySQL里经常说到的 WAL(Write-Ahead Logging) 技术。

redo log是InnoDB存储引擎层的日志,又称重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。在实例和介质失败(media failure)时,redo log文件就能派上用场,如数据库掉电,InnoDB存储引擎会使用redo log恢复到掉电前的时刻,以此来保证数据的完整性。

在一条更新语句进行执行的时候,InnoDB引擎会把更新记录写到redo log日志中,然后更新内存,此时算是语句执行完了,然后在空闲的时候或者是按照设定的更新策略将redo log中的内容更新到磁盘中,这里涉及到WAL即Write Ahead logging技术,他的关键点是先写日志,再写磁盘。

有了redo log日志,那么在数据库进行异常重启的时候,可以根据redo log日志进行恢复,也就达到了crash-safe。

redo log日志的大小是固定的,即记录满了以后就从头循环写。

binlog日志模块

  1. 主从复制 :在 Master 端开启 binlog ,然后将 binlog 发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
  2. 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。

binlog是属于MySQL Server层面的,又称为归档日志,属于逻辑日志,是以二进制的形式记录的是这个语句的原始逻辑,依靠binlog是没有crash-safe能力的;

binlog 可以用于主从复制中,从库利用主库上的 binlog 进行重播,实现主从同步。用于数据库的基于时间点、位点等的还原操作。binlog 的模式分三种:Statement、Row、Mixed。

在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW。日志格式通过 binlog-format 指定。

undo log日志模块

数据库事务四大特性中有一个是 原子性 ,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。实际上, 原子性 底层就是通过 undo log 实现的。undo log 主要记录了数据的逻辑变化,比如一条 INSERT 语句,对应一条 DELETE 的 undo log ,对于每个 UPDATE 语句,对应一条相反的 UPDATE 的 undo log ,这样在发生错误时,就能回滚到事务之前的数据状态。

此外,保存了事务发生之前的数据的一个版本,作用:

  • 可以用于回滚;
  • 同时可以提供多版本并发控制下的读(MVCC),也即非锁定读;
  • 事务开始之前,将当前事务版本生成 undo log,undo log 也会产生 redo log 来保证 undo log 的可靠性;
  • 当事务提交之后,undo log 并不能立马被删除,而是放入待清理的链表;
  • 由 purge 线程判断是否有其它事务在使用 undo 段中表的上一个事务之前的版本信息,从而决定是否可以清理 undo log 的日志空间。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从源码层面解读spring中bean的循环依赖解决方案(2)

发表于 2021-11-21

写在前面的话

我们最终都要远行,

最终都要跟稚嫩的自己告别。

也许路途注定艰辛,注定孤独,

但熬过了痛苦,我们才能得以成长,

要知道生活把我们磨圆是为了让我们滚得更远。

之前我们已经结束了依赖循环的定义种类和解决原理,接下来我们从源码层面来解读下spring是如何实现的解决依赖循环。

让我们先了解一些前置知识点,

FactoryBean

FactoryBean是一个工厂Bean,可以生成某一个类型Bean实例,它最大的一个作用是:可以让我们自定义Bean的创建过程。

一般情况下,Spring是通过反射机制利用bean的class属性指定实现类来实例化bean的。

但是在某些特殊情况下,实例化bean过程比较复杂,属性要求很多,这时如果再继续采用传统的xml配置方式,

就需要在</bean/>中提供大量的配置信息了,灵活性受到了限制。

在这样的背景下,就产生了采用编码方式提供一个org.springframework.beans.factory.FactoryBean的工厂类,用户可以通过实现该接口定制化实例化bean逻辑的简单方案。

以下是源码:

1
2
3
4
5
6
7
8
9
java复制代码public interface FactoryBean<T> {
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}

定义了三个方法:

  • T getObject():返回FactoryBaen创建的bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中。
  • Class<?> getObjectType():返回FactoryBean创建的bean类型。
  • boolean isSingleton():返回由FactoryBean创建的bean实例的作用域是singleton还是prototype。

具体解释下:

当配置文件中</bean/>的class属性配置的实现类是FactoryBean时,

通过getBean()方法放回的不是FactoryBean本身,

而是FactoryBean#getObject()方法返回的对象,相当于FactoryBean#getObject()代理了getBean()。

从缓存中获取单例bean

接下来我们就可以了解bean的加载过程了。

下面就是我们重要的原理了:

单例在Spring的同一个容器内只会被创建一次,

后续需要再获取bean时直接从单例缓存中获取即可,

当然这里也只是尝试创建,首先尝试从缓存中加载,

然后再次尝试从singletonFactories中加载。

这样加载的原因是:

因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖,

Spring创建bean的原则是不等bean创建完成就会将创建的ObjectFactory提早曝光加入到缓存中,

一旦下一个bean创建需要依赖到上一个bean时,就可以直接使用ObjectFactory。

1
2
3
4
typescript复制代码public Object getSingleton(String beanName) {
//参数true设置标识运行早期依赖
return getSingleton(beanName, true);
}
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
kotlin复制代码protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
//如果为空,则锁定全局变量并进行处理
synchronized (this.singletonObjects) {
//如果当前bean正在加载则不处理
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
//当某些方法需要提前初始化的时候则会调用addSingletonFactory方法将对应的
//ObjectFactory初始化策略存储在singletonFactories中
//这里就是将存储的ObjectFactory取出来操作。
ObjectFactory/</?/>/ singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//调用预先设定的getObject方法
singletonObject = singletonFactory.getObject();
//记录在缓存中,earlySingletonObjects和singletonFactories互斥
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}

接下来解读下上面的源代码

在这个方法中我们就已经涉及到了关键的循环依赖检测了同时也涉及很多变量的记录存取。

方法分下面步骤:

  • 1.首先尝试从singletonObject里面获取实例,
  • 2.如果获取不到就会从earlySingletonObjects里面获取,
  • 3.如果还是获取不到,才会从singletonFactories里面获取beanName对应的ObjectFactory,

调用这个ObjectFactory的getObjiect来创建bean,

  • 4.将bean放到earlySingletonObjects里面去,同时从singletonFactories里面remove

掉上面获取到的ObjectFactory,

而对于后续所有内存操作都只是为了循环依赖检测时使用,也就是源码中的allowEarlyReference为true的情况下才会使用。

上面涉及到多个存储bean的map,就是我们常说的3级缓存。

到这我们可以说:Spring就是通过三级缓存来解决循环依赖的。

1
2
3
4
5
6
7
8
9
swift复制代码/**1级缓存,单例对象的缓存:bean名称——> bean实例*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);


/** 2级缓存,早期单例对象的缓存:bean名称——> bean实例 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** 3级缓存,单例工厂的缓存:bean名称——> ObjectFactory*/
private final Map<String, ObjectFactory<?/>/>/ singletonFactories = new HashMap<>(16);
  • singletonObjects:用于保存BeanName和创建bean实例之前的关系,bean name–>bean instance。
  • earlySingletonObjects:也是保存BeanName和创建bean实例之前的关系,
    但是与singletonObjects的不同之处在于,当一个单例bean被加入这里之后,

即使bean还在创建过程中,也可以通过getBean方法获取到了,其目的是用来检测循环引用。

  • singletonFactories:用于保存BeanName和创建bean的工厂之间的关系,
    bean name–>ObjectFactory。

写在后面的话

到这里我们已经知道了解决方案是Spring的3级缓存,通过提前暴露创建bean的ObjectFactory工厂,将其加入缓存,

获取单例的时候优先在去缓存中找,如果没有找到再进行bean的创建。

某些方法需要提前初始化的时候则会调用addSingletonFactory方法将对应的ObjectFactory初始化策略存储在singletonFactories中,

至于如何将bean初始化,在什么时候初始化,如果缓存中没有bean时会怎么办?

我们下一篇会继续解释。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

访问者模式

发表于 2021-11-21

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

概述

定义:

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

结构

访问者模式包含以下主要角色:

  • 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
  • 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

类图如下:

代码如下:

创建抽象访问者接口

1
2
3
4
5
java复制代码public interface Person {
void feed(Cat cat);

void feed(Dog dog);
}

创建不同的具体访问者角色(主人和其他人),都需要实现 Person接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class Owner implements Person {

@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}

@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}

public class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}

@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}

定义抽象节点 – 宠物

1
2
3
java复制代码public interface Animal {
void accept(Person person);
}

定义实现Animal接口的 具体节点(元素)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class Dog implements Animal {

@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,汪汪汪!!!");
}
}

public class Cat implements Animal {

@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}

定义对象结构,此案例中就是主人的家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();

public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}

//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());

Owner owner = new Owner();
home.action(owner);

Someone someone = new Someone();
home.action(someone);
}
}

优缺点

1,优点:

  • 扩展性好

在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好

通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为

通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

2,缺点:

  • 对象结构变化很困难

在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则

访问者模式依赖了具体类,而没有依赖抽象类。

使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

扩展

访问者模式用到了一种双分派的技术。

1,分派:

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

2,动态分派:

通过方法的重写支持动态分派。

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
java复制代码public class Animal {
public void execute() {
System.out.println("Animal");
}
}

public class Dog extends Animal {
@Override
public void execute() {
System.out.println("dog");
}
}

public class Cat extends Animal {
@Override
public void execute() {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();

Animal a1 = new Cat();
a1.execute();
}
}

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。

3,静态分派:

通过方法重载支持静态分派。

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
java复制代码public class Animal {
}

public class Dog extends Animal {
}

public class Cat extends Animal {
}

public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}

public void execute(Dog d) {
System.out.println("dog");
}

public void execute(Cat c) {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();

Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}

运行结果:

这个结果可能出乎一些人的意料了,为什么呢?

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

4,双分派:

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

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
java复制代码public class Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}

public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}

public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}

public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}

public void execute(Dog d) {
System.out.println("dog");
}

public void execute(Cat c) {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();

Execute exe = new Execute();
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。

运行结果如下:

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

随机数实现扑克牌洗牌 Java随笔记

发表于 2021-11-21

「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」


相关文章

Java随笔记:Java随笔记


  • 最近刷leetCood有点着魔了,突然想写一个扑克牌洗牌的实现方式。
  • 大脑中第一印象就是用随机数来实现:
+ 随机数范围为1-54
+ 建立一个集合来存放随机生成的数
+ 新随机出来的数如果该集合有,则丢弃,反之则加进去。
  • 先看下代码:
+ 
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
csharp复制代码//实现方式一  使用随机数 随机生成54个在1-54范围内的数字,当这些数字不重复时,完成洗牌
   public static void ShuffleTheDeck() {
       System.out.println("开始时间:" + new Date());
       List reslutList = new ArrayList();
       //开始随机洗牌
       Random random = new Random();
       while (true) {
           int endRand = random.nextInt(54);
           reslutList.add(endRand+1);
           if (ifRepeat(reslutList) == false) {
               reslutList.remove(reslutList.size() - 1);
          }
​
           if (reslutList.size() >= 54) {
               break;
          }
      }
       System.out.println("结束时间:" + new Date());
       for (int j = 0; j < reslutList.size(); j++) {
           System.out.println(reslutList.get(j).toString());
      }
  }
​
   //该方法是判断集合中是否有重复元素的,如果有返回false,反之返回true
   public static boolean ifRepeat(List list) {
       HashSet set = new HashSet<>(list);
       Boolean bool = set.size() == list.size() ? true : false;
       return bool;
  }
+ 执行结果: + ![image-20211121221302561.png](https://gitee.com/songjianzaina/juejin_p6/raw/master/img/e20b1510f7206dc8ce77b450e03266bc14bf6bdcded70b9659a21dd226b2a18b) + 勉强算是可以吧?
  • 但是,我们实际的扑克牌是啥?
+ 1-10 J Q K 大王 小王
+ 一共15种
+ 除大小王单个以外,其他的都是四个,并且分别是,红桃、黑桃、方块、梅花。
+ 如何优化呢?
  • 这里先加个实体类来存储每次生成的扑克牌吧。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    typescript复制代码class poker
    {
       public poker(String color, String num)
      {
           super();
           this.color = color;
           this.num = num;
      }
       String color;
       String num;
    ​
       public String toString()
      {
           return color+num;
      }
    ​
    }
  • 先写生成,然后再洗牌。

  • 区别就是定义好花色和牌面。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ini复制代码public static void buildPoker(){
           //生成52张扑克牌
           String[] colors = {"黑桃","方块","梅花","红桃"};
           String[] nums = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
           LinkedList pokers = new LinkedList();
           for(int i=0;i < colors.length;i++)
          {
               for(int j=0;j<nums.length;j++)
              {
                   pokers.add(new poker(colors[i],nums[j]));
              }
          }
           pokers.add(new poker("小王","黑"));
           pokers.add(new poker("大王","红"));
    }
  • image-20211121222236466.png

  • 到这里整幅扑克牌已经生成好了,下面就是如何将其随机打乱。

  • 洗牌:还是使用随机数来控制!

  • 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
    ini复制代码public static void buildPoker(){
           //生成52张扑克牌
           String[] colors = {"黑桃","方块","梅花","红桃"};
           String[] nums = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
           LinkedList pokers = new LinkedList();
           for(int i=0;i < colors.length;i++)
          {
               for(int j=0;j<nums.length;j++)
              {
                   pokers.add(new poker(colors[i],nums[j]));
              }
          }
           pokers.add(new poker("小王","黑"));
           pokers.add(new poker("大王","红"));
           //洗牌
           LinkedList shuffledpokers = new LinkedList();
           while(shuffledpokers.size()<pokers.size())
          {
               Random x = new Random();
               poker poke = (poker) pokers.get(x.nextInt(pokers.size()));
               if(!shuffledpokers.contains(poke))
              {
                   shuffledpokers.add(poke);
              }
          }
           System.out.println("洗牌前:"+pokers);
           System.out.println("洗牌后:"+shuffledpokers);
      }
  • 执行结果:

  • image-20211121222418961.png

  • 可以做到洗牌啦!
  • 个人所想,虽然很捞,但是也算个思路吧。
  • 如有不对,敬请指出。

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《Elasticsearch核心技术与实战》笔记 -- 第三

发表于 2021-11-21

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

视频课程:极客时间 – 《Elasticsearch核心技术与实战》– 配套代码 GitHub

系列文章:

《Elasticsearch核心技术与实战》笔记 – 第一章:概述

《Elasticsearch核心技术与实战》笔记 – 第二章:安装上手

《Elasticsearch核心技术与实战》笔记 – 第三章:入门(1)

倒排索引介绍

正排与倒排索引

图书与搜索引擎的对比

  • 图书
    • 正排索引 - 目录页
    • 倒排索引 - 索引页
  • 搜索引擎
    • 正排索引 - 文档 id 到文档内容和单词的关联
    • 倒排索引 - 单词到文档 id 的关系

实际案例

1、倒排索引.png

倒排索引的核心组成

  • 倒排索引包含两个部分
    • 单词词典 (Term Dictionary),记录所有文档的单词,记录单词到倒排列表的关联关系
      • 单词词典一般比较大,可以通过 B+ 树或哈希拉链发实现,以满足高性能的插入与查询
    • 倒排列表(Posting List) - 记录了单词对应的文档组合,由倒排索引项组成
      • 倒排索引项目(Posting)
        • 文档 ID
        • 词频 TF - 该单词在文档中出现的次数,用于相关性评分
        • 位置(Position)- 单词在文档中分词的位置,用于语句搜索(phrase query)
        • 偏移(Offset)- 记录单词的开始结束位置,实现高亮显示

2、posting list.png

ElasticSearch 的倒排索引

  • ElasticSearch 的 JSON 文档中的每个字段,都有自己的倒排索引
  • 可以指定对某些字段不做索引
    • 优点:节省存储空间
    • 缺点:字段无法被搜索

通过Analyzer进行分词

Analysis 与 Analyzer

  • Analysis - 文本分析是把全文本转换为一系列单词(term / token)的过程,也叫分词
  • Analysis 是通过 Analyzer 来实现的
    • 可使用 ElasticSearch 内置的分析器 / 或者按需定制的分析器
  • 除了在数据写入时转换词条,匹配 Query 语句时候也需要相同的分析器对查询语句进行分析

3、Analysis分词.png

Analyzer 的组成

  • 分词器是专门处理分词的组件,Analyzer 由三部分组成
    • Character Filters :针对原始文本处理,例如去除 html
    • Tokenizer:按照规则切分为单词
    • Token Filter:将切分的单词进行处理、小写,删除 stopwords,增加同义词

4、Analyzer的组成.png

ElasticSearch 的内置分词器

  • Standard Analyzer – 默认分词器,按词切分,小写处理
  • Simple Analyzer – 按照非字母切分(符号被过滤),小写处理
  • Stop Analyzer – 小写处理,停用词过滤 (the,a,is)
  • Whitespace Analyzer – 按照空格切分,不转小写
  • Keyword Analyzer – 不分词,直接将输入当作输出
  • Patter Analyzer – 正则表达式,默认 \W+ (非字符分割)
  • Language – 提供了30多种常见语言的分词器
  • Customer Analyzer – 自定义分词器

使用 _analyzer API

  • 直接使用 Analyzer 进行测试
1
2
3
4
5
6
json复制代码# standard
GET _analyze
{
"analyzer": "standard",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening;"
}
  • 指定索引字段进行测试
1
2
3
4
5
6
json复制代码# 对案例索引 kibana_sample_data_ecommerce中的product_name进行测试
POST kibana_sample_data_ecommerce/_analyze
{
"field": "products.product_name",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening;"
}
  • 自定义分词器进行测试
1
2
3
4
5
6
json复制代码# 自定义分词器
POST _analyze
{
"tokenizer": "standard",
"filter": ["lowercase"],
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening;"

Standard Analyzer(默认分词器)

5、standard-analyzer.png

api 操作

一、standard_analyzer.gif

Simple Analyzer

6、simple-analyzer.png
api 操作

二、simple_analyzer.gif

Whitespace Analyzer

7、whitespace-analyzer.png

api 操作

三、whitespace_analyzer.gif

Stop Analyzer

8、stop_analyzer.png

api 操作

四、stop_analyzer.gif

KeyWord Analyzer

9、keyword-analyzer.png

api 操作

五、keyword_analyzer.gif

Pattern Analyzer

10、pattern-analyzer.png
\W:匹配任何非单词字符,等价于 [^A-Z a-z 0-9_] ;W后有了+,X内的字符可以取任意多个。

api 操作

六、pattern_analyzer.gif

Language Analyzer

www.elastic.co/guide/en/el…

11、language-analyzer.png

api 操作

七、language_analyzer.gif

中文分词的难点

  • 中文句子,切分成一个一个词(不是一个一个字)
  • 英文中,单词由自然的空格作为分隔
  • 一句中文,在不同的上下文,有不同的理解
    • 这个苹果,不大好吃 / 这个苹果,不大,好吃

ICU Analyzer

12、icu-analyzer.png

附上上次安装 icu_analyzer 的步骤:

《Elasticsearch核心技术与实战》笔记 – 第二章:安装上手

13、安装插件.png

api 操作

八、icu_analyzer.gif

更多的中文分词器

  • IK
    • 支持自定义词库,支持热更新分词字典
    • github.com/medcl/elast…
  • THULAC
    • thulac analysis plugin for elasticsearch,清华大学自然语言处理和社会人文计算实验室的一套中文分词器
    • github.com/microbun/el…

Search API 概览

Search API

  • URI Search
    • 在 URL 中使用查询参数
  • Request Body Search
    • 使用 ElasticSearch 提供的,基于 JSON 格式的更加完备的 Query Domain Specific Language (DSL)

指定查询的索引

语法 范围
/_search 集群上所有的索引
/index1/_search index1
/index1,index-2/_search index1和index2
/index*/_search 以index开头的索引

URI 查询

  • 使用 “q” ,指定查询字符串
  • “query string syntax”。KV键值对

14、URI查询.png

Request Body

15、Request Body.png

搜索 Response

16、搜索Response.png

搜索的相关性 Relevance

17、搜索的相关性.png

  • 搜索是用户和搜索引擎的对话
  • 用户关心的是搜索结果的相关性
    • 是否可以找到所有相关的内容
    • 由多少不相关的内容被返回了
    • 文档的打分是否合理
    • 结合业务需求,平衡结果排名

Web 搜索

18、web搜索.png

  • Page Rank 算法
    • 不仅仅是内容
    • 更重要的是内容的可信度

电商搜索

19、电商搜索.png

  • 搜索引擎扮演 - 销售 的角色
    • 提高用户购物体验
    • 提升网站销售业绩
    • 去库存

衡量相关性

  • Information Retrieval (信息检索)
    • Precision(查准率) - 尽可能返回较少的无关文档
    • Recall(查全率)- 尽量返回较多的相关文档
    • Ranking(排序) - 是否能够按照相关度进行排序?

Precision & Recall

20、p&r.png

True Positive:正相关;False Positive 负相关;True Negative :正否定;False Negative 负否定;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

linux之拖拽上传文件

发表于 2021-11-21

lrzsz是一款在linux里可代替ftp上传和下载的程序。在linux中支持直接拖拽上传的插件;同时也支持rz和sz进行命令上传和下载。

服务器上安装插件

1
markdown复制代码> yum -y install lrzsz

上传一个文件

上传可以直接拖动,上传文件至当前目录。也可以用命令,选择需要上传文件,一次可以指定多个文件。

1
markdown复制代码> rz

使用xshell会弹出一个文件选择框。

image-20210703225453909

下载一个文件

1
markdown复制代码> sz rumenz.txt

或弹出一个保存文件的选择框

image-20210703225714183

同时下载多个文件

1
markdown复制代码> sz rumenz.txt rumenz1.txt

下载rumenz目录下所有文件,不包含rumenz下的文件夹

1
markdown复制代码> sz  rumenz/*

原文链接:rumenz.com/rumenbiji/l…
微信公众号:入门小站

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hystrix 源码解读

发表于 2021-11-21

入口

image.png

1
css复制代码@EnableCircuitBreaker 这注解也能开启Hystrix的相关功能

image.png

image.png
其中读取spring.factories哪个文件的入口在此
org.springframework.cloud.commons.util.SpringFactoryImportSelector#selectImports

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
typescript复制代码public String[] selectImports(AnnotationMetadata metadata) {
if (!isEnabled()) {
return new String[0];
}
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(this.annotationClass.getName(), true));

Assert.notNull(attributes, "No " + getSimpleName() + " attributes found. Is "
+ metadata.getClassName() + " annotated with @" + getSimpleName() + "?");

// 因为是EnableCircuitBreaker这个注解读取的是META-INF/spring.factories 中org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerConfiguration 这个配置
List<String> factories = new ArrayList<>(new LinkedHashSet<>(SpringFactoriesLoader
.loadFactoryNames(this.annotationClass, this.beanClassLoader)));

if (factories.isEmpty() && !hasDefaultFactory()) {
throw new IllegalStateException("Annotation @" + getSimpleName()
+ " found, but there are no implementations. Did you forget to include a starter?");
}

if (factories.size() > 1) {
// there should only ever be one DiscoveryClient, but there might be more than
// one factory
log.warn("More than one implementation " + "of @" + getSimpleName()
+ " (now relying on @Conditionals to pick one): " + factories);
}

return factories.toArray(new String[factories.size()]);
}

image.png
现在知道对应配置是怎么自动读取的吧!

相关源码分析

image.png

image.png
核心逻辑com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect#methodsAnnotatedWithHystrixCommand

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
ini复制代码@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable {
Method method = getMethodFromTarget(joinPoint);
Validate.notNull(method, "failed to get method from joinPoint: %s", joinPoint);
if (method.isAnnotationPresent(HystrixCommand.class) && method.isAnnotationPresent(HystrixCollapser.class)) {
throw new IllegalStateException("method cannot be annotated with HystrixCommand and HystrixCollapser " +
"annotations at the same time");
}
MetaHolderFactory metaHolderFactory = META_HOLDER_FACTORY_MAP.get(HystrixPointcutType.of(method));
MetaHolder metaHolder = metaHolderFactory.create(joinPoint);
HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);
ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ?
metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType();

Object result;
try {
if (!metaHolder.isObservable()) {
result = CommandExecutor.execute(invokable, executionType, metaHolder);
} else {
result = executeObservable(invokable, executionType, metaHolder);
}
} catch (HystrixBadRequestException e) {
throw e.getCause() != null ? e.getCause() : e;
} catch (HystrixRuntimeException e) {
throw hystrixRuntimeExceptionToThrowable(metaHolder, e);
}
return result;
}

封装

image.png

image.png

image.png
默认为
SYNCHRONOUS
image.png

image.png

image.png
下面是Rxjava的相关编程,个人不是很熟

执行成功的逻辑

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png
这里就是调用的逻辑,由于是使用reactor模式的调用,我也做不了详细的解释,惭愧啊!

推荐相关的源码解释:www.iocoder.cn/categories/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

InheritableThreadLocal在全链路中的作用

发表于 2021-11-21

由于 ThreadLocal 在父子线程交互中子线程无法访问到存储在父线程中的值,无法满足某些场景的需求,例如链路跟踪,例如如下场景:

image.png
为了解决上述问题,JDK 引入了 InheritableThreadLocal,即子线程可以访问父线程中的线程本地变量,更严谨的说法是子线程可以访问在创建子线程时父线程当时的本地线程变量,因为其实现原理就是在创建子线程将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

类图

image.png
从类的继承层次来看,InheritableThreadLocal 只是在 ThreadLocal 的 get、set、remove 流程中,重写了 getMap、createMap 方法,整体流程与 ThreadLocal 保持一致,故我们初步来看一下InheritableThreadLocal 是如何重写上述这两个方法的。

image.png
从代码得知,ThreadLocal 操作的是 Thread 对象的 threadLocals 属性,而 InheritableThreadLocal 操作的是 Thread 对象的 inheritableThreadLocals 属性。

温馨提示:createMap 被执行的条件是调用 InheritableThreadLocal#get、set 时如果线程的inheritableThreadLocals 属性为空时才会被调用。

那问题来了,InheritableThreadLocal 是如何继承自父对象的线程本地变量的呢?

线程上下文环境如何从父线程传递到子线程

这部分的代码入口为:Thread#init 方法

image.png
子线程是通过在父线程中通过调用 new Thread() 方法来创建子线程,Thread#init 方法就是在 Thread的构造方法中被调用。

代码@1:获取当前线程对象,即待创建的线程的父线程。

代码@2:如果父线程的 inheritableThreadLocals 不为空并且 inheritThreadLocals 为 true(该值默认为true),则使用父线程的 inherit 本地变量的值来创建子线程的 inheritableThreadLocals 结构,即将父线程中的本地变量复制到子线程中。

image.png
上述代码就不一一分析,类似于 Map 的复制,只不过其在 Hash 冲突时,不是使用链表结构,而是直接在数组中找下一个为 null 的槽位。

温馨提示:子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承 InheritableThreadLocal 并重写 childValue 方法

验证 InheritableThreadLocal 的特性

验证代码如下:

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
csharp复制代码public class Service {

private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

Integer reqId = new Integer(5);

Service a = new Service();

a.setRequestId(reqId);

}

public void setRequestId(Integer requestId) {

requestIdThreadLocal.set(requestId);

doBussiness();

}

public void doBussiness() {

System.out.println("首先打印requestId:" + requestIdThreadLocal.get());

(new Thread(new Runnable() {

@Override

public void run() {

System.out.println("子线程启动");

System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());

}

})).start();

}

}

执行结果如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

符合预期,在子线程中如愿访问到了在主线程中设置的本地环境变量。

InheritableThreadLocal 局限性

全链路压测必备基础组件之线程上下文管理之“三剑客”

原创2020-05-06 22:38·中间件兴趣圈

说起本地线程变量,我相信大家首先会想到的是 JDK 默认提供的 ThreadLocal,用来存储在整个调用链中都需要访问的数据,并且是线程安全的。

由于本文的写作背景是笔者需要在公司落地全链路压测平台,一个基本并核心的功能需求是压测标记需要在整个调用链中进行传递,线程上下文环境成为解决这个问题最合适的技术。

温馨提示:本从从 ThreadLocal 原理入手分析,并抛出其缺点,再逐一引出InheritableThreadLocal、TransmittableThreadLocal。文章篇幅稍长,但由于循序渐进,层层递进,故精华部分在后面。

1、ThreadLocal 详解

ThreadLocal对外提供;的API如下:

  • public T get()

从线程上下文环境中获取设置的值。

  • public void set(T value)

将值存储到线程上下文环境中,供后续使用。

  • public void remove()

清除线程本地上下文环境。

上述API使用简单,关键是要理解 ThreadLocal 的内部存储结果。

1.1 ThreadLocal存储结构

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal 存储结构

上图的几个关键点如下:

  • 数据存储位置

当线程调用 threadLocal 对象的 set(Object value) 方法时,数据并不是存储在 ThreadLocal 对象中,而是存储在 Thread 对象中,这也是 ThreadLocal 的由来,具体存储在线程对象的threadLocals 属性中,其类型为 ThreadLocal.ThreadLocalMap。

  • ThreadLocal.ThreadLocalMap

Map 结构,即键值对,键为 threadLocal 对象,值为需要存储到线程上下文的值(threadLocal#set)方法的参数。

1.2 源码分析 ThreadLocal

1.2.1 源码分析 get

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal get 方法

代码@1:获取当前线程。

代码@2:获取线程的 threadLocals 属性,在上图中已展示其存储结构。

代码@3:如果线程对象的 threadLocals 属性不为空,则从该 Map 结构中,用 threadLocal 对象为键去查找值,如果能找到,则返回其 value 值,否则执行代码@4。

代码@4:如果线程对象的 threadLocals 属性为空,或未从 threadLocals 中找到对应的键值对,则调用该方法执行初始化

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal#setInitialValue

代码@1:调用 initialValue() 获取默认初始化值,该方法默认返回 null,子类可以重写,实现线程本地变量的初始化。

代码@2:获取当前线程。

代码@3:获取该线程对象的 threadLocals 属性。

代码@4:如果不为空,则将 threadLocal:value 存入线程对象的 threadLocals 属性中。

代码@5:否则初始化线程对象的 threadLocals,然后将 threadLocal:value 键值对存入线程对象的threadLocals 属性中。

1.2.2 源码分析set

全链路压测必备基础组件之线程上下文管理之“三剑客”

ThreadLocal#set

在掌握了 get 方法实现细节,set 方法、remove 其实现的逻辑基本一样,就是对线程对象的threadLocals 属性进行操作( Map结构)。

1.3 ThreadLocal局限性

经过上面的剖析,对 ThreadLocal 的内部存储与 set、get、remove 等实现细节都已理解,但ThreadLocal 无法在父子线程之间传递,示例代码如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

父子传递 Demo

运行结果如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

Demo 示例结果

从结果上来看,在子线程中无法访问在父线程中设置的本地线程变量,那我们该如何来解决该问题呢?

为了解决该问题,JDK引入了另外一个线程本地变量实现类 InheritableThreadLocal,接下来将重点介绍 InheritableThreadLocal 的实现原理。

2、InheritableThreadLocal

由于 ThreadLocal 在父子线程交互中子线程无法访问到存储在父线程中的值,无法满足某些场景的需求,例如链路跟踪,例如如下场景:

全链路压测必备基础组件之线程上下文管理之“三剑客”

为了解决上述问题,JDK 引入了 InheritableThreadLocal,即子线程可以访问父线程中的线程本地变量,更严谨的说法是子线程可以访问在创建子线程时父线程当时的本地线程变量,因为其实现原理就是在创建子线程将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

2.1 类图

全链路压测必备基础组件之线程上下文管理之“三剑客”

InheritableThreadLocal 类图

从类的继承层次来看,InheritableThreadLocal 只是在 ThreadLocal 的 get、set、remove 流程中,重写了 getMap、createMap 方法,整体流程与 ThreadLocal 保持一致,故我们初步来看一下InheritableThreadLocal 是如何重写上述这两个方法的。

全链路压测必备基础组件之线程上下文管理之“三剑客”

InheritableThreadLocal#getMap

从代码得知,ThreadLocal 操作的是 Thread 对象的 threadLocals 属性,而 InheritableThreadLocal 操作的是 Thread 对象的 inheritableThreadLocals 属性。

温馨提示:createMap 被执行的条件是调用 InheritableThreadLocal#get、set 时如果线程的inheritableThreadLocals 属性为空时才会被调用。

那问题来了,InheritableThreadLocal 是如何继承自父对象的线程本地变量的呢?

2.2 线程上下文环境如何从父线程传递到子线程

这部分的代码入口为:Thread#init 方法

全链路压测必备基础组件之线程上下文管理之“三剑客”

Thread#init

子线程是通过在父线程中通过调用 new Thread() 方法来创建子线程,Thread#init 方法就是在 Thread的构造方法中被调用。

代码@1:获取当前线程对象,即待创建的线程的父线程。

代码@2:如果父线程的 inheritableThreadLocals 不为空并且 inheritThreadLocals 为 true(该值默认为true),则使用父线程的 inherit 本地变量的值来创建子线程的 inheritableThreadLocals 结构,即将父线程中的本地变量复制到子线程中。

全链路压测必备基础组件之线程上下文管理之“三剑客”

createInheritedMap

上述代码就不一一分析,类似于 Map 的复制,只不过其在 Hash 冲突时,不是使用链表结构,而是直接在数组中找下一个为 null 的槽位。

温馨提示:子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承 InheritableThreadLocal 并重写 childValue 方法。

2.3 验证 InheritableThreadLocal 的特性

验证代码如下:

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
csharp复制代码public class Service {

private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

Integer reqId = new Integer(5);

Service a = new Service();

a.setRequestId(reqId);

}

public void setRequestId(Integer requestId) {

requestIdThreadLocal.set(requestId);

doBussiness();

}

public void doBussiness() {

System.out.println("首先打印requestId:" + requestIdThreadLocal.get());

(new Thread(new Runnable() {

@Override

public void run() {

System.out.println("子线程启动");

System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());

}

})).start();

}

}

执行结果如下:

全链路压测必备基础组件之线程上下文管理之“三剑客”

符合预期,在子线程中如愿访问到了在主线程中设置的本地环境变量。

2.4 InheritableThreadLocal 局限性

InheritableThreadLocal 支持子线程访问在父线程的核心思想是在创建线程的时候将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。但我们提到并发、多线程就理不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用 InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱,验证代码如下:

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
114
115
116
117
118
119
java复制代码import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class Service {

/**

* 模拟tomcat线程池

*/

private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

/**

* 业务线程池,默认Control中异步任务执行线程池

*/

private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);

/**

* 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。

*/

private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) {

for(int i = 0; i < 10; i ++ ) { // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,

// 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量

tomcatExecutors.submit(new ControlThread(i));

}

//简单粗暴的关闭线程池

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}

businessExecutors.shutdown();

tomcatExecutors.shutdown();

}

/**

* 模拟Control任务

*/

static class ControlThread implements Runnable {

private int i;

public ControlThread(int i) {

this.i = i;

}

@Override

public void run() {

System.out.println(Thread.currentThread().getName() + ":" + i);

requestIdThreadLocal.set(i);

//使用线程池异步处理任务

businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));

}

}

/**

* 业务任务,主要是模拟在Control控制层,提交任务到线程池执行

*/

static class BusinessTask implements Runnable {

private String parentThreadName;

public BusinessTask(String parentThreadName) {

this.parentThreadName = parentThreadName;

}

@Override

public void run() {

//如果与上面的能对应上来,则说明正确,否则失败

System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());

}

}

}

执行效果如下:

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
makefile复制代码pool-1-thread-1:0

pool-1-thread-2:1

pool-1-thread-3:2

pool-1-thread-4:3

pool-1-thread-5:4

pool-1-thread-6:5

pool-1-thread-7:6

pool-1-thread-8:7

pool-1-thread-9:8

pool-1-thread-10:9

parentThreadName:pool-1-thread-7:6

parentThreadName:pool-1-thread-4:6

parentThreadName:pool-1-thread-3:6

parentThreadName:pool-1-thread-2:6

parentThreadName:pool-1-thread-1:6

parentThreadName:pool-1-thread-9:6

parentThreadName:pool-1-thread-10:6

parentThreadName:pool-1-thread-8:7

parentThreadName:pool-1-thread-6:5

parentThreadName:pool-1-thread-5:4

从这里可以出 thread-7、thread-4、thread-3、thread-2、thread-1、thread-9、thread-10 获取的都是6,在子线程中出现出现了线程本地变量混乱的现象,在全链路跟踪与压测出现这种情况是致命的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

log4j、log4j2、slf4j、logback什么关系

发表于 2021-11-21

这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

公众号Mac代码分割阅读链接

前言

之前都是使用SparkStreaming开发,最近打算学习一下Flink,就从官网下载了Flink 1.11,打算搞一个客户端,将程序提交在yarn上。因为Flink从1.7之后就不再提供Hadoop的依赖,所以很多依赖就要自己下载,于是各种ClassNotFoundException。

其中以log*.class为首的格外猖狂,可能是因为flink和Hadoop的日志实现有点区别,就一直哐哐哐报错,slf4j、log4j、logback各种jar包十几个,百度好久也没搞清各个jar有什么区别,用在何处,就打算自己总结一下。

log发展历史

Long long Ago,和刚学Java的时候一样,都是用System.out.println控制台打印来检查程序输出是否符合自己的预期,这是一种比较原始的方法,无法自动区分日志的类型,几乎无法用于生产系统中。

从JDK1.4开始提供java.until.logging日志框架来打印日志,但是大佬觉得JUL太难用了,就自己手撸了个log4j,后来log4j发现安全漏洞,加上代码结构问题难以维护,于是从1.2就停止更新log4j,并又重新手撸了个log4j2,再后来,这个大佬又双手撸了一个性能更高、功能更全的logback。

从此,这个大佬构建了log的世界,也创造了最常见的日志框架:log4j、log4j2、logback。

SLF4J( Simple Logging Facade for Java )

目前已经提及了四个日志框架,如果我们想用来记录日志,除了必要的配置文件,还需要在代码中获取Logger,打印日志。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 使用log4j,需要log4j.jar
import org.apache.log4j.Logger;
Logger logger_log4j = Logger.getLogger(Test.class);
logger_log4j.info("Hello World!");

// 使用log4j2,需要log4j-api.jar、log4j-core.jar
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Logger logger_log4j2 = LogManager.getLogger(Test.class);
logger_log4j2.info("Hello World!");

// logback,需要logback-classic.jar、logback-core.jar
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
Logger logger_logback = new LoggerContext().getLogger(Test.class);
logger_logback.info("Hello World!");

// java.until.logging,简称jul
import java.util.logging.Logger;
Logger logger_jul = Logger.getLogger("java.Test");

为什么要使用门面系统

从上面不难看出,使用不同的日志框架,就要引入不同的jar包,使用不同的代码获取Logger。

假设一个项目在漫长的升级过程中,想从jul升级到logback,那么就需要修改代码来获取新的Logger。如果100个class中使用了jul,就得修改100个地方,这是多么一个繁琐的工作!!

门面系统的作用

于是Apache Commons Logging出现了。

Common-logging提供了一个日志入口,称作”门面日志”,即它不负责写日志,而是提供用一个统一的接口,通过jar来决定使用的日志框架,这样就不要再更换框架的时候再修改代码了。后来开发了log4j的大佬又因为嫌弃Common-logging难用,开发了门面日志框架slf4j,今天就拿slf4j讲述门面日志。

门面日志和设计模式中的外观模式如出一辙,本身不提供服务,为子系统提供统一的入口,封装子系统的复杂性,便于客户端调用。slf4j就像是菜鸟驿站,本身没有快递服务,但是提供顺丰、中通等快递服务,至于你想用顺丰还是用中通,完全取决于你的想法。

使用slf4j的代码如下:

1
2
3
4
java复制代码import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(Test.class);
logger.info("Hello World!")

这行代码就像是你在菜鸟驿站里要寄东西(logger),思考到底用哪家快递?A minute later… 决定用顺丰(logback),就填了顺丰的快递单(放入logback.jar),但是你看微信余额还有10块,钱不够,只能用中通(log4j),于是你就退了顺丰的单子(移除logback.jar),填了中通的快递单(放入log4j.jar),然后发出快递(打印日志)。

那么slf4j如何决定使用哪个框架日志呢,并且引入哪些jar包呢?

如slf4j官方图所示:

依赖关系图

如图就是slf4j和日志框架的组合依赖结构图,使用slf4j需要首先导入slf4j-api.jar,

和log4j配合,需要导入log4j.jar,以及桥接包slf4j-log412.jar。

官方图美中不足的是没有log4j2依赖jar的关系,和log4j2配合需要导入log4j2的log4j-api.jar、log4j-core.jar和桥接包log4j-slf4j-impl.jar。

logback只需要导入logback-classic.jar和logback-core.jar即可,不需要桥接包。

什么是桥接包,为什么logback没有

先让来让我们看看slf4j从LoggerFactory.getLogger()开始,到底干了什么。

流程图如下:

slf4j工作流程图

原理就是就是让ClassLoader从classpath(依赖的jar)中找到StaticLoggerBinder这个类,然后利用他来返回log4j、logback中的Logger,然后打印日志。

所谓的桥接包,就是实现StaticLoggerBinder类,用来连接slf4j和日志框架。因为log4j和log4j2刚开始没有StaticLoggerBinder这个类,为了不改变程序结构,只能重新写一个新的jar来实现StaticLoggerBinder。而logback出现slf4j之后,于是在logback本身的jar中实现了StaticLoggerBinder,所以就不需要桥接包。

StaticLoggerBinder实现了使用底层日志框架创建Logger的功能,各自的StaticLoggerBinder为slf4j提供的Logger,再提供给用户打印日志。

log4j和log4j2桥接包及logback依赖里,都有StaticLoggerBinder类。

logback的StaticLoggerBinder

log4j的StaticLoggerBinder

log4j2的StaticLoggerBinder

使用总结

“Class path contains multiple SLF4J bindings.”

在使用slf4j的时候会遇到以上的报告信息。我也曾遇到过web服务因为slf4j问题启动失败。究其根本是因为logback-classic、log4j-slf4j-impl、slf4j-log412、slf4j-jdk这些jar不能同时存在。他们都实现了StaticLoggerBinder类而导致冲突,slf4j无法确定到底用哪个日志框架。

结语

以上是结合官网以及自己调试代码的一些总结,希望对大家了解slf4j及其应用有帮助,其中不足的地方还望指出,共同进步,共勉!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…246247248…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%