juc-05

final

final类

表示该类无法扩展。也就是说,无法创建final类的子类。 由于无法创建final类的子类,所以final类中声明的方法也不会被重写。

final方法

表示该方法不会被子类的方法重写。 如果在静态方法的声明上加上final,则表示该方法不会被子类的方法隐藏(hide)。如果试图重写或隐藏final
方法,编译时会提示错误。

在设计模式的Template Method中,有时候模板方法会声明为final方法。

final字段

表示该字段只能赋值一次。

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
final字段赋值有两种方式:
1.字段声明的时候赋值
class Something{
final int value = 123;
}

2.字段在构造方法中赋值
class Something{
final int value;
Something{
this.value = 123;
}
}

final静态字段赋值两种方法:
1.字段声明时赋值
class Something{
static final int value = 123;
}
2.静态代码块中赋值
class Something{
static final int value;
static {
value = 123;
}

注意: final字段不可用setValue这样的setter方法再次赋值.

final与创建线程安全的实例

从创建线程安全的角度来说,将字段声明为final是非常重要的。

final变量和final参数

局部变量和方法的参数也可用声明为final。final变量只可以赋值一次。 而final参数不可以赋值,因为在调用方法时,已经对其赋值了。

juc-04

java.util.concurrent包和计数信号量

计数信号量和Semaphore类

Single Threaded Execution模式用于确保某个区域“只能有一个线程”执行。下面我们将这种模式扩展。确保某个区域“最多只能由N个线程”
执行。这时就要用计数信号量来控制线程数量。

假设能够使用的资源个数为N个,而需要这些资源的线程个数又多于N个。此时就会导致竞态。

java.util.concurrent包中提供了表示信号量的Semaphore类。

资源的许可个数(permits)将通过Semaphore的构造函数来指定。

Semaphore的acquire方法用于确保存在可用资源。当存在可用资源的时候,线程会立即从acquire方法返回,同时信号量内部的资源
个数会减1。如果没有可用资源,线程则阻塞在acquire方法内,知道出现可用资源。

Semaphore的release方法用于释放资源。释放资源后,信号量内部的资源个数会增加1。另外,如果acquire中存在等待的线程,那么其中一个线程
会被唤醒,并从acquire方法返回。

使用Semaphore类的示例程序

使用例子

例子说明

juc-03

关于synchronized

synchronized语法&Before/After模式

1
2
3
synchronized void method(){}

synchronized (obj){}

其实可以看做是“{”处获得锁,“}”处释放锁。

假设,存在一个获得锁的lock方法,或释放锁的unlock方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
显示处理锁的方法:
void method(){
lock();
...
unlock();
}

如果在lock方法和unlock方法之间存在return,那么锁将无法被释放。
void method(){
lock();
if(条件表达式){
return; //这里执行了return,unlock()就不会被调用。
}
unlock();
}

void method(){
lock();
doMethod(); //如果该方法抛出异常,unlock()就不会被调用
unlock();
}

上述问题并不仅仅在于return语句,异常处理也存在这样的问题,调用的方法(或该方法调用的方法) 抛出异常时候,锁也就无法被释放。

避免异常导致的问题,必须使用finally语句

1
2
3
4
5
6
7
8
void method(){
lock();
try{
...
}finally{
unlock();
}
}

synchronized在保护什么

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
public class Gate {
//已经通过的人数
private int counter = 0;
//最后一个通行者的“姓名”
private String name = "Nodody";
//最后一个通行者的出生地
private String address = "Nowhere";

/**
* 添加:synchronized
* 表示通过门
* @param myname
* @param myaddress
*/
public synchronized void pass(String myname, String myaddress) {
this.counter++;
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.address = address;
}
}

比如上面的代码,已经在pass方法上加了synchronized方法。 如果set和get方法都同时加上synchronized并不能保证安全
img.png

原子操作

synchronized方法只允许一个线程同时执行。如果某个线程正在执行synchronized方法,
其他线程就无法进入该方法。也就是说,从多线程的角度来看,这个synchronized方法执行
的操作是“不可分割的操作”。这种不可分割的操作通常称为“原子atomic”操作。

atom是物理学中的“原子”,本意为不可分割的物体。

long与double的操作不是原子的

Java中定义了一些原子的操作。 例如char,int等基本类型的赋值和引用都是原子的;引用类型的赋值和引用操作也是原子的。
因此,就算不使用synchronized也不会被分割。

例外:long和double的赋值和引用操作并不是原子的。 最简单的方法就是在synchronized方法中执行;或字段上加上volatile关键字。

总结:

