ZB-030-02从零开始爬虫

代码仓库

从零开始

  • Github-new
  • 新建项目
    • mvn archetype 使用骨架 2400+种选择
    • IDEA new 使用 ide创建
    • 从别人那里抄一个 (最常用)
  • .gitignore
  • README
  • 配置基本的代码质量检查插件
    • 越早代价越低 (保证代码质量)

开始写代码

001 github新建仓库

002 去 https://github.com/hcsp/read-write-files 里 把 src和 pom.xml抄过来

修改 .gitignore

1
2
3
4
5
*.iml
target/
.idea/

以上三个目录是不改被提交的

003 IDEA open项目 选择 我们的文件夹里的pom.xml

1
2
3
4
cp -r [你的clone目录]/read-write-files-master/.circleci  .

# 删除 .circleci/verifyWhitelist.sh
# 删除 .circleci/whitelist.txt

项目的设计流程-自顶向下 (团队))

  • 多人协作
  • 模块化
    • 各模块指责明确,界限清晰
    • 基本的文档
    • 基本的接口
  • 小步提交
    • 因为大的变更难以review
    • 因为大的变更冲突更加棘手

项目的设计流程-自低向上 (自己)

  • 单打独斗
    • 先实现功能
    • 在实现功能过程中不停的抽取公共部分
      • 每当写出很啰嗦的代码的时候,就该重构了
      • DRY:每当你复制/粘贴的时候,就该重构了
    • 通过重构实现模块化/接口化

项目的演进:可扩展性

  • 单线程->多线程
  • console -> H2 database
  • H2 database -> Elasticsearch

项目的演进:正确性

如何保证代码改动不会破坏原先的功能?

  • 测试

如何养成好的代码习惯

你都看不下去的代码

  • 不要妥协,你自己看代码都觉得说不过去。

项目目标

爬取新浪数据

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>

添加测试的依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>

添加冒烟测试类SmokeTest.java

1
2
3
4
5
6
7
8
9
package com.github.hcsp;

import org.junit.jupiter.api.Test;

public class SmokeTest {
@Test
public void test(){
}
}

运行 mvn test 成功则提交我们的代码

  • 如果你不小心 git add .把你的 .idea目录add了
    • 你不想add的文件咋办? —> git reset HEAD .idea 取消它的 add
  • 如果你不小心 git add .把你的 .idea目录add了 而且还 commit了怎么办?
    • 第一个办法git reset HEAD~1 向后回滚一个提交
    • 第二个办法 IDEA里 version control 选择log 选择上一次提交 右键Reset Current Branch To Here
  • 如果你不小心 git add .把你的 .idea目录add了 而且还 commit了 甚至 git push了怎么办?
    • 由于我们现在只有一个 master ,所以你去看代码 应该有一个 不想提交的文件 .idea
      • 第一步 如果是在 master在你本地 git log 查看你刚刚提交的 id
      • 第二步 git revert id号 形如 git revert 05995e358c631ebdccc2f1fb34b5ff44719fc303
      • 它的缺点是会撤销刚才的提交,而且你之前的提交也没了
      • 如果你是在主干的化,建议直接把它删掉
    • 推荐的做法:
      • 先还原代码 git log 看所有提交id
      • git reset id号
      • git reset HEAD --hard
  • 实际操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
commit 了 还 push 了
如果是在自己的分支上,那就用同样的方式操作,并且 force push
否则,如果在主干上,或者你害怕不敢那么操作,老老实实把多提交的文件删掉


具体操作如下
git reset HEAD~1
# 修改 .gitignore 添加 .idea
git add .
git commit -m "我修改了刚刚的错误"
# 此时你看提交信息
git status
# 提示你如下内容
# 位于分支 master
# 您的分支和 'origin/master' 出现了偏离,
# 并且分别有 1 和 1 处不同的提交。
# (使用 "git pull" 来合并远程分支)

# 此时你直接 push 是 失败的
# 你只能 force
git push -f

修改 .circleci.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: 2
jobs:
test:
docker:
- image: circleci/openjdk:8u212-jdk-stretch
steps:
- checkout
- restore_cache:
key: hcsp-{{ checksum "pom.xml" }}
- run:
name: Run Maven tests
command: mvn clean test
- save_cache: # saves the project dependencies
paths:
- ~/.m2
key: hcsp-{{ checksum "pom.xml" }}
workflows:
version: 2
default:
jobs:
- test

提交

1
git add .

此时提交有个潜规则

IDEA点击 commit

提交信息内容

1
2
3
修改CI配置

使之能够在CircleCI上运行。

然后 push

CircleCI上添加项目

1
2
3
4
5
6
7
8
搜索 circleci
得到 https://circleci.com/
登录 用 github
右上角 go to app
add project > 选择你的项目 --》 set up Project ---> Start building
然后开始自动化测试

此后你每次 push代码的时候 都会自动 运行测试,保证代码正确性的不二选择

新开分支,提 PR 然后合并主分支

开分支

1
git checkout -b new-feature

此时,你去掉了代码里一段注释

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) throws IOException {
// String html = getIndexPage();
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("https://sina.cn/");
try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
System.out.println(EntityUtils.toString(entity1));
}
}
}

提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git add .
git commit -m "Remove comments"

git push
# 此时失败了 因为 远程没有你的新分支
git push --set-upstream origin new-feature

# 此时打开你的代码仓库
# 这时这个新的提交在仓库上显示了 点击 Compare & pull request

# 文本框里输入你的描述
在这里写你的新功能描述,等待CI检查通过,或者你的 team leader 的 code review.

# 点击 create pull request

# 测试通过之后 点击 merge pull request

ZB-030-01多线程爬虫实战

做一个项目的原则(潜规则)

把每个项目当作人生里最好的一个项目来精雕细琢

  • 积累自己的Reputation(声誉)
  • 一丝不苟的写好文档
  • 代码质量++ (你当前能力阶段最好的质量)
  • 你的认真是肯定能获得回报的

使用标准化/业界公认的模式和流程

  • 没有多余的内容 如 .idea 文件夹就是多余的 IDEA生成的

(几乎)没有本地依赖,使用者能够无障碍地运行

  • 你的java项目里引用了一个本地库文件,只在你的代码里有

小步快跑

  • 不要妄图一气呵成
  • 成就感
  • 越小的变更越容易debug

我们的项目原则

[强制]使用 GitHub + 主干/分支模型进行开发

  • 禁止直接push master
  • 所有的变更通过 PR 进行

[强制]自动化代码质量检查 + 测试

  • Checkstyle/SpotBugs
  • 最基本的自动化测试覆盖

一切工作自动化

  • 数据库建表
  • 创建测试数据

规范化提交流程

  • 经过代码工具检查

ZB-029-03线程池和Callable和Future

线程池与Callable/Future

什么是线程池

  • 线程是昂贵的(Java线程模型的缺陷)
    • 一个线程就是一个工人,每开一个线程意味着 你的JVM里就一个工人开始执行你的代码
    • 生活里公司里人员流动的成本是很高的,你要重新培养它。线程也是一样
    • java的线程和操作系统的线程相绑定,java的线程调度依赖于操作系统的线程调度,因此很昂贵,随意你不能随心所欲的在操作系统里开线程
  • 线程池是预先定义好的若⼲个线程
  • Java中的线程池

    Callable/Future

  • 类⽐Runnable,Callable可以返回值,抛出异常

  • Future代表⼀个“未来才会返回的结果”
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
import java.util.concurrent.*;

