一、类和对象

二、方法详解

三、成员变量和局部变量

四、隐藏和封装

五、深入构造器

六、类的继承

七、多态

八、继承与组合

九、初始化块

十、本章小结

一、与用户互动

1、运行Java程序的参数

2、使用Scanner获取键盘输入

二、系统相关

1、System类

2、Runtime类与Java9的ProcessHandle

三、常用类

1、Object类

2、操作对象的Objects工具类

3、Java9改进的String、StringBuilder和StringBuffer类

4、Math类

5、ThreadLocalRandom与Random

6、BigDecimal类

四、Java8的日期、时间类

1、Date类

2、Calendar类

3、新的日期、时间包

五、正则表达式

1、创建正则表达式

2、使用正则表达式

六、变量处理和方法处理

1、Java9增强的MethodHandle

2、Java9增强的VarHandle

七、Java11改进的国际化与格式化

1、Java国际化思路

2、Java支持的国家和语言

3、完成程序国际化

4、使用MessageFormat处理器包含占位符的字符串

5、使用类文件代替资源文件

6、Java9新增的日志API

7、使用NumberFormat格式化数字

8、使用DateFormat格式化日期、时间

9、使用SimpleFormat格式化日期

八、Java8新增的日期、时间格式器

1、使用DateTimeFormatter完成格式化

2、使用DateTimeFormatter解析字符串

九、本章小结

一、Vocabulary

1、ballet

n.芭蕾舞

2、martial

adj.战争的,军事的

3、intangible

adj.难以形容的;n.不可捉摸的东西

4、heritage

n.遗产,传统

5、variety

n.品种,多样化

6、originate

v.起源;发端于;创立

7、innovative

adj.引进新思想的,采用新方法的

二、Phrase

1、perch on

n.(鸟的)栖木,栖息处;v.栖息于,位于

2、take advantage of

利用(时机、他人的弱点等);占···便宜;勾引

三、SpecificNoun

1、martial arts movie

武侠片

2、intangible cultural heritage

非物质文化遗产

3、Chinese Bamboo Drifting

中国竹子漂流

Java集合大致可分为 Set、List、Queue和Map四种体系,Java集合就像一种容器,可以将多个对象扔进去。

一、Java集合概述

为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类。所有的集合类位于 java.util包下,后来为了处理多线程环境下并发安全的问题,Java5还在 java.util.concurrent包下提供了一些多线程支持的集合类。

集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用);而集合类只能保存对象(实际上保存的是对象的引用变量,但通常看作对象),

Java集合类主要由两个接口派生出来:

  • Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口包含了一些子接口或实现类。如下图就是Collection接口、子接口及其实现类的继承树:
  • 下图是Map体系的继承树,所有的Map实现类用于保存具有映射关系的数据

从下图可以看出,如果访问List集合中的元素,可以直接根据元素的索引来访问;如果访问Map集合中的元素,可以根据每项元素的key值来访问其value;如果访问Set中的元素,则只能根据元素本身来访问

二、Java11增强的 Collection Iterator接口

Collection接口是List、Set和Queue的父接口,该接口定义的方法可以自行去查Java API文档,无非就是添加对象、清空容器、判断容器是否为空等等

1
2
3
4
5
6
7
public class CollectionTest {
public static void main(String[] args) {
var strColl = List.of("Java", "Kotlin", "Swift", "Python");
String[] sa = strColl.toArray(String[]::new);
System.out.println(Arrays.toString(sa));
}
}

