转载请申明出处:http://blog.csdn.net/xmxkf/article/details/9945499
01. 传统线程技术回顾
创建线程的两种传统方式:
1、在Thread子类覆盖的run方法中编写运行代码;
涉及一个以往知识点,能否在run方法声明上抛出InterruptedException异常?以便省略run方法内部对Thread.sleep()语句的try……catch处理?
答:不能,因为Thread类的run方法没有抛异常,子类覆盖run方法时也不能抛异常。
2、在传递给Thread对象的Runnable对象的run方法中编写代码;
Thread thread1 =new Thread(){
@Override
publicvoid run()
{
while(true)
{
try {
Thread.sleep(500);
} //不能让run方法throws异常,因为父类run没有抛异常
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());//this.getName()也行
}
}
};
thread1.start();
//创建Thread对象,并传入一个Runnble对象到构造方法
Thread thread2 =
new Thread(new Runnable(){
@Override
publicvoid run()
{
while(true)
{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此处就不能用this.getName,因为this代表Runnable子类对象
System.out.println(Thread.currentThread().getName());
}
}
});
这两种方式有什么区别?为什么通常人们采用第二种(实现Runnable接口)?
都是创建一个线程,只是第二种方式更加体现面向对象的思想;创建一个线程,线程要运行的代码封装到Runnable对象中(run方法)。
3、总结:查看Thread对象的run方法的源代码,可以看到其实这两种方式都是在调用Thread对象的run方法,如果Thread类的run方法没有被覆盖(不是继承Thread)并且为该Thread对象设置了一个Runnable对象,该run方法会调用Runnable对象的run方法。
Public void run()
{
if(target != null)
{//当Thread构造方法中传递了Runnable对象就执行Runnable的run方法。
target.run();
}
}
4、问题:如果在Thread子类覆盖的run方法中编写了运行代码,也为Thread子类对象传递了一个Runnable对象,那么,线程运行时的执行代码是子类的run方法的代码还是Runnable对象的run方法的代码?
答:(Thread子类的run),因为子类覆盖了父类的run方法,子类对象调用start方法时,会到自己的类中找run方法,如果没有找到就会执行父类的run方法,而父类(Thread)的run方法中会去调用Runnable对象的run方法。只要覆盖了Thread中的run方法,就不会执行Runnable对象的run方法。
涉及到一个以往的知识点:匿名内部类对象的构造方法如何调用父类的非默认构造方法。
5、多线程机制会提高程序的运行效率吗?为什么会有多线程下载呢?
答:多线程不会提高程序的运行效率甚至更慢,因为cpu同一时间只能执行一个线程,如果要在几个线程之间切换需要时间。
计算机并没有快,只是抢了服务器的带宽。因为多(10)个线程下载,服务器以为多(10)个客户端在下载,所以给每个客服端提供20k,合起来就200k。
02. 传统定时器技术回顾
java.util
类 Timer
一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
void |
|
void |
schedule(TimerTask task, long delay, long period) |
java.util
类 TimerTask 由 Timer安排为一次执行或重复执行的任务。实现了Runnable接口。
boolean |
cancel() |
abstract void |
run() |
定时器的应用:
//两个时间间隔发生,方式一:定义计数器
class MyTimerTaskextends TimerTask
{
@Override
publicvoid run()
{
count= (count+1)%2;
System.out.println("bombing!");
new Timer().schedule(new
MyTimerTask(), 2000+2000*count);
}
}
new Timer().schedule(new MyTimerTask(), 2000); //启动炸弹,2秒后
03. 传统线程互斥技术
线程的互斥与同步通信:
1、线程安全问题可以用银行转账来解释;两个业务同时拿到余额,一个存一个取。
2、使用synchronized代码块及其原理; 任意对象都可作为锁
3、使用synchronized方法; this
4、分析静态方法所使用的同步监视器对象是什么?
方法所在类文件字节码
总结:如果多个线程执行的代码要求同步,需要加上synchronized;即一个线程没有将synchronized修饰的代码执行完时,其他线程不能进来执行。这里要注意,需要同步的代码(同一份)可以定义在不同的块中,但是为了同步,必须保证使用同一把锁。
线程的同步互斥的图文解说:
线程1 线程2 synchronized(obj)的作用主要是其中obj的作用
| |
------------------------------- a和b这两段代码被两个线程执行时要互斥;
a……… b…… 要互斥的代码必须用synchronized代码块包围。
第二组代码要互斥,即c与d互斥,但a与c不互斥
线程3 线程4 cpu可同时进入a和c中进行替换
| |
------------------------------- 用什么进行互斥分组?就是synchronized中的obj
c……… d…… 值不同,而分为不同的互斥组。
04. 传统线程同步通信技术
5、wait与notify实现线程间的通信;
用面试宝典中的子线程循环10次和主线程循环5次,两者交替运行50的例子讲解;
为了观察程序的运行结果,可以在eclipse中设置运行对话框,让结果重定向到一个文本文件,然后用UE去查看该文件中的特殊的行号处是否正好对应了线程的切换点;
经验:要用到共同数据(包括同步锁)或同步算法的若干个方法应该归在同一个类身上,这种设计正好体现了高类聚和程序的健壮性。
package cn.itcast.heima;
/**
*
面试题:子线程循环10次,接着主线程循环10次;
*
接着又回到子线程循环10次,然后再让主线程循环10此。
*
交替循环50次
* @author Administrator
*
*/
publicclass TraditionalThreadCommunication
{
publicstaticvoid main(String[] args)
{
final Business business =new Business(); //为什么要加final
//子线程
new Thread(new Runnable(){
publicvoid run()
{
for(int i=1;i<=50;i++)
{
business.sub(i); //每次循环,执行十次,然后等待唤醒main
}
}
}).start();
for(int i=1;i<=50;i++)
{
business.main(i);
}
}
//此处不加static,在main方法中new内部类对象会出错,
//是为什么?因为静态只能访问静态?
staticclass Business
{
privatebooleanbShouldSub =true;
publicsynchronizedvoid sub(int
i)
{
while(!bShouldSub) //不该子线程运行了就让子线程等
{
try {
this.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int j=1;j<=10;j++)
{
System.out.println("sub Thread sequece of"+j+",loop
of "+i);
}
bShouldSub=false;
this.notify(); //唤醒主线程
}
publicsynchronizedvoid main(int
i)
{
while(bShouldSub)
{
try {
this.wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int j=1;j<=20;j++)
{
System.out.println("main Thread "+j+",loop
of "+i);
}
bShouldSub=true;
this.notify(); //唤醒子线程
}
}
}
05. 线程范围内共享变量的概念与作用
publicclass ThreadScopeShareData
{
//存放线程和数据的映射关系
privatestatic Map<Thread, Integer>threadData
=new HashMap<Thread, Integer>();
publicstaticvoid main(String[] args)
{
for(int i=0;i<2;i++) //产生两个线程
{
new Thread(new Runnable(){
publicvoid run()
{
int data =new Random().nextInt();
System.out.println(Thread.currentThread().getName()+" get data
"+data);
threadData.put(Thread.currentThread(), data);
new A().get();
new B().get();
}
}).start();
}
}
06. ThreadLocal类及应用技巧
ThreadLocal实现线程范围的共享变量:
1、每个线程调用全局ThreadLocal对象的set方法,就相当于往其内部的map中增加一条记录,key分别是各自的线程,value是各自的set方法传进去的值。在线程结束时可以调用ThreadLocal.clear()方法,这样会更块释放内存,不调用也可以,因为线程结束后也可以自动释放相关的ThreadLocal变量。
2、ThreadLocal的应用场景:
订单处理包一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中;
银行转账包含一系列操作:把转出账户的余额减少,把转入账户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库对象,转入和转出操作的代码分别是两个不同的账户对象的方法。
例如Strut2的ActionContext,同一段代码被不同的线程调用运行的,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在那个模块中getContext方法,拿到的都是同一个。
实例:
publicclass ThreadLocalTest
{
//定义一个全局变量的ThreaadLocal变量,只能存放一个数据
privatestatic ThreadLocal<Integer>x
=new ThreadLocal<Integer>();
//定义一个全局变量的ThreaadLocal变量,存放一个对象,线程的所有数据封装在这个对象中
privatestatic ThreadLocal<MyThreadScopeData>myThreadScopeData
=
new ThreadLocal<MyThreadScopeData>();
publicstaticvoid main(String[] args)
{
for(int i=0;i<2;i++) //产生两个线程
{
new Thread(new Runnable(){
publicvoid run()
{
int data =new Random().nextInt();
System.out.println(Thread.currentThread().getName()+" get data
"+data);
x.set(data); //向ThreadLocal变量x中存储一个随机值
MyThreadScopeData.getThreadInstance().setName("name"+data);
MyThreadScopeData.getThreadInstance().setAge(data);
//调用其他类的方法,获取x的值,多个类在同一线程中获取到的值是相同的
new A().get();
new B().get();
}
}).start();
}
}
staticclass A
{
publicvoid get()
{
//读取这个ThreadLocal变量的值
int data =x.get();
System.out.println("A from "+Thread.currentThread().getName()+"
get data: "+data);
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("A from "+Thread.currentThread().getName()
+" getMyData: "+ myData.getName()+","+myData.getAge());
}
}
staticclass B
{
publicvoid get()
{
int data =x.get();
System.out.println("B from "+Thread.currentThread().getName()+"
get data: "+data);
MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("B from "+Thread.currentThread().getName()
+" getMyData: "+ myData.getName()+","+myData.getAge());
}
}
//将线程的局部变量封装成对象,这样ThreadLocal中存放一个对象就等于存了几个数据
staticclass MyThreadScopeData
{
//单例设计模式
private MyThreadScopeData(){}
publicstatic/*synchronized*/ MyThreadScopeData
getThreadInstance()
{
MyThreadScopeData instance =map.get();
if(instance ==null)
{
instance =new MyThreadScopeData();
map.set(instance);
}
return instance;
}
//private static MyThreadScopeData instance = null;
privatestatic ThreadLocal<MyThreadScopeData>map
=
new ThreadLocal<MyThreadScopeData>();
private Stringname;
privateintage;
public String getName() {
returnname;
}
publicvoid setName(String name) {
this.name = name;
}
publicint getAge() {
returnage;
}
publicvoid setAge(int
age) {
this.age = age;
}
}
}
07. 多个线程之间共享数据的方式探讨
1、如果每个线程执行的代码相同,可使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做。
2、如果每个线程执行的代码不同,这时就需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
A、将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象,每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
B、将这些Runnable对象作为某个类中的内部类,共享数据作为这个外部类中的成员变量。每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥通信,作为内部类的各个Runnable对象调用外部类的这些方法。
C、上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现他们之间的同步互斥和通信。
08. java5原子性操作类的应用
1、了解java.util.concurrent.atomic包;
看atomic包文档页下面的介绍,可以对基本数据AtomicInteger,对数组中的基本数据AtomicIntegerArray,对类中的基本数据AtomicIntegerFieldUpdater等进行操作。
09. java5线程并发库的应用
Java5中的线程并发库:
2、看java.util.concurrent包及子包的API帮助文档;
看concurrent包的帮助文档,对并发库中涉及的内容有一个总体上的介绍。
类Executors
此包中所定义的Executor
、ExecutorService
、ScheduledExecutorService
、ThreadFactory
和Callable
类的工厂和实用方法
3、了解java.util.concurrent.lock包
线程池:
线程池的概念与Executors类的应用
A、理解:在线程池编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池拿到任务后,它就在内部招有无空闲的线程,再把任务交给内部某个空闲线程,这就是封装。记住,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池中提交多个任务。
B、创建固定大小的线程池:
ExecutorService threadPool = Executors.newFixedThreadPool(3);
创建缓存线程池:
ExecutorService threadPool = Executors.newCachedThreadPool();
创建单一线程池:(如何实现线程死掉后重新启动一个线程)
ExecutorService threadPool = Executors.newSingleThreadExecutor();
C、关闭线程池:
void |
shutdown() |
shutdownNow() |
D、用线程池启动定时器:
调用ScheduleExecutorService的schedule方法,返回的SchedulFuture对象可以取消任务。
支持间隔重复任务的定时方式,不直接支持绝对定时方式,需要转换成相对时间方式。
实例:
Executors.newScheduledThreadPool(3).scheduleAtFixedRate(
new Runnable(){@Override
publicvoid run() {
System.out.println(Thread.currentThread()+" bombing!");
//Thread[pool-2-thread-1,5,main] bombing!
}},
1, //10秒后炸
2, //以后每隔2秒炸一次
TimeUnit.SECONDS);
10. Callable与Future的应用
java.util.concurrent
接口 Callable<V>
返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做call 的方法。Callable 接口类似于Runnable
,两者都是为那些其实例可能被另一个线程执行的类设计的。但是Runnable
不会返回结果,并且无法抛出经过检查的异常。
接口 Future<V>表示异步计算的结果
//创建线程池
ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); CompletionService<Integer> completionService =
new ExecutorCompletionService(threadPool2);
for(int i=1;i<=10;i++) //提交10个任务,任务会返回结果
{
finalint seq = i;
completionService.submit(new Callable<Integer>() {
public Integer call()throws
Exception {
Thread.sleep(new Random().nextInt(5000));//等待5秒以内随机
return seq;
}
});
}
//获取任务执行结果
for(int i=0;i<10;i++)
{
try {
//获取并移除表示下一个已完成任务的 Future,如果目前不存在这样的任务,则等待。
System.out.println(completionService.take().get());
} catch (Exception e) {
e.printStackTrace();
}
}
11. java5的线程锁技术
java.util.concurrent.locks
接口 LockLock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作。
锁本身也应该是一个对象,两个线程执行的代码片段要实现同步互斥,他们必须使用用一个Lock对象。锁是上在代表要操作的资源的类的内部方法中,而不是线程代码中。
接口 Condition
Condition
将 Object
监视器方法(wait
、notify
和 notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意Lock
实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock
替代了synchronized
方法和语句的使用,Condition
替代了 Object 监视器方法的使用。
12. java5读写锁技术的妙用
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥。这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,就上写锁。总之,读的时候上读锁,写的时候上写锁。
一个面试题:写一个缓存类。
import java.util.*;
import java.util.concurrent.locks.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
写一个缓存系统,
*
获取数据,当系统中有这个数据时,直接返回这个数据,、
*
当没有这个数据时,去数据库中找,找到后缓存起来,
*
下次获取这个数据时就直接返回。
* @author Administrator
*
*/
publicclass CacheDemo
{
private Map<String, Object>cache =new
HashMap<String, Object>();
publicstaticvoid main(String[] args)
{
}
private ReadWriteLockrw1 =new
ReentrantReadWriteLock();
public Object getData(String key)
{
rw1.readLock().lock(); //多个线程可同时读
Object value =
null;
try{
value =
cache.get(key);
if(value ==null) //缓存中没有这个对象
{
rw1.readLock().unlock();
rw1.writeLock().lock(); //只有一个线程去从数据库拿
try{
//如果3个线程同时来读,到rw1.writeLock().lock();
//有两个线程等待了,当第一个线程已经从数据库中拿数据
//避免另外两个线程醒来时又去拿,还要判断一下
if(value ==null)
{
value ="aaaa"; //实际是去queryDB
}
}finally{
rw1.writeLock().unlock();
}
rw1.readLock().lock();
}
}finally{
rw1.readLock().unlock();
}
return value;
}
}