黑马程序员--高新技术之多线程
2014-01-04 15:54
246 查看
------- android培训、java培训、期待与您交流!
----------
回顾一下至今为止接触的所有的java面试题,大概可以分为两部分:一部分是,对于一些关于java基础的某个知识点的理解,比如说说明一下面向对象的思想,类的加载机制等,另外一部分也是比较实际比较棘手的一部分,就是给你一个项目,让你回去做,
而这些项目的实现大都用到了java多线程技术,比如说张老师所讲的两个7k面试题,交通灯和银行调度系统,都是借助了多线程的技术,
所以个人感觉java多线程这部分的内容还是很重要的,至少对于解决面试来说是重要的。所以这篇学习日记记录的是jdk1.5以后出现的
关于java多线程的新技术。
一、多线程技术
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。
当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,
再把任务交给内部某个空闲的线程,这就是封装。记住,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
线程池的分类:
固定尺寸的线程池、可变尺寸连接池
jdk中对于线程池提供的api:
Executor接口:是用来执行Runnable任务的
方法:
execute(Runnable command):执行Ruannable类型的任务
ExecutorService 接口 ExecutorService继承了Executor的方法,并提供了执行Callable任务和中止任务执行的服务
方法:
void shutdown() --在完成已提交的任务后关闭服务,不再接受新任务
Future<T> submit(Runnable task) --提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
Future<T> submit(Callable<T> task) --提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
实现这个接口的类有:
ThreadPoolExecutor --它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。
ScheduledExecutorService 接口,可安排在给定的延迟后运行或定期执行的命令(用作定时器)
方法:
ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit) --创建并执行在给定延迟后启用的一次性操作。
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay, long period,TimeUnit unit)
--解读参数:
command - 要执行的任务
initialDelay - 首次执行的延迟时间
period - 连续执行之间的周期
unit - initialDelay 和 period 参数的时间单位
实现类
ScheduledThreadPoolExecutor --继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
callable(Runnable task): 将Runnable的任务转化成Callable的任务
newSingleThreadExecutor: 产生一个ExecutorService对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
newCachedThreadPool(): 产生一个ExecutorService对象,这个对象带有一个线程池,线程池的大小会根据需要调整,线程执行完任务后返回线程池,供执行下一次任务使用。
newFixedThreadPool(int poolSize):产生一个ExecutorService对象,这个对象带有一个大小为poolSize的线程池,若任务数量大于poolSize,任务会被放在一个queue里顺序执行。
newSingleThreadScheduledExecutor:产生一个ScheduledExecutorService对象,这个对象的线程池大小为1,若任务多于一个,任务将按先后顺序执行。
newScheduledThreadPool(int poolSize): 产生一个ScheduledExecutorService对象,这个对象的线程池大小为poolSize,若任务数量大于poolSize,任务会在一个queue里等待执行
代码实现: package com.itheima.newthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPoolTest { public static void main(String[] args) { //用3个大小的固定线程池去执行10个内部循环10次就结束的任务,线程池内只有3个线程在执行, //固定线程池下的其他任务一直再等待 //ExecutorService service = Executors.newFixedThreadPool(3); //有多少任务就分配多少个线程 ExecutorService service = Executors.newCachedThreadPool(); for(int i=1;i<=10;i++){ final int sequence = i; //仔细品味runnable对象放到循环里面和外面的区别,为了让每个对象有自己独立的编号 service.execute(new Runnable(){ public void run() { try{Thread.sleep(200);}catch(Exception e){} for(int j=1;j<=5;j++){ System.out.println(Thread.currentThread().getName() + " is serving " + sequence + " task:" + "loop of " + j); } } }); } /* 用下面这句代码来说明上面的代码是在提交任务,并且所有的任务都已经提交了,但任务是什么时候执行的,则是由线程池调度的! */ System.out.println("all task have committed!"); //注意与service.shutdownNow()的区别。 service.shutdown(); /* 测试线程池的定时器*/ //定义一个定时器的线程池,线程池的大小是1,即只有池内只有一个定时器 ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(1); //给定时器线程池分配的任务 scheduledService.scheduleAtFixedRate( new Runnable(){ public void run() { System.out.println("bomb!!!"); }}, 5, 1, TimeUnit.SECONDS); } }
二、带有返回结果的线程
jdk中的api的支持:
Callable 接口 -- Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。
但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
方法:
V call() 计算结果,如果无法计算结果,则抛出一个异常 。相似于run()方法
Future 接口 -- 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。
计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。
方法:
V get(long timeout, TimeUnit unit) --如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
FutureTask 类 :是 Future 的一个实现,Future 可实现 Runnable,所以可通过 Executor 来执行。
代码实现:
package com.itheima.newthread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class ReturnThread { public static void main(String[] args) throws Exception { ExecutorService th = Executors.newSingleThreadExecutor(); Future<String> future = th.submit(new Callable<String>() { @Override public String call() throws Exception { // 可以查询数据库中的内容返回 return "data from ds"; } }); //获取线程得返回值 String str = future.get(); //打印结果 System.out.println("thread returned data:"+str); } }三、jdk1.5对锁的新实现
在说明这个问题之前,首先我想说一下在5.0之前锁定的锁定的功能是由Synchronized关键字来实现的,这样做存在几个问题:
每次只能对一个对象进行锁定。若需要锁定多个对象,编程就比较麻烦,一不小心就会出现死锁现象。
如果线程因拿不到锁定而进入等待状况,是没有办法将其打断的。
而jdk1.5对锁的实现却很好的避免这个问题:
jdk中对新锁的api支持:
Lock 接口 -- 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
方法:
lock(): --请求锁定,如果锁已被别的线程锁定,调用此方法的线程被阻断进入等待状态。
unlock():取消锁定,需要注意的是Lock不会自动取消,编程时必须手动解锁。
实现类:ReentrantLock 无特别的方法
ReadWriteLock 接口:
为了提高效率有些共享资源允许同时进行多个读的操作,但只允许一个写的操作,比如一个文件,只要其内容不变可以让多个线程同时读,不必做排他的锁定,
排他的锁定只有在写的时候需要,以保证别的线程不会看到数据不完整的文件。ReadWriteLock可满足这种需要。ReadWriteLock内置两个Lock,一个是读的Lock,一个是写的Lock。
多个线程可同时得到读的Lock,但只有一个线程能得到写的Lock,而且写的Lock被锁定后,任何线程都不能得到Lock。ReadWriteLock提供的方法有:
readLock(): 返回一个读的lock
writeLock(): 返回一个写的lock, 此lock是排他的。
Condition 接口:
有时候线程取得lock后需要在一定条件下才能做某些工作,比如说经典的Producer和Consumer问题,Consumer必须在篮子里有苹果的时候才能吃苹果,否则它必须暂时放弃对篮子的锁定,等到Producer往篮子里放了苹果后再去拿来吃。而Producer必须等到篮子空了才能往里放苹果,否则它也需要暂时解锁等Consumer把苹果吃了才能往篮子里放苹果。
在Java 5.0以前,这种功能是由Object类的wait(), notify()和notifyAll()等方法实现的,在5.0里面,这些功能集中到了Condition这个接口来实现,Condition提供以下方法:
await():使调用此方法的线程放弃锁定,进入睡眠直到被打断或被唤醒。
signal(): 唤醒一个等待的线程
signalAll():唤醒所有等待的线程
代码实现: package com.itheima.newthread; import java.util.Random; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockTest { public static void main(String[] args) { //将对象声明为final的形式,以便于在匿名内部中调用(即后面的new Thread(){}) final Queue queue = new Queue(); //循环产生6个线程,用于读写数据 for(int i=0;i<3;i++) { new Thread(){ public void run(){ while(true){ queue.get(); } } }.start(); new Thread(){ public void run(){ while(true){ queue.put(new Random().nextInt(10000)); } } }.start(); } } } //将线程的共享数据和相关的操作封装到类中 class Queue{ //共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。 private Object data = null; //定义读写锁 ReadWriteLock rwl = new ReentrantReadWriteLock(); //读数据的方法 public void get(){ rwl.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to read data!"); //随机休眠 Thread.sleep((long)(Math.random()*1000)); System.out.println(Thread.currentThread().getName() + "have read data :" + data); } catch (InterruptedException e) { e.printStackTrace(); }finally{ //将关闭锁的语句放入到finally语句块中,是因为上面的try语句块中可能会出现异常而导致不能解锁 rwl.readLock().unlock(); } } public void put(Object data){ //写数据的方法 rwl.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to write data!"); Thread.sleep((long)(Math.random()*1000)); this.data = data; System.out.println(Thread.currentThread().getName() + " have write data: " + data); } catch (InterruptedException e) { e.printStackTrace(); }finally{ rwl.writeLock().unlock(); } } }
下面在给出一个缓存系统设计的代码
package com.itheima.newthread; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class CacheDemo { //定义一个map集合用于存储需要缓存的数据 private Map<String, Object> cache = new HashMap<String, Object>(); //定义一个读写锁 private ReadWriteLock rwl = new ReentrantReadWriteLock(); //定义从缓存中获取数据的方法 public Object getData(String key){ rwl.readLock().lock(); Object value = null; try{ value = cache.get(key); if(value == null){ //先解读锁,在开写锁,这样即使是多个线程执行到这时, //也只是一个线程会获得写锁 rwl.readLock().unlock(); rwl.writeLock().lock(); try{ if(value==null){ value = "aaaa";//实际失去queryDB(); } }finally{ rwl.writeLock().unlock(); } rwl.readLock().lock(); } }finally{ rwl.readLock().unlock(); } return value; } }
新的锁的总结:
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。
两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,
可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
在等待 Condition 时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,
因为 Condition 应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,
但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。
一个锁内部可以有多个Condition,即有多路等待和通知,可以参看jdk1.5提供的Lock与Condition实现的可阻塞队列的应用案例,
从中除了要体味算法,还要体味面向对象的封装。在传统的线程机制中一个监视器对象上只能有一路等待和通知,要想实现多路等待和通知,
必须嵌套使用多个同步监视器对象。(如果只用一个Condition,两个放的都在等,一旦一个放的进去了,那么它通知可能会导致另一个放接着往下走。)
四、接下来是关于java多线程的几个工具类
Semaphore 类 --可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,
构造方法:
Semaphore(int permits) --创建具有给定的许可数和非公平的公平设置的 Semaphore
方法:
void acquire() --从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
void release() --释放一个许可,将其返回给信号量。
简单的代码实现(主要演示其使用):
package com.itheima.newthread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; public class SemaphoreTest { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); final Semaphore sp = new Semaphore(3); for(int i=0;i<10;i++){ Runnable runnable = new Runnable(){ public void run(){ try { //记录有一个线程来到,阻塞 sp.acquire(); } catch (InterruptedException e1) { e1.printStackTrace(); } System.out.println("线程" + Thread.currentThread().getName() + "进入,当前已有" + (3-sp.availablePermits()) + "个并发"); try { Thread.sleep((long)(Math.random()*10000)); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程" + Thread.currentThread().getName() + "即将离开"); sp.release(); //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元 System.out.println("线程" + Thread.currentThread().getName() + "已离开,当前已有" + (3-sp.availablePermits()) + "个并发"); } }; service.execute(runnable); } } }
CountDownLatch 类: --CountDownLatch是个计数器,它有一个初始数,等待这个计数器的线程必须等到计数器倒数到零时才可继续
构造方法:
CountDownLatch(int count) --构造一个用给定计数初始化的 CountDownLatch
方法:
void await():使调用此方法的线程阻断进入等待
void countDown(): 倒计数,将计数值减1
long getCount(): 得到当前的计数值
简单代码实现: package com.itheima.newthread; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class CyclicBarrierTest { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); final CyclicBarrier cb = new CyclicBarrier(3); for(int i=0;i<3;i++){ Runnable runnable = new Runnable(){ public void run(){ try { Thread.sleep((long)(Math.random()*10000)); System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点1,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候")); cb.await(); Thread.sleep((long)(Math.random()*10000)); System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点2,当前已有" + (cb.getNumberWaiting()+1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候")); cb.await(); Thread.sleep((long)(Math.random()*10000)); System.out.println("线程" + Thread.currentThread().getName() + "即将到达集合地点3,当前已有" + (cb.getNumberWaiting() + 1) + "个已经到达," + (cb.getNumberWaiting()==2?"都到齐了,继续走啊":"正在等候")); cb.await(); } catch (Exception e) { e.printStackTrace(); } } }; service.execute(runnable); } service.shutdown(); } }
Exchanger 类:让两个线程可以互换信息。用于实现两个人之间的数据交换,每个人在完成一定的事务后想与对方交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据。
构造方法:
Exchanger() --创建一个新的 Exchanger
方法:V exchange(V x) 等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
代码实现: package com.itheima.newthread; import java.util.concurrent.Exchanger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExchangerTest { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); final Exchanger exchanger = new Exchanger(); service.execute(new Runnable(){ public void run() { try { String data1 = "zxx"; System.out.println("线程" + Thread.currentThread().getName() + "正在把数据" + data1 +"换出去"); Thread.sleep((long)(Math.random()*10000)); String data2 = (String)exchanger.exchange(data1); System.out.println("线程" + Thread.currentThread().getName() + "换回的数据为" + data2); }catch(Exception e){ } } }); service.execute(new Runnable(){ public void run() { try { String data1 = "lhm"; System.out.println("线程" + Thread.currentThread().getName() + "正在把数据" + data1 +"换出去"); Thread.sleep((long)(Math.random()*10000)); String data2 = (String)exchanger.exchange(data1); System.out.println("线程" + Thread.currentThread().getName() + "换回的数据为" + data2); }catch(Exception e){ } } }); } }
阻塞队列:
阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,
直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。
BlockingQueue 接口,阻塞队列的顶层接口
方法:
void put(E e) --将指定元素插入此队列中,将等待可用的空间 阻塞式的
E take() -- 获取并移除此队列的头部,在元素变得可用之前一直等待 ,阻塞式的
实现类有:
ArrayBlockingQueue, LinkedBlockingDeque, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue
代码实现: package com.itheima.newthread; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class BlockingQueueTest { public static void main(String[] args) { final BlockingQueue queue = new ArrayBlockingQueue(3); for(int i=0;i<2;i++){ new Thread(){ public void run(){ while(true){ try { Thread.sleep((long)(Math.random()*1000)); System.out.println(Thread.currentThread().getName() + "准备放数据!"); queue.put(1); System.out.println(Thread.currentThread().getName() + "已经放了数据," + "队列目前有" + queue.size() + "个数据"); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } new Thread(){ public void run(){ while(true){ try { //将此处的睡眠时间分别改为100和1000,观察运行结果 Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "准备取数据!"); queue.take(); System.out.println(Thread.currentThread().getName() + "已经取走数据," + "队列目前有" + queue.size() + "个数据"); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } }
同步集合:
传统集合类在并发访问时的问题说明,见附件
传统方式下用Collections工具类提供的synchronizedCollection方法来获得同步集合,分析该方法的实现源码。
传统方式下的Collection在迭代集合时,不允许对集合进行修改。
用空中网面试的同步级线程题进行演示
根据AbstractList的checkForComodification方法的源码,分析产生ConcurrentModificationException异常的原因。
Java5中提供了如下一些同步集合类:
通过看java.util.concurrent包下的介绍可以知道有哪些并发集合
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
ps:终于完了,java多线程的新技术和我的最后一篇学习日记,说句心里话,通过写这10篇的博客,
我已经深感到程序员的不容易与辛苦。有人说程序员工资高,但那却是我们艰苦拼搏的结果,我们受得起!!
继续奋斗吧!!
相关文章推荐
- 黑马程序员---张老师高新技术之多线程及java5中多线程新技术
- 黑马程序员——高新技术---Java基础-多线程2
- 黑马程序员——Java高新技术——多线程
- 黑马程序员0902_多线程部分
- 黑马程序员_高新技术学习笔记(第六、七章)
- [置顶] 黑马程序员 -- 多线程
- 黑马程序员-----多线程之间的通信(生产者消费者)
- [黑马程序员]多线程_join使用
- 黑马程序员——多线程(一)
- 黑马程序员-java多线程
- 黑马程序员——Java之多线程
- 黑马程序员----多线程之生产者消费者问题
- 黑马程序员—JAVA高新技术之JDK1.5的新特性
- 黑马程序员-高新技术(枚举)
- 【黑马程序员】张孝祥Java高新技术_装箱拆箱、枚举、反射
- 黑马程序员--多线程--
- 黑马程序员-高新技术-反射基石-Class类
- 黑马程序员java之多线程
- 黑马程序员--JAVA基础复习之多线程(一)概念与创建方法
- 黑马程序员--高新技术<一>eclipse开发工具及相关知识加强