  • 基本类型,引用类型的赋值和引用是原子操作
  • long和double的赋值和引用都是非原子操作
  • long或double在线程间共享时,需要将其放入synchronized中操作,或声明为volatile。

juc-02-评价标准

多线程-评价标准

安全性(必备条件):不损坏对象

对象损坏是一种比喻,实际上,对象是内存上的一种虚拟事物,并不会实际损坏。对象损坏是指对象的状态和设计者的意愿不一致, 通常是指对象的字段的值并非预期值。

如果一个类即使被多个线程同时使用,也可确保安全性,那么这个类就称为线程安全(thread-safe)类。由于类库中还存在非线程安全的类,所有在多线程的程序中
需要特别注意。比如:java.util.Vector类是线程安全的类,而java.util.ArrayList则是非线程安全的类。

  • 线程安全和线程兼容:虽然ArrayList是非线程安全的,当通过执行适当的互斥处理,也可用安全使用。
  • synchronized和线程安全:某个线程是线程安全的还是非线程安全的,与这个类的方法是否synchronized方法无关。

生存性(必备条件):必要的处理能够被执行

生存性(liveness)是无论是什么时候,必要的处理都一定能够被执行。

即使对象没有损坏,也不代表程序就一定好。极端一点说,假如程序在运行过程中突然停止了,这时,由于处理已经停止,对象的状态就不会发生变化了,所以对象状态
也就不会异常。这虽然符合前面将的“安全性”条件,当无法运行的程序根本没有任何意义。无论是什么时候,必要处理都一定能够被执行。

有时候安全性和生存性是相互制约的。例如,有时只重视安全性,生存性就会下降。最典型的是死锁(deadlock),即多个线程相互等待对方释放锁的情形。

可复用性(提升质量): 类可重复利用

类如果能够作为组件从正常运行的软件中分割出来,就说明这个类有很高定复用性。

编写多线程的时候如果能够巧妙地将线程的互斥机制和方针隐藏到类中,那这就是一个可复用性高的程序。

性能(提升质量): 快速,大批量地执行处理

  • 吞吐量:单位时间内完成的处理数量。能完成的处理越多,则吞吐量越大。
  • 响应性(等待时间):从发出请求到收到响应的时间。
  • 容量:可同时进行的处理数量

其他

  • 效率:
  • 可伸缩性:
  • 降级:

juc-1

多线程-入门知识

线程

概念

线程:我们追踪程序运行的流程,其实就是在追踪线程的流程。

单线程: “在某一时间点执行的处理只有一个”。(在研究单线程的时,我们忽略Java的后台线程,如:GC等)

多线程:常见的多线程常见有以下几个。

  • 耗时的IO处理:
  • 多个客户端:如果让服务端针对不同的客户端执行处理,程序是很复杂的。 客户端连接到服务器时,为客户端准备一个线程,这样看来服务器程序好像只处理一个客户端。

ps:nio即便不使用多线程,也可用执行兼具性能和可扩展性的IO处理。

“本线程” & “当前线程”

  • 本线程: 表示this(以及this对应的线程)的意思
  • 当前线程: 表示调用对象方法的线程

顺序 并行 并发

