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

------- 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篇的博客,
我已经深感到程序员的不容易与辛苦。有人说程序员工资高,但那却是我们艰苦拼搏的结果,我们受得起!!
继续奋斗吧!!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黑马程序员多线程练习题主要包括两个问题。第一个问题是如何控制四个线程在打印log之前能够同时开始等待1秒钟。一种解决思路是在线程的run方法中调用parseLog方法,并使用Thread.sleep方法让线程等待1秒钟。另一种解决思路是使用线程池,将线程数量固定为4个,并将每个调用parseLog方法的语句封装为一个Runnable对象,然后提交到线程池中。这样可以实现一秒钟打印4行日志,4秒钟打印16条日志的需求。 第二个问题是如何修改代码,使得几个线程调用TestDo.doSome(key, value)方法时,如果传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果。一种解决方法是使用synchronized关键字来实现线程的互斥排队输出。通过给TestDo.doSome方法添加synchronized关键字,可以确保同一时间只有一个线程能够执行该方法,从而实现线程的互斥输出。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [黑马程序员——多线程10:多线程相关练习](https://blog.csdn.net/axr1985lazy/article/details/48186039)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值