一些JavaSE学习过程中的思路整理(四)(主观性强,持续更新中...)
一些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)也存在一些缺陷:
-
不能中断一个正在尝试获得锁的线程
-
不能指定尝试获得锁的超时时间
-
每个锁仅有一个条件可能是不够的
关于Java集合框架的综述
- 抽象类:抽象类中必须有抽象方法,可以有实例方法,抽象类无法直接用new关键字初始化,可以通过继承抽象类后由子类进行初始化,前提是该子类实现了抽象父类的所有抽象方法,否则子类依旧为抽象类
- 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
}
- 集合框架的家谱的由来:先定义了很多功能的集合接口(接口之间也有继承,功能逐渐细分),接着由抽象类实现接口中的部分抽象方法(完成从接口到类的跨越,逐步实现需要的功能),随着抽象类继续被子类继承,抽象方法全部被实现,最终得到了不同功能的实现类。(就是我们可以使用的Java集合实现类,它们有不同的功能实现,适合不同的应用场景)
关于链表LinkedList中添加删除对象的细节
- LinkedList实例的add方法用于将对象添加到链表的尾部,但是无法添加的到链表的中间
- 集合类库提供了Iterator的一个子接口ListIterator(原因是Iterator接口只有四个功能,显然是不够的),其中包含add,remove等方法,可以使实现类通过迭代器向链表的中间插入删除对象。LinkdeList 类的 listIterator 方法返回一个实现了 ListIterator 接口的迭代器对象,可以通过在这个迭代器对象上调用实例方法实现上述功能
- 关于迭代器指向的位置:可以理解为,迭代器位于两个对象之间,起初位于第一个对象的左侧,通过调用next方法移动到第一个与第二个对象之间,每次调用迭代器的add方法,将在迭代器“前”插入对象(或者理解为在迭代器处插入一个对象,然后迭代器指向新插入对象与之前右侧的对象之间)
- 关于3中的插入操作用|表示迭代器位置,用字母表示对象则可以有四个插入位置:|ABC、A|BC、AB|C、ABC|
- 注意:实现了ListIterator接口的对象,该对象的remove方法和set方法都是去删除或者覆盖迭代器上一个越过的对象(next()与previous()方法执行后迭代器位置向后或者向前移动一个位置,并且返回越过的对象)
- 上述提到的各种接口中的方法,在Java的实现类完成对实现了该接口的抽象类的继承后,已经根据需求实现了对应的方法,这是Java集合实现类的设计者的工作(记得每个类可以单继承,但是一个类可以实现多个接口,所以这是一个组装+优化的过程)
- 关于linkedList的get方法,是一个“虚假”的随机访问下标对象的方法,它只有一个小的优化,当查询位置大于size()/2时从链表的末尾开始依次查询(如果使用了这个方法,你很可能用错了数据结构)
- 实现了ListIterator接口的对象的nextIndex方法和previousIndex方法返回该迭代器位置前后对应对象在链表中的下标,效率高(得益于迭代器保存当前位置的计数器),而集合对象获取ListIterator迭代器对象的方法有一个重载:
ListIterator<String> listIterator = staff.listIterator(n);//返回索引为n的对象与n-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);