public class WordCount {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建固定数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);

//new Callable<T>(){...} T代表返回值

// 返回数字
Future<Integer> future1 = threadPool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 0;
}
});

// 返回字符串
Future<String> future2 = threadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "abc";
}
});

// 抛出异常
Future<Object> future3 = threadPool.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
throw new RuntimeException();
}
});

/*
submit 和 new Thread().start(); 一样会立刻执行,是异步的 不会阻塞当前线程的执行
submit 的返回值是个 Future
threadPool.submit( ... );


Future 代表一个未来
*/

// 它会等这个结果回来打印
System.out.println(future1.get());
System.out.println(future2.get());
System.out.println(future3.get());

// 你等不及了,取消一个
// future2.cancel(100);

}
}

实战:多线程的WordCount

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
package com.github.hcsp.multithread;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

public class WordCount {
private final int threadNum;
private ExecutorService threadPool;
public WordCount(int threadNum) {
threadPool = Executors.newFixedThreadPool(threadNum);
this.threadNum = threadNum;
}

public static void main(String[] args) {

}

// 统计文件中各单词的数量
public Map<String, Integer> count(File file) throws FileNotFoundException, ExecutionException, InterruptedException {
BufferedReader reader = new BufferedReader(new FileReader(file));

List<Future<Map<String,Integer>>> futures = new ArrayList<>();

// 开多个线程,每个线程读取文件的一行内容,并将其中的单词统计结果返回
// 最后,主线程将 工作线程返回的结果汇总在一起
for (int i = 0; i < threadNum; i++) {
futures.add(threadPool.submit(new WorkerJob(reader)));
}

// 合并
Map<String,Integer> finalResult = new HashMap<>();
for (Future<Map<String,Integer>> future:futures) {
Map<String,Integer> resultFromWorker = future.get();
mergeWorkerResultIntoFinalResult(resultFromWorker,finalResult);
}


return finalResult;
}

private void mergeWorkerResultIntoFinalResult(Map<String, Integer> resultFromWorker,
Map<String, Integer> finalResult) {
for (Map.Entry<String,Integer> entry: resultFromWorker.entrySet()) {
String word = entry.getKey();
int mergedResult = finalResult.getOrDefault(word,0) + entry.getValue();
finalResult.put(word,mergedResult);
}
}

static class WorkerJob implements Callable<Map<String ,Integer>> {
private BufferedReader reader;

public WorkerJob(BufferedReader reader) {
this.reader = reader;
}

@Override
public Map<String, Integer> call() throws Exception {
String line = null;
Map<String,Integer> result = new HashMap<>();
while ((line=reader.readLine())!= null){
String[] words = line.split(" ");
for (String word: words) {
result.put(word,result.getOrDefault(word,0) + 1);
}
}
return result;
}
}
}

ZB-029-02生产者消费者模型

生产者消费者模型

  • 生产者和消费者之间有一个盘子
  • 盘子只能有一个包子
  • 生产者创造了一个包子 放置到盘子上。此时不会继续生产,而是等消费者把盘子上的包子吃了,再去生产 即盘子是空的菜生产
  • 消费者吃掉一个包子后,就不会在吃了,而是等待生产者 生产 即盘子上有包子才消费

1.Object.wait/notify解决

2.Lock/Condition解决

3.BlockingQueue解决

方式一:Object.wait/notify

Boss.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
package com.github.hcsp.multithread;

public class Boss {
public static void main(String[] args) throws InterruptedException {
// 请实现一个生产者/消费者模型,其中:
// 生产者生产10个随机的整数供消费者使用(随机数可以通过new Random().nextInt()获得)
// 使得标准输出依次输出它们,例如:
// Producing 42
// Consuming 42
// Producing -1
// Consuming -1
// ...
// Producing 10086
// Consuming 10086
// Producing -12345678
// Consuming -12345678

Container container = new Container();
Object lock = new Object();

Producer producer = new Producer(container, lock);
Consumer consumer = new Consumer(container, lock);

producer.start();
consumer.start();

producer.join();
producer.join();
}
}

Container.java 放置包子的盘子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.github.hcsp.multithread;

import java.util.Optional;

public class Container {
private Optional<Integer> value = Optional.empty();

public Optional<Integer> getValue() {
return value;
}

public void setValue(Optional<Integer> value) {
this.value = value;
}
}

Consumer.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
package com.github.hcsp.multithread;

import java.util.Optional;

public class Consumer extends Thread {
Container container;
Object lock;

public Consumer(Container container, Object lock) {
this.container = container;
this.lock = lock;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
// 只要盘子是空的 它就等待
while (!container.getValue().isPresent()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 盘子不是空的就消费
Integer value = container.getValue().get();
// 清空盘子的内容
container.setValue(Optional.empty());
System.out.println("Consuming " + value);

// 唤醒生产者
lock.notify();
}
}
}
}

Producer.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
package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.Random;

public class Producer extends Thread {
Container container;
Object lock;

public Producer(Container container, Object lock) {
this.container = container;
this.lock = lock;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
// 只要盘子里有东西 它就必须等待
while (container.getValue().isPresent()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 盘子是空的就生产
int r = new Random().nextInt();
System.out.println("Producing " + r);
container.setValue(Optional.of(r));
// 唤醒消费者
lock.notify();
}
}
}
}

方式二:Lock/Condition

Boss.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
package com.github.hcsp.multithread;

import java.util.concurrent.locks.ReentrantLock;

public class Boss {
public static void main(String[] args) throws InterruptedException {
// 请实现一个生产者/消费者模型,其中:
// 生产者生产10个随机的整数供消费者使用(随机数可以通过new Random().nextInt()获得)
// 使得标准输出依次输出它们,例如:
// Producing 42
// Consuming 42
// Producing -1
// Consuming -1
// ...
// Producing 10086
// Consuming 10086
// Producing -12345678
// Consuming -12345678

ReentrantLock lock = new ReentrantLock();
Container container = new Container(lock);

Producer producer = new Producer(container, lock);
Consumer consumer = new Consumer(container, lock);

producer.start();
consumer.start();

producer.join();
producer.join();
}
}

Container.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
package com.github.hcsp.multithread;

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

public class Container {

private Condition notConsumedYet; // 还没被消费掉
private Condition notProducedYet; // 还没被生产出来
private Optional<Integer> value = Optional.empty();

public Container(ReentrantLock lock) {
this.notConsumedYet = lock.newCondition();
this.notProducedYet = lock.newCondition();
}

public Condition getNotConsumedYet() {
return notConsumedYet;
}

public Condition getNotProducedYet() {
return notProducedYet;
}

public Optional<Integer> getValue() {
return value;
}

public void setValue(Optional<Integer> value) {
this.value = value;
}
}

Consumer.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
package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.concurrent.locks.ReentrantLock;

public class Consumer extends Thread {
Container container;
ReentrantLock lock;

public Consumer(Container container, ReentrantLock lock) {
this.container = container;
this.lock = lock;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
lock.lock();
// 只要盘子是空的 它就等待
while (!container.getValue().isPresent()) {
try {
container.getNotProducedYet().await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 盘子不是空的就消费
Integer value = container.getValue().get();
// 清空盘子的内容
container.setValue(Optional.empty());
System.out.println("Consuming " + value);

// 唤醒生产者
container.getNotConsumedYet().signal();
} finally {
lock.unlock();
}
}
}
}