  • 顺序:依次处理
  • 并行:同时处理
  • 并发: 将一个操作分隔成多个部分并且允许无序处理

img.png

启动

两种方式

其他方式

-

java.util.concurrent.ThreadFactory利用ThreadFactory启动线程

无论那种方式,启动新线程的方法最终都是Thread类的start方法。

ps:Thread类本身还实现Runnable接口,并持有run方法,但run()主体是空的。仍需要子类重写。

线程类的实例 & 线程

需要注意的是线程类的实例并不等于线程。线程就算停止,线程类的实例并不会消失。

暂停(休眠)

在实际程序中,使用sleep的频率并不高。
Sleep的例子

  • interrupt方法:用于中途唤醒被Thread.sleep休眠的线程

互斥

线程A和线程B相互竞争引起与预期相反的情况称为竞态条件(race condition)或数据竞争(data race)。

处理上述的竞态的操作称为互斥。

synchronized方法(同步方法)

synchronized例子

图解synchronized

图解synchronized

synchronized代码块

synchronized代码块用于精准控制互斥处理的执行范围。

synchronized实例方法和synchronized代码块(this加锁)

以下两种写法是等效的。也就是说synchronizedui实例方法是使用this的锁来执行线程的互斥处理的。

1
2
3
synchronized void method(){
...
}
1
2
3
4
5
6
void method(){
synchronized(this){
....
}
}

synchronized静态方法和synchronized代码块(类对象加锁)

synchronized静态方法每次只能由一个线程运行,这一点和synchronized实例方法相同。
但是,synchronized静态方法使用的锁和synchronized实例方法使用的锁是不一样的。

1
2
3
4
5
class Something{
static synchronized void method(){
...
}
}
1
2
3
4
5
6
7
8
9
class Something{
static void method(){
synchronized(Something.class){
...
}
}
}
也就是说,synchronized静态方法是使用该类的类对象的锁来执行线程的互斥处理的。
Something.class是Something类对应的java.lang.class类的实例。

协作

wait方法,notify方法,notifyAll方法(都是java.lang.Object类的方法,所以既不是Thread类中的方法,但又是)

注意:
它们只能由持有要调用的实例的锁的线程调用。如果未持有锁的线程调用上面方法,异常java.lang.IllegalMonitorStateException会抛出。

  • 为什么放在java.lang.Object包? 这三个方法与其说是针对线程的操作,不如说是针对实例的等待队列的操作。由于所有的实例都有等待队列。它们确实
    不是Thread包的方法,但Object是所有类的父类,所以也可用说它们是Thread类的方法。

等待队列(线程的休息室)

所有实例都拥有一个等待队列,它是在实例的wait方法执行后停止操作的线程的队列。

线程退出等待队列

等待队列是一个虚拟的概念, 既不是实例中的字段,也不是用于获取正在实例上等待的线程的列表的方法。

  • 其他线程的notify或notifyAll方法来唤醒线程
  • 其他线程的interrupt方法流唤醒线程
  • wait方法超时

wait方法:线程放入等待队列

要执行wait方法,线程必须持有锁(这是规则)。如果线程进入等待队列之后,便会释放其实例的锁。

1
2
wait();
this.wait(); //等同于上面, 这里可以说线程正在this上wait

图解wait过程

notify方法

要执行notify方法,线程也必须持有调用的实例的锁。

1
2
obj.notify();
那么obj的等待队列中的线程便会被选中和唤醒,然后就会退出等待队列。
  • notify唤醒的线程并不会在执行notify的一瞬间重新运行。因为在执行notify的那一瞬间,执行notify的线程还持有着锁,所以其他
    线程还无法获取这个实例的锁。

  • 执行notify后如何选择线程:假如在执行notify方法之后,正在等待队列中等待的线程不止一个,对于“这时该如何来选择线程”这个
    问题规范中并没有做出规定。究竟是选择最先wait的线程,还是随机选择,或采取其他方法取决于Java平台运行环境。因此编写程序时需要
    注意,最好不要编写依赖于所选线程的程序。

图解notify过程

notifyAll方法

notifyAll方法会将等待队列中所有的线程都取出来。

1
2
3
notifyAll();
this.notifyAll();

notify和notifyAll的区别

