线程4-多线程和并发编程2-第一阶段-第十八天

本文介绍了Java并发编程中的ReentrantLock可重入锁,包括其可中断性、条件变量和公平锁特性,并展示了如何在生产者消费者模式中使用。此外,还讨论了线程安全的ArrayList替代方案,如CopyOnWriteArrayList和BlockingQueue,并解释了它们的适用场景。最后,提到了ConcurrentHashMap和线程池ExecutorService的概念及其在并发编程中的重要性。

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集合经过测试是线程不安全的!!!

解决方案有三种:

  1. 使用线程安全版的ArrayList Vector ,Vector 在解决线程安全问题的时候采用的是加锁的方式,对程序执行效率影响太大,不推荐使用
  2. 使用Collections工具类中提供的静态工具方法Collections.synchronizedxxx(Collection/Map)进行线程安全处理并返回一个新的集合,这种方式本质上和第一种没有区别,都是采取加锁的方式来保证线程的安全
  3. 在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

原理部分:

  1. jdk1.7中采用分段锁的方式来保证线程的安全
  2. 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();
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值