一些JavaSE学习过程中的思路整理(四)(主观性强,持续更新中...)

未经作者允许,不可转载,如有错误,欢迎指正o( ̄▽ ̄)o

多线程编程:资源类&任务&运行机制的解耦合

一下是《Java核心技术卷一》中的一个样例,用多线程并发模拟银行账户的转账过程,该样例还未使用上锁机制,是一个有问题的样例,但是鉴于只是为了用于讲解解耦合,所以无伤大雅,我写了点注释,算是到目前为止我所理解的解耦合

资源类

public class Bank {
//final关键字必须初始化
private final double[] accounts; //这里将原本的构造函数屏蔽了
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
} //这里是资源类,定义了一次转账交易
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) return;
System.out.println(Thread.currentThread().getName());
accounts[from] -= amount;
System.out.printf("%.2f from %d to %d\n", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %.2f\n", getTotalBalance());
} public double getTotalBalance() {
double sum = 0; for (double a : accounts)
sum += a; return sum;
} public int size() {
return accounts.length;
}
}

测试类

public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10; public static void main(String[] args) {
var bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
//假设操作系统采用抢占式,则即使是while循环,在时间片耗尽后依旧需要释放资源,保存当前进程执行的进度,进入可运行态
//这里将while循环写在Runnable函数式接口内,这时任务部分(多次执行转账)
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
//运行机制 (该demo中资源类只写该类具有的功能,接口负责写需要用资源类去执行的任务,最后线程实例负责运行线程)
new Thread(r, String.valueOf(i)).start();
}
}
}

关于重入锁(ReentrantLock)的细节

  • 之所以称之为重入锁,是因为一个线程可以反复获得已经拥有的锁,锁有一个持有计数来跟踪对lock方法的嵌套调用,线程每次调用lock方法后都要unlock释放锁,被一个锁保护的代码可以调用另一个使用相同锁的方法

  • 假设有一个实例对象甲,它有一个方法①(甲.①)被重入锁锁定,而①中调用了方法②,那么调用②的时候也会封锁甲对象,此时甲对象重入锁的持有计数为2,当方法②退出时,持有计数变为1,当方法①退出时,持有计数变为0,线程释放锁

  • 重入锁默认时非公平锁,公平锁倾向于等待时间最长的线程,但是公平锁要比常规锁慢很多,人为强制干预线程的调度方式只适用于特定的环境

条件对象配合重入锁的使用

  • newCondition(),返回一个与这个锁相关联的条件对象(一个锁可以有多个条件对象)

  • await(),将该线程放在这个条件对象所管理的等待集合

  • signalAll(),解除该条件等待集合中的所有线程的阻塞状态

  • signal(),随机解除该条件的等待集合中的某一个线程的阻塞状态

下面依旧用银行转账的这个例子稍作改写,体验一下线程安全下的随机两个账户的转账过程

资源类

public class Bank {
//final关键字必须初始化
private final double[] accounts;
private ReentrantLock reentrantLock;
private Condition sufficientFunds; //这里我将原本的构造函数屏蔽了
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
reentrantLock = new ReentrantLock();
//需要从一个特定的锁对象获得条件对象
sufficientFunds = reentrantLock.newCondition();
} //这里是资源类,定义了一次转账交易
public void transfer(int from, int to, double amount) {
reentrantLock.lock();
try {
//如果资金不足则需要进入等待集,等待singal(singalAll)命令返回可运行态,每次重新获得锁,进入后从之前暂停的
//地方继续执行,所以有可能依旧会不满足条件而进入等待集,并释放锁
while (accounts[from] < amount)//通过控制while内的条件,就可以控制条件对象调用await的条件(使用if我认为这个不是原子操作,所以很可能会出现问题)
sufficientFunds.await();//进入等待集,并释放锁
System.out.println(Thread.currentThread().getName());
accounts[from] -= amount;
System.out.printf("%.2f from %d to %d\n", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %.2f\n", getTotalBalance());
//一旦完成一笔转账,那些等待集中的线程就有可能可以继续执行,所以释放所有等待集中的线程
sufficientFunds.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
} public double getTotalBalance() {
double sum = 0; for (double a : accounts)
sum += a; return sum;
} public int size() {
return accounts.length;
}
}

测试类

public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10; public static void main(String[] args) {
var bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
//假设操作系统采用抢占式,则即使是while循环,在时间片耗尽后依旧需要释放资源,保存当前进程执行的进度,进入可运行态
//这里将while循环写在Runnable函数式接口内,这时任务部分(多次执行转账)
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
//运行机制 (该demo中资源类只写该类具有的功能,接口负责写需要用资源类去执行的任务,最后线程实例负责运行线程)
new Thread(r, String.valueOf(i)).start();
}
}
}

