ReentrantLock重入锁(公平锁)的使用
可重入锁(明锁),相比于synchronized来说,可重入锁具有以下特点:
1.可中断性:可以让没有获得到锁的线程放弃对锁的获取,解除线程阻塞状态
2.可以设置多个条件变量
3.可以设置公平锁
该类是由JUC包中提供的
使用方法:
创建一个重入锁对象
ReentrantLock lock = new ReentrantLock();
Condition cond = lock.newCondition();
产生锁:lock.lock();
释放锁:lock.unlock;
等待:cond.await();
解除等待:cond.signal();
解除所有等待:cond.signalAll();
使用举例:(生产者消费者模式)
package com.qianfeng.day17;
import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockWork02 {
public static void main(String[] args) {
MessageQueue mq=new MessageQueue(4);
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
mq.put(UUID.randomUUID().toString());
}
},"生产者:" + (i+1)).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
mq.take();
}
}
}).start();
}
}
//创建出生产者和消费者共享的消息队列
class MessageQueue{
ReentrantLock lock = new ReentrantLock();
Condition cond = lock.newCondition();
//保存消息数据
private LinkedList<String> queue=new LinkedList<String>();
//队列的最大容量
private int capcity;
public MessageQueue(int capcity) {
super();
this.capcity = capcity;
}
//为生产者提供一个往队列中存储消息的方法
public void put(String message) {
//分两种不同的情况进行分别的处理
//1.如果队列已满,生产者线程必须进入等待
//2.队列未满,直接向队列的尾部添加新的消息
String tname = Thread.currentThread().getName();
try {
lock.lock();
if(queue.size()==capcity) {
try {
System.out.println(tname + "队列已满," + tname + "进入等待....");
cond.await();
//queue.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
queue.addLast(message);
System.out.println(tname + "往队列种放入一个消息:" + message);
cond.signal();
Thread.sleep(2000);
//queue.notify();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
//提供给消费者调用的方法
public String take() {
try {
lock.lock();
//消费的时候也存在两种情况
//1.如果队列为空则进入等待状态
//2.如果队列种存在消息则从队列的头部删除一个元素并返回
if(queue.size()==0) {
try {
System.out.println("队列已空,消费者线程进入等待...");
cond.await();
//queue.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
String message=queue.removeFirst();
System.out.println("消费者线程从队列种消费了一条消息:" + message);
cond.signal();
Thread.sleep(2000);
//queue.notify();
return message;
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
return null;
}
}
常见的线程安全集合
ArrayList集合经过测试是线程不安全的!!!
解决方案有三种:
- 使用线程安全版的ArrayList Vector ,Vector 在解决线程安全问题的时候采用的是加锁的方式,对程序执行效率影响太大,不推荐使用
- 使用Collections工具类中提供的静态工具方法Collections.synchronizedxxx(Collection/Map)进行线程安全处理并返回一个新的集合,这种方式本质上和第一种没有区别,都是采取加锁的方式来保证线程的安全
- 在JUC包中提供了很多的线程安全集合类供开发人员使用,相比于前面两种方式来说具有更好的并发性(推荐使用)
1 CopyOnWriteArrayList
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//获取原来的数组
int len = elements.length;//获取原来数组的长度
Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝原来的数组并进行扩容1个
newElements[len] = e;//完成新元素的添加
setArray(newElements);//使用新的数组引用替换掉原来的数组引用
return true;
} finally {
lock.unlock();
}
}
通过源码的查看我们发现只有在写数据的时候才会上锁,在写的过程中其他线程依然可以去读取原来的数据内容,有点类似于之前的读写锁,性能相对较高,同时它也存在一定的问题:1.读取的数据不一致的 2.使用空间来换取线程的安全
我们认为该集合适用于读多写少的情况,对数据的读取的一致性要求不是非常严格的情况
CopyOnWriteArraySet底层就是一个CopyOnWriteArrayList,只是在添加元素的时候做了一个元素是否已经存在的判断
//static ArrayList<String> list=new ArrayList<String>();
//static Vector<String> list=new Vector<String>();
//static List<String> list=Collections.synchronizedList(new ArrayList<String>());
static CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>();
public static void main(String[] args) {
List<Thread> threadList=new ArrayList<Thread>();
for (int i = 0; i < 5; i++) {
threadList.add(new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <10; j++) {
list.add(UUID.randomUUID().toString());
}
}
}));
}
for (int i = 0; i < threadList.size(); i++) {
threadList.get(i).start();
}
for (int i = 0; i < threadList.size(); i++) {
try {
threadList.get(i).join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("集合中的元素总个数:" + list.size());
2 BlockingQueue 阻塞队列
需求使用jdk提供的BlockingQueue来实现生产消费模式
增加了两个无限期等待的方法。
put():添加,如果没有空间则等待。
take():获取,如果没有元素则等待。
1 ArrayBlockingQueue
有界队列,使用数组实现,事先在构造时先确定队列空间大小。
2 LinkedBlockingQueue
无界队列,使用链表实现。
使用BlockingQueue来实现的生产消费模式。
package com.qianfeng.day18;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Work02 {
public static void main(String[] args) {
//创建一个容量为5的线程安全阻塞队列
BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
for(int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
String temp = ""+(int)(Math.random()*100+10);
try {
//通过put方法往队列中存储数据
queue.put(temp);
System.out.println(Thread.currentThread().getName() + "成功将消息" + temp + "加入队列");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName() + "结束工作");
}
},"生产者" + (i+1)).start();;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
long t1 = System.currentTimeMillis();
while(true) {
//通过take方法从队列中取出数据
String message = queue.take();
System.out.println(Thread.currentThread().getName() + "取出消息为" + message);
Thread.sleep(1000);
long t2 = System.currentTimeMillis();
if(t2 - t1 > 10000l) {
break;
}
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"消费者").start();
}
}
3 ConcurrentHashMap 线程安全版的HashMap
原理部分:
- jdk1.7中采用分段锁的方式来保证线程的安全
- jdk1.8中采用CAS的方式来保证线程的安全 CAS:Compare And Swap 比较并交换,这是一种无锁的机制,也是乐观锁的一种实现,synchronized和ReentrantLock都属于悲观锁
CAS这种策略可以保证线程的安全,同时效率也不会受到很大的影响,但是这种方式对CPU资源的消耗比较严重
线程池
问题:如果在一个时刻一下来了很多的任务,这时我们的线程是越多越好吗?
答案:不是的
原因有三点:
- 线程对象本身的创建和销毁就是非常耗费系统的资源
- 线程数量太多造成CPU在多个线程之间来回进行切换,这个成本也是很高的
- 线程的数量不可控,但是内存的资源是有限的
线程池采用的就是一个“共享的思想”来解决上述问题
Executor是线程池的顶级接口
开发中使用比较多的是ExecutorService这个接口
使用举例
package com.qianfeng.day18;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Work01 {
public static void main(String[] args) {
//创建一个有六个线程的线程池
ExecutorService es = Executors.newFixedThreadPool(6);
//创建18个任务交给线程池完成
for(int i = 1; i <= 18; i++) {
int temp = i;
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->"
+ temp + " * " + "6" + " = " + temp*6 + "\t");
}
});
}
//回收线程池,但是等待线程任务结束
es.shutdown();
}
}