Producer.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
package com.github.hcsp.multithread;

import java.util.Optional;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class Producer extends Thread {
Container container;
ReentrantLock lock;

public Producer(Container container, ReentrantLock lock) {
this.container = container;
this.lock = lock;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
lock.lock();
// 只要盘子里有东西 它就必须等待
while (container.getValue().isPresent()) {
try {
container.getNotConsumedYet().await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 盘子是空的就生产
int r = new Random().nextInt();
System.out.println("Producing " + r);
container.setValue(Optional.of(r));
// 唤醒消费者
container.getNotProducedYet().signal();
} finally {
lock.unlock();
}
}
}
}

方式三:BlockingQueue

signalQueue 是控制线程调度的 实际就是个开关

Boss.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
package com.github.hcsp.multithread;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class Boss {
public static void main(String[] args) throws InterruptedException {
// 请实现一个生产者/消费者模型,其中:
// 生产者生产10个随机的整数供消费者使用(随机数可以通过new Random().nextInt()获得)
// 使得标准输出依次输出它们,例如:
// Producing 42
// Consuming 42
// Producing -1
// Consuming -1
// ...
// Producing 10086
// Consuming 10086
// Producing -12345678
// Consuming -12345678

BlockingDeque<Integer> queue = new LinkedBlockingDeque<>(1);
// 控制线程的调度
BlockingDeque<Integer> signalQueue = new LinkedBlockingDeque<>(1);

Producer producer = new Producer(queue, signalQueue);
Consumer consumer = new Consumer(queue, signalQueue);

producer.start();
consumer.start();

producer.join();
producer.join();
}
}

Consumer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.github.hcsp.multithread;

import java.util.concurrent.BlockingDeque;

