您的位置:首页 > 职场人生

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

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篇的博客,

我已经深感到程序员的不容易与辛苦。有人说程序员工资高,但那却是我们艰苦拼搏的结果,我们受得起!!

继续奋斗吧!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息