黑马程序员_JAVA多线程(二)

来源:互联网 发布:手机淘宝. 编辑:程序博客网 时间:2024/05/10 01:20
------- android培训、java培训、期待与您交流! ----------

四、ThreadLocal:线程本地变量

很多人对ThreadLocal存在误解,认为使用ThreadLocal就可以避免锁,其实这是个误区。ThreadLocalsynchronizedvolatile不具备可比性。ThreadLocal的某些特性在某些特定场景下可以和synchronized比较,但大多数情况下两者的使用场景是不同的。

ThreadLocal从另一个角度来解决多线程编程中对共享数据的并发操作,为每个线程维护一个局部变量的副本,各个副本之间相互隔离,某个线程对数据的修改不会影响到另一个线程。

 ThreadLocal的使用场景:

1)    线程个数有限,每个线程都对应一个公共服务类,该公共服务类是非线程安全的。示例如下:

       SHA256是个公共类,提供将给定的字符串做哈希编码,返回编码之后的字节数组给调用端:

public class SHA256 {private static MessageDigest messageDigest;// 定义密码静态变量.static {// 初始化信息摘要类try {messageDigest = MessageDigest.getInstance("SHA-256");//获取产生SHA-256算法的实例} catch (Exception e) {messageDigest = null;}}public byte[] encrypt(byte[] source) {try {messageDigest.update(source);///处理信息return messageDigest.digest();//产生哈希字符} catch (Exception e) {return null;}}}

当多个线程同时调用 encrypt接口的时候,发现会存在并发问题,编码的结果有时是空、有的是非预期值。对于客户端线程来说,对应的是相同的算法,使用ThreadLocal定义可以解决多线程访问的并发问题。

修改之后的示例:

public class SHA256 {// 定义密码静态变量private static ThreadLocal<MessageDigest> messageDigest = new ThreadLocal<MessageDigest>();static {// 初始化信息摘要类try {messageDigest.set(MessageDigest.getInstance("SHA-256"));} catch (Exception e) {messageDigest = null;}}public byte[] encrypt(byte[] source) {try {messageDigest.get().update(source);return messageDigest.get().digest();} catch (Exception e) {return null;}}}

使用ThreadLocal之后,不需要对encrypt加锁,和修改之前的代码相比性能没有任何损失;这就是典型的“空间换效率”的做法。为每个线程维护本地变量的拷贝相,比与同步块,内存开销会增加,但是,通常情况下这种开销都是可以接受的;

五、ReadWriteLock:细粒度的读写锁

使用ReentrantReadWriteLocksynchronized更复杂一些,但是,ReentrantReadWriteLock完全可以替代synchronized,尽管它并不一定都能带来性能的提升。

ReentrantReadWriteLock的优点:

1)     在读操作远远超过写操作的时候,可以极大提升系统的并发性;

2)     锁得粒度更细,在某些场景下使用,可以提升系统的并发能力;

ReentrantReadWriteLock的使用相关条件:

1)     获取Reader lock的条件:写入锁没有分配给其它任何线程,写入是自由的;

2)     获取write lock的条件:write lock没有分配给其它任何线程,同时Reader lock没有分配给任何其它的线程,读取是自由的;

3)     读写锁的重入:读和写锁都可以重入(锁的递归),例如,某个线程获取写锁之后,可以在不释放写锁的情况下继续获取写锁,示例代码如下:

ReentrantReadWriteLock readWriterLock = new ReentrantReadWriteLock(true);//使用给定的公平策略创建一个新的 ReentrantReadWriteLockreadWriterLock.writeLock().lock();//TODOreadWriterLock.writeLock().lock();//TODOreadWriterLock.writeLock().unlock();redWriterLock.writeLock().unlock();

4)      锁的降级:先获取写锁,然后获取读锁,随后释放写锁,最后释放读锁;但是不允许类似的锁的升级,即先获取读锁,然后获取写锁,再释放读锁;


读写锁的典型应用场景:

1)     使用读写锁实现的Sequence