public class Consumer extends Thread {
BlockingDeque<Integer> queue;
BlockingDeque<Integer> signalQueue;

public Consumer(BlockingDeque<Integer> queue, BlockingDeque<Integer> signalQueue) {
this.queue = queue;
this.signalQueue = signalQueue;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println("Consuming " + queue.take());
signalQueue.put(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

Producer.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
package com.github.hcsp.multithread;

import java.util.Random;
import java.util.concurrent.BlockingDeque;

public class Producer extends Thread {
BlockingDeque<Integer> queue;
BlockingDeque<Integer> signalQueue;

public Producer(BlockingDeque<Integer> queue, BlockingDeque<Integer> signalQueue) {
this.queue = queue;
this.signalQueue = signalQueue;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
int r = new Random().nextInt();
System.out.println("Producing " + r);
try {
queue.put(r);
signalQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

ZB-029-01多线程初步

线程不安全的表现

  • 数据错误
  • 死锁
  • 著名的HashMap的死循环问题
  • 写⼀段代码来重现死锁
  • 预防死锁产⽣的原则:
    • 所有的线程都按照相同的顺序获得资源的锁
  • 死锁问题的排查
  • 多线程的经典问题:哲学家⽤餐

数据错误

  • i++

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.io.demo;

    public class Main {
    public static long i =0;
    public static void main(String[] args) {
    for (int j = 0; j < 1000; j++) {

    new Thread(()-> {
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(++i);
    }).start();
    }

    }
    }

    // 执行后不是 1000
  • if-then-do

    • HashMap 线程不安全容器
    • 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
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      package com.io.demo;

      import java.util.HashMap;
      import java.util.Map;
      import java.util.Random;

      public class Main {
      public static Map<Integer,Integer> map = new HashMap<>();
      public static void main(String[] args) {
      for (int j = 0; j < 1000; j++) {
      new Thread(Main::putIfAbsent).start();
      }
      }

      // 随机丢入 HashMap 一个1~10数字 如果它不存在就丢进去
      private static void putIfAbsent(){
      try {
      Thread.sleep(1);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      int r = new Random().nextInt(10);
      if(!map.containsKey(r)){
      map.put(r,r);
      System.out.println("put:" + r);
      }
      }
      }
      // 执行后会发生 一个数被 put 多次

      /*
      这里有一个共享的 map
      在某一时刻
      线程1 随机了一个数字 0 ,同时它的执行时间到了
      与此同时:一另一个 线程2 开始执行了, 它也生成了一个0 并成功把它 put 进入 map里
      此时 线程1 恢复执行 获得资源调度继续执行 于是。。。 出现了 put 2次 0 的情况
      */

为什么要多线程

  • 为了让程序跑的更快,但是你的程序跑的更快,如果结果不对跑的再快也没用。
  • 这就是线程不安全 最要命的问题 它会产生错误的数据

死锁

  • java里任何一个对象都可以当作
  • 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
package com.io.demo;


public class Main {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
new Thead1().start();
new Thead2().start();
}

static class Thead1 extends Thread{
@Override
public void run() {
synchronized (lock1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock2){
System.out.println("thread1");
}
}
}
}

static class Thead2 extends Thread{
@Override
public void run() {
synchronized (lock2){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock1){
System.out.println("thread2");
}
}
}
}
}

死锁问题的排查

如何排查线程死锁问题呢?

第一步:jps 找到程序的进程编号

  • 所有的 java 都跑在 jvm 里,而操作系统看来 一个jvm 就是一个普通进程
  • 首先要找到进程
    • windows 上 任务管理器
    • 类 linux 系统使用 ps aux | grep java 可以列出所有 java 进程
    • jps :java自带的命令,只要配置了 java_home 那个目录里有这个命令 显示所有java的进程

第二步:jstack 进程号

  • 打印所有 该 java进程中的 栈信息

线程安全

  • 实现线程安全的基本⼿段

    • 不可变类
    • Integer/String/….

      1
      2
      3
      4
      5
      // 字符串 "a" 地址为 201
      String s = "a";
      s = "b";
      // 201地址的字符串没变 实际变的是 引用
      // 字符串 "b" 的地址是 301
    • 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
      // 解决 多线程 共享数据问题
      package com.io.demo;

      public class Main2 {
      private static int i = 0;
      private static final Object lock = new Object();

      public static void main(String[] args) {
      for (int j = 0; j < 1000; j++) {
      new Thread(Main2::modifySharedVariable).start();
      }
      }

      public static void modifySharedVariable(){
      try {
      Thread.sleep(1);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      synchronized (lock){
      i+=1;
      }
      System.out.println(i);
      }
      }

      另一种方式,明显感觉比之前慢了 因为此时的锁是 这个 class

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.io.demo;

      public class Main2 {
      private static int i = 0;
      private static final Object lock = new Object();

      public static void main(String[] args) {
      for (int j = 0; j < 1000; j++) {
      new Thread(Main2::modifySharedVariable).start();
      }
      }

      public synchronized static void modifySharedVariable(){
      try {
      Thread.sleep(1);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      i+=1;
      System.out.println(i);
      }
      }

      移除 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
      package com.io.demo;

      public class Main2 {
      private static int i = 0;
      private static final Object lock = new Object();

      public static void main(String[] args) {
      Main2 main2 = new Main2();
      for (int j = 0; j < 1000; j++) {
      new Thread(main2::modifySharedVariable).start();
      }
      }

      public synchronized void modifySharedVariable(){
      try {
      Thread.sleep(1);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      i+=1;
      System.out.println(i);
      }

      // 等价于
      public void modifySharedVariable2(){
      synchronized(this){
      try {
      Thread.sleep(1);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      i+=1;
      System.out.println(i);
      }
      }
      }
    • 同步块同步了什么东⻄?

      • synchronized(⼀个对象) 把这个对象当成锁
      • static synchronized ⽅法 把Class对象当成锁
      • 实例的synchronnized⽅法把该实例当成锁
    • Collections.synchronized

ZB-028-01多线程原理

多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.io.demo;

public class Main {

public static void main(String[] args) throws InterruptedException {
a();
System.out.println(1);
}

public static void a() throws InterruptedException {
Thread.sleep(10000);
b();
}
public static void b() throws InterruptedException {
Thread.sleep(1000);
}
}

11秒后打印1

执行流程

  • main调用的时候等待 a方法执行结束
  • a执行过程中先Thread.sleep(10000) 睡了10秒
  • 然后 a 方法要等待 b方法
  • b方法中有 Thread.sleep(1000) 睡了1秒
  • b方法执行结束,==> a 方法结束 ==> main 中 打印 1 ==> main 方法结束

java执行模型是同步/阻塞的

为什么需要多线程

  • CPU:你们都慢!死!了!

    1
    2
    3
    4
    5
    6
    7
    CPU 3GHz  --> 0.3ns 做一个事
    内存 中 --> 10ms a += 1 的操作远远大于 CPU的执行运算的时间
    更何况
    硬盘里 读写文件 一来一回呢?

    这就导致 CPU是那么的快 ,但是 内存 硬盘 是那么的慢
    这是不能接受的,所以 CPU很想在等待 其他小伙伴返回的时候做些其他的事
  • 现代CPU都是多核的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    CPU 的单位是 GHz

    1GHz == 1G时钟周期/s ==> 1ns

    随着技术的进步 1.4 > 2.2 > 2.7 > 3.4
    时钟周期变短了 :意味着CPU做的事更多了

    为什么现在的 CPU 少有突破3GHz / 10GHz / 100GHz 呢?

    焦耳定律: (I平方)Rt = 发热量
    任何东西,它的 发热量和电流的平方成正比的
    后果就是: 频率越快 发热量越快。 所以发热量严重的制约了 CPU 频率的上升

    那怎么办?

    堆核心
    1个厨师一个时间能做一道菜
    雇佣4个厨师就可以做四个菜
  • Java的执⾏模型是同步/阻塞(block)的

  • 默认情况下只有⼀个线程
    • 处理问题⾮常⾃然
    • 但是具有严重的性能问题

耗时的案例

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
package com.io.demo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {

public static void main(String[] args) throws InterruptedException {
long t0 = System.currentTimeMillis();
writeFile();
writeFile();
writeFile();
writeFile();
long t1 = System.currentTimeMillis();
System.out.println("耗时:"+ (t1- t0) + "ms");
}

public static void writeFile() {
System.out.println("我开始写入了~");
try {
File tmp = File.createTempFile("tmp","");
for (int i = 0; i < 10000; i++) {
try(FileOutputStream fileOutputStream = new FileOutputStream(tmp)){
fileOutputStream.write(i);
}
}
} catch (IOException e) {
e.printStackTrace();
}

}
}

/*
执行四次耗时
我开始写入了~
我开始写入了~
我开始写入了~
我开始写入了~
耗时:6743ms
*/

多线程的方式改写

1
2
3
4
5
6
7
// 
new Thread(new Runnable() {
@Override
public void run() {
writeFile();
}
})
  • 注意!IDEA里任何出现叹号的地方你都可以alt + enter
  • 注意!IDEA里任何出现叹号的地方你都可以alt + enter
  • 注意!IDEA里任何出现叹号的地方你都可以alt + enter
1
2
3
4
5
// 第一次优化
new Thread(() -> writeFile());
// 第二次优化
new Thread(Main::writeFile);
// 简单吧!

注意线程必须 start才会开启

1
new Thread(Main::writeFile).start();

再来看执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) throws InterruptedException {
long t0 = System.currentTimeMillis();
writeFile();
new Thread(Main::writeFile).start();
new Thread(Main::writeFile).start();
new Thread(Main::writeFile).start();

long t1 = System.currentTimeMillis();
System.out.println("耗时:"+ (t1- t0) + "ms");
}

public static void writeFile() {...}
}

/*
我开始写入了~
我开始写入了~
我开始写入了~
我开始写入了~
耗时:2857ms
*/

start 和 run 区别

1
2
3
4
5
6
7
8
9
10
11
12
13
new Thread(Main::writeFile).start();
new Thread(Main::writeFile).start();
new Thread(Main::writeFile).start();

开启三个线程去运行,主线程不等它们
// 并行


new Thread(Main::writeFile).run();
new Thread(Main::writeFile).run();
new Thread(Main::writeFile).run();
// 串行
第一个执行结束 开始执行第二个 然后第二个执行结束执行第三个

开启⼀个新的线程

  • Thread
  • Java中只有这么⼀种东⻄代表线程
  • start⽅法才能并发执⾏!
  • 每多开⼀个线程,就多⼀个执⾏流
  • ⽅法栈(局部变量)是线程私有的
  • 静态变量/类变量是被所有线程共享的
    • 所有多线程导致问题的根源

线程难的问题根源——它所共享的变量

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
package com.io.demo;

public class Main {
public static long i =0;
public static void main(String[] args) {
long t0 = System.currentTimeMillis();
for (int j = 0; j < 1000; j++) {
new Thread(Main::add).start();
}

long t1 = System.currentTimeMillis();
System.out.println("耗时:"+ (t1- t0) + "ms");
}

public static void add() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

i += 1;
System.out.println(i);
}
}

/*
1
2
3



980
979
977
976
973
972
971
983
987
990
993
995
985
986
984
983
984
994
992
991
989
988
*/

线程难的本质原因是
你要看着同⼀份代码,
想象不同的⼈在疯狂地以乱序执⾏它

首先 i++ 不是原子操作

实际过程如下:

1
2
3
4
i++
取 i 的值
把 i 的值 加 1
把修改后的值 写回 i

多线程适合什么场合

  • 对于IO密集型应⽤极其有⽤
    • ⽹络IO(通常包括数据库)
    • ⽂件IO
  • 对于CPU密集型应⽤稍有折扣
  • 性能提升的上限在哪⾥?
    • 单核CPU 100%
    • 多核CPU N*100%
      如你的电脑开的程序太多,你非常直观的感觉就是卡

ZB-027-01-数据库原理

DDL-SQL

  • create table
  • drop table
  • alter table 修改表,添加列,建索引

随用随查

基本SQL

  • insert into
  • delete from
  • update
  • select

重要概念

  • sql的关键字不区分大小写
  • 命名风格是 user_name 下划线形式
  • 物理删除 、 逻辑删除
  • 创建表的时候 如果字段是 sql的关键字 请使用这样的形式:“反引号包裹字段名”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 用户表
CREATE TABLE USER (
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(100) NOT NULL,
-- 密码一定不能存明文, 参考 CSDN 明文存密码
PASSWORD VARCHAR(100) NOT NULL,
TEL VARCHAR (200) NOT NULL UNIQUE,
ADDRESS VARCHAR (100),
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
UPDATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
-- 逻辑删除的标识
-- STATUS TINYINT NOT NULL
);

-- 新增列,由于之前已经有了数据 ,再次新增列 not null 就会失败,所以设置 default
alter table USER add STATUS TINYINT NOT NULL default 1

-- delete 操作是非常危险的

-- 删除用户 id=2的数据的正确姿势,应该同步更新 updated_at字段
update user set status=0, updated_at=now() where id=2

select之一次数据库查询的时间是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CPU时钟周期
CPU 3Ghz的电脑 = 3G/s 30亿次/秒
指令周期 0.3ns = 0.3 x 10的-9次方 秒

内存
寻址时间:10us 微秒 = 10x10的-6次方 秒 (比CPU慢一个数量级)

SSD
寻址时间:100us = 100x10的-6次方 秒

HDD
寻址时间:1~10ms 毫秒 = 10的-3次方 ~ 10的-2次方 秒

同机房网络IO
1ms 毫秒 = 10的-3次方 秒

数据库操作
典型时间: 1ms (查询算快的情况)

数据库存多少算是多的?

  • 过早优化乃万恶之源!!!
  • 过早优化乃万恶之源!!!
  • 过早优化乃万恶之源!!!
1
2
3
4
5
1w / 100w / 1000w / 1亿

对于mysql来说 100w ~ 1000w 对它没啥本质区别

上亿才会对他性能有所下架

money这种敏感信息如何存

  • int分 *100 有点low
  • decimal

使用JDBC访问数据库

  • Java Database Connection
    • 连接串
    • 用户名
    • 密码
  • Statement 语句
  • PrepareStatement 防SQL注入的
  • ResultSet

Statement

  • 最好别用,有注入问题
1
2
3
4
5
6
7
// 如果用户这样输入 字符串 name = "zhangsan' or '1'='1" 就会骗过mysql 查出所有的用户
try (Connection conn = DriverManager.getConnection(Config.JDBC_URL);
Statement st = conn.createStatement()) {
ResultSet rs = null;
rs = st.executeQuery("select * from USER where NAME='"+name +"'");
printResult(rs);
}
  • 原因是 Statement 是把sql整个字符串 变成 sql的 AST

最好使用 PrepareStatement

  • 防注入
1
2
PreparedStatement st = conn.prepareStatement("select * from USER where NAME= ?")) {
st.setString(1,name);
  • PreparedStatement 是先把sql字符串 变成 sql的 AST,然后在把对应内容 设置到AST的节点上

sql操作

建表语句和灌入数据

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
-- 用户表
CREATE TABLE USER (
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(100) NOT NULL,
TEL VARCHAR (200) NOT NULL UNIQUE,
ADDRESS VARCHAR (100),
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
UPDATED_AT TIMESTAMP NOT NULL DEFAULT NOW()
);

-- 商品表
CREATE TABLE GOODS(
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(100) NOT NULL,
PRICE DECIMAL NOT NULL,
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
UPDATED_AT TIMESTAMP NOT NULL DEFAULT NOW()
);

-- 订单表
CREATE TABLE `ORDER`(
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
USER_ID BIGINT,
GOODS_ID BIGINT,
GOODS_NUM INT, -- 下单的商品数量
GOODS_PRICE DECIMAL NOT NULL, -- 下单时的商品单价
CREATED_AT TIMESTAMP NOT NULL DEFAULT NOW(),
UPDATED_AT TIMESTAMP NOT NULL DEFAULT NOW()
);

INSERT INTO USER (ID, NAME, TEL, ADDRESS) VALUES (1, 'zhangsan', 'tel1', 'beijing');
INSERT INTO USER (ID, NAME, TEL, ADDRESS) VALUES (2, 'lisi', 'tel2', 'shanghai');
INSERT INTO USER (ID, NAME, TEL, ADDRESS) VALUES (3, 'wangwu', 'tel3', 'shanghai');
INSERT INTO USER (ID, NAME, TEL, ADDRESS) VALUES (4, 'zhangsan', 'tel4', 'shenzhen');

INSERT INTO GOODS (ID, NAME, PRICE) VALUES (1, 'goods1', 10);
INSERT INTO GOODS (ID, NAME, PRICE) VALUES (2, 'goods2', 20);
INSERT INTO GOODS (ID, NAME, PRICE) VALUES (3, 'goods3', 30);
INSERT INTO GOODS (ID, NAME, PRICE) VALUES (4, 'goods4', 40);
INSERT INTO GOODS (ID, NAME, PRICE) VALUES (5, 'goods5', 50);

INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (1,1, 1, 5, 10);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (2,2, 1, 1, 10);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (3,2, 1, 2, 10);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (4,4, 2, 4, 20);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (5,4, 2, 100, 20);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (6,4, 3, 1, 20);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (7,5, 4, 1, 20);
INSERT INTO `ORDER` (ID,USER_ID, GOODS_ID, GOODS_NUM, GOODS_PRICE) VALUES (8,5, 6, 1, 60);
  • 表信息
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
用户表:
+----+----------+------+----------+
| ID | NAME | TEL | ADDRESS |
+----+----------+------+----------+
| 1 | zhangsan | tel1 | beijing |
+----+----------+------+----------+
| 2 | lisi | tel2 | shanghai |
+----+----------+------+----------+
| 3 | wangwu | tel3 | shanghai |
+----+----------+------+----------+
| 4 | zhangsan | tel4 | shenzhen |
+----+----------+------+----------+
商品表:
+----+--------+-------+
| ID | NAME | PRICE |
+----+--------+-------+
| 1 | goods1 | 10 |
+----+--------+-------+
| 2 | goods2 | 20 |
+----+--------+-------+
| 3 | goods3 | 30 |
+----+--------+-------+
| 4 | goods4 | 40 |
+----+--------+-------+
| 5 | goods5 | 50 |
+----+--------+-------+
订单表:
+------------+-----------------+------------------+---------------------+-------------------------------+
| ID(订单ID) | USER_ID(用户ID) | GOODS_ID(商品ID) | GOODS_NUM(商品数量) | GOODS_PRICE(下单时的商品单价) |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 1 | 1 | 1 | 5 | 10 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 2 | 2 | 1 | 1 | 10 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 3 | 2 | 1 | 2 | 10 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 4 | 4 | 2 | 4 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 5 | 4 | 2 | 100 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 6 | 4 | 3 | 1 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 7 | 5 | 4 | 1 | 20 |
+------------+-----------------+------------------+---------------------+-------------------------------+
| 8 | 5 | 6 | 1 | 60 |
+------------+-----------------+------------------+---------------------+-------------------------------+

sql练习

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
# 01 查询所有用户
select * from USER;

# 02 查询用户个数
select count(*) from USER;

# 03 查询 地址 = 'shanghai' 用户
select * from USER where ADDRESS='shanghai'

# 04 查询 地址 = 'shanghai' 用户个数
select count(*) from USER where ADDRESS='shanghai'

# 05 查询具体的列
select id,name from USER where ADDRESS='shanghai'

# 06 在上海的用户 id降序排列
select id,name from USER where ADDRESS='shanghai' order by id desc;

# 07 用户列表分页 limit<从第几个元素开始>,<最多返回几个元素>
# limit <(pageNo-1)*pageSize>,<pageSize>
select * from USER limit 2,2;

# 08 按地区分组
select ADDRESS from USER group by ADDRESS;

# 09 每个地区有多少人
select ADDRESS,count(*) from USER group by ADDRESS;

# 10 订单表:每个商品下了几单
select GOODS_ID,count(*) from `ORDER` group by GOODS_ID;

# 11 订单表:每个商品下了几单 ——列名:重新命名
select GOODS_ID,count(*)as count from `ORDER` group by GOODS_ID;

# 12 订单表:每个订单商品和对应的总价格
select GOODS_ID,(GOODS_NUM*GOODS_PRICE)as total_price from `ORDER`;

# 13 订单表:每个商品对应的销售额
select GOODS_ID,sum(GOODS_NUM*GOODS_PRICE) from `ORDER` group by GOODS_ID;

# 14 订单表:每个商品对应的销售额降序排列
select GOODS_ID,sum(GOODS_NUM*GOODS_PRICE)as total from `ORDER` group by GOODS_ID order by total desc;

进阶:JOIN操作

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
# join操作,
# 单独一个 join 是内连接 inner join 两个表共有数据

# 15 订单信息表, 订单和商品名称
select `ORDER`.id,`ORDER`.USER_ID,`ORDER`.GOODS_ID,GOODS.NAME from `ORDER`
join GOODS
on `ORDER`.GOODS_ID = GOODS.ID;
# 简化
select o.id,o.USER_ID,o.GOODS_ID,g.NAME from `ORDER` as o
join GOODS as g
on o.GOODS_ID = g.ID;

# 16 查询所有订单的信息,和对应的 商品名称 ,哪怕这个goods没有
select o.id,o.USER_ID,o.GOODS_ID,g.NAME from `ORDER` as o
left join GOODS as g
on o.GOODS_ID = g.ID;

# 17 连表 订单信息,商品名称,用户信息(存在的用户和订单)
select o.id,o.USER_ID,o.GOODS_ID,g.NAME,u.NAME,u.TEL,u.ADDRESS from `ORDER` as o
join GOODS as g
on o.GOODS_ID = g.ID
join USER as u
on o.USER_ID = u.ID;

# 18 连表 订单信息,商品名称,用户信息,(即使用户不存在)
select o.id,o.USER_ID,o.GOODS_ID,g.NAME,u.NAME,u.TEL,u.ADDRESS from `ORDER` as o
join GOODS as g
on o.GOODS_ID = g.ID
left join USER as u
on o.USER_ID = u.ID;

# 19 查询 beijing 的用户订单信息
select o.id,o.USER_ID,o.GOODS_ID,g.NAME,u.NAME,u.TEL,u.ADDRESS from `ORDER` as o
join GOODS as g
on o.GOODS_ID = g.ID
join USER as u
on o.USER_ID = u.ID
where u.ADDRESS = 'beijing';

综合练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 01 查询有多少用户买过指定商品 如 goods_id = 1
# 01-1 查询指定商品
select * from `ORDER` where GOODS_ID = 1;
# 有三条 uid=1的一条订单, uid=2的两条订单
# 所以你不能直接 这样 因为没去重复 返回的是3 ,应该是2
select count(*) from `ORDER` where GOODS_ID = 1;
# 第一种方式
# 01-2 谁曾经下过单 distinct 去重复
select distinct USER_ID from `ORDER` where GOODS_ID = 1;
# 01-3 count 统计
select count(distinct USER_ID) from `ORDER` where GOODS_ID = 1;

# 第二种方式 子查询
# 01-4
select * from USER where id in (
select USER_ID from `ORDER` where GOODS_ID = 1
);
# 等价于 select * from USER where id in (1,2,2);
# 01-5
select count(*) from USER where id in (
select USER_ID from `ORDER` where GOODS_ID = 1
);

代码参考

ZB-025-02-异常的类型体系

throw/throws

throw 丢出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
b();
}

private static void b(){
c();
}

private static void c(){
throw new RuntimeException();
}
}

所有人都不处理这个异常会怎么样

  • 答案是: 终止这个线程,如果都没人抓这个异常,异常就会把当前的线程杀掉,这是默认的处理方式

throws 用于声明方法将来可能会丢出什么样的东西

1
2
3
4
5
private static void b() throws  Exception{
if(true){
throw new Exception();
}
}

如果一个方法抛出了异常,那么调用它的地方都要 主动抛出异常 或者 try/catch

throw/throws区别

  • throw 是一个语句,可以声明自己要抛出的异常,不管这个异常怎样被处理,只管往外抛
  • throws 只是声明当前方法 有可能 会抛出 XXException,代价就是调用该方法的地方都要 throws XXException 或者 try/catch

让你诧异的地方

throws RuntimeException 时候 a 不报错

1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws RuntimeException{

}

throws Exception 时候 a 竟然报错了

1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws Exception{

}

Java的异常体系

1
2
3
4
5
6
7
8
Throwable 
所有 错误 和 异常的父类 ,任何的 throwable 都可以被丢出来
它是有毒的
-|Exception (checked exception) 受检异常,有毒
-|RuntionException 运行时异常 无毒
_|NullPointerException

-|Error 错误 无毒

什么是 有毒 什么是 无毒

”有毒“学名叫做 受检异常

  • 抛出了Throwable 或者 Throwable 的子类 都是有毒的,就是你必须处理它 要么你也 throws 要么 try/catch
1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws Throwable{

}

“无毒” 学名叫做 不受检异常

  • 不用去处理,你可以随便抛
    • RuntimeException
    • NullPointerException
    • Error
1
2
3
4
5
6
7
private static void a(){
b();
}

private static void b() throws RuntimeException{

}

java异常体系的最初的设计想法

1994年 ,假如显示的抛出异常,这样别人就必须处理,是不是能让软件更加安全呢?

  • 当时的想法:是的
  • 后来发现:实在太麻烦了,于是大家纷纷讨厌了这种设计思路。

Exception 代表我预期,可能会发生的情况的一种状态

  • 预期之内的, 如文件找不到,断网了
  • 所以当时的思路是,任何调用throws Exception 的方法的人都该对预期之内的情况进行处理 ,这就是最初设计的目的

RuntimeException 代表什么呢?

  • 意料之外的异常,因此不需要声明,所以你可以随便丢
  • 发生了就是一个bug

Error 是什么

  • 异常是可以恢复的,而 Error 是不可以恢复的异常
  • 意思就是 “错误”
  • 大多数情况 代表 不正常 的情况

你可以丢出不同的异常,因为它们都是 Exception 的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void b(){
try {
c();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件没找到");
} catch (EOFException e) {
e.printStackTrace();
System.out.println("文件末尾");
} catch (Exception e) {
e.printStackTrace();
}
}


private static void c () throws Exception {
if(fileNotFound()){
throw new FileNotFoundException();
}else if(readFileEnd()){
throw new EOFException();
}else{
throw new IOException();
}
}

如果一个方法抛出多个异常,你应该按照 从小到大 的顺序捕获异常

  • 为什么? 因为 Exception 是其他异常的父类,优先被匹配了,这样就不会走进其他 catch 里了
1
2
3
4
5
6
7
8
9
10
11
12
13
try {
c();
} catch (NullPointerException e) {
e.printStackTrace();
System.out.println("文件没找到");
} catch (RuntimeException e) {
e.printStackTrace();
System.out.println("文件末尾");
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable t){

}

比如一个方法抛出了多个异常,其中有两个处理都是 打印日志,你可以 折叠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void b(){
try {
c();
} catch (NullPointerException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Throwable t){

}
}

// 折叠它 对处理方式一样的异常 来合并 catch 块

private static void b(){
try {
c();
} catch (NullPointerException | IllegalAccessException e) {
e.printStackTrace();
} catch (Throwable t){

}
}

异常的栈轨迹 Stacktrace

  • 排查问题最重要的信息,没有之一
  • 排查问题最重要的信息,没有之一
  • 排查问题最重要的信息,没有之一
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
package com.io.demo;

public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
b();
}

private static void b(){
c();
}

private static void c () {
d();
}

private static void d () {
if(true){
throw new RuntimeException();
}
}
}

栈轨迹如下

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.lang.RuntimeException
at com.io.demo.Main.d(Main.java:23)
at com.io.demo.Main.c(Main.java:18)
at com.io.demo.Main.b(Main.java:14)
at com.io.demo.Main.a(Main.java:10)
at com.io.demo.Main.main(Main.java:6)

沿着栈轨迹 我们知道方法的执行过程,从哪里报错了
这是排查错误最重要的信息

为异常添加额外信息,以便排查

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
package com.io.demo;

import java.sql.SQLException;

public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
processUserData();
}