1、使用Lambda表达式遍历集合

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CollectionEach {
public static void main(String[] args) {
//创建一个集合
HashSet<Object> books = new HashSet<>();
books.add("轻量级Java EE企业级应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
//遍历forEach()方法遍历集合
books.forEach(obj -> {
System.out.println("迭代集合元素:" + obj);
});
}
}

2、使用Iterator遍历集合元素

Iterator接口也是Java集合框架的成员,但是他与Collection系列、Map系列的集合不一样,:Collection系列集合、Map系列集合主要用于乘装其他对象,而Iterator则主要用于遍历(即迭代访问)Collection中的元素,Iterator对象也称为迭代器。

下面直接上代码看看Iterator遍历元素的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class IteratorTest {
public static void main(String[] args) {
//创建一个集合
HashSet<Object> books = new HashSet<>();
books.add("轻量级Java EE企业级应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
//获取books对象的迭代器
Iterator<Object> it = books.iterator();
while(it.hasNext()) {
//强制转换
String book = (String) it.next();
System.out.println(book);
if (book.equals("疯狂Java讲义")) {
//从集合中删除上一次next()方法返回的元素
it.remove();
}
//为测试变量赋值,不会改变几个元素本身
book = "测试字符串";
}
System.out.println(books);
}
}
  • Iterator必须依附于Collection对象,它本身并不提供乘装对象的能力
  • 使用Iterator对集合元素遍历时,Iterator并非将集合元素本身交给了迭代变量,而是将集合元素的值传给了迭代变量
  • 使用Iterator迭代过程中,一旦发现集合被修改,会抛出异常,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IteratorErrorTest {
public static void main(String[] args) {
//创建一个集合
HashSet<Object> books = new HashSet<>();
books.add("轻量级Java EE企业级应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
//获取books对象的迭代器
Iterator<Object> it = books.iterator();
while(it.hasNext()) {
//强制转换
String book = (String) it.next();
System.out.println(book);
books.remove(book);
}
System.out.println(books);
}
}

Iterator迭代器采用的是快速失败的(fail-fast机制),一旦检测到迭代过程该集合已经被修改,就会抛出 ConcurrentModificationException异常。

3、使用Lambda表达式遍历Iterator

Java8为Itreater新增了一个 forEachRemaining(Consumer action);方法,该方法所需的Consumer参数同样也是函数式接口。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IteratorEach {
public static void main(String[] args) {
//创建一个集合
HashSet<Object> books = new HashSet<>();
books.add("轻量级Java EE企业级应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
//获取books对应的迭代器
Iterator<Object> iterator = books.iterator();
//使用Lambda表达式来遍历集合元素
iterator.forEachRemaining(obj -> {
System.out.println("迭代元素集合:" + obj);
});
}
}

4、使用foreach循环遍集合元

直接上代码:

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
```



#### 5、使用Predicate操作集合



#### 6、使用Stream操作集合





### 三、Set集合

#### 1、HashSet类



#### 2、LinkedHashSet类

HashSet有一个子类,LinkedHashSet,LinkHsahSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护了元素的次序。

当遍历LinkedHashSet集合里的元素时,会按照元素插入的顺序来访问。

```java

虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,他不允许元素重复。

3、TreeSet类

4、EnumSet类

5、各Set实现类的性能分析

四、List集合

1、改进的List接口和ListIterator

2、ArrayList和Vector实现类

3、固定长度的List

五、Queue集合

1、PriorityQueue实现类

2、Deque接口与ArrayDeque实现类

3、LinkedList实现类

4、各种线性表的性能分析

六、增强的Map集合

1、Java8为Map新增的方法

2、改进的HashMap和Hashtable实现类

3、LinkedHashMap实现类

4、使用Properties读写属性文件

5、SortedMap接口和TreeMap实现类

6、WeakHashMap实现类

七、HashSet和HashMap的性能选择

八、操作集合的工具类:Collections

1、排序操作

2、查找、替换操作

3、同步控制

4、设置不可变集合

5、Java9新增的不可变集合

九、繁琐的接口:Enumeration

十、本章小结

一、变量

1、系统变量

系统变量:由系统提供,并不是用户定义的,属于服务层面,由于作用域的不同分为以下两种:

  • 全局变量
  • 会话变量

a、查看所有的系统变量

1
2
SHOW GLOBAL | [SESSION] VARIABLES
-- SESSION(可省略) 或者 GLOBAL

执行结果如下:

b、查看满足条件的系统变量

1
SHOW GLOBAL | [SESSION] VARIABLES LIKE '%char%'

c、查看指定系统变量的值

1
SELECT @@GLOBAL | [SESSION].系统变量名

d、为某个系统变量赋值

  • 方式一:

    1
    SET GLOBAL | [SESSION] 系统变量名 = 值
  • 方式二:

    1
    SET @@GLOBAL | [SESSION].系统变量名 = 值

注意事项:

  • 如果是全局级别,则需要加GLOBAL,如果是会话级别,则需要加SESSION,不写默认SESSION
  • 全局变量对所有会话均有效,会话变量仅仅对当前会话有效

2、自定义变量

由用户自定义,非系统提供的变量

a、用户变量

使用步骤: 声明 赋值 使用(查看、比较、运算等);作用域:仅仅对当前会话(连接)有效,同会话变量

1、声明并初始化

赋值的操作符: =或者 :=,注:SELECT只能用 :=

  • 方式一:

    1
    SET @用户变量名=值
  • 方式二:

    1
    SET @用户变量名:=值
  • 方式三:

    1
    SELECT @用户变量名:=值
2、赋值

可以由上面的三种方式赋值,也可以将查询结果赋值给变量

1
2
-- SELECT 字段 INTO @变量名 FROM 表名
SELECT COUNT(*) INTO @amount FROM activity_check
3、使用(查看用户变量)
1
SELECT @用户变量名

b、局部变量

作用域:仅仅定义在它的 begin end块中有效

1、声明
1
2
DECLARE 变量名 类型;
DECLARE 变量名 类型 DEFAULT 值;
2、赋值

与用户变量一致,只不过使用 SET或者 SELECT INTO时,无需用@符号

3、使用
1
SELECT 局部变量名

用户变量与局部变量区别:

变量类型 作用域 定义与使用位置 语法
用户变量 当前会话 会话中的任何位置 必须加@符号,不用限定类型
局部变量 BEGIN END块中 只能在BEGIN END中,且为第一句话 一般不加@符号,需要限定类型

二、存储过程

三、函数

MySQL5添加了对视图的支持,那么它到底是个森么东西呢?我们一起来看看吧!

一、视图的介绍

1、什么是视图?

所谓视图(View),其实是一种虚拟存在的表,同真实表一样,视图也由列和行构成,但视图并不实际存在于数据库中。行和列的数据来自于定义视图的查询中所使用的表,并且还是在使用视图时动态生成的。意思就是:他存的是你写的SQL查询语句。

2、使用视图有什么好处?

  • 重用SQL语句
  • 简化复杂的SQL查询
  • 使用表的组成部分而不是整个表
  • 保护数据,可以给用户授予表的特定访问权限而不是整个表的访问权限。
  • 更改数据的格式和表示

而且在视图创建之后,可以用与表基本相同的方式利用它们(查询、过滤、排序···),并且可以将视图联结到其他视图或表,甚至能添加或更新数据。

性能问题:因为视图不包含任何数据,所以每次使用视图,都必须处理查询执行时所需的任一个检索。如果使用多个联结和过滤创建了复杂的视图或者嵌套了视图,可能会发现性能下降的很厉害。

二、视图的使用

  • 创建: CREATE VIEW AS
  • 查看: SHOW CREATE VIEW viewname
  • 删除: DROP VIEW viewname
  • 更新:先删除再创建,或者 CREATE OR REPLACE VIEW

三、视图的使用案例

先建表:

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
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for category
-- ----------------------------
DROP TABLE IF EXISTS `category`;
CREATE TABLE `category` (
`id` int(5) NOT NULL AUTO_INCREMENT COMMENT '商品类别主键',
`category_name` varchar(255) NOT NULL COMMENT '商品类别名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of category
-- ----------------------------
BEGIN;
INSERT INTO `category` VALUES (1, '电脑组件');
INSERT INTO `category` VALUES (2, '图书文娱');
INSERT INTO `category` VALUES (3, '手机');
COMMIT;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` int(5) NOT NULL AUTO_INCREMENT COMMENT '商品表主键,自增',
`product_name` varchar(255) NOT NULL COMMENT '商品名称',
`product_category` int(5) NOT NULL COMMENT '商品类别',
`product_price` decimal(10,2) NOT NULL COMMENT '商品价格',
PRIMARY KEY (`id`),
KEY `product_category` (`product_category`),
CONSTRAINT `product_category` FOREIGN KEY (`product_category`) REFERENCES `category` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of product
-- ----------------------------
BEGIN;
INSERT INTO `product` VALUES (1, 'AMD 锐龙7 5800x', 1, 3199.00);
INSERT INTO `product` VALUES (2, '华硕 TUF-RTX3090-GAMING', 1, 25999.00);
INSERT INTO `product` VALUES (3, 'Java编程思想', 2, 74.50);
INSERT INTO `product` VALUES (4, 'SpringBoot编程思想', 2, 57.80);
INSERT INTO `product` VALUES (5, 'IPhone 12 Pro', 3, 9299.00);
INSERT INTO `product` VALUES (6, '华为Mate 40 pro', 3, 6299.00);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

1、创建视图

1
2
3
4
CREATE VIEW pro_cate AS
SELECT product_name, category.category_name, product.product_price
FROM product
INNER JOIN category ON product.product_category = category.id

2、查看视图

1
SHOW CREATE VIEW  pro_cate

查询结果如下所示:

  • View: pro_cate

  • Create View: CREATE ALGORITHM=UNDEFINED DEFINER=root@%SQL SECURITY DEFINER VIEWpro_cateAS selectproduct.product_nameASproduct_name,category.category_nameAScategory_name,product.product_priceASproduct_price from (productjoincategory on((product.product_category=category.id)))

  • collation_connection: utf8mb4_general_ci

  • character_set_client: utf8mb4

3、更新视图

1
2
3
4
CREATE OR REPLACE VIEW pro_cate AS
SELECT product.id, product.product_name, category.category_name, product.product_price
FROM product
INNER JOIN category ON product.product_category = category.id

4、删除视图

1
DROP VIEW pro_cate

四、在视图里增删改数据

不建议!

一、Vocabulary

1、regulate

v.(用规则条例)约束,控制;调节

2、femur

n.股骨

3、adrenaline

n.肾上腺素

4、jelly

二、Phrase

1、to contract at full strength

全力收缩

2、capable of

有能力的;有本领的

三、SpecificNoun

1、tight-fitting

修身的

2、one-piece dress

连衣裙

Java语言提供了非常优秀的多线程支持,程序可以通过非常简单的方式来启动多线程,接下来就让我们一起来学习Java多线程编程的相关知识吧,包括:创建、启动、控制线程,以及多线程的同步操作,还有通过Java内建支持的线程池来提高多线程的性能。

一、线程概述

每个运行的程序就是一个进程,而当一个程序运行时,内部可能有多个顺序执行流,每个顺序执行流就是一个线程。

1、线程和进程

a、进程

程序进入内存运行时,就变成了一个进程(process),进程是处于运行过程中的程序,并且具有一定的独立性,进程是系统进行资源调度和分配的一个独立单位。一般而言,进程具有如下三个特征:

  • 独立性:
  • 动态性:
  • 并发性:

并发行和并行性是两个不同的概念:

  • 并行:同一时刻,多个指令在多个处理器上同时处理
  • 并发:同一时刻只能有一条指令执行,但是多个进程指令被快速轮换执行,适得其在宏观上具有多个进程同时执行的效果

2、多线程的优势

二、线程的创建和启动

1、继承Thread类创建线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FirstThread extends Thread {
private int i;

//重写run方法
@Override
public void run() {
super.run();
for (i = 0; i < 100; i++) {
//当线程类启动Thread时,直接使用this即可获取当前的线程
//Thread的getName()方法返回当前线程的名字
System.out.println(getName() + " " + i);
}
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}

2、实现Runnable接口创建线程类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SecondThread implements Runnable {
private int i;

@Override
public void run() {
for (; i < 100; i++) {
//当线程类实现Runnable接口时
//如果想获取当前线程,只能用Thread.currentThread()方法得到当前进程
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
SecondThread st = new SecondThread();
//通过new方法创建进程
new Thread(st, "进程1").start();
new Thread(st, "进程2").start();
}
}
}
}

3、使用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
public class ThirdThread {
public static void main(String[] args) {
//先使用Lambda表达式创建Callable<Integer>对象
//使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<>( (Callable<Integer>)() -> {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
//call()可以有返回值
return i;
});
for (int i = 0; i <100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
//实质还是以Callable对象来创建并启动线程的
new Thread(task, "有返回值的线程").start();
}
}
//需等线程结束后才能获得值
try {
//获取线程的返回值
System.out.println("子线程的返回值" + task.get());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

4、创建线程的三种方式对比

三、线程的生命周期

1、新建和就绪状态

2、运行和阻塞状态

3、线程死亡

四、控制线程

1、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
public class JoinThread extends Thread {
//提供一个带参数的构造器,用于设置该线程的名称
public JoinThread(String name) {
super(name);
}
//重写run()方法,定义线程执行体
@SneakyThrows
@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " " + i);
if (i == 30)
Thread.sleep((long) 0.1);
}
}
public static void main(String[] args) throws InterruptedException {
//启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100; i++) {
if (i == 20) {
Thread jt = new JoinThread("被join的线程");
jt.start();
//main()调用了jt线程的join方法,main(线程),必须等到jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}

2、后台线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DaemonThread extends Thread {
//定义后台线程的线程执行体与普通线程没有多大区别
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
//启动子线程
DaemonThread t = new DaemonThread();
//将此线程设置成后台线程
t.setDaemon(true);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
//线程执行到此处,前台线程(main)结束,后台线程随之结束
System.out.println("结束了");
}
}

3、线程睡眠:sleep

1
2
3
4
5
6
7
8
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}

4、改变线程优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class PriorityTest extends Thread {
//定义一个有参构造器,用于创建线程时指定名称
public PriorityTest(String name) {
super(name);
}
//线程执行体
@Override
public void run() {
super.run();
for (int i = 0; i< 500; i++) {
System.out.println(getName() + "的优先级为:" + getPriority() + ",循环变量值为" + i);
}
}
public static void main(String[] args) {
//设置主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0; i < 30; i++) {
if (i == 10) {
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的线程优先级:" + low.getPriority());
//设置该线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if (i == 20) {
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的线程优先级:" + high.getPriority());
//设置该线程为最高优先级
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}

五、线程同步

1、线程安全问题

a、定义账户类:
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 Account {
//封装账户编号、账户余额的两个成员变量
private String accountNo;
private Double balance;
//构造器
public Account(String accountNo, Double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//setter and getter
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
//根据accountNo重写 equals() 和 hashCode() 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
} else {
return false;
}
}
//hashCode()方法用于返回字符串的哈希码
@Override
public int hashCode() {
return accountNo.hashCode();
}
}
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
30
31
public class DrawThread extends Thread {
//模拟用户账户
private Account account;
//当前取钱线程所希望取得钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多个线程修改同一共享数据时,将涉及到数据安全问题
@Override
public void run() {
super.run();
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount) {
//吐出钞票
System.out.println(getName() + " " + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为:" + account.getBalance());
} else {
System.out.println("取钱失败!余额不足!");
}
}
}
c、取钱逻辑:
1
2
3
4
5
6
7
8
9
public class DrawTest {
public static void main(String[] args) {
//创建一个账户
Account acct = new Account("123", 1000.0);
//模拟两个线程向同一账户取钱
new DrawThread("甲", acct, 800).start();
new DrawThread("乙", acct, 800).start();
}
}

2、同步代码块

将上面的 DrawThread修改成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class DrawThread extends Thread {
//模拟用户账户
private Account account;
//当前取钱线程所希望取得钱数
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多个线程修改同一共享数据时,将涉及到数据安全问题
@Override
public void run() {
super.run();
//使用account作为同步监视器,任何线程在进入下面的代码块之前,必须先获得对account对象的锁定,其他对象无法获得锁,也就无法修改它
//"加锁-修改-释放锁"的步骤
synchronized (account) {
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount) {
//吐出钞票
System.out.println(getName() + " " + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("余额为:" + account.getBalance());
} else {
System.out.println("取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}

3、同步方法

4、释放同步监视器的锁定

5、同步锁(lock)

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
46
47
48
49
50
51
52
53
54
55
56
class A {
public synchronized void foo(B b) {
System.out.println("当前线程名称:" + Thread.currentThread().getName() + " 进入了A实例的foo()方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名称:" + Thread.currentThread().getName() + " 企图调用B的last()方法");
b.last();
}
public synchronized void last() {
System.out.println("进入了A类l的ast()方法内部");
}
}

class B {
public synchronized void bar(A a) {
System.out.println("当前线程名称:" + Thread.currentThread().getName() + " 进入了B实例的bar()方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名称:" + Thread.currentThread().getName() + " 企图调用A的last()方法");
a.last();
}
public synchronized void last() {
System.out.println("进入了B类的last()方法内部");
}
}

public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
//调用A的foo()方法
a.foo(b);
System.out.println("进入了主线程之后");
}
@Override
public void run() {
Thread.currentThread().setName("副线程");
//调用b对象的bar()方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
//以d1为target启动新线程
new Thread(d1).start();
//调用init()方法
d1.init();
}
}

六、线程通信

1、传统的线程通信

a、定义账户类:
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
@Data
public class Account {
//封装账户编号、账户余额
private String accountNo;
private double balance;
//标识账户中是否有存款
private boolean flag = false;
//构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//因为账户余额不允许修改,因此只提供getter方法
public double getBalance() {
return this.balance;
}
public synchronized void draw(double drawAmount) {
try {
//如果flag为假,表明账户中没有人存钱进去,取钱方法阻塞
if (!flag) {
wait();
} else {
//执行取钱操作
System.out.println(Thread.currentThread().getName() + "取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
//将标识账户是否已有存款的旗标设为false
flag = false;
//唤醒其他线程
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void deposit(double depositAmount) {
try {
//如果flag为真,表明账户中已经有人存钱进去,存钱方法阻塞
if (flag) {
wait();
} else {
//执行存钱操作
System.out.println(Thread.currentThread().getName() + "存钱:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
//将标识账户是否已有存款的旗标设为true
flag = true;
//唤醒其他线程
notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
b、定义取钱线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DrawThread extends Thread {
//模拟用户账户
private Account account;
//当前取钱线程所希望的线程
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//重复100次存钱取钱操作
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.draw(drawAmount);
}
}
}
c、定义存钱类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DepositThread extends Thread {
//模拟用户账户
private Account account;
//当前取钱线程所希望的线程
private double drawAmount;
public DepositThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//重复100次存钱取钱操作
@Override
public void run() {
for (int i = 0; i < 100; i++) {
account.deposit(drawAmount);
}
}
}

2、使用Condition控制线程通信

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
public class Account {
//显示定义Lock对象
private final Lock lock = new ReentrantLock();
//获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
//封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
//标识账户中是否已有存款的旗标
private boolean flag = false;
//构造器
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//getter 和 setter方法
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
//取钱操作
public void draw(double drawAmount) {
//加锁
lock.lock();
try {
//如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if(!flag) {
cond.await();
} else {
//执行取钱操作
System.out.println(Thread.currentThread().getName() + "取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
//将标识账户是否有存款的标志设为false
flag = false;
//唤醒其他线程
cond.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//存钱操作
public void deposit(double depositAmount) {
//加锁
lock.lock();
try {
//如果flag为真,表明账户中已经有人存钱进去,存钱方法阻塞
if(flag) {
cond.await();
} else {
//执行存钱操作
System.out.println(Thread.currentThread().getName() + "存钱:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
//将标识账户是否有存款的标志设为true
flag = true;
//唤醒其他线程
cond.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//hashCode()和equals()方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Double.compare(account.balance, balance) == 0 && flag == account.flag && Objects.equals(lock, account.lock) && Objects.equals(cond, account.cond) && Objects.equals(accountNo, account.accountNo);
}
@Override
public int hashCode() {
return Objects.hash(lock, cond, accountNo, balance, flag);
}
}

3、使用阻塞队列(Blocking Queue)控制线程通信

阻塞队列简单使用:

1
2
3
4
5
6
7
8
9
10
11
public class BlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
//定义一个长度为2的阻塞队列
BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
//也可以用bq.add("Java");和bq.offer("Java");
bq.put("Java");
bq.put("Java");
//此时会形成阻塞
bq.put("Java");
}
}

使用 BlockingQueue(阻塞队列)实现线程通信

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
//生产者,往队列里放元素
class Producer extends Thread {
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq) {
this.bq = bq;
}
@Override
public void run() {
var strArr = new String[] {"Java","Structs","Spring"};
for (var i = 0; i < 9999999999L; i++) {
System.out.println(getName() + "生产者准备生产集合元素");
try {
Thread.sleep(200);
//尝试放入元素,如果队列已满,则线程被阻塞
bq.put(strArr[i % 3]);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "生产完成:" + bq);
}
}
}

//消费者,往队列里取元素
class Consumer extends Thread {
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq) {
this.bq = bq;
}
@Override
public void run() {
while (true) {
System.out.println(getName() + "消费者准备消费集合元素!");
try {
Thread.sleep(200);
//尝试取出元素,如果队列已空,则线程被阻塞
bq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "消费完成:" + bq);
}
}
}

//主类
public class BlockingQueueTest2 {
public static void main(String[] args) {
//创建一个容量为1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
//启动三个生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
//启动一个消费者线程
new Consumer(bq).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
//线程类
class MyThread extends Thread {
//提供指定线程名的构造器
public MyThread(String name) {
super(name);
}
//提供指定线程名、线程组的构造器
public MyThread(ThreadGroup threadGroup, String name) {
super(threadGroup, name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName() + " 线程i的变量" + i);
}
}
}
//线程测试主类
public class ThreadGroupTest {
public static void main(String[] args) {
//获取主线程所在的线程组,这是所有线程默认的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("主线程组的名称:" + mainGroup.getName());
System.out.println("主线程组是否是后台线程组:" + mainGroup.isDaemon());
new MyThread("主线程组的线程").start();
//创建了一个新的线程组,并将该线程组设置为后台线程组
var tg = new ThreadGroup("新线程组");
tg.setDaemon(true);
System.out.println("tg线程组是否是后台线程组:" + tg.isDaemon());
var tt = new MyThread(tg, "tg组的线程甲");
tt.start();
new MyThread(tg, "tg组的线程乙").start();
}
}

下面程序为主线程设置了异常处理器,当主线程运行抛出未处理异常时,该异常处理器将会起作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义自己的异常处理器
class MyExHandler implements Thread.UncaughtExceptionHandler {
//实现uncaughtException()方法,该方法将处理线程的未处理异常
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t + "线程出现了异常:" + e);
}
}
public class ExHandler {
public static void main(String[] args) {
//设置主线程的异常处理器
Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler());
var a = 5 / 0;
System.out.println("程序正常结束!");
}
}

八、线程池

1、使用线程池管理线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadPoolTest {
public static void main(String[] args) {
//创建一个具有固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//使用Lambda表达式创建Runnable对象
Runnable target = () -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "值为" + i);
}
};
//向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
pool.submit(target);
//关闭线程池
pool.shutdown();
}
}

2、使用ForkJoinPool利用多CPU

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
//继承RecursiveAction来实现"可分解"的任务
class PrintTask extends RecursiveAction {
//每个小任务最多只打印50个数
private static final int THRESHOLD = 50;
//开始和结束
private int start,end;
//打印从start到end的任务
public PrintTask(int start, int end) {
this.end = end;
this.start = start;
}
//总的大任务
@Override
protected void compute() {
//当end和start之间的差距小于THRESHOLD时开始打印
if (end - start < THRESHOLD) {
for (int i = start; i < end; i++) {
System.out.println(Thread.currentThread().getName() + "值为" + i);
}
} else {
//当end和start之间的差距大于THRESHOLD时,将大任务分解成小任务
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//执行两个小任务
left.fork();
right.fork();
}
}
}

public class ForkJoinPoolTest {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
//提交可分解的PrintTask任务
pool.submit(new PrintTask(0, 300));
pool.awaitTermination(2, TimeUnit.SECONDS);
//关闭线程池
pool.shutdown();
}
}

下面程序示范使用了 RecursiveTask对一个长度为100的数组元素值进行累加

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
//继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer> {
//每个小任务最多只打印20个数
private static final int THRESHOLD = 20;
//开始和结束
private int start,end;
//数组
int arr[];
//打印从start到end的任务
public CalTask(int[] arr, int start, int end) {
this.arr = arr;
this.end = end;
this.start = start;
}
//总的大任务
@Override
protected Integer compute() {
int sum = 0;
//当end和start之间的差距小于THRESHOLD时开始累加
if (end - start < THRESHOLD) {
for (int i = start; i < end; i++) {
sum += arr[i];
}
return sum;
} else {
//当end和start之间的差距大于THRESHOLD时,将大任务分解成小任务
int middle = (start + end) / 2;
CalTask left = new CalTask(arr, start, middle);
CalTask right = new CalTask(arr, middle, end);
//执行两个小任务
left.fork();
right.fork();
//把两个小任务的累加结果合并起来
return left.join() + right.join();
}
}
}

public class Sum {
public static void main(String[] args) throws InterruptedException, ExecutionException {
int[] arr = new int[100];
Random rand = new Random();
int total = 0;
//初始化100个数字元素
for (int i = 0, len = arr.length; i < len; i++) {
int tmp = rand.nextInt(20);
//对数组元素赋值
total += (arr[i] = tmp);
}
System.out.println(total);
//创建一个通用池
ForkJoinPool pool = ForkJoinPool.commonPool();
//提交可分解的CalTask任务
Future<Integer> future = pool.submit(new CalTask(arr, 0, arr.length));
System.out.println(future.get());
//关闭线程池
pool.shutdown();
}
}

九、线程相关类

1、ThreadLocal类

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
class Account {
//定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
//每个线程将会保留该变量的一个副本
private ThreadLocal<String> name = new ThreadLocal<>();
//定义一个初始化name成员变量的构造器
public Account(String str) {
this.name.set(str);
//下面代码用于访问当前线程的name副本的值
System.out.println("---" + this.name.get());
}
//name的setter和getter方法
public String getName() {
return this.name.get();
}
public void setName(String str) {
this.name.set(str);
}
}

class MyTest extends Thread {
//定义一个Account类型的成员变量
private Account account;
public MyTest(Account account, String name) {
super(name);
this.account = account;
}
public void run() {
//循环10次
for(var i = 0; i < 10; i++) {
//当i==6时输出账户名代替换成当前线程名
if(i == 6) {
account.setName(getName());
}
//输出同一个账户的账户名称和循环变量
System.out.println(account.getName() + "账户的i值" + i);
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
//启动两个线程,两个线程共享一个Account
var at = new Account("初始名");
/**
* 虽然两个线程共享一个账户,但是只有一个账户名
* 但由于账户名是ThreadLocal类型的,所以每个线程
* 都完全拥有各自的账本账户名副本,因此在i==6之后将看到
* 两个线程访问同一个账户时出现两个不同的账户名
*/
new MyTest(at, "线程甲").start();
new MyTest(at, "线程乙").start();
}
}

2、包装线程不安全的集合

1
2
//使用 Collections 的 synchronizedMap 方法将一个HashMap包装成线程安全的类
HashMap m = (HashMap) Collections.synchronizedMap(new HashMap());

3、线程安全的集合类

4、Java9新增的发布-订阅框架

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
//创建我自己的订阅者
class MySubscriber<T> implements Flow.Subscriber {
//发布者与订阅者之间的纽带
private Flow.Subscription subscription;
//订阅时会触发该问题
@Override
public void onSubscribe(Flow.Subscription subscription) {
this.subscription = subscription;
//开始请求数据
subscription.request(1);
}
//接收到数据是会触发该方法
@Override
public void onNext(Object item) {
System.out.println("获取到数据" + item);
//请求下一条数据
subscription.request(1);
}
//订阅出错时
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
synchronized ("java") {
"java".notifyAll();
}
}
//订阅结束时触发该方法
@Override
public void onComplete() {
System.out.println("订阅结束时");
synchronized ("java") {
"java".notifyAll();
}
}
}
public class PubSubTest {
public static void main(String[] args) {
//创建一个SubmissionPublisher作为发布者
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
//创建订阅者
MySubscriber<String> subscriber = new MySubscriber<>();
//注册订阅者
publisher.subscribe(subscriber);
//发布几个数据项
System.out.println("开发发布数据...");
List.of("Java", "Go", "Erlang", "Swift", "Lua")
.forEach(im -> {
//提交数据
publisher.submit(im);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//发布结束
publisher.close();
//发布结束之后,为了让线程不会死亡,暂停线程
synchronized ("java") {
try {
"java".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

十、本章小结

一、使用JMS发送消息

JMS是一个Java标准,定义了使用消息代理(message broker)的通用API。Spring通过基于模板的抽象为JMS的功能提供了支持,这个模板就是JmsTemplate。借助JmsTemplate,我们可以非常容易地在消息生产方发送队列和主题消息,在消费者那一方,也能够非常容易的接收消息。Spring还提供了消息驱动POJO的理念:这是一个简单的Java对象,能够以异步的方式响应队列或主题上到达的消息。

1、搭建JMS环境

首先在项目中添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>

接下来在yml配置文件里添加:

1
2
3
4
5
6
7
8
9
10
spring:
artemis:
#代理的主机
host: 47.107.73.216
#代理的端口
port: 61617
#用来访问代理的用户(可选)
user: tacoweb
#用来访问代理的密码(可选)
password: 123456

这会让Spring创建到 Artemis的代理连接,让 Attemis代理监听 47.107.73.216的端口61617,他还为应用代理设置了代理交互的凭证(可选)。

2、使用JmsTemplate发送消息

JMS start依赖添加到构建文件之后,Spring Boot会自动配置一个JmsTemplate,我们可以将它注入到其他bean中,使用它来发送和接收消息。