public class Sequence {private long seqIndex;ReentrantReadWriteLock readWriterLock = new ReentrantReadWriteLock(true);public long getCurrentVal() {try {readWriterLock.readLock().lock();//获取读锁return seqIndex;} finally {readWriterLock.readLock().unlock();//释放读锁}}public long getNextVal() {try {readWriterLock.writeLock().lock();return ++seqIndex;} finally {readWriterLock.writeLock().unlock();}}}

2)      细粒度的控制,提高并发性能:

class CachedData {Object data;volatile boolean cacheValid;ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();void processCachedData() {rwl.readLock().lock();//首先通过获取读锁来判断缓存的数据是否可用,如果不可用,紧接着尝试获取写锁,if (!cacheValid) {//手动进行锁的升级rwl.readLock().unlock();   // 必须先释放读锁,才能获得写锁rwl.writeLock().lock();//释放读锁之后并不一定能得到写锁,但无论如何,肯定会有一个线程最终可以获取写锁,这主要取决于时序和公平策略;if (!cacheValid) { //获取写锁之后紧接着又做了一次缓存数据是否可用的检验,因为释放读锁之后不一定可以获取写锁,有可能在这期间已经有另一个线程获取了写锁并对缓存数据进行了更新data = ...cacheValid = true;}// 锁的降级,不能等释放写锁之后再获取读锁,一旦释放写锁之后不一定能获取读锁,如果这时其它线程获取了写锁,此时就会处于等待状态,降级就会失败rwl.readLock().lock();  // 在没有释放写锁的情况之下,可以获得读锁,再释放写锁rwl.writeLock().unlock(); //写锁虽然释放,读锁继续把持}use(data);rwl.readLock().unlock();}}

上面的例子摘自JDK自带的Demo,感觉这个例子非常好,充分展示了如何通过读写锁更细粒度的来解决并发问题,同时又能最大限度的提升系统的并发性。

虽然上面这个例子也可以使用synchronized来做缓存数据更新的并发保护,但是性能会大打折扣。

3)    读取频率远远超过更新频率,需要使用读写锁来提升并发性:

List<Tast> tasks = new ArrayList<Task>();//任务列表final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();//读写锁、public boolean registerTask(Task task){//注册任务try{rwLock.writeLock().lock();return tasks.add(task);}finally{rwLock.writeLock().unlock();}}public boolean unRegisterTask(Task task){//注销任务try{rwLock.writeLock().lock();return tasks.remove(task);}finally{rwLock.writeLock().unlock();}}public boolean sendMsg2Task(int hashID){rwLock.readLock().lock();Task destTask = tasks.getTaskByHashID(hashID);rwLock.readLock().unlock();destTask.doBusiness();}

如果注册和取消注册调用频率非常低,大多数情况都是调用sendMsg2Task接口用于消息处理,此时使用读写锁系统的并发性能远远高于synchronized

读写锁的使用注意事项:

1)       对于生产者、消费者模型,由于生产者和消费者都需要不断的更新消息列表,因此适合使用synchronized而不是读写锁

2)         推荐在finally语句中释放锁,因为我们不能保证100%锁会释放,特别是发生运行时异常或其它异常时

六、线程组和线程池

线程池有多种,本质上来讲,线程池简化了自己维护多线程和消息队列的工作。通过使用线程池,我们甚至不需要自己考虑同步操作和wait()notify()等,大大简化了多线程开发的难度。

 

线程池的分类:

1)         固定大小的线程池:

ExecutorService executors = ExecutorService. newFixedThreadPool(8);//创建大小为8的线程池//执行任务线程:executors. execute(Runnable command);

2)         线程个数为1的单线程线程池:

ExecutorService executor = ExecutorService. newSingleThreadExecutor();

3)         通用线程池:ThreadPoolExecutor,该线程池中提供了非常灵活的能力,例如活动线程数和最大线程数可配等。

ThreadPoolExecutor的使用示例:

int corePoolSize = 6;//线程池核心线程数int maxPoolSize = 10;//线程池最大线程数int keepAliveTime = 5;//空闲线程生命周期int cacheQueueCapacity = 8000;//任务队列大小ThreadPoolExecutor threadExecutor = null;//线程池public void init(){//初始化线程池,Task实现了Runnable接口this.threadExecutor = new ThreadPoolExecutor(this.corePoolSize, this.maxPoolSize, this.keepAliveTime, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(this.cacheQueueCapacity),new ThreadPoolExecutor.AbortPolicy()//使用AbortPolicy策略的队列当任务满之后,会发生RejectedExecutionException异常        );}

将每个消息封装成一个Task,将Task放入线程池任务列表中,由线程池负责消息的调度

public void execut(SubReq req){SgpTask task = new SgpTask(req);//Task实现了Runnable接口if (getQueueSize() < this.cacheQueueCapacity){try {// 将单条消息封装成任务线程后放入线程池中执行this.threadExecutor.execute(task);} catch (RejectedExecutionException e){// 当任务队列已满的时候,缓存任务}}else{// 如果任务队列已满,将后到的任务存入内存数据库,当系统闲时再发送// 当任务队列已满的时候,缓存任务}}

线程池的核心线程和最大线程数可以配置,线程池会根据任务队列的拥塞情况在指定范围内动态调整工作线程数,这样可以最大限度的利用系统资源。

上面的例子中没有看到队列、锁、notifywait,这就是JDKconcurrent包提供给我们简化多线程编程的线程池






 

 

 

 

 


------- android培训、java培训、期待与您交流! ----------