线程高级
一、锁的介绍
重入锁
昨天讲解了同步锁(同步代码块和同步方法);相比同步锁,重入锁的执行性能会更高,因为重入锁是手动进行加锁和释放锁,灵活性更强;但是重入锁是手动处理锁,容易出现死锁,需要谨慎使用。
应用:在使用上这些锁没有区别,锁的注意事项也是一致的
案例:模拟List的安全隐患问题,以及处理隐患。
//模拟List集合,使用重入锁处理---ReentrantLock
class MyList{
Lock lock = new ReentrantLock(); //重入锁
String[] a = {null,null};
int index; //记录下标,从0开始
public void add(String value) {
try {
lock.lock(); //获取锁对象
a[index] = value;
index++;
} finally { //无论如何都会释放
lock.unlock(); //释放锁对象
}
}
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyList list = new MyList();
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
list.add("hello");
}
});
th1.start();
Thread th2 = new Thread(new Runnable() {
@Override
public void run() {
list.add("world");
}
});
th2.start();
th1.join(); th2.join();
System.out.println(Arrays.toString(list.a));
}
}
读写锁
读写锁:ReentrantReadWriteLock (了解)
描述:将读锁和写锁进行分离,读与读之间不阻塞,读与写会阻塞,写与写也会阻塞
场景:只有读远远高于写的情况,才使用读写锁
后面线程安全中,会有并发集合(并发读和写,读不加锁,性能更高)的使用可以取代读写锁
什么是读? 什么是写? 读—-打印共享数据,获取值(查询); 写—-存储,修改,删除
//准备了20个线程---18个进行读,2个进行写
ExecutorService es = Executors.newFixedThreadPool(20);
long start = System.currentTimeMillis();
//20个线程进行读和写...
es.shutdown(); //关闭线程池
while(!es.isTerminated()); //类似join,阻塞,等待子线程都执行完主线程才能会自行
//下面的主线程操作需要等到20个子线程都执行完才能统计最终时间
System.out.println(System.currentTimeMillis()-start);
二、线程安全集合
Collections同步
//线程安全的集合: Collections创建同步锁(了解),直接加的锁,没有性能的优化提升
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
for(int i=1;i<=5;i++) {
final int t = i; //需要重新赋值,在匿名内部类中打印;
new Thread(new Runnable() {
@Override
public void run() {
list.add(t);//因为i会变更,这里不能打印i 写
//System.out.println(list); //读 不是并发读写,可能会出现异常
}
}).start();
}
并发ArrayList
//并发集合:CopyOnWriteArrayList,可以并发的读和写
//原理:在内部拷贝了一份副本进行写;可以在原始的数组中读;即可做到读写分离
//读不加锁,写有锁;读写之间不互斥;只有写与写会阻塞
//相对Collections的同步锁,性能会更高,比读写锁也更高
List<Integer> list = new CopyOnWriteArrayList<Integer>();
for(int i=1;i<=5;i++) {
final int t = i;
new Thread(new Runnable() {
@Override
public void run() {
list.add(t); //写
System.out.println(list); //读
}
}).start();
}
结论:最终确保5个线程,每个线程存数据的位置是不同的
并发ArraySet
底层通过并发的ArrayList实现的,只不过会判断存储元素是否相等,如果相等,则丢弃副本
存储特点:存储的元素唯一
Set<String> set = new CopyOnWriteArraySet<String>();
set.add("zs");
set.add("ls");
set.add("zs");
System.out.println(set);
并发的HashMap
描述:内部的hash表进行了锁分段机制,分了16段;每个hash表位置进行加锁。
理论上可以16个线程同时并发的写
Map<String, Integer> map = new ConcurrentHashMap<String, Integer>();
for(int i=1;i<=5;i++) {
int t = i;
new Thread(new Runnable() {
@Override
public void run() {
map.put("线程"+t, t); //写
System.out.println(map); //读
}
}).start();
}
三、并发的队列
队列与集合是类似的,也是个容器,底层也可以通过数组或链表实现队列的功能
特点:先进先出
接口: Queue队列接口
ConcurrentLinkedQueue
该队列是并发队列中,性能最高的队列;因为没有加锁
ConcurrentLinkedQueue实现类:内部通过CAS无锁交换算法进行线程安全的处理
//ConcurrentLinkedQueue实现类:内部采用CAS无锁交换算法-没有加锁也能确保安全
//队列的特点,先进先出
//内部实现:维护了三个参数 V-要替换的值 E-预期值 N-新值
//如果在执行中一直能匹配V==E;则新值可以替换旧值V=N;说明没有人更改过值,即可存进来;
//否则,舍弃N的存储
//CAS的三个参数的判断是基于原子性操作
Queue<String> queue = new ConcurrentLinkedQueue<String>();
queue.add("zs");
queue.add("ls");
//前面没有add元素,则后面的移除和获取都会报异常
System.out.println(queue.remove()); //返回并移除 移除zs
//System.out.println(queue.element()); //返回并不移除
System.out.println(queue);
Queue<String> queue2 = new ConcurrentLinkedQueue<String>();
//queue2.offer("zs");
//queue2.offer("ls");
//没有存元素,不会报错,获取null
//System.out.println(queue2.poll()); //返回并移除 移除zs
System.out.println(queue2.peek()); //返回并不移除
System.out.println(queue2);
四、总结与作业
总结
1.锁的介绍
重入锁:与同步锁的区别;特点-手动操作锁(重点)
了解读写锁,读写分离-读与读不阻塞
2.线程安全集合
Collections同步锁(了解-性能低)
CopyOnWriteArrayList-并发读写的ArrayList,读无锁,读写之间不阻塞(重点)
CopyOnWriteArraySet-底层由CopyOnWriteArrayList完成,自身特点-存储唯一性
ConcurrentHashMap-并发HashMap,采用锁分段机制(锁hash表位置)-可以并发写(重点)
3.并发队列
队列接口:Queue
实现类:ConcurrentLinkedQueue-性能队列中最高
内部原理:CAS无锁交换算法;原子性判断
作业
1.理解安全锁:重入锁、同步代码块、同步方法的区别
2.理解线程安全集合:并发ArrayList,并发ArraySet,并发HashMap
3.理解线程安全队列: 并发队列(CAS算法)
提示:今天作业不交