synchronized关键字修饰非静态方法与静态方法

  • 修饰实例方法:宏观上可以理解为将锁定该实例对象,当一个线程正在调用synchronized修饰的实例方法时,没有其他线程可以调用该对象的该方法或者其他同步的实例方法(由synchronized修饰)
  • 修饰静态方法:宏观上可以理解为将锁定该类对象,没有其他线程可以调用这个类的该静态方法或者其他同步的静态方法(由synchronized修饰)

以下是通过synchronized关键字和条件实现上述重入锁与条件对象相同的功能的银行转账例子

资源类

public class Bank {
//final关键字必须初始化
private final double[] accounts; //这里我将原本的构造函数屏蔽了
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
} //java中每个对象都有内部锁,声明synchronized关键字,这个对象的锁将保护整个方法
public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
while (accounts[from] < amount)
wait(); //只有一个条件对象,如果资金不足会调用wait方法,将当前线程放入wait管理的等待集中(阻塞),并释放锁
System.out.println(Thread.currentThread().getName());
accounts[from] -= amount;
System.out.printf("%.2f from %d to %d\n", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %.2f\n", getTotalBalance());
//无论是wait还是notifyAll和notify都是Object类的方法
notifyAll();
} public synchronized double getTotalBalance() {
double sum = 0; for (double a : accounts)
sum += a; return sum;
} public int size() {
return accounts.length;
}
}

测试类:未修改

但是使用内部锁(synchronized)也存在一些缺陷:

  1. 不能中断一个正在尝试获得锁的线程

  2. 不能指定尝试获得锁的超时时间

  3. 每个锁仅有一个条件可能是不够的

关于Java集合框架的综述

  1. 抽象类:抽象类中必须有抽象方法,可以有实例方法,抽象类无法直接用new关键字初始化,可以通过继承抽象类后由子类进行初始化,前提是该子类实现了抽象父类的所有抽象方法,否则子类依旧为抽象类
  2. for each 循环可以处理任何实现了Iterable接口的对象,这个接口只包含了一个抽象方法,而Java集合类的基本接口是Collection接口,而Collection接口扩展了Iterable接口(接口继承接口),所以标准类库中的任何集合都可以使用for each循环
public interface Iterable<E> {
Iterator<E> iterator();//该方法返回一个实现了Iterator接口的对象,这里用的是多态的方式(is-a)
...
}

Iterator 迭代器接口

public interface Iterator<E> {
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}

使用迭代器与for each遍历集合中的元素的简单示例,该样例中只是展示迭代器和for each方法使用的逻辑,而不是可以直接运行的代码,具体要结合Java标准集合实现类去使用

Collection<String> c = ...;
//Collection接口继承额Iterable接口后获得了它的iterator方法
Iterator<String> iter = c.iterator();
//用迭代器
while (iter.hasNext()) {
String element = iter.next();
do something with element
}
//用for each
for (String element : c) {
do something with element
}
  1. 集合框架的家谱的由来:先定义了很多功能的集合接口(接口之间也有继承,功能逐渐细分),接着由抽象类实现接口中的部分抽象方法(完成从接口到类的跨越,逐步实现需要的功能),随着抽象类继续被子类继承,抽象方法全部被实现,最终得到了不同功能的实现类。(就是我们可以使用的Java集合实现类,它们有不同的功能实现,适合不同的应用场景)

