目录
一、锁的介绍
1.重入锁
同步锁(同步代码块和同步方法);相比同步锁,重入锁的执行性能会更高,因为重入锁是手动进行加锁和释放锁,灵活性更强;但是重入锁是手动处理锁,容易出现死锁,需要谨慎使用。
应用:在使用上这些锁没有区别,锁的注意事项也是一致的
案例:模拟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));
}
}
2.读写锁
读写锁: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);
二、线程安全集合
1.Collection同步
线程安全的集合的案例:
//线程安全的集合: 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 写;
//但是我可以通过t,虽然t也是final不可变的,
//但是每次循环的时候我都创建一个新的t,并不是改变原来的t
//System.out.println(list); //读 不是并发读写,可能会出现异常
}
}).start();
}
2.并发ArrayList
//并发集合:CopyOnWriteArrayList,可以并发的读和写
//原理:在内部拷贝了一份副本进行写;可以在原始的数组中读;即可做到读写分离
//读不加锁,写有锁;读写之间不互斥;只有写与写会阻塞
//相对Collections的同步锁,性能会更高,比读写锁也更高
并发ArrayList案例:
读不加锁,写加锁,所以读写不互斥 只有写写互斥
所以开启五个线程 每个线程都有可能其他线程在写完成还未读的时候,进行读写,因为读写不互斥,因为这五个线程是给cpu调度的,具体谁先执行这是无法预估的
但是在写未结束的时候,其他线程不可写
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个线程,每个线程存数据的位置是不同的
3.并发ArraySet
底层通过并发的ArrayList实现的,只不过会判断存储元素是否相等,如果相等,则丢弃副本
存储特点:存储的元素唯一
简单使用:
Set<String> set = new CopyOnWriteArraySet<String>();
set.add("zs");
set.add("ls");
set.add("zs");
System.out.println(set);
4.并发的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队列接口
1.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);
四、阻塞队列
BlockingQueue
是Queue的子接口,有两个实现类:ArrayBlockingQueue何LinkedBlockingQueue
ArrayBlockingQueue是有界队列;队列中存储的元素可以限制(推荐)
LinkedBlockingQueue是无界队列;队列中存储的元素无限制,可以一直存储
阻塞队列中提供了存储阻塞的方法:take和put
案例:
//阻塞队列接口:BlockingQueue; 两个实现类:有界和无界队列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
queue.put("zs"); //存储-超过个数则阻塞
queue.put("zs");
queue.put("ww");
//queue.put("zl");
System.out.println(queue);
System.out.println(queue.take()); //获取,
System.out.println(queue.take()); //获取,
System.out.println(queue.take()); //获取,
System.out.println(queue.take()); //获取,超过了,则阻塞
模拟生产者消费者
//通过阻塞队列模拟生产者和消费者模型
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
new Thread(new Runnable() {
@Override
public void run() {
//一直负责生产的线程
for(int i=1;i<=100;i++) {
try {
queue.put(i);
} catch (InterruptedException e) {}
System.out.println("正在生产第"+i+"件货");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
//一直负责消费的线程
for(int i=1;i<=100;i++) {
try {
System.out.println("正在消费第"+queue.take()+"件货");
} catch (InterruptedException e) {}
}
}
}).start();
五、总结
总结
1.锁的介绍
重入锁:与同步锁的区别;特点-手动操作锁(重点)
了解读写锁,读写分离-读与读不阻塞
2.线程安全集合
Collections同步锁(了解-性能低)
CopyOnWriteArrayList-并发读写的ArrayList,读无锁,读写之间不阻塞(重点)
CopyOnWriteArraySet-底层由CopyOnWriteArrayList完成,自身特点-存储唯一性
ConcurrentHashMap-并发HashMap,采用锁分段机制(锁hash表位置)-可以并发写(重点)
3.并发队列
队列接口:Queue
实现类:ConcurrentLinkedQueue-性能队列中最高
内部原理:CAS无锁交换算法;原子性判断