在我们的并行程序中,避免不了某些写成要预先规定好的顺序执行,例如:先新增后修改,先买后卖,先进后出,对于这些场景,使用JUC的Conditon对象再合适不过了。
JUC中提供了Condition对象,用于让指定线程等待与唤醒,按预期顺序执行。它必须和ReentrantLock重入锁配合使用
Condition用于代替wait()/notify()方法
wait和notify是属于Object的,可以对线程唤醒(notify只能随机唤醒等待的线程,而Condition可以唤醒指定线程,这有利于更好的控制并发程序)
Condition核心方法:
await():阻塞当前线程,直到signal唤醒
signal():唤醒被await的线程,从中断处继续执行
signalAll():唤醒所有被await阻塞的线程(不太常用)
通过使用ReentrantLock和Condition的使用让线程有顺序的执行(有规划的、等待、唤醒的过程)
代码案例:
public class ConditionSample {
public static void main(String[] args) {
final ReentrantLock lock = new ReentrantLock();//Condition必须配合ReentrantLock来使用
final Condition c1 = lock.newCondition();//创建Condition
final Condition c2 = lock.newCondition();
final Condition c3 = lock.newCondition();
new Thread(new Runnable() {
public void run() {
lock.lock();//加锁
try {
c1.await();//阻塞当前线程,只有调用c1.signal的时候,线程继续激活执行
Thread.sleep(1000);
System.out.println("粒粒皆幸苦");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放
}
}
}).start();
new Thread(new Runnable() {
public void run() {
lock.lock();//加锁
try {
c2.await();//阻塞当前线程,只有调用c1.signal的时候,线程继续激活执行
Thread.sleep(1000);
System.out.println("谁知盘中餐");
c1.signal();//线程t1唤醒继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放
}
}
}).start();
new Thread(new Runnable() {
public void run() {
lock.lock();//加锁
try {
c3.await();//阻塞当前线程,只有调用c1.signal的时候,线程继续激活执行
Thread.sleep(1000);
System.out.println("汗滴禾下土");
c2.signal();//线程t2唤醒继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放
}
}
}).start();
new Thread(new Runnable() {
public void run() {
lock.lock();//加锁
try {
Thread.sleep(1000);
System.out.println("锄禾日当午");
c3.signal();//t3线程继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();//释放
}
}
}).start();
}
}
public class FutureCallableSample {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
//创建线程池
for(int i = 2;i<=10000 ;i++){
Computer c = new Computer();
c.setNum(i);
//Future是对用于计算的线程的监听,因为计算是在其它线程中进行的,所以这个返回结果是异步的
Future fu = threadPool.submit(c);//将c对象提交给线程池,如果有空闲线程立即执行call方法
try {
Boolean result = fu.get();//用于获取返回值,如果线程内部的call没有计算完毕,则进入等待状态,直到计算完成
if(result){
System.out.println(c.getNum());
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
threadPool.shutdown();
}
}
如何保证既能线程安全,又可以有一定效率
线程安全—-并发容器
ArrayList —-CopyOnWriteArrayList —写复制列表
HashSet —–CopyOnWriteArraySet —写复制集合
HashMap —-ConcurrentHashMap —分段锁映射
public class CopyOnWriteArrayListSample {
public static void main(String[] args) {
List list = new ArrayList();
for(int i =0; i<1000;i++){
list.add(i);
}
Iterator it = list.iterator();
while(it.hasNext()){
Integer i = (Integer)it.next();
list.remove(i);
}
System.out.println(list);
}
}
代码会报错:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at com.wanghaoxin.threadpool.CopyOnWriteArrayListSample.main(CopyOnWriteArrayListSample.java:15)
对于ArrayList默认采用连续存储,但是会有并发问题,边读取边删除产生异常
CopyOnWriteArrayList采用即可解决这个问题:
public class CopyOnWriteArrayListSample {
public static void main(String[] args) {
/* List list = new ArrayList();
for(int i =0; i<1000;i++){
list.add(i);
}
Iterator it = list.iterator();
while(it.hasNext()){
Integer i = (Integer)it.next();
list.remove(i);
}
System.out.println(list);*/
//写复制列表
List list = new CopyOnWriteArrayList();
for(int i =0; i<1000;i++){
list.add(i);
}
Iterator it = list.iterator();
while(it.hasNext()){
Integer i = (Integer)it.next();
list.remove(i);
}
System.out.println(list);
}
}
写复制列表:CopyOnWriteArrayList并发原理:
它通过副本解决并发问题
多个线程各执一份副本,采用副本的方式,再复制的过程中,不同线程是同步的
查看源码得知:采用可重入锁
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
ConcurrentHashMap
Segment——分段锁,提高了批量同步的性能
HashMap线程不安全的,输出值总是小于5000,但是如果代码中修改为HashTable则输出值为5000(HashTable中所有方法都是synchronized 只允许同一时间同一线程修改)
为了解决效率低下的问题,产生了ConcurrentHashMap,输出值始终为5000—表示线程安全
public class ConcurrentHashMapSample {
private static int user = 100;//同时模拟的并发用户访问数量 --为1的话看不出来效果
//private static int user = 10;
private static int dowloadCounts = 5000;//用户的真实下载数
private static HashMap count= new HashMap();//计数器
public static void main(String[] args) {
//调度器,jdk1.5之后引入current对于并发的支持
ExecutorService executor = Executors.newCachedThreadPool();
//信号量 ,用于模拟并发用户数
final Semaphore semaphore = new Semaphore(user);
for(int i =0;ifinal int index = i;
//通过多线程模拟多个用户访问的下载次数
executor.execute(new Runnable() {
@Override
public void run() {
try{
semaphore.acquire();
count.put(index, index);
semaphore.release();
}catch(Exception e){
e.printStackTrace();
}
}
});
}
try {
//延迟主线程结束--让for循环中代码执行完毕
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.size());
}
}
ConcurrentHashMap实现原理:
采用分段锁Segment机制
HashTable:所有操作方法都是同步的,其他线程必须等待
这样效率很低
ConcurrentHashMap把区域分为一个个的很小区域,segment,不同线程访问不同的数据,只要不是同一段内都可以操作,但是如果操作的数据在同一个段内,这样需要线程排队
这样效率很快,(极限)16倍,把原来的存储空间进行分段加锁处理,段的长度都是2的n次方
Atomic包是java.util.concurrent下的另一个专门为线程安全设计的java包,包含多个原子操作类
Atomic包:
代码:
public class AtomicIntegerSample {
private static int user = 10;//同时模拟的并发用户访问数量
//private static int user = 10;
private static int dowloadCounts = 5000;//用户的真实下载数
private static AtomicInteger count= new AtomicInteger(0);
public static void main(String[] args) {
//调度器,jdk1.5之后引入current对于并发的支持
ExecutorService executor = Executors.newCachedThreadPool();
//信号量 ,用于模拟并发用户数
final Semaphore semaphore = new Semaphore(user);
for(int i =0;i//通过多线程模拟多个用户访问的下载次数
executor.execute(new Runnable() {
@Override
public void run() {
try{
semaphore.acquire();
add();
semaphore.release();
}catch(Exception e){
e.printStackTrace();
}
}
});
}
try {
//延迟主线程结束--让for循环中代码执行完毕
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
/* private synchronized static void add(){
count++;
}*/
private static void add(){
count.getAndIncrement();//count++
}
//此处并没有用synchronized和锁机制
}
CAS算法:
锁是用来做并发的最简单的机制,当然其代价也是很高的,独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致所有需要锁的线程刮起,等待持有锁的线程释放锁
所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
其中CAS(比较与交换 Compare And Swap ),是一种有名的无锁算法
比较的是期望值与实际操作的结果
Atomic的应用场景:
虽然基于CAS的线程安全机制很好很搞笑,但是要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合于一些锁粒度比较小型:例如计数器这样的需求用起来才更有效果,否则也不会有锁的存在了
对于大量数据操作反而会损耗性能,因为做了很多次