关于链表LinkedList中添加删除对象的细节

  1. LinkedList实例的add方法用于将对象添加到链表的尾部,但是无法添加的到链表的中间
  2. 集合类库提供了Iterator的一个子接口ListIterator(原因是Iterator接口只有四个功能,显然是不够的),其中包含add,remove等方法,可以使实现类通过迭代器向链表的中间插入删除对象。LinkdeList 类的 listIterator 方法返回一个实现了 ListIterator 接口的迭代器对象,可以通过在这个迭代器对象上调用实例方法实现上述功能
  3. 关于迭代器指向的位置:可以理解为,迭代器位于两个对象之间,起初位于第一个对象的左侧,通过调用next方法移动到第一个与第二个对象之间,每次调用迭代器的add方法,将在迭代器“前”插入对象(或者理解为在迭代器处插入一个对象,然后迭代器指向新插入对象与之前右侧的对象之间)
  4. 关于3中的插入操作用|表示迭代器位置,用字母表示对象则可以有四个插入位置:|ABC、A|BC、AB|C、ABC|
  5. 注意:实现了ListIterator接口的对象,该对象的remove方法和set方法都是去删除或者覆盖迭代器上一个越过的对象(next()与previous()方法执行后迭代器位置向后或者向前移动一个位置,并且返回越过的对象
  6. 上述提到的各种接口中的方法,在Java的实现类完成对实现了该接口的抽象类的继承后,已经根据需求实现了对应的方法,这是Java集合实现类的设计者的工作(记得每个类可以单继承,但是一个类可以实现多个接口,所以这是一个组装+优化的过程)
  7. 关于linkedList的get方法,是一个“虚假”的随机访问下标对象的方法,它只有一个小的优化,当查询位置大于size()/2时从链表的末尾开始依次查询(如果使用了这个方法,你很可能用错了数据结构)
  8. 实现了ListIterator接口的对象的nextIndex方法和previousIndex方法返回该迭代器位置前后对应对象在链表中的下标,效率高(得益于迭代器保存当前位置的计数器),而集合对象获取ListIterator迭代器对象的方法有一个重载:
ListIterator<String> listIterator = staff.listIterator(n);//返回索引为n的对象与n-1对象之间的迭代器,效率低,毕竟对于链表,一切与索引有关的操作都很低效
  1. 并发修改异常(ConcurrentModificationException):一个迭代器发现自己的集合结构被另一个迭代器修改了(发生在线程不同步的集合类中),集合可以跟踪更改操作,但所谓的集合结构被修改不包括调用ListIterator.set()方法,Java并没有将值的修改归于集合结构发生了变化

数组列表ArrayList的使用场景

ArrayList封装了一个动态再分配的对象数组,它是线程不同步的,而对于使用动态数组,Vector类是线程同步的,两个线程可以安全的访问同一个vector对象,但是为了同步就会有额外开销,所以如果在单线程中需要使用动态数组,且不会发生两个迭代器去操作集合的结构(不需要使用线程同步时)推荐使用ArrayList而不是使用Vector

散列集 HashSet & 树集 TreeSet

  • 散列集是无序集合,不可重复(Set都是不可重复集),用迭代器Iterator进行遍历(而非ListIterator,这是用于有序集合的)Java散列表用邻接表实现(数组 + 链表),并根据使用情况进行数据结构的优化
  • 树集存储有序集合,其排序使用红黑树,每次将一个元素添加到树中,都会将其放置在正确的排序位置上,迭代器总是以有序的方式顺序访问每个元素,要使用树集,必须能够比较元素,这些元素必须实现Comparable接口或者构造集时必须提供一个Comparator,树集虽快,但是构造比较函数有时不方便,所以根据需求进行选择

知识点回顾:Java中 == 用于引用的直接比较,注意基本数据类型与对象引用在栈内存和堆内存的存储方式

以下代码通过实现Comparable接口和自定义类实现Comparator接口的两种方式实现堆Item类的排序,第一种排序先按序号从小到大,序号相同按字符串从小大到,第二种直接按字符串从小到大排序

public class Test1 {
public static void main(String[] args) {
var parts = new TreeSet<Item>();
parts.add(new Item("T", 1234));
parts.add(new Item("A", 1234));
parts.add(new Item("M", 9912));
System.out.println(parts); //如果不通过实现Comparable接口的方式 可以通过自定义一个实现Comparator接口的类实现排序
//下面会通过lambda表达式更方便的去使用Comparator接口实现自定义排序功能
var sortByDescription = new TreeSet<Item>(new DescriptionComparator()); sortByDescription.addAll(parts);
System.out.println(sortByDescription);
}
} //自定义类实现Comparator接口,注意泛型接口的类型要对应树集的泛型类型
class DescriptionComparator implements Comparator<Item> { @Override
public int compare(Item o1, Item o2) {
return o1.getDescription().compareTo(o2.getDescription());
}
} //一个类实现一个接口如果没有完全实现接口的抽象方法,则该类为抽象类
class Item implements Comparable<Item> {
private String description;
private int partNumber; @Override
public String toString() {
return "Item{" +
"description='" + description + '\'' +
", partNumber=" + partNumber +
'}';
} public Item(String description, int partNumber) {
this.description = description;
this.partNumber = partNumber;
} @Override
public boolean equals(Object o) {
if (this == o) return true;
//所属类名不同一定不同
if (o == null || getClass() != o.getClass()) return false;
//多态
Item item = (Item) o;
//这里比较两个字符串用的时Object类的方法?可以吗?字符串常量池?
return partNumber == item.partNumber &&
Objects.equals(description, item.description);
} @Override
public int hashCode() {
return Objects.hash(description, partNumber);
} @Override
public int compareTo(Item o) {
//第一个参数小返回-1 相同返回0 第一个参数大返回1
// 实现定义中的 从小大到排序,如此一来,只要第一个参数小时返回1
//则可以实现从大到小排序
int diff = Integer.compare(partNumber, o.partNumber);
//如果相同则按字典序(转成字节数组比较)比较String的大小,从小到大
return diff != 0 ? diff : description.compareTo(o.description);
} public String getDescription() {
return description;
} public void setDescription(String description) {
this.description = description;
} public int getPartNumber() {
return partNumber;
} public void setPartNumber(int partNumber) {
this.partNumber = partNumber;
}
}

lambda表达式+Comparator接口实现与上述代码中第二个树集的按字符串字典序排序相同的功能

  • Java中有许多封装代码块的接口,如Runnable,Comparator等,lambda表达式与这些接口是兼容的,对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式(而lambda表达式的参数列表就对应接口中抽象方法的参数列表,方法体也是相对应),这种接口称为函数式接口。
		//如果不通过实现Comparable接口的方式 可以通过自定义一个实现Comparator接口的类实现排序
//下面会通过lambda表达式更方便的去使用Comparator接口实现自定义排序功能
//var sortByDescription = new TreeSet<Item>(new DescriptionComparator());
//lambda表达式()中参数的类型可以不指定
Comparator<Item> c = (o1, o2) -> {
return o1.getDescription().compareTo(o2.getDescription());
};
var sortByDescription = new TreeSet<Item>(c);

一些JavaSE学习过程中的思路整理(四)(主观性强,持续更新中...)的更多相关文章

  1. 一些JavaSE学习过程中的思路整理(主观性强,持续更新中...)

    目录 一些JavaSE学习过程中的思路整理(主观性强,持续更新中...) Java书写规范 IDEA的一些常用快捷键 Java类中作为成员变量的类 Java源文件中只能有一个public类 Java中 ...

  2. iOS --- 总结Objective-C中经常使用的宏定义(持续更新中)

    将iOS开发中经常使用的宏定义整理例如以下,仅包括Objective-C. 而对于Swift,不能使用宏,则能够定义全局函数或者extension.请參考博客iOS - 总结Swift中经常使用的全局 ...

  3. 最简单的 IntelliJ IDEA 中使用 GitHub 进行版本控制教程(持续更新中)

    一.在 IntelliJ IDEA 中新建一个项目并提交到 GitHub 1. 运行 IDEA,点击[Create New Project],在 IDEA 中新建一个项目. 2. 在选择项目类型对话框 ...

  4. java视频教程 Java自学视频整理(持续更新中...)

    视频教程,马士兵java视频教程,java视频 1.Java基础视频 <张孝祥JAVA视频教程>完整版[RMVB](东西网) 历经5年锤炼(史上最适合初学者入门的Java基础视频)(传智播 ...

  5. 2020年腾讯实习生C++面试题&持续更新中(3)

    2020年腾讯实习生C++面试题&持续更新中(3) hello,大家好,我是好好学习,天天编程的天天. 来给大家大家分享腾讯实习生面经了. 天天希望大家看到面经后一定要做充分的准备,结合自己掌 ...

  6. fastadmin 后台管理框架使用技巧(持续更新中)

    fastadmin 后台管理框架使用技巧(持续更新中) FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架,具体介绍,请查看文档,文档地址为:https://doc. ...

  7. 2020年腾讯实习生C++面试题&持续更新中(1)

    2020年腾讯实习生C++面试题&持续更新中(1) 腾讯面试整理(1) 最近大三的学生找实习生的同学非常多,给大家分享一篇腾讯实习生的面试题,关于面试题,会持续更新~~~ 也算是今天开通博客的 ...

  8. 中国.NET:各地微软技术俱乐部汇总(持续更新中...)

    中国.NET:各地微软技术俱乐部汇总(持续更新中...)   本文是转载文,源地址: https://www.cnblogs.com/panchun/p/JLBList.html by ​史记微软. ...

  9. html知识点汇总(持续更新中)

    本人从事前端行业三年多,打算从今天开始整理一些关于前端的一些比较经典的知识点,持续更新中...希望能对一些相关知识点有疑问的朋友有一些帮助! HTML篇: 1.常见的行内元素/块级元素/空元素有哪些? ...

  10. EOS开发经验总结——不定期持续更新中

    一.新手安装mysql乱码问题 1.数据库安装时设置默认编码格式为UTF8或者打开mysql安装目录下my.ini,变更default-character-set=utf8: 2.打开EOS的Gove ...

随机推荐

  1. Window10安装linux子系统及子系统安装1Panel面板

    原文地址:Window10安装linux子系统及子系统安装1Panel面板 - Stars-One的杂货小窝 最近看到halo博客发布了2.10.0,终于是新增了个备份功能,于是有了念头想要升级下 但 ...

  2. ELK日志企业案例:(5.3版本)

    1.shell三剑客同居.分析nginx日志: 1)在企业生产环境中,日志内容主要用来做什么? 日志内容主要用于运维人员.开发人员.DBA排错软件服务故障的,因为通过日志内容能够第一时间找到软件服务的 ...

  3. Error resolving template [sys/prod/prod/list], template might not exist or might not be accessible by any of the configured Template Resolvers

    新的商城模板调试接口,一个商品列表的接口调用,返回报错 org.thymeleaf.exceptions.TemplateInputException: Error resolving templat ...

  4. SpringBoot数据响应、分层解耦、三层架构

    响应数据 @ResponseBody 类型:方法注解.类注解 位置:Controller方法.类上 作用:将方法返回值直接响应,如果返回值类型是 实体对象/集合 ,将会转换为json格式响应 说明:@ ...

  5. 3款免费又好用的 Docker 可视化管理工具

    前言 Docker提供了命令行工具(Docker CLI)来管理Docker容器.镜像.网络和数据卷等Docker组件.我们也可以使用可视化管理工具来更方便地查看和管理Docker容器.镜像.网络和数 ...

  6. command_execution

    前置知识 可以通过ping的TTL来判断系统的版本 判断了是Linux之后就使用Linux的连接命令来进行操作 这里直接全局搜索flag相关的文件 linux全局查询文件_linux全局查找某个文件- ...

  7. easyupload

    打开界面就是一个文件上传 的界面 然后在bp试了很多种方法都没有成功,还是看了wp 这里需要利用到.use.ini那为什么不用.heaccess?好像这种方法被过滤了,当时我用的时候没有成功 这里的话 ...

  8. .NET 8.0 中有哪些新的变化?

    1性能提升 .NET 8在整个堆栈中带来了数千项性能改进 .默认情况下会启用一种名为动态配置文件引导优化 (PGO) 的新代码生成器,它可以根据实际使用情况优化代码,并且可以将应用程序的性能提高高达 ...

  9. 二叉搜索树 & 平衡树

    二叉搜索树 & 平衡树 专题 0x00 前言 我 AFO 了,但不代表不写 Code 了... CSP-S 在数据结构上吃了大亏,就差这一点就一等了,所以觉得好好整整. 本篇博客主要研究二叉搜 ...

  10. js朗读实现

    js 利用window实现朗读功能 ` 发音