private static class UserAlreadyExistException extends RuntimeException{
public UserAlreadyExistException(String msg, Throwable cause) {
super(msg,cause);
}
}

private static void processUserData () {
Integer userId = 1;
try {
insertIntoDatabase();
} catch (SQLException e) {
// 额外包一层 用于方便排查信息
throw new UserAlreadyExistException("插入id值为:" + userId + "的数据发生了异常",e);
}
}

private static void insertIntoDatabase () throws SQLException {
throw new SQLException("重复的键值");
}
}

// 栈轨迹如下

Exception in thread "main" com.io.demo.Main$UserAlreadyExistException: 插入id值为:1的数据发生了异常
at com.io.demo.Main.processUserData(Main.java:27)
at com.io.demo.Main.a(Main.java:12)
at com.io.demo.Main.main(Main.java:8)
Caused by: java.sql.SQLException: 重复的键值
at com.io.demo.Main.insertIntoDatabase(Main.java:32)
at com.io.demo.Main.processUserData(Main.java:24)
... 2 more

异常链 Caused by

异常抛出的原则

  • 能⽤if/else处理的,不要使⽤异常

    • https://github.com/hcsp/http-login-and-use-cookie/pull/7
      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
      a(){
      try{
      return xxx.length();
      }catch(NullPointerException e){
      return 0;
      }
      }

      a(){
      if(xxx != null){
      return xxx.length();
      }else{
      return 0;
      }
      }

      public int getNameLength() {
      // Fix the NullPointerException thrown in this method
      // 在本方法中,修复抛出的空指针异常(NullPointerException)
      - return name.length();
      + try {
      + return name.length();
      + }catch(NullPointerException e) {
      如果可以使用“正常”的流程完成,尽量不要使用try/catch。
      原因有二:
      1. 无法保证catch到的异常一定是你想要抓住的。你可以错误的catch到了更深处的异常。比如说,这里你怎么保证catch到的一定是name为null抛出来的,而不是name.length()方法里面抛出来的?(当然这个例子可能有点极端)
      2. 相比正常的if判断,异常的创建是非常,非常昂贵的操作
  • 尽早抛出异常

    • 当你碰到一个异常时?
    • 首先问自己 能处理这个异常吗?
    • 如果不能处理就立刻抛出来
  • 异常要准确、带有详细信息
    • 一个写错的异常,比没有异常更可怕,你可以定义很多异常
    • IOException 是很宽泛的异常,你该抛出更具体的如 FileNotFoundException / EOFException等更准确的异常
    • 尽可能带有更多的信息,如上面我们自己定义的UserAlreadyExistException
  • 抛出异常也⽐悄悄地执⾏错误的逻辑强的多
    • 抓住了异常,就直接处理不要继续往下走流程
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      a(){
      String result = "";
      try {
      result = xxx;
      } catch (IOException e){
      e.printStackTrace();
      // 这样要么抛出异常 要么return
      }
      // 上面抓到了异常,此时就不该继续返回 result
      return result;
      }

      b(){
      try {
      result = xxx;
      } catch (IOException e){
      // e.printStackTrace();
      不打印栈轨迹,你抓住了,但是你把异常吃掉了,很难排查问题
      }
      }

异常的处理原则

  • 本⽅法是否有责任处理这个异常?
    • 不要处理不归⾃⼰管的异常
  • 本⽅法是否有能⼒处理这个异常?
    • 如果⾃⼰⽆法处理,就抛出
  • 如⾮万分必要,不要忽略异常
    1
    2
    3
    4
    try {
    result = xxx;
    } catch (IOException e){}
    这里就是把异常吃了,如果出错,没人发现的了这个异常

了解和使⽤JDK内置的异常

  • NullPointerException
  • ClassNotFoundException/NoClassDefFoundError
  • IllegalStateException
  • IllegalArgumentException
  • IllegalAccessException
  • ClassCastException

ZB-025-01异常入门和控制流

异常

  • 在 return 语句之外,为方法提供另外一种出口
  • IOException 通常代表“预期之内的异常”

有毒的 IOException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
private static void readFile(File file) throws IOException {
InputStream inputStream = new FileInputStream(file);
while(true){
int b = inputStream.read();
if(b == -1){
break;
}
System.out.print((char) b);
}
}

private static void 同事A() throws IOException {
readFile(new File("my-file"));
}

private static void 同事B() throws IOException {
readFile(null);
}
}

由于一个方法readFile丢出了一个异常,就使得所有使用该方法的人全部被传染了,被迫都要加上这个 throws IOException

这个烦人的东西 源头是什么

1
2
3
4
// 这里面声明了 throws 
public FileInputStream(File file) throws FileNotFoundException {
...
}

结论就是:任何声明了 throws XXXException 的方法,再被调用的时候都必须显示的声明 throws XXX XXXException 或者 trg/catch

万能的解决方案

1
2
3
4
5
6
7
private static void 同事B(){
try {
readFile(null);
} catch (IOException e) {
e.printStackTrace();
}
}

Exception 是干嘛的?

对应任何一个函数都有入口和出口

但是 异常 给程序提供了另一种出口

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 FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}

