java多线程实例
❶ java多线程有几种实现方法线程之间如何同步
一、为什么要线程同步
因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。
二、不同步时的代码
Bank.Java
packagethreadTest;
/**
*@authorww
*
*/
publicclassBank{
privateintcount=0;//账户余额
//存钱
publicvoidaddMoney(intmoney){
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
publicvoidsubMoney(intmoney){
if(count-money<0){
System.out.println("余额不足");
return;
}
count-=money;
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
publicvoidlookMoney(){
System.out.println("账户余额:"+count);
}
}
SyncThreadTest.java
packagethreadTest;
publicclassSyncThreadTest{
publicstaticvoidmain(Stringargs[]){
finalBankbank=newBank();
Threadtadd=newThread(newRunnable(){
@Override
publicvoidrun(){
//TODOAuto-generatedmethodstub
while(true){
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
bank.addMoney(100);
bank.lookMoney();
System.out.println(" ");
}
}
});
Threadtsub=newThread(newRunnable(){
@Override
publicvoidrun(){
//TODOAuto-generatedmethodstub
while(true){
bank.subMoney(100);
bank.lookMoney();
System.out.println(" ");
try{
Thread.sleep(1000);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
}
}
});
tsub.start();
tadd.start();
}
}
余额不足
账户余额:0
余额不足
账户余额:100
1441790503354存进:100
账户余额:100
1441790504354存进:100
账户余额:100
1441790504354取出:100
账户余额:100
1441790505355存进:100
账户余额:100
1441790505355取出:100
账户余额:100
三、使用同步时的代码
(1)同步方法:
即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
修改后的Bank.java
packagethreadTest;
/**
*@authorww
*
*/
publicclassBank{
privateintcount=0;//账户余额
//存钱
(intmoney){
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
(intmoney){
if(count-money<0){
System.out.println("余额不足");
return;
}
count-=money;
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
publicvoidlookMoney(){
System.out.println("账户余额:"+count);
}
}
再看看运行结果:
余额不足
账户余额:0
余额不足
账户余额:0
1441790837380存进:100
账户余额:100
1441790838380取出:100
账户余额:0
1441790838380存进:100
账户余额:100
1441790839381取出:100
账户余额:0
瞬间感觉可以理解了吧。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
(2)同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
Bank.java代码如下:
packagethreadTest;
/**
*@authorww
*
*/
publicclassBank{
privateintcount=0;//账户余额
//存钱
publicvoidaddMoney(intmoney){
synchronized(this){
count+=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
publicvoidsubMoney(intmoney){
synchronized(this){
if(count-money<0){
System.out.println("余额不足");
return;
}
count-=money;
}
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
publicvoidlookMoney(){
System.out.println("账户余额:"+count);
}
}
运行结果如下:
余额不足
账户余额:0
1441791806699存进:100
账户余额:100
1441791806700取出:100
账户余额:0
1441791807699存进:100
账户余额:100
效果和方法一差不多。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
(3)使用特殊域变量(volatile)实现线程同步
a.volatile关键字为域变量的访问提供了一种免锁机制
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
Bank.java代码如下:
packagethreadTest;
/**
*@authorww
*
*/
publicclassBank{
privatevolatileintcount=0;//账户余额
//存钱
publicvoidaddMoney(intmoney){
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
publicvoidsubMoney(intmoney){
if(count-money<0){
System.out.println("余额不足");
return;
}
count-=money;
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
publicvoidlookMoney(){
System.out.println("账户余额:"+count);
}
}
运行效果怎样呢?
余额不足
账户余额:0
余额不足
账户余额:100
1441792010959存进:100
账户余额:100
1441792011960取出:100
账户余额:0
1441792011961存进:100
账户余额:100
是不是又看不懂了,又乱了。这是为什么呢?就是因为volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不适用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。
(4)使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
Bank.java代码修改如下:
packagethreadTest;
importjava.util.concurrent.locks.Lock;
importjava.util.concurrent.locks.ReentrantLock;
/**
*@authorww
*
*/
publicclassBank{
privateintcount=0;//账户余额
//需要声明这个锁
privateLocklock=newReentrantLock();
//存钱
publicvoidaddMoney(intmoney){
lock.lock();//上锁
try{
count+=money;
System.out.println(System.currentTimeMillis()+"存进:"+money);
}finally{
lock.unlock();//解锁
}
}
//取钱
publicvoidsubMoney(intmoney){
lock.lock();
try{
if(count-money<0){
System.out.println("余额不足");
return;
}
count-=money;
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}finally{
lock.unlock();
}
}
//查询
publicvoidlookMoney(){
System.out.println("账户余额:"+count);
}
}
运行效果怎么样呢?
余额不足
账户余额:0
余额不足
账户余额:0
1441792891934存进:100
账户余额:100
1441792892935存进:100
账户余额:200
1441792892954取出:100
账户余额:100
效果和前两种方法差不多。
如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
(5)使用局部变量实现线程同步
Bank.java代码如下:
packagethreadTest;
/**
*@authorww
*
*/
publicclassBank{
privatestaticThreadLocal<Integer>count=newThreadLocal<Integer>(){
@Override
protectedIntegerinitialValue(){
//TODOAuto-generatedmethodstub
return0;
}
};
//存钱
publicvoidaddMoney(intmoney){
count.set(count.get()+money);
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
publicvoidsubMoney(intmoney){
if(count.get()-money<0){
System.out.println("余额不足");
return;
}
count.set(count.get()-money);
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
publicvoidlookMoney(){
System.out.println("账户余额:"+count.get());
}
}
运行效果:
余额不足
账户余额:0
余额不足
账户余额:0
1441794247939存进:100
账户余额:100
余额不足
1441794248940存进:100
账户余额:0
账户余额:200
余额不足
账户余额:0
1441794249941存进:100
账户余额:300
看了运行效果,一开始一头雾水,怎么只让存,不让取啊?看看ThreadLocal的原理:
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,知识名字相同而已。所以就会发生上面的效果。
ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
❷ 多线程的Java
Java对多线程的支持是非常强大的,他屏蔽掉了许多的技术细节,让我们可以轻松的开发多线程的应用程序。
Java里面实现多线程,有2个方法
继承 Thread类 classMyThreadextendsThread{publicvoidrun(){//这里写上线程的内容}publicstaticvoidmain(String[]args){//使用这个方法启动一个线程(newMyThread()).start();}}实现 Runnable接口 {publicvoidrun(){//这里写上线程的内容}publicstaticvoidmain(String[]args){//使用这个方法启动一个线程(newThread(newMyThread())).start();}}一般鼓励使用第二种方法,因为Java里面只允许单一继承,但允许实现多个接口。第二个方法更加灵活。
C++ 11
ISO C++ 11 标准在STL中提供了std::thread类,因此多线程变得非常容易。 #include<thread>usingnamespacestd;voidthreadFunc(){//这里写上线程的内容}intmain(){threadt(threadFunc);//启动线程t.join();//等待线程运行完毕return0;}一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。
线程同步
在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。在Visual Basic中提供了三种方法来完成线程的同步。在Java中可用synchronized关键字。
代码域同步
使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段。
手工同步
可以使用不同的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)创建自己的同步机制。这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和解除由于对共享资源的等待而造成的死锁。
上下文同步
使用SynchronizationAttribute为ContextBoundObject对象创建简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。所有在同一个上下文域的对象共享同一个锁。
虽然多线程能给大家带来好处,但是也有不少问题需要解决。例如,对于像磁盘驱动器这样独占性系统资源,由于线程可以执行进程的任何代码段,且线程的运行是由系统调度自动完成的,具有一定的不确定性,因此就有可能出现两个线程同时对磁盘驱动器进行操作,从而出现操作错误;又例如,对于银行系统的计算机来说,可能使用一个线程来更新其用户数据库,而用另外一个线程来读取数据库以响应储户的需要,极有可能读数据库的线程读取的是未完全更新的数据库,因为可能在读的时候只有一部分数据被更新过。使隶属于同一进程的各线程协调一致地工作称为线程的同步。下面我们只介绍最常用的四种线程同步方式:
临界区(critical section)
事件(event)
互斥量(mutex)
信号量(semaphore)
通过这些类,可以比较容易地做到线程同步。
HT定义
超线程(HT)是英特尔所研发的一种技术,于2002年发布。超线程的英文是HT技术,全名为Hyper-Threading,中文又名超线程。超线程技术原先只应用于Xeon处理器中,当时称为Super-Threading。之后陆续应用在Pentium 4中,将技术主流化。早期代号为Jackson。
特点
通过此技术,英特尔成为第一间公司实现在一个实体处理器中,提供两个逻辑线程。之后的Pentium D纵使不支援超线程技术,但就集成了两个实体核心,所以仍会见到两个逻辑线程。超线程的未来发展,是提升处理器的逻辑线程,英特尔有计划将8核心的处理器,加以配合超线程技术,使之成为16个逻辑线程的产品。
英特尔表示,超线程技术让(P4)处理器增加5%的裸晶面积,就可以换来15%~30%的效能提升。但实际上,在某些程式或未对多执行绪编译的程式而言,超线程反而会降低效能。除此之外,超线程技术亦要操作系统的配合,普通支援多处理器技术的系统亦未必能充分发挥该技术。例如Windows 2000,英特尔并不鼓励使用者在此系统中利用超线程。原先不支援多核心的Windows XPHome Edition却支援超线程技术。
AMDBulldozer“推土机”
据相关消息透露,在HotChips会议上,AMD宣布下一代代号为Bulldozer“推土机”的处理器架构将采用单核多线程技术(multi-threadingtechnology),类似于Intel着名的超线程技术.
AMD没有透露有关其多线程能力和更多的细节,只说推土机处理器将在2011年推出,支持单核多线程技术.不过,AMD的做法和Intel的 HT是不同的,更类似于Sun的同步多线程技术(SimultaneousMulti-Threading),由1个物理核心扩展到4个线程.“推土机扩展出的单核心多线程技术和Intel的超线程采用的是不同方式.”AMD的代表PatConway也证实了这一点. 有趣的是,早些时候AMD还表示暂不考虑SMT或其他多线程技术,并将它应用在当下的处理器中.然而,AMD也认同步多线程是未来处理器产品大幅提升性能的必要特征.
推土机是AMD下一代微架构的处理器,事实上,它将是AMD自2003年后第一次对处理器架构进行重大改变.新一代的处理器将提供远高于现代产品的高性能,同时也加入SSE5指令集.
首款推土机系列桌面处理器代号为Orochi,将会拥有超过4个以上的处理器核心,8M以上的缓存并支持DDR3内存,基于32nm工艺.服务器版处理器代号为Valencia和Interlagos,这两款处理器将会拥有6、8以及12个处理器核心.
AMD至今从未采用过同步多线程(SMT)也就是Intel所称的超线程技术。虽然这样的技术在当年的P4时代显得并无实际用途,但到了2015年为止,越发普及的多线程环境让超线程重新焕发了青春。
发展前景
截止到2014年,以应用环境来看,超线程技术可以让一些特定应用程序显着提速达10到15%。除了Intel的在Nehalem、Atom等中引入的超线程,无论IBM的Power系列,Sun的T1/T2/Rock系列等处理器架构都应用了类似的SMT同步多线程技术,用少量的晶体管带来大幅度的多线程性能提升。
一位AMD工程师日前向媒体坦诚,不支持单核多线程技术让Opteron处理器看起来性能比不上Intel的低端Xeon。据称,AMD内部高层已经承认,没有早早引入此类技术是一项技术选择上的失误。
不过,AMD副总裁兼服务器工作站业务总经理Patrick Patla接受采访时,并没有明确透露单核多线程技术的未来,而是继续重申已经公布的Opteron路线图:“如果你看一下我们路线图以及我们在多线程处理器市场的表现就会知道,我们相信每条线程都拥有完整的核心是目前的最佳选择。2010年,我们就会推出12核处理器,2011年16核。我们相信未来几年内我们就能够完善支持48或64线程环境,让我们来看看2012到2013年会带来些什么吧。”
既然2011年才是16核,那么2012到2013直接跳跃到48甚至64核似乎并不是那么正常。另外,Patrick Patla前面句句都在讲“核”,而到了后面又变成了“线程”,似乎就在暗示到时AMD可能会采纳单核多线程技术。
❸ java 多线程操作hashtable(添加,删除,遍历)
public class TestThread {
public static void main(String[] args){
Map<Integer, Object> tables = new Hashtable<Integer, Object>();
Thread add = new Thread(new ThreadAdd(tables));
Thread del = new Thread(new ThreadDel(tables));
Thread count = new Thread(new ThreadCount(tables));
//启动线程
add.start();
del.start();
count.start();
}
/**
*添加对象线程
*/
private static class ThreadAdd implements Runnable{
private Map<Integer, Object> table;
public ThreadAdd(Map<Integer, Object> tables){
this.table=tables;
}
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++){
table.put(i, new Object());
System.out.println("添加对象,序号为:"+i);
}
}
}
/**
*删除对象线程
*/
private static class ThreadDel implements Runnable{
private Map<Integer, Object> table;
public ThreadDel(Map<Integer, Object> table){
this.table=table;
}
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
table.remove(i);
System.out.println("移除对象,序号为:"+i);
}
}
}
/**
*统计线程
*/
private static class ThreadCount implements Runnable{
private Map<Integer, Object> table;
public ThreadCount(Map<Integer, Object> table){
this.table=table;
}
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("当前队列还剩"+table.size()+"个对象");
}
}
}
}
这是我的写的demo,不知道符合不符合你的意思,大家共同交流共同进步。
❹ 在Java 中多线程的实现方法有哪些,如何使用
1、 认识Thread和Runnable
Java中实现多线程有两种途径:继承Thread类或者实现Runnable接口。Runnable是接口,建议用接口的方式生成线程,因为接口可以实现多继承,况且Runnable只有一个run方法,很适合继承。在使用Thread的时候只需继承Thread,并且new一个实例出来,调用start()方法即可以启动一个线程。
Thread Test = new Thread();
Test.start();
在使用Runnable的时候需要先new一个实现Runnable的实例,之后启动Thread即可。
Test impelements Runnable;
Test t = new Test();
Thread test = new Thread(t);
test.start();
总结:Thread和Runnable是实现java多线程的2种方式,runable是接口,thread是类,建议使用runable实现java多线程,不管如何,最终都需要通过thread.start()来使线程处于可运行状态。
2、 认识Thread的start和run
1) start:
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到spu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
2) run:
run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
3、 线程状态说明
线程状态从大的方面来说,可归结为:初始状态、可运行状态、不可运行状态和消亡状态,具体可细分为上图所示7个状态,说明如下:
1) 线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口,但不管怎样,当我们new了thread实例后,线程就进入了初始状态;
2) 当该对象调用了start()方法,就进入可运行状态;
3) 进入可运行状态后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态;
4) 进入运行状态后case就比较多,大致有如下情形:
·run()方法或main()方法结束后,线程就进入终止状态;
·当线程调用了自身的sleep()方法或其他线程的join()方法,就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配时间片;
·当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被锁牢(synchroniza,lock),将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入可运行状态,等待OS分配CPU时间片;
·当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池,等待获取锁标记。
·当线程调用stop方法,即可使线程进入消亡状态,但是由于stop方法是不安全的,不鼓励使用,大家可以通过run方法里的条件变通实现线程的stop。
❺ 在Java 中多线程的实现方法有哪些,如何使用
Java多线程的创建及启动
Java中线程的创建常见有如三种基本形式
1.继承Thread类,重写该类的run()方法。
复制代码
1 class MyThread extends Thread {
2
3 private int i = 0;
4
5 @Override
6 public void run() {
7 for (i = 0; i < 100; i++) {
8 System.out.println(Thread.currentThread().getName() + " " + i);
9 }
10 }
11 }
复制代码
复制代码
1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 for (int i = 0; i < 100; i++) {
5 System.out.println(Thread.currentThread().getName() + " " + i);
6 if (i == 30) {
7 Thread myThread1 = new MyThread(); // 创建一个新的线程 myThread1 此线程进入新建状态
8 Thread myThread2 = new MyThread(); // 创建一个新的线程 myThread2 此线程进入新建状态
9 myThread1.start(); // 调用start()方法使得线程进入就绪状态
10 myThread2.start(); // 调用start()方法使得线程进入就绪状态
11 }
12 }
13 }
14 }
复制代码
如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。
2.实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
复制代码
1 class MyRunnable implements Runnable {
2 private int i = 0;
3
4 @Override
5 public void run() {
6 for (i = 0; i < 100; i++) {
7 System.out.println(Thread.currentThread().getName() + " " + i);
8 }
9 }
10 }
复制代码
复制代码
1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 for (int i = 0; i < 100; i++) {
5 System.out.println(Thread.currentThread().getName() + " " + i);
6 if (i == 30) {
7 Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
8 Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
9 Thread thread2 = new Thread(myRunnable);
10 thread1.start(); // 调用start()方法使得线程进入就绪状态
11 thread2.start();
12 }
13 }
14 }
15 }
复制代码
相信以上两种创建新线程的方式大家都很熟悉了,那么Thread和Runnable之间到底是什么关系呢?我们首先来看一下下面这个例子。
复制代码
1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 for (int i = 0; i < 100; i++) {
5 System.out.println(Thread.currentThread().getName() + " " + i);
6 if (i == 30) {
7 Runnable myRunnable = new MyRunnable();
8 Thread thread = new MyThread(myRunnable);
9 thread.start();
10 }
11 }
12 }
13 }
14
15 class MyRunnable implements Runnable {
16 private int i = 0;
17
18 @Override
19 public void run() {
20 System.out.println("in MyRunnable run");
21 for (i = 0; i < 100; i++) {
22 System.out.println(Thread.currentThread().getName() + " " + i);
23 }
24 }
25 }
26
27 class MyThread extends Thread {
28
29 private int i = 0;
30
31 public MyThread(Runnable runnable){
32 super(runnable);
33 }
34
35 @Override
36 public void run() {
37 System.out.println("in MyThread run");
38 for (i = 0; i < 100; i++) {
39 System.out.println(Thread.currentThread().getName() + " " + i);
40 }
41 }
42 }
复制代码
同样的,与实现Runnable接口创建线程方式相似,不同的地方在于
1 Thread thread = new MyThread(myRunnable);
那么这种方式可以顺利创建出一个新的线程么?答案是肯定的。至于此时的线程执行体到底是MyRunnable接口中的run()方法还是MyThread类中的run()方法呢?通过输出我们知道线程执行体是MyThread类中的run()方法。其实原因很简单,因为Thread类本身也是实现了Runnable接口,而run()方法最先是在Runnable接口中定义的方法。
1 public interface Runnable {
2
3 public abstract void run();
4
5 }
我们看一下Thread类中对Runnable接口中run()方法的实现:
复制代码
@Override
public void run() {
if (target != null) {
target.run();
}
}
复制代码
也就是说,当执行到Thread类中的run()方法时,会首先判断target是否存在,存在则执行target中的run()方法,也就是实现了Runnable接口并重写了run()方法的类中的run()方法。但是上述给到的列子中,由于多态的存在,根本就没有执行到Thread类中的run()方法,而是直接先执行了运行时类型即MyThread类中的run()方法。
3.使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
看着好像有点复杂,直接来看一个例子就清晰了。
复制代码
1 public class ThreadTest {
2
3 public static void main(String[] args) {
4
5 Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象
6 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
7
8 for (int i = 0; i < 100; i++) {
9 System.out.println(Thread.currentThread().getName() + " " + i);
10 if (i == 30) {
11 Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
12 thread.start(); //线程进入到就绪状态
13 }
14 }
15
16 System.out.println("主线程for循环执行完毕..");
17
18 try {
19 int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
20 System.out.println("sum = " + sum);
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 } catch (ExecutionException e) {
24 e.printStackTrace();
25 }
26
27 }
28 }
29
30
31 class MyCallable implements Callable<Integer> {
32 private int i = 0;
33
34 // 与run()方法不同的是,call()方法具有返回值
35 @Override
36 public Integer call() {
37 int sum = 0;
38 for (; i < 100; i++) {
39 System.out.println(Thread.currentThread().getName() + " " + i);
40 sum += i;
41 }
42 return sum;
43 }
44
45 }
复制代码
首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:
1 public class FutureTask<V> implements RunnableFuture<V> {
2
3 //....
4
5 }
1 public interface RunnableFuture<V> extends Runnable, Future<V> {
2
3 void run();
4
5 }
于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕..”则很可能是在子线程循环中间输出。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕..”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?
原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。
上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。
你好,本题已解答,如果满意
请点右下角“采纳答案”。