  • 锁在谁的手里? 被唤醒的线程会去获取其他线程在进入wait状态时释放的锁。其实是执行notifyAll的线程正持有着锁。因此,唤醒的线程虽然都推出了
    等待队列,但都在等待获取锁,处于阻塞状态。只有在执行notifyAll的线程释放锁以后,其中的一个幸运儿才能够实际运行。

-

使用notify方法还是notifyAll方法?由于notify唤醒的线程较少,所以处理速度要比使用notifyAll快。但使用notify时,如果处理不好,程序便
可能会停止。一般来说,使用notifyAll时的代码会比notify时更为健壮。

线程的状态转移

参考下图的线程状态转移:

线程的简单状态转移

策略

策略模式:简单而有效的解决方案

策略模式是软件开发中最常用的模式之一,它可以帮助开发者分离算法,并使其可以在运行时互换。

Java开发者可以使用策略模式来轻松实现复杂的业务逻辑。例如,一个应用可以根据用户输入的金额来决定选择何种支付方式,这就可以用策略模式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//定义支付策略的抽象类
public abstract class PaymentStrategy {
public abstract void pay(int amount);
}

//实现支付策略的具体类
public class CreditCardStrategy extends PaymentStrategy {
public void pay(int amount) {
System.out.println("使用信用卡支付" + amount + "元");
}
}

public class CashStrategy extends PaymentStrategy {
public void pay(int amount) {
System.out.println("使用现金支付" + amount + "元");
}
}

//定义一个类来控制这一过程
public class PaymentContext {
private PaymentStrategy paymentStrategy;

public PaymentContext(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}

public void pay(int amount) {
paymentStrategy.pay(amount);
}
}

//使用策略模式
public class Main {

public static void main(String[] args) {
PaymentContext paymentContext = null;
int amount = 1000;
//选择使用信用卡支付
paymentContext = new PaymentContext(new CreditCardStrategy());
paymentContext.pay(amount);

//选择使用现金支付
paymentContext = new PaymentContext(new CashStrategy());
paymentContext.pay(amount);
}
}

从上面的代码可以看出,策略模式是一种简单而有效的解决方案,它能够使代码变得容易维护,因为算法的变化不会影响到已有的代码。

代理

代理模式:当你无法自己处理事情时,找个代理

众所周知,程序里面很多时候需要处理一些繁琐的事情,比如访问一个远程服务、打印一张大图等等,这些任务可能占用大量的内存和时间,导致程序出现卡顿甚至崩溃的情况。如果你是一个懒人,你会选择怎么做?

没错,找个代理人!代理人可以替你完成这些事情,你只需要告诉他你的需求,让他去完成就好了。在程序里也是一样,我们可以使用代理模式来处理这些繁琐的事情。

代理模式是什么?

代理模式是一种结构型设计模式,它允许我们为其他对象提供一个代理,以控制对这个对象的访问。代理对象通常充当客户端和目标对象之间的中介,它可以隐藏目标对象的复杂性并且可以在不改变目标对象的情况下,为客户端添加额外的功能。

代理模式怎么用?

我们先来看一个例子:假设你需要访问一个远程服务获取一些数据,但是这个服务需要认证才能使用。你不想把认证信息暴露给客户端,也不想让客户端直接访问服务,怎么办?

首先,我们定义一个接口Service,用来让代理和目标对象实现:

1
2
3
public interface Service {
void getData();
}

然后,我们定义一个目标对象实现这个接口,用来访问远程服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RealService implements Service {
private String authCode;

public RealService(String authCode) {
this.authCode = authCode;
}

@Override
public void getData() {
// 访问远程服务获取数据
System.out.println("获取数据");
}
}

接下来,我们定义一个代理类实现这个接口,并在构造函数中传入目标对象和认证信息。代理类的getData()方法里面先进行身份认证,然后再调用目标对象的getData()方法获取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ProxyService implements Service {
private final Service realService;
private final String authCode;

public ProxyService(Service realService, String authCode) {
this.realService = realService;
this.authCode = authCode;
}

@Override
public void getData() {
// 进行身份认证
System.out.println("进行身份认证");

// 调用目标对象的方法获取数据
realService.getData();
}
}

最后,我们就可以在客户端中创建代理对象,而不是直接访问远程服务:

1
2
3
4
5
6
7
8
9
public class Client {
public static void main(String[] args) {
// 创建代理对象
Service service = new ProxyService(new RealService("auth"), "auth");

// 使用代理对象获取数据
service.getData();
}
}

这样,我们就利用代理模式来实现了目标对象的访问控制,客户端无需知道目标对象的实现细节和认证信息,而且可以在代理对象中添加额外的功能,比如缓存数据、限流等。

总结

代理模式是一种非常常用的设计模式,它能够让我们在不改变目标对象的情况下,对目标对象进行访问控制和功能扩展。在实际开发中,我们可以使用代理模式来优化程序的性能和安全性,减少系统的负担,提高用户体验,让程序变得更加健壮。

责任链

人类的责任感和责任心是非常重要的,有时候我们需要把一些任务交给另外一个人完成,但是我们不能确定是哪一个人能够承担这个任务。那么,我们就需要使用“责任链模式”(Chain of Responsibility Pattern)来解决这个问题。

责任链模式是一种行为型模式,它为多个对象(接收者)提供了解决请求的方法,但是客户端只需要知道第一个对象,它会自动把请求传递给下一个对象,直到有一个对象能够处理请求为止。这个过程就像是一条链,所以称之为责任链模式。

我们来看一个例子,假设我们需要处理用户在网站上的注册请求。首先,我们需要验证用户的用户名和密码是否符合要求。如果通过验证,就要提交给下一个处理者来检查用户的邮箱和电话号码是否正确。如果还是通过了,就会提交给下一个处理者检查用户的公司和职位信息是否完整。最后,如果全部通过了,就把用户信息保存到数据库中。

下面是责任链模式的Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
// 抽象处理者,定义处理请求的接口
public abstract class Handler {
protected Handler successor; // 维持一个后继对象

public void setSuccessor(Handler successor) {
this.successor = successor;
}

public abstract void handleRequest(User user); // 处理请求的抽象方法
}

// 具体处理者1,处理用户名和密码验证请求
public class UserNameAndPasswordHandler extends Handler {
@Override
public void handleRequest(User user) {
if (user.getName() != null && user.getPassword() != null) {
System.out.println("用户名和密码验证通过");
if (successor != null) {
successor.handleRequest(user); // 传递给下一个处理者
}
} else {
System.out.println("用户名或密码未填写");
}
}
}

// 具体处理者2,处理用户邮箱和电话验证请求
public class EmailAndPhoneHandler extends Handler {
@Override
public void handleRequest(User user) {
if (user.getEmail() != null && user.getPhone() != null) {
System.out.println("用户邮箱和电话验证通过");
if (successor != null) {
successor.handleRequest(user); // 传递给下一个处理者
}
} else {
System.out.println("用户邮箱或电话未填写");
}
}
}

// 具体处理者3,处理用户公司和职位信息验证请求
public class CompanyAndPositionHandler extends Handler {
@Override
public void handleRequest(User user) {
if (user.getCompany() != null && user.getPosition() != null) {
System.out.println("用户公司和职位信息验证通过");
// 不需要传递给下一个处理者了,直接保存到数据库
System.out.println("用户信息已保存到数据库");
} else {
System.out.println("用户公司或职位信息未填写");
}
}
}

// 用户类
public class User {
private String name;
private String password;
private String email;
private String phone;
private String company;
private String position;

// 省略getter和setter方法
}

// 客户端类
public class Client {
public static void main(String[] args) {
// 创建处理者对象
Handler handler1 = new UserNameAndPasswordHandler();
Handler handler2 = new EmailAndPhoneHandler();
Handler handler3 = new CompanyAndPositionHandler();

// 构建责任链
handler1.setSuccessor(handler2);
handler2.setSuccessor(handler3);

// 创建用户对象
User user = new User();
user.setName("Tom");
user.setPassword("123456");
user.setEmail("tom@abc.com");
user.setPhone("12345678901");
user.setCompany("ABC公司");
user.setPosition("程序员");

// 处理请求
handler1.handleRequest(user);
}
}

小编觉得,责任链模式其实类似于我们生活中的“小环节串连起来变成大环节”,让我们在面对各种任务和问题时更加游刃有余和从容不迫。希望大家能够掌握这种使用广泛的设计模式,用好责任感和责任心,成为一个更好的人。

Redis vs MySQL

Redis与MySQL对比

Redis和MySQL是两个常用的开源数据库,虽然它们的使用场景不同,但是它们也有许多相似之处。在这篇文章中,我们将对比Redis和MySQL的一些关键区别,并探究它们的优劣势。

Redis和MySQL的区别