// 这里给程序提供了另一种出口
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}

举个例子

  • 假设文件不存在你去调用 readFile 就会丢出一个RuntimeException异常
  • 它会击穿方法栈里所有的东西,一直向上抛,抛到有人处理它为止
1
2
3
4
5
6
7
8
9
10
11
12
13
private static void readFile(File file) throws IOException {
if(file == null){
throw new RuntimeException();
}
InputStream inputStream = new FileInputStream(file);
while(true){
int b = inputStream.read();
if(b == -1){
break;
}
System.out.print((char) b);
}
}

异常:异常击穿栈帧过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
b();
}

private static void b(){
c();
}

private static void c(){
d();
}

private static void d(){
throw new RuntimeException();
}
}

/*
调用 main => a => b ==> c ==> d
此时 d 抛出一个异常,导致异常击穿当前方法,使得当前方法退出
击穿 c ==> 击穿 b ==> 击穿 a ==> 抛给 main
击穿所有栈帧,直接结束掉
*/

java的异常体系

处理异常

  • a方法处 try/catch
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
public class Main {

public static void main(String[] args) {
a();
}

private static void a(){
try{
b();
}catch (Exception e){
e.printStackTrace();
}
}

private static void b(){
c();
}

private static void c(){
d();
}

private static void d(){
throw new RuntimeException();
}
}


