0822(036天 线程/进程07 Lock接口、集合框架01 Iterator迭代器)
每日一狗(田园犬西瓜瓜)
线程/进程07 Lock接口、集合框架01 Iterator迭代器
1. Lock接口
为啥要引入Lock接口?
synchronized不行
- 是一个Java的关键字,是一个修饰符
- 没有区分读写锁(写和写应该隔离,但是读和读操作不应该有隔离,当然读和写应该是隔离的,synchronized是排他锁,而有些操作需要的是共享锁)
1.1 Lock接口
Lock是java1.5中引入的线程同步工具,主要用于多线程下共享资源的控制。
- 需要用户主动释放锁
- 可中断,设置超市等待
- 默认也是非公平锁,可以设置成公平锁
- 所绑定多个cindition用来精确唤醒
1.2 Lock常见方法
void unlock();释放锁,为了保证一定释放,一般使用try/finally结构
void lock();// 申请锁
前场一下
lock.lock();
try {
// 代码块
} finally {lock.unlock();}
申请不到阻塞,等到能申请到为止
搞一个加加减减的小测试
package com.yang1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test01 {
public static void main(String[] args) {
NumOperation no = new NumOperation();
Thread[] ts = new Thread[8];
for (int i = 0; i < 8; i++) {
if (i % 2 == 0) {
ts[i] = new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
no.add();
}
});
} else {
ts[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
no.sub();
}
});
}
ts[i].start();
}
for (Thread t : ts) {
if (t != null) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("main" + no.getNum());
}
}
class NumOperation {
public Long getNum() {
return num;
}
private Long num = 0L; // 共享数据
private static final Lock lock = new ReentrantLock(); // 可重入锁
public void add() {
lock.lock();// 申请锁
try {
System.out.println(Thread.currentThread() + "这是加加前" + num);
num++;
System.out.println(Thread.currentThread() + "这是加加后" + num);
} finally {
lock.unlock(); // 为了保证一定释放使用try、finally结构
}
}
public void sub() {
lock.lock();
try {
System.out.println(Thread.currentThread() + "这是渐渐前" + num);
num--;
System.out.println(Thread.currentThread() + "这是渐渐后" + num);
} finally {
lock.unlock();
}
}
}
Lock提供的实现类
Lock有三个实现类,底层使用都依赖于juc包抽象队列同步器AbstractQueuedSynchronizer
- ReetrantLock(排他锁)
- ReadLock(ReetrantReadWriteLock)(共享锁)
- WriteLock(ReetrantReadWriteLock)(排他锁)
特性:
- 支持重入,重入多少次就要退出多少次,// lock.lock();try{}finally{lock.unlock();}
- condition.await(); // 阻塞会释放锁,不管层数,
- condition.signal(); // 唤醒线程时会继续上次阻塞的位置继续执行(自动获取锁,当时阻塞时获取了多少次锁,恢复时就要重新获取多少次锁)
- ReentrantReadWriteLock是Lock的另一种实现方式
- ReentrantLock是一个排他锁,
- ReentrantReadWriteLock中有读操作和写操作,其中只有读操作和读操作是可并行的,写写,读写之间都是排他的
ReentrantReadWriteLock相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。
ReentrantLock底层实现
底层实现都依赖于Sync( 抽象队列同步器AbstractQueuedSynchronizer的子类来实现的)
ReetrantLock有两个静态内部类NonfairSync和FairSync分别代表非公平锁和公平锁,其中带参构造器中的boolean代表公平与否。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock一些常见的方法
void await() throws InterruptedException; // 阻塞并释放锁,可指定唤醒条件
void signal(); // 随机唤醒一个,可以指定条件
void signalAll(); // 唤醒满足条件的所有线程
boolean isFair(); // 可以判断锁是否为公平锁
boolean isLocked(); // 判断锁是否被任何线程获取了
boolean isHeldByCurrentThread() ;// 判断锁是否被当前线程获取
boolean hasQueuedThreads(); // 判断是否有线程在等待该锁
1.3 Condition接口
条件变量就是表示条件的一种变量。但是必须说明,这里的条件是没有实际含义的,仅仅是个标记而已,并且条件的含义往往通过代码来赋予其含义。条件变量都实现了java.util.concurrent.locks.Condition接口,条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的,这样,条件就和一个锁对象绑定起来了。因此,Java 中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。
条件变量Condition接口定义了等待/通知两种类型的方法,在线程调用这些方法时,需要提前获取Condition对象关联的锁(在基于wait/notify方法实现的方案中需要获取的是对象锁)。
一个架子:满足某种条件的一部分线程。
Condition与Lock配合完成等待通知机制。Condition对象是需要关联Lock对象的,经调用Lock对象的newCondition()对象创建而来,也就是说Condition的使用是需要依赖Lock对象的。
两个架子,满足那个架子,就去唤醒那个架子的线程,新来的线程会先判定满不满足当支持的架子所需要的线程,满足则争抢锁,不满足就去满足的条件的那个架子上阻塞着。
private static final Lock lock = new ReentrantLock(); // 创建一把锁
private static final Condition acon = lock.newCondition(); // 建存放A线程的一个架子
acon.await(); // 把当前线程挂到这个架子上
acon.signal(); // 唤醒在Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意的
bcon.signalAll(); // 把这个架子上的线程全部唤醒,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程
前场一下
实现多个线程依次输出ABABABABAB…
package com.yang1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test02 {
public static void main(String[] args) {
OutResource or = new OutResource();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int k = 0; k < 10; k++) {
or.printA();
}
}).start();
new Thread(() -> {
for (int k = 0; k < 10; k++) {
or.printB();
}
}).start();
}
}
}
class OutResource {
private static final Lock lock = new ReentrantLock();
private static final Condition acon = lock.newCondition(); // 建存放A线程的一个架子
private static final Condition bcon = lock.newCondition(); // 建存放B线程的一个架子
private boolean isA = false; // 标识输出的是啥
public void printA() {
lock.lock();
try {
while (isA) {
try {
acon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A" + Thread.currentThread() + " ");
isA = true;
bcon.signalAll();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (!isA) {
try {
bcon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B" + Thread.currentThread() + " ");
isA = false;
acon.signalAll();
} finally {
lock.unlock();
}
}
}
使用Condition接口可以实现精确唤醒,用于生产者消费者中,可以提高线程唤醒的精确度,也可以一定程度的降低枪锁的难度。
可以创建两个Condition 架子,一个用于存放生产者,另一个用于存放消费者,在创建或消费了一个产品后精确唤醒另一方中的一个线程,降低因为枪锁锁导致的无用运行。
package com.yang2; import java.util.Date; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test03 { public static void main(String[] args) { Basket1 b = new Basket1(); for (int i = 0; i < 5; i++) { new Thread(() -> { for (int j = 1; j < 5; j++) { b.production(new Date()); // 生产 } }).start(); new Thread(() -> { for (int j = 1; j < 5; j++) { b.consumption(); // 消费 } }).start(); } } } class Basket1 { private volatile Object obj = null; private final Lock lock = new ReentrantLock(); private final Condition productionCon = lock.newCondition(); private final Condition consumptionCon = lock.newCondition(); public void production(Object obj) { lock.lock(); try { while (this.obj != null) { try { // 放到生产者的架子上 productionCon.await(); } catch (InterruptedException e) { e.printStackTrace(); } } this.obj = obj; // 唤醒消费者中的一个线程 consumptionCon.signal(); System.out.println(Thread.currentThread() + "生产" + obj); } finally { lock.unlock(); } } public void consumption() { lock.lock(); try { while (obj == null) { try { // 条件阻塞,放到那个消费者的架子上 consumptionCon.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread() + "消费" + obj); this.obj = null; //条件唤醒,只需要将生产者中的一个线程进行唤醒 productionCon.signal(); } finally { lock.unlock(); } } }
1.7 问:synchronized和Lock的区别
synchronized | Lock | |
---|---|---|
实现 | Java修饰符,内置的语言实现 | 是一个接口 |
区分读写锁 | 否 | 是ReentrantReadWriteLock中内置了读和写锁。 |
是否可重入 | 是 | 是 |
判定是否获取锁 | 无法判定,Thread类中提供了一个静态方法,可以判断boolean holdsLock(Object obj) | ReentrantLock 的boolean isLocked();可以判定改锁是否被某个对象获取 |
读操作的性能 | 竞争不激烈的时候差不多,竞争多的时候就会用到重量级锁,性能大大下降 | 在读操作中使用的为共享锁,按理来说性能不会因为并发量有所下降 |
可中断 | 否,等待的线程会一直等待下去,不能够响应中断 | 是,可以让等待锁的线程响应中断 |
公平 | 否 | 默认否,可以设定为是 |
是否有分区 | 没有,只能全部唤醒 | 锁绑定多个condition用来精确唤醒 |
发生异常 | 会自动释放锁,不会导致死锁的现象发生 | 只有unlokc才能释放锁,所以释放锁一般配合try/finally结构,在finally中释放锁 |
1.8 共享锁、排他锁
读锁:就是共享锁
写锁:就是排他锁
ReadWriteLock接口上和Lock接口无关,提供了通过分开读锁和写锁,控制锁阻塞的方法,提高程序的执行效率,就是因为有些操作和有些操作之间并不会有干扰,才会出现共享锁,但是这个锁和其他锁之间又有可能会互相排斥。
public interface ReadWriteLock {
Lock readLock(); // 用于获取读锁,读锁之间不相互阻塞
Lock writeLock(); // 用于获取写锁,写锁和其它锁互斥
}
功能在于将文件的读写操作分开,分成2个锁分配给线程,从而实现多个线程可以同时执行读操作,提高读操作之间的吞吐量,而且多数文件都是读的频率远远高于写的频率。
ReadWriteLock接口的实现类ReentrantReadWriteLock
提供了写锁和读锁的实现
2. 数据结构
2.1 线性表
**数组:**长度固定,创建即分配空间,而且内存还要求时连续的,故空间复杂度很大,导致对开辟地区要求相当严格。
**优点:**由于内存连续分配的,寻址索引高效,时间复杂度为O(1)),只需要在数组的头部加上指定长度的偏移量即可获得指定索引的地址。
缺点:
- 插入和删除难度较大,可能会引发一半以上的数据元素移动,时间复杂度为O(n));
- 创建即分配空间,不管你用没用,反正我就是占着,导致在数组没有填满前,有多数空间被白白浪费了,而且部分空间可能根本就用不着。
- Java中的数组是定长的,如果需要变长数组需要自行编码实现
数组在存储引用数据类型变量时,在数组中存储的只是一个引用地址,长度大小可算。
变长数组
实现数组的增加、删除和修改。
package com.yang3;
import java.util.Arrays;
public class ArraysList {
private Object[] data;
private int count = 0;
public ArraysList() {
this(10);
}
public ArraysList(int length) {
if (length < 0) {
length = 10;
}
data = new Object[length];
}
public void add(Object obj) {
data[count++] = obj;
if (count >= data.length) {
resize();
}
}
private void resize() {
Object[] newData = new Object[data.length * 3 / 2];
System.arraycopy(data, 0, newData, 0, count);
data = newData;
}
public void delete(int index) {
if (index < 0 || index >= count) {
throw new ArrayIndexOutOfBoundsException();
}
System.arraycopy(data, index + 1, data, index, count - 1 - index);
data[--count] = null;
}
public void update(int index, Object data) {
if (index < 0 || index >= count) {
throw new ArrayIndexOutOfBoundsException();
}
this.data[index] = data;
}
public static void main(String[] args) {
ArraysList arr = new ArraysList(5);
System.out.println(Arrays.toString(arr.data));
for (int i = 0; i < 10; i++) {
arr.add(i);
}
arr.delete(0);
arr.update(1, 99);
System.out.println(Arrays.toString(arr.data));
}
}
2.2 链表
单向链表:
一环套一环。你的尾部存储着我的头信息。不是连续存放的
优点:
- 针对于数组来说,链表的占用空间是动态分配的,用多少,占多少,没有
- 插入和删除数据比较简单。时间复杂度为O(1))。
缺点:
- 由于空间不是连续存放的,索引效率低,不能直接通过索引来进行数据的定位,只能用上一个链子来定位下一个数据。时间复杂度为O(n))
// 由于本程序仅用于链表的操作的底层操作的探索
// 故对其部分细节没有进行过多考虑,存在索引上的漏洞
package com.yang3;
public class LinkedList {
public static void main(String[] args) {
LinkedList ll = new LinkedList();
for (int i = 0; i < 10; i++) {
ll.add(i);
}
ll.show();
ll.insert(1, -1);
ll.show();
ll.delect(5);
ll.show();
}
private Node header;
public void add(Object data) {
if (header == null) {
header = new Node(data);
} else {
Node n = header;
for (; n.next != null; n = n.next)
;
n.next = new Node(data);
}
}
// 在指定位置进行插入操作
public void insert(int index, Object data) {
Node p = header;
for (int i = 1; i < index; i++) {
p = p.next;
}
Node tmp = new Node(data);
tmp.next = p.next;
p.next = tmp;
}
public void delect(int index) {
Node p1 = header;
for (int i = 1; i < index; i++) {
p1 = p1.next;
}
p1.next = p1.next.next;
}
public void show() {
Node n = header;
for (; n != null; n = n.next) {
System.out.print(n.getData() + " ");
}
System.out.println();
}
class Node {
private Object data;
private int count;
public Node next;
public Node(Object data) {
this.data = data;
count++;
}
public Object getData() {
return data;
}
public int getCount() {
return count;
}
}
}
// 运行结果
0 1 2 3 4 5 6 7 8 9
0 -1 1 2 3 4 5 6 7 8 9
0 -1 1 2 3 5 6 7 8 9
2.3 程序
程序=算法(计算方法)+数据结构(数据是如何存储的)
算法:在既定规模内,可以在一定执行次数内结束的程序为算法。
算法的优劣
1、程序执行时间:硬件和软件平台的不一致导致无法统一标识算法的优劣
2、时间复杂度:根据输入数据的规模变化最大的代码块部分的执行次数的式子可以客观的衡量算法的好坏,使用大O表示法需要找到式子中的最高次幂,消除常量。
3. 集合
3.1 基本定义
-
集合中可以存放不限数量、不限格式的数据。默认集合的数据类型为Object
-
集合就是一个容器
-
能用于存放对象,存储简单类型会转换为对应的包装类。
-
几个存放的只是对象的引用,真正的数据还是存放在内存中,这一点和对象数组一致。
-
无序,可重复
比较
针对Java中的数组定长,Java提出了集合框架,实现了一种变长存储数据的容器—集合
数组 | 集合 | |
---|---|---|
存放的数量 | 无法判定,length只能告诉我们数组的长度,只能遍历自行判定 | 可以通过int size();获取已经存储的个数 |
不是面向对象的,存在明显的缺陷 | 集合弥补了数组的缺点,比数组更灵活更实用, | |
存储的对象 | 对于简单类型存储数据值,对于引用类型存储引用值 | 集合类存放的都是对象的引用,而非对象本身 |
容量扩展性 | 定长无法扩展 | 集合类容量动态改变 |
有序,顺序 | 无序,可重复,实现类中可以对其进行限定 | |
开发扩展性 | 无法扩展 | 集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性即可实现各种复杂操作,大大提高了软件的开发效率 |
3.2 Iterator迭代器
Iterator迭代器:走访器,可以理解为集合中元素的指针
它是Java集合的顶层接口(不包括map系列的集合,Map接口是map系列集合的顶层接口)
public interface Iterator<E> {
boolean hasNext(); // 判断是否有后续元素
E next(); // 指针向后移动,同时返回指向的数据
default void remove() { // 删除指针所指向的元素
throw new
UnsupportedOperationException("remove");
}
// 使用lambda表达式的方式遍历所有元素
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
// Iterable接口用以表示实现类是可以迭代的
Iterator<T> iterator();
前场一下
package com.yang3;
import java.util.ArrayList;
import java.util.Iterator;
public class Test01 {
public static void main(String[] args) {
ArrayList arr = new ArrayList();
for (int i = 0; i < 100; i++) {
arr.add(i);
}
// 直接使用arr的forEach结构遍历
arr.forEach(System.out::println);
arr.forEach((obj) -> {
System.out.println(obj);
});
// 获取数组的迭代器
Iterator it = arr.iterator();
// 使用next遍历
while (it.hasNext()) {
Object tmp = it.next();
System.out.println(tmp);
}
// 使用迭代器的forEach迭代剩下的
it.forEachRemaining(System.out::println);
it.forEachRemaining((obj) -> {
System.out.println(obj);
});
}
}
使用arrayList.forEach((obj)->{});可以重复遍历
使用iterator.forEachRemaining(()->{}); // 只能迭代遍历剩下的
3.3 顶级接口Collection
无序、允许重复
int size(); // 获取集合中的元素个数 区分容积和元素个数
boolean isEmpty(); // 判断集合中的元素个数是否为0
注意:只判断是否没有元素,但是并不判断集合对象是否为null
boolean contains(Object o); //用于判断集合中是否包含对象
boolean add(Object o); //用于向集合中追加元素o,成功true失败false
boolean remove(Object o); //删除集合中的指定元素o,成功true失败false
Iterator<E> iterator(); // 获取迭代器,通过迭代器遍历集合中的每个元素
Object[] toArray(); // 将集合转换为数组
void clear(); // 删除集合中的所有元素
public static void main(String[] args) throws InterruptedException {
Collection cc = new ArrayList();
cc.add(123);
cc.add("bbbb");
cc.add(new Date());
cc.size(); // 3
cc.isEmpty(); // false
cc.contains("bbbb"); // true
Thread.sleep(20);
cc.contains(new Date()); // false
cc.remove(123); // true
cc.size(); // 2
}