1.初步了解“进程”、“线程”、“多线程”
说到多线程,大多都会联系到“进程”和“线程”。那么这两者到底是什么呢?两者又有什么关系呢?
先来看进程的定义:
进程:进程是操作系统结构的基础;是一次程序的执行;是一个程序机器数据在处理机上顺序执行时所发生的活动;是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
初看这个定义会觉得抽象,难以理解,但可以打开Windows任务管理器,那些“xxx.exe”就是一系列“进程”,所以可以把进程理解为正在操作系统中运行的程序,进程是受操作系统管理的基本运行单元。
那什么是线程呢?线程可以理解成是进程中独立运行的子任务。比如说,QQ.exe运行时就有很多的子任务在同时运行,你可以在与别人聊天时,同时向对方传送文件或者接收来自对方的文件,也可以同时打开多个对话窗口。这里边的QQ.exe就是进程,而聊天窗口、文件传输等都是线程。
再来说说多线程。上面的例子就已经涉及到多线程,它能让你用电脑同时做多件事情(至少给我们的感觉是这样)。那么使用多线程有什么优点呢?使用多任务操作系统,如Windows,可以最大限度地利用CPU的空闲时间来处理其他任务,比如一边让打印机打印数据,一边使用Word编辑文档。而CPU在这些任务之间不停地切换,由于切换速度非常快,所以给我们的感受就是这些任务是似乎是在同时运行。
为了更深理解多线程的优势,再举个例子:现在有2个完全独立且互不相干的任务,任务1、任务2,任务1运行时需要等待远程服务器的返回数据,以便后期的处理, 所以任务1执行完时用时10秒,任务2执行时间非常短,比如说几毫秒,假如这2个任务运行在单任务的环境,且任务1先运行,那么,即使任务2运行时间非常短,但也得等到任务1执行完毕之后,任务2才能执行,也就是说在任务1等待远程服务器返回数据这段时间内,CPU一直处于空闲状态,而任务2又在等待CPU资源,这样的结果就会造成CPU资源的利用率大幅度降低;而如果运行在多任务的环境,在任务1等待的时候,CPU可以去执行任务2及其他更多的任务,那么CPU资源的利用及能得到提升,也能大幅度节约任务运行的总时间。
2.使用多线程
一个进程运行时,至少会有一个线程在运行,在java中也是这样,比如调用main()方法的线程就是这样,它是有JVM创建的。下面看一个例子:
public class Test { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
运行这段代码,能看到控制台输出“main”的字样。Thread.currentThread()能获取当前正在运行的线程对象,这里是指“main”这个线程,所以调用getName()方法会返回“main”,即线程的名称。顺便提一下,线程的默认名称的格式是:“Thread-x”,其中“x”是一个大于等于0的整数,可以通过调用setName()方法对线程的名称进行设置,例子如下:
public class Run { public static void main(String[] args) { new Thread(){ @Override public void run(){ System.out.println(this.getName()); } }.start(); Thread t = new Thread(){ @Override public void run(){ System.out.println(this.getName()); } }; t.setName("MyThread"); t.start(); new Thread(){ @Override public void run(){ System.out.println(this.getName()); } }.start(); } }
结果如下:
在java中,实现多线程编程的方式主要有两种,一种是继承Thread类,另一种是实现Runnable接口。
在学习如何创建新线程前,先来看看Thread类的结构,如下:
public class Thread implements Runnable
从上面的源代码中可以发现,Thread类其实也是实现了Runnable接口,它们之间具有多态的关系。接下来看看如何创建新线程。创建包“t1”,代码如下:
第一种,通过继承Thread类:
创建MyThread类
public class MyThread extends Thread { @Override public void run(){ System.out.println(Thread.currentThread().getName()); } }
创建运行测试类:
public class Run { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); System.out.println("运行结束"); } }
运行结果如下:
从运行结果来看,MyThread.java类中的run方法执行的时间比较晚,这也说明在使用多线程技术时,代码运行结果与代码执行顺序或者调用顺序是无关的。另外,同时start多个线程时,执行start()方法的顺序不代表线程启动的顺序。比如,同时创建5个线程,并依次调用start()方法。创建包“startSeq”,示例代码如下:
MyThread类:
public class MyThread extends Thread { @Override public void run(){ System.out.println(this.getName()); } }
Run类:
public class Run { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); MyThread t4 = new MyThread(); MyThread t5 = new MyThread(); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } }
可能出现的结果如下:
"Thread-"后面的数字越小,说明该线程越先被创建,其中实例t1最先创建,t5最晚创建,但输出结果说明实例t5中的run()方法却不是最晚被执行的。
接下来看第二种创建线程的方式,实现Runnable接口:
创建包“t2”,代码如下:
创建一个实现Runnable接口的类MyRunnable,代码如下:
public class MyRunnable implements Runnable{ @Override public void run() { System.out.println("运行中"); } }
如何使用这个MyRunnable.java类呢?先来看一下Thread.java的构造函数,如下图所示:
上图中,有2个构造函数可以传递Runnable接口,Thread(Runnable target)和Thread(Runnable target,String name),说明构造函数支持传入一个实现了Runnable接口的对象。运行类代码如下:
public class Run { public static void main(String[] args) throws InterruptedException { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); Thread.sleep(100); System.out.println("运行结束"); } }
运行结果:
这是构造函数Thread(Runnable target)的使用,而构造函数Thread(Runnable target,String name)的使用与第一个大同小异,只是多了name这个参数,该参数是用来给该线程设置名称的。与调用线程的setName()的作用一样。
上文中提到,Thread类也是实现了Runnable接口,那也意味着Thread(Runnable target)不光可以传入Runnable接口对象,也可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。
3. 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。
(1)不共享情况,创建“t3”包,示例代码如下:
MyThread.java类
public class MyThread extends Thread { private int count = 5; public MyThread(String name) { this.setName(name);//设置线程名称 } @Override public void run(){ super.run(); while(count>0){ count--; System.out.println("由 " + this.getName() + " 计算,count=" + count); } } }
运行类Run.java
public class Run { public static void main(String[] args) { MyThread a = new MyThread("A"); MyThread b = new MyThread("B"); MyThread c = new MyThread("C"); a.start(); b.start(); c.start(); } }
运行结果如下图:
由上图可以看到,创建的3个线程对各自拥有的变量count的运算,并不影响到其他线程。
(2)共享数据的情况
共享数据的情况就是多个线程可以访问同一个变量,比如在实现抢票功能,票的总数是一定,只要有一个用户(线程)抢到票,总数就减1,知道为0。
创建包“t4”,示例代码如下:
MyThread.java类
public class MyThread extends Thread { private int count = 5; @Override synchronized public void run(){ super.run(); count--; System.out.println("由 " + this.getName()+" 计算,count="+count); } }
Run.java类
public class Run { public static void main(String[] args) { MyThread mythread = new MyThread(); Thread a = new Thread(mythread,"A"); Thread b = new Thread(mythread,"B"); Thread c = new Thread(mythread,"C"); Thread d = new Thread(mythread,"D"); Thread e = new Thread(mythread,"E"); a.start(); b.start(); c.start(); d.start(); e.start(); } }
结果如下:
从上图可以看到,线程A线程B打印出的count的值都是3,说明A和B同时对count进行处理,产生了“非线程安全”问题。
在某些JVM中,i--的操作分成3步:
1)取得原有i值;
2)计算i-1;
3)对i进行赋值。
在这3步中,如果有多个线程同时访问,那么一定会出现非线程安全问题。比如,线程A执行到第2步的时候,CPU分配的时间片用完了,必须让出CPU资源,这时线程A虽然已经对i进行了减1操作,但还没有对i进行赋值更没更新到主内存中,而这时线程B抢到了CPU资源,从主内存取到i的值还是未改变的,所以结果就有可能出现重复打印的情况。
而我们想要得到的结果却是不能重复的,且是依次递减的。这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作,我们用到了关键字“synchronized”。更改代码如下:
public class MyThread extends Thread { private int count = 5; @Override // public void run(){ synchronized public void run(){ count--; System.out.println("由 " + Thread.currentThread().getName()+" 计算,count="+count); } }
重新运行程序,就不会出现值一样的情况了,如上图所示。
通过在run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronize里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。
上文提到一个术语“非线程安全”。非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
4. isAlive() 方法
方法isAlive()的功能是判断当前的线程是否处于活动状态。活动状态就是线程已经启动,即还未调用start()方法,且尚未终止,这个“终止”可能是run()方法执行完了或调用了stop()方法强制终止线程,但stop()这个方法已经被停用,具体原因,后文会说。
另外,在使用isAlive()方法时,由于Thread.currentThread()和this的差异,可能会出现不一样的结果,当然,你可能会说那this.currentThread()呢?其实跟Thread.currentThread()是一样的,因为currentThread()是Thread类的静态方法。
run()方法中,Thread.currentThread()得到的对象是当前的线程;而this指的是当前对象,可分为2种情况,一种是指当前线程(通过继承Thread的方式创建线程类),与Thread.currentThread()一样都是指当前线程对象;第2种情况是指当前对象,而不是线程对象,什么意思呢?当前对象是指实现Runnable接口的实例或者就是一个线程对象(继承Thread类),然后把这个实例以构造参数的方式传递给Thread对象,也就是上文说的创建Thread对象的第二种方式,代码示例如下,其中MyThread.java实现了Runnable接口,此时,MyThread类的run()方法中的this指mythread实例,而Thread.currentThread()才是指当前线程,所以this.isAlive()返回的是false,只有Thread.currentThread()返回的才是true。具体代码,请读者自己实现。
... MyThread mythread = new MyThread(); Thread thread = new Thread(mythread); ...
5. sleep()方法
方法sleep(long millis)的作用是在指定的毫秒内让当前“正在执行的线程”休眠(暂停执行),参数millis是大于0的long型,单位为毫秒。注意与wait()方法区别开,具体的区别待后文详述。使用方法如下:
... Thread.sleep(5000);//线程会暂停5秒,然后再接着执行下面的代码 ...
6. getId()方法
作用是取得线程的唯一标识,这个笔者没怎么用过,至少在这本书中很少出现。
7. 停止线程
java中有3种方法可以终止正在运行的线程,如下:
1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止;
2)使用stop方法强行终止线程(但不推荐使用这个方法,因为跟suspend和resume一样,都是已作废的方法,使用它们可能产生不接预料的结果);
3)使用interrupt()方法。使用interrupt()方法,效果并不像for+break语句,马上就停止循环。调用interrupt()方法仅仅是在当前线程打了一个停止的标记,并不是真的停止线程。
创建包“t11”,代码如下:
MyThread类
public class MyThread extends Thread{ @Override public void run(){ for(int i=0;i<500000;i++){ System.out.println("i="+(i+1)); } } }
Run类:
public class Run { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
结果如下:
可以看到,调用interrupt()方法后,线程并没有被停止,而是知道run方法执行完才正常退出(调用stop()的话,肯定就可以,读者可以自己测试)。
正如前面所说,interrupt()方法,只是给当前的线程添加一个标记,并没有真正去停止线程。那么要怎么利用interrupt方法正确停止线程呢?在介绍之前,先来看一下如何判断线程的状态是不是停止的。Java的SDK中,Thread.java类提供了两种方法,如下:
1)interrupted():测试当前线程是否已经中断;方法的声明如下:
public static boolean interrupted()
2)isInterrupted():测试线程是否已经中断。
public boolean isInterrupted()
这两个方法有什么区别呢?先来看interrupted()方法的解释:测试当前线程是否已经中断,当前线程是指运行this.interrupted()方法的线程。先来看个例子,创建包“t12”,代码如下:
MyThread类:
public class MyThread extends Thread { @Override public void run(){ //此处暂时先别调用sleep方法来使线程暂停,后文会介绍 while(true){ } } }
Run1类:
public class Run1 { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); myThread.interrupt(); System.out.println("当前线程:"+Thread.currentThread().getName()); System.out.println("线程停止1:"+myThread.interrupted()); System.out.println("线程停止2:"+myThread.interrupted()); } }
运行结果如下:
为什么会出现都是false呢,明明已经调用interrupt()方法了?因为当前的线程是main主线程,而调用interrupt()方法的是myThread线程,即标记的是myThread线程。再创建Run2类,代码如下:
public class Run2 { public static void main(String[] args) { System.out.println("当前线程:"+Thread.currentThread().getName()); Thread.currentThread().interrupt(); System.out.println("线程停止1:"+Thread.interrupted()); System.out.println("线程停止2:"+Thread.interrupted()); } }
运行结果如下:
从上述结果来看,方法interrupted()的确判断出但前线程是否是停止状态。但为什么第二个布尔值是false呢?来看一下官方对它的解释:
测试当前线程是否已经中断。线程的中断状态由该方法清除。
换句话说,如果连续两次调用该方法,则第二次调用将返回false(再次中断除外)。
介绍完interrupted(),再来看isInterrupted()方法。创建Run3类,代码如下:
public class Run3 { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.setName("myThread"); myThread.start(); myThread.interrupt(); Thread.currentThread().interrupt(); System.out.println("当前线程:"+Thread.currentThread().getName()); System.out.println(Thread.currentThread().getName()+"线程停止1:"+Thread.currentThread().isInterrupted()); System.out.println(Thread.currentThread().getName()+"线程停止2:"+Thread.currentThread().isInterrupted()); System.out.println(myThread.getName()+"线程停止:"+myThread.isInterrupted()); } }
可以看到,方法isInterrupted()并未清除标记,只是纯粹的判断线程是否有中断标记。另外,isInterrupted()方法判断的不是当前线程,而是哪个线程调用该方法,判断的就是哪个线程。因为isInterrupted()方法是普通方法而不是静态方法,上文Run1.java中的代码片段:myThread.interrupted(),是不规范的,会报警告,因为interrupted()是一个静态方法,之所以这样写是为了更容易理解interrupted()方法。
了解了interrupt()、interrupted()和isInterrupted(),读者可能会想:要想停止一个线程,只要用一个循环,一直判断线程是否被标记了中断标记,只要被标记了,就break循环,达到中断线程的目的。但这样做有多个弊端,第一,你必须一直死循环监听线程是否被打了标记;第二,如果循环后面如果还有代码,还是会继续运行的。读者可以自行测试。
那到底如何停止线程呢?有如下几种方法:
1)异常法;
2)在沉睡中停止(①先沉睡后停止 ②先停止后沉睡);
3)暴力停止(stop()方法,已弃用);
4)使用return
首先来看异常法:
创建包“t13_1”,类MyThread.java代码如下:
public class MyThread extends Thread { @Override public void run(){ try { for(int i=0;i<500000;i++){ if(this.isInterrupted()){ System.out.println("被打了中断标记,要退出了"); throw new InterruptedException(); } System.out.println("i="+(i+1)); } System.out.println("我在for下面!"); } catch (InterruptedException e) { System.out.println("进入MyThread类run方法中的catch!"); e.printStackTrace(); } } }
Run.java代码如下:
public class Run { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
运行结果如下:
可以看到,线程确实被中断了,而原本要打印到500000才结束的,现在才打印了70000+,而且for循环后的内容也没被执行,而是捕获到了for循环抛出的InterruptedException异常直接进入catch代码块,真正实现了线程的终止。
第二种,在沉睡中停止。“沉睡”即线程调用了sleep()方法,进入了沉睡。该沉睡分两种情况,一种是调用sleep()方法后才打上标记(先沉睡后停止),第二种是调用sleep()方法前就被打了标记(先停止后沉睡)。
先来看“先沉睡后停止”的情况,创建包“t14”。MyThread类代码如下:
public class MyThread extends Thread { @Override public void run(){ try { System.out.println("run begin! "+System.currentTimeMillis()); Thread.sleep(20000); System.out.println("run end"); } catch (InterruptedException e) { System.out.println("在沉睡中被停止!进入catch!this.isInterrupted():"+this.isInterrupted()+" "+System.currentTimeMillis()); e.printStackTrace(); } } }
Run类代码:
public class Run { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); Thread.sleep(200); thread.interrupt(); } }
结果如下:
从打印结果来看,线程thread并没sleep满20秒,“run begin”也没被打印出来,证明线程确实被终止了。
如果在sleep状态下停止某一线程,会抛出InterruptedException,并且清除停止状态,使之变成false。
再来看“先停止后沉睡”,创建包“t15”,MyThread类代码如下:
public class MyThread extends Thread { @Override public void run(){ try { System.out.println("run begin! "); for(int i=0;i<5000;i++){ System.out.println("i="+(i+1)); } System.out.println("打印完,遇到调用sleep()时间:"+System.currentTimeMillis()); Thread.sleep(20000); System.out.println("run end"); } catch (InterruptedException e) { System.out.println("先停止,再遇到sleep!进入catch!this.isInterrupted():"+this.isInterrupted()+" "+System.currentTimeMillis()); e.printStackTrace(); } } }
Run类:
public class Run { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); System.out.println("调用interrupt()方法时间:"+System.currentTimeMillis()); thread.interrupt(); } }
结果如下:
从打印结果看,如果线程先打上中断标记,那么线程会继续执行下去,直到进入sleep状态(这里暂时只考虑进入sleep来实现线程中断)。结合这两种情况,可以总结出,中断标记和sleep状态两者相遇的话,会让线程终止。
接着是“暴力停止”。这种方法很简单,无非就是想让线程停止的时候,就调用stop方法,线程就会立即被终止。但可能会造成不可预判的结果。来看一个例子,创建包“stopThrowLock”。
SynchronizedObject类如下:
public class SynchronizedObject { private String username = "a"; private String password = "aa"; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } synchronized public void printString(String username,String password) { try { this.username = username; Thread.sleep(100000); this.password = password; } catch (InterruptedException e) { e.printStackTrace(); } } }
MyThread类:
public class MyThread extends Thread{ private SynchronizedObject object; public MyThread(SynchronizedObject object) { super(); this.object = object; } @Override public void run(){ object.printString("b", "bb"); } }
Run类:
public class Run { public static void main(String[] args) { try { SynchronizedObject object = new SynchronizedObject(); MyThread thread = new MyThread(object); thread.start(); Thread.sleep(500); thread.stop(); System.out.println(object.getUsername()+" "+object.getPassword()); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果:
从上图可以看到,因为强制stop线程,造成了数据不一致。
最后来看,使用return停止线程。interrupt()方法与return结合使用也能实现停止线程的效果。创建包“useReturnInterrupt”,代码如下。
MyThread类:
public class MyThread extends Thread { @Override public void run(){ while(true){ if(this.isInterrupted()){ System.out.println("被打上中断标记了,线程停止了"); return; } System.out.println("timer"+System.currentTimeMillis()); } } }
Run类:
public class Run { public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.interrupt(); } }
结果如下:
从结果来看,线程确实也被停止了。
至此,终止线程的方法已介绍完,其中暴力停止的方法是不推荐的,因为可能会造成不可预料的结果;虽然其他的方法也能正确停止线程(在真正终止线程之前,可以做一些善后的工作,比如确保数据的一致),但还是建议使用“抛异常”的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。
8. 暂停线程
在Java多线程中,可以使用suspend()方法暂停线程,使用resume()方法恢复线程的执行。来看看这2个方法的使用。创建包“suspend_resume_test”,代码如下:
MyThread类:
public class MyThread extends Thread { private long i = 0; public long getI() { return i; } public void setI(long i) { this.i = i; } @Override public void run() { while (true) { i++; } } }
Run类:
public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.suspend(); System.out.println("A= " + System.currentTimeMillis() + " i="+ thread.getI()); Thread.sleep(1000); System.out.println("A= " + System.currentTimeMillis() + " i=" + thread.getI()); thread.resume(); Thread.sleep(1000); thread.suspend(); System.out.println("B= " + System.currentTimeMillis() + " i=" + thread.getI()); Thread.sleep(1000); System.out.println("B= " + System.currentTimeMillis() + " i=" + thread.getI()); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果如下:
从结果来看,线程的确被在暂停了,而且还可以恢复成运行的状态。
从上面的例子来看,这似乎很好用的样子,为何会被弃用呢?来看看它们的缺点--“独占”,使得其他线程无法访问公共同步对象。创建包“suspend_resume_deal_lock”,代码如下:
SynchronizedObject类:
public class SynchronizedObject { synchronized public void printString() { System.out.println("begin"); if (Thread.currentThread().getName().equals("a")) { System.out.println("a线程永远 suspend了!"); Thread.currentThread().suspend(); } System.out.println("end"); } }
Run类:
public class Run { public static void main(String[] args) { try { final SynchronizedObject object = new SynchronizedObject(); Thread thread1 = new Thread() { @Override public void run() { object.printString(); } }; thread1.setName("a"); thread1.start(); Thread.sleep(1000); Thread thread2 = new Thread() { @Override public void run() { System.out.println("thread2启动了,但进入不了printString()方法!只打印1个begin"); System.out.println("因为printString()方法被a线程锁定并且永远的suspend暂停了!"); object.printString(); } }; thread2.start(); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果如下:
从打印结果看,启动了2个线程,但第二个线程进入不了printString方法。因为要执行printString方法,必须要获得object对象的锁,但线程a却被暂停了,又不释放object的对象锁,因此线程b就永远进入不了printString方法。
还有一种情况需要注意,稍有不慎,就会被坑惨。创建包“suspend_resume_LockStop”,代码如下:
MyThread类:
public class MyThread extends Thread { private long i = 0; @Override public void run(){ while(true){ i++; // System.out.println("i="+(i+1)); } } }
Run类:
public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.suspend(); System.out.println("main end!"); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果如下:
结果打印了“main end”。接着,把MyThread中被注释的代码去掉注释。再次运行,得到的结果如下:
线程打印到了54000+的时候,线程被暂停了,但却没有打印“main end”。为什么呢?原因是,当程序运行到println()方法内部停止时,同步锁未被释放。println方法源代码如下:
public void println(boolean x) { synchronized (this) { print(x); newLine(); } }
使用suspend与方法resume方法时也容易因为线程的暂停而导致数据不同步的情况。创建包“suspend_resume_nosameValue”,代码如下:
MyObject类:
public class MyObject { private String username = "1"; private String password = "11"; public void setValue(String u,String p) { this.username = u; if(Thread.currentThread().getName().equals("a")) { System.out.println("停止a线程"); Thread.currentThread().suspend(); } this.password = p; } public void printUsernamePassword(){ System.out.println(username + " " +password); } }
Run类:
public class Run { public static void main(String[] args) { final MyObject myObject = new MyObject(); Thread thread1 = new Thread(){ @Override public void run() { super.run(); myObject.setValue("a", "aa"); } }; thread1.setName("a"); thread1.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Thread thread2 = new Thread(){ @Override public void run() { super.run(); myObject.printUsernamePassword(); } }; thread2.start(); } }
结果如下:
结果出现了值不同步的情况。所以在使用suspend方法和resume方法是要格外注意。
9. yield方法
yield方法作用时放弃当前的CPU资源,将它让给其他任务去使用。但只是放弃当前的CPU资源,之后又会参与到竞争CPU资源的大军中去。也就是说,有可能刚刚放弃,马上又获得CPU时间片(CPU资源)。创建包“yield”,代码如下:
MyThread类:
public class MyThread extends Thread { @Override public void run(){ while(true){ System.out.println(this.getName()+"放弃了CPU资源"); Thread.yield(); } } }
Run类:
public class Run { public static void main(String[] args) { MyThread a = new MyThread(); MyThread b = new MyThread(); MyThread c = new MyThread(); a.setName("A"); b.setName("B"); c.setName("C"); a.start(); b.start(); c.start(); } }
截取可能的结果如下:
由上图可看到,虽然当前线程放弃了CPU资源,但下次可能还是该线程获得CPU资源。另外,调用yield方法使线程放弃当前的CPU资源,当下次获得CPU资源时会继续执行yield方法下的内容。读者可以自行测试。
10. 线程优先级
在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则会抛出异常,即throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;
在Java中,线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。可以通过线程对象的getPriority()方法取得线程的优先级;当然,也可以用setPriority()方法给线程设置优先级。
高优先级的线程并不代表就先执行完,而是,在竞争CPU资源时,高优先级的线程获得CPU资源的概率比较大,但并不一定获得资源。
来看一个例子,有助理解线程优先级。创建包“countPriority”,代码如下:
MyThread类:
public class MyThread extends Thread { long i = 0; public long getI(){ return i; } @Override public void run(){ while(true){ i++; } } }
Run类:
public class Run { public static void main(String[] args) throws InterruptedException { MyThread A = new MyThread(); MyThread B = new MyThread(); A.setName("A"); B.setName("B"); A.setPriority(Thread.NORM_PRIORITY-3); B.setPriority(Thread.NORM_PRIORITY+3); A.start(); B.start(); Thread.sleep(5000); A.stop(); B.stop(); System.out.println(A.getName()+":"+A.getI()); System.out.println(B.getName()+":"+B.getI()); } }
结果如下:
B线程的优先级高,到线程停止的时候,i的值基本都是会比A线程的大。
11. 守护线程(daemon)
Java线程有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,它的特性有“陪伴”的含义。当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护进程,垃圾回收线程也就没有存在的必要了,则自动销毁。创建包“daemonThread”,代码如下:
MyThread类:
public class MyThread extends Thread { private int i=0; @Override public void run(){ while(true) { System.out.println(i++); } } }
Run类:
public class Run { public static void main(String[] args) { MyThread thread = new MyThread(); thread.setDaemon(true); thread.start(); try { Thread.sleep(10); System.out.println("setDaemon需要在start方法调用之前使用; "+ "线程划分为用户线程和后台(daemon)进程,setDaemon将线程设置为后台进程; "+ "主线程结束后,用户线程将会继续运行, 如果没有用户线程,都是后台进程的话,那么jvm结束。"); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果如下:
从打印结果来看,守护线程thread在主线程main结束后就停止运行了,即销毁。上图那段很长的话后还打印了很多数字,是因为main线程打印的时候也需要时间,而线程thread在这段时间继续打印,直到main线程打印完,main线程销毁后,由于没有其他用户线程了,thread线程也跟着一起销毁了。
至此,第一章结束。第一章只是为接下来的几章打下基础,所以读者可以多自己写几个例子加深印象,往后的几章学习起来也比较容易。
2016-12-29 22:42