/*
调用 main => a => b ==> c ==> d
此时 d 抛出一个异常,导致异常击穿当前方法,使得当前方法退出
异常丢到 c,c没有处理异常被击穿
==>
异常丢到 b,b没有处理异常被击穿
==>
异常丢到 a,a 里面有 try/catch 尝试捕获异常,
抓到后变成了 e ==> e.printStackTrace();

这就是 java的异常处理
*/

Finally

无论程序正常/异常都执行的操作

  • 释放 数据库连接
  • 关闭 文件
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 Main {
public static void main(String[] args) {
System.out.println(a());
}

private static int a(){
try{
b();
return 0;
}catch (Exception e){
e.printStackTrace();
return 1;
}finally{
System.out.println(222);
}
}

private static void b(){
c();
}

private static void c(){
d();
}

private static void d(){
throw new RuntimeException();
}
}

/*
java.lang.RuntimeException
at com.io.demo.Main.d(Main.java:35)
at com.io.demo.Main.c(Main.java:31)
at com.io.demo.Main.b(Main.java:27)
at com.io.demo.Main.a(Main.java:16)
at com.io.demo.Main.main(Main.java:11)
222
1
*/

// 捕获异常
// 无论a结果如何 执行 finally里的语句
// 返回 1

不要在 finally里写 return

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 class Main {

public static void main(String[] args) {
// a();
System.out.println(a());
}

private static int a(){
try{
b();
return 0;
}catch (Exception e){
e.printStackTrace();
return 1;
}finally{
System.out.println(222);
return 3;
}
}

private static void b(){
c();
}

private static void c(){
d();
}

private static void d(){
throw new RuntimeException();
}
}

