黑马程序员--高新技术之多线程

来源:互联网 发布:深圳联通移动网络 编辑:程序博客网 时间:2024/04/29 19:17

------- 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>() {@Overridepublic 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篇的博客,
我已经深感到程序员的不容易与辛苦。有人说程序员工资高,但那却是我们艰苦拼搏的结果,我们受得起!!
继续奋斗吧!!
0 0