  1. 数据结构

Redis的数据可以存储在不同的数据结构中,例如:strings,hashes,lists,sets和sorted sets。与此不同,MySQL是一个传统的关系型数据库,数据按行列存储在表格中,使用SQL进行操作。

  1. 数据查询

由于Redis的数据存储方式,它通常被用于高速读写,可以快速访问单个对象或数据集合。Redis支持使用key-value的方式操作数据,可以高效地进行数据查询、写入和删除等操作。虽然MySQL也支持高速读写,但它通常被用于大型数据存储和复杂查询。

  1. 数据持久化

Redis的默认配置是将数据保存在内存中。虽然这使得它可以快速访问和处理数据,但也导致了数据易丢失。为了防止数据丢失,Redis提供了数据持久化机制,可以将数据写入磁盘中,以保护数据。而MySQL默认情况下则将数据保存在磁盘中,并提供了多种数据备份和恢复工具。

  1. 扩展性

Redis可以通过集群方式扩展,可以实现无限的水平扩展,在处理高流量负载时表现出色。MySQL也可以通过集群方式扩展,但它有一些限制,例如水平扩展仅限于读取操作,写操作仍然受限于单节点的机器性能,无法有效地满足高流量负载需求。

Redis与MySQL的优缺点

Redis的优点

  • 高速读写:由于Redis将数据存储在内存中,它的读写速度非常快,可以轻松地操作大量数据。
  • 数据结构丰富:Redis的数据结构非常灵活,支持不同的数据类型,可以方便地对特定数据进行处理。
  • 可扩展:Redis通过集群方式可以轻松地实现伸缩性,在处理高流量负载时表现出色。

Redis的缺点

  • 数据容易丢失:由于Redis默认将数据存储在内存中,如果服务器发生故障或出现断电的情况下,数据将会丢失。
  • 基于内存存储:Redis的内存存储方式意味着存储容量与服务器内存容量有关,存储容量有比较严格的限制。

MySQL的优点

  • 数据安全:MySQL提供了多种数据备份和恢复工具,可以保证数据安全。
  • 支持复杂查询:MySQL是一个丰富的关系型数据库管理系统,可以轻易地处理复杂的数据查询和管理操作。
  • 成熟的生态系统:MySQL早已成熟,有着完善的生态系统和用户社区支持。

MySQL的缺点

  • 数据读写速度较慢:相比Redis,MySQL的读写速度较慢,无法有效地处理大量数据请求。
  • 限制扩展:MySQL的扩展方式较为受限,无法轻易地伸缩性。

结论

Redis和MySQL都是非常流行的数据库,各自有着优缺点。选择哪个数据库取决于您要处理的是什么类型的数据、负载容量和安全需求。如果您的业务需要处理高并发读写请求,那么Redis可能更适合您。如果您的业务处理需要更多的查询和数据操作,那么MySQL可能是您的不二之选。