执行结果

/*
java.lang.RuntimeException
at com.io.demo.Main.d(Main.java:36)
at com.io.demo.Main.c(Main.java:32)
at com.io.demo.Main.b(Main.java:28)
at com.io.demo.Main.a(Main.java:16)
at com.io.demo.Main.main(Main.java:11)
222
3
*/
  • finally 里的 return 会打破原来的流程,取代 catch里的 return
  • finally 里 应该只执行资源的清理工作
  • finally 不是必要的,其实 catch 也不是必要的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 可以没有 catch
    private static int a(){
    try{
    b();
    return 0;
    }finally{
    System.out.println(222);
    return 3;
    }
    }

    // 不能只有 try ,报错
    private static int a(){
    try{
    b();
    return 0;
    }
    }

如果你catch里什么都不做,别人想杀了你

  • 一般来说,必须在 catch 里做处理
  • 不做处理,可能项目上线一周发现问题都很难排查,因为没有任何错误信息,这个错误信息被你吃了
1
2
3
4
5
6
7
8
9
10
11
private static void a(){
try{
b();
}catch (Exception e){
// 异常被你吃掉了~
// 异常被你吃掉了~
// 异常被你吃掉了~
}finally{
System.out.println(222);
}
}

你应该在 catch里这样

  • 处理异常
  • 在日志里打印异常
  • 返回对应的异常处理

file-leak-detector

它是用于针对那些粗心的开发人员在自己的程序里忘了写 close导致各种各样的闹鬼问题,用于检测这种情况。

try with resources (JDK7+)

语法糖

1
2
3
4
5
6
7
8
9
10
// 以前你要在 finally里 关闭资源
private static void XXX(Connection conn) throws SQLException {
PreparedStatement preparedStatement = null;
try{
preparedStatement = conn.prepareStatement("sql");
preparedStatement.executeQuery();
}finally {
preparedStatement.close();
}
}

try with resources改写后

1
2
3
4
5
6
7
8
9
private static void XXX(Connection conn) throws SQLException {
try (PreparedStatement preparedStatement = conn.prepareStatement("sql")) {
preparedStatement.executeQuery();
}
}

// 你在 try 的括号里 声明了一个语句
1. 你可以不加 finally ,java 会自动给你加 finally
2. 在 try离开的时候 把括号内的资源清理掉

问题来了,什么样的东西可以自动被清理掉呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// PreparedStatement 点进去看看
public interface PreparedStatement extends Statement {
...
}
// Statement 点进去看看
public interface Statement extends Wrapper, AutoCloseable {
...
}

// 应该就是它了
/**
* An object that may hold resources (such as file or socket handles)
* until it is closed. The {@link #close()} method of an {@code AutoCloseable}
* object is called automatically when exiting a {@code
* try}-with-resources block for which the object has been declared in
* the resource specification header. This construction ensures prompt
* release, avoiding resource exhaustion exceptions and errors that
* may otherwise occur.
*/
public interface AutoCloseable {
...
}

// 任何实现 AutoCloseable 的对象在离开 try() 语句块的时候,都将调用 close();

一个注意点

try with resource 的限制,只能在同一方法里使用。如果你的业务要求你 在某处打开文件,另一个地方关闭文件,它就做不到

ZB-024-02-NIO

NIO (Java 7+)

  • New IO 新的IO
  • Non-blocking IO ⾮阻塞IO
  • NIO的Path - 就是旧版本的File
    • 它们之间可以转换
    • Path.toFile()
    • File.toPath()

关于NIO你仅仅需要了解 Files

比较有用的几个api

  • Files.walkFileTree
  • Files.readAllLines 不依赖第三方库的时候使用

NIO到底干什么的

经典的IO模型是基于流的

  • 就是你访问磁盘/网络的时候都是以的形式

它的优点是: 非常的直观,方便抽象 读 read / 写 write

它的缺点是: 慢,因为它是 流 就像一个水流,一个字节一个字节按顺序的读或写,严重限制了它的性能。

NIO模型是基于块的

好处是: 块和块之间没有顺序的,可以同时的写,比基于流的要快

还有一个原因就是 上篇文章里说的 CPU说:这个世界太慢了 所导致的 CPU和硬盘之间的矛盾

由于IO慢逼迫我们这样做

1.缓冲

  • buffer
  • cache
1
2
3
4
5
6
7
8
9
cpu往硬盘写一个文件
你有两个选择

1. 一个一个字节的写
这就导致了
写一个字节 等一个磁盘写入时间
再写一个字节 在等一个磁盘写入时间
循环往复
2. 攒一堆一块写, 比如缓冲区里是 1MB 攒够了一次写入磁盘

2.并发(多线程)

IO太慢了,怎么办?

  • BufferedReader/Writer
  • BufferedReader - ⼀次性读取好多东⻄到缓冲区⾥
  • BufferedWriter - ⼀次性写好多东⻄到缓冲区⾥
  • 在内存中创建好,⼀次写⼊

换⾏符的故事

  • windows 换行是 \r\n
  • linux 换行是 \n

导致文件从不同系统复制过来后显示会有问题

这个缓冲区到底是什么

  • 实际就是char cb[],它是在内存里的

其实……不需要重复发明轮⼦

  • 需要任何IO的功能,尽管搜索,肯定有⼈把轮⼦造好了。
    • FileUtils
    • IOUtils

练习