第一章 Java多线程技能
主要有以下知识点:
- 进程和多线程的概念及线程的优点
- 实现多线程编程的方式
- 线程的启动
- 如何使线程暂停
- 如何使线程停止
- 线程安全相关问题
- 线程的优先级
- 守护线程
进程和多线程的概念及线程的优点
百度百科对”进程”的解释如下:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
那什么是线程呢?线程可以理解成是在进程中独立运行的子任务。比如QQ.exe运行时就有很多的子任务在同时进行。再如,好友视频线程、下载文件线程、传输数据线程等,这些不同的任务或者说功能都可以同时运行。多线程的优点可以最大限度地利用CPU的空闲时间来处理其他的任务。
看下图来理解一下单任务的缺点:
在图1-3中,任务一和任务二是两个完全独立、互不相关的任务,任务1是在等待远程服务器返回数据,以便进行后期的处理,这时CPU一直处于等待状态,一直在空运行。使得CPU的使用率大幅降低。
在图1-4中,CPU完全可以在任务一和任务二之间来回切换,使任务二不必等到10秒再运行,系统的运行效率大大得到提升。这就是要使用多线程技术、要学习多线程的原因。这是多线程的优点,使用多线程也就是使用异步。
实现多线程编程的方式
在JAVA的JDK开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编程。实现多线程编程的方式主要有两种,一种是继承Thread类,另一种是实现Runnable接口。
在学习如何创建新的线程前,先来看看Thread类的结构,如下:
public class Thread implements Runnable
从上面的源代码中可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。
其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单根继承,所以为了支持多继承,完全可以实现Runnable接口的方式,一边实现一遍继承。单用这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。
Thread类实现:
public class MyThread extends Thread{
@Override
public void run(){
super.run();
System.out.println("MyThread");
}
}
run方法为线程要执行的任务
运行类代码:
public class Run {
public static void main(String[] args){
MyThread myThread = new MyThread();
myThread.start();
System.out.println("运行结束!");
}
}
由上图的运行结果可以得知,MyThread.java类的run方法执行的时间比较晚,这也说明在使用多线程技术时,代码的运行结果与代码的执行顺序或调用顺序时无关的。
线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法,所以就会出现先打印“运行结束!”后输出“MyThread”这样的结果了。
Thread.java类中的start()方法通知”线程规划器”此线程已经准备就绪,等待调用线程的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法。也就是使线程得到运行,启动线程,具有异步执行的效果。如果直接调用run()方法的话就不是异步了,而是同步。
注意:如果多次调用start()方法,则会出现异常Exception in thread “main” java.lang.IllegalThreadStateException.
Runnable接口实现:
如果欲创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口来应对这样的情况。
public class MyRunnable implements Runnable{
public void run() {
System.out.println("运行中!");
}
}
如何使用这个MyRunnable.java类呢?这就要看一下Thread.java类的构造函数了,如下图所示:
注意:可从网址 http://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html 获取Thread.java类的详细介绍
在Thread.java类的8个构造函数中,有两个构造函数 Thread(Runnable target) 和 Thread(Runnable target, String name) 可以传递Runnable接口,说明构造函数支持传入一个Runnable接口的对象。
运行类代码:
public class Run1 {
public static void main(String[] args){
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("运行结束!");
}
}
另外需要说明的是,Thread.java类也实现了Runnable接口,如图:
那也就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程调用。
实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。
(1)不共享数据的情况
举一个示例:
public class MyThread3 extends Thread{
private int count = 5;
public MyThread3(String name){
super();
this.setName(name);
}
@Override
public void run(){
super.run();
while (count > 0){
count--;
System.out.println("由" + this.currentThread().getName() + "计算. count=" + count);
}
}
运行类:
public class Run3 {
public static void main(String[] args){
MyThread3 mt1 = new MyThread3("a");
MyThread3 mt2 = new MyThread3("b");
MyThread3 mt3 = new MyThread3("c");
mt1.start();
mt2.start();
mt3.start();
}
}
由上图可以看到,一共创建了3个线程,每个线程都有各自的count变量,自己减少的自己的count变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个示例变量的情况。
(2) 共享数据的情况
public class MyThread4 extends Thread{
private int count = 5;
@Override
public void run(){
super.run();
count--;
System.out.println("由 "+this.currentThread().getName()+" 计算. count="+count);
}
}
运行类:
public class Run4 {
public static void main(String[] args){
MyThread4 mt1 = new MyThread4();
Thread a = new Thread(mt1, "A");
Thread b = new Thread(mt1, "B");
Thread c = new Thread(mt1, "C");
Thread d = new Thread(mt1, "D");
Thread e = new Thread(mt1, "E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
从图上得知E和D线程打印出来的count值都是1,说明E和D同时对count进行处理,产生了“非线程安全”问题。而我们想到得到的打印结果却不是重复的,而是依次递减的。
在某些JVM中,i–的操作要分成如下3步:
(1)取得原有i的值。
(2)计算i-1。
(3)对i进行赋值。
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
为了应对这种情况,就需要使多个线程之间进行同步,也就是按照顺序排队的方式进行减1操作。
更改代码如下:
public class MyThread5 extends Thread{
private int count = 5;
@Override
synchronized public void run(){
super.run();
count--;
System.out.println("由 "+this.currentThread().getName()+" 计算. count="+count);
}
}
大家应该注意到了,run方法前加了一个synchronized关键字。synchronized使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明其他线程正在调用run方法,必须等待其他线程对run方法调用结束后才可以执行run方法。synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到锁,那么这个线程就可以执行synchronized里面的代码。如果拿不到这把锁,那个这个线程就会不断地尝试拿这把锁,直到能够拿到为止。而且是有多个线程同时去争抢这把锁。
非线程安全:只要是指多个线程对同一个对象的同一个实例变量进行操作时会出现值被更改、值不同步的情况。进而影响程序的执行流程。
Thread类相关方法:
1.currentThread(): 可返回代码段正在被哪个线程调用的信息。
用法:Thread.currentThread()
2.isAlive(): 判断当前的线程是否处于活动状态。
活动状态: 就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,被认为线程是“存活”的。
3.sleep(): 是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个正在执行的线程是指this.currentThread()返回的线程。
4.getId():取得线程的唯一标识。
停止线程
停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。虽然这看起来非常简单,但是必须做好防范措施,以便达到预期的效果。
在Java中有以下3种方法可以终止正在运行的线程:
1.使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2.Thread.stop() : 不安全的(unsafe),而且是已被弃用的( deprecated),在将来的java版本中,这个方法将不可用或不被支持。
3.Thread.interrupt(): 这个方法需要加入一个判断才可以完成线程的终止。
调用interrupt()方法来停止线程,但interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环了。调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。
判断线程是否是停止状态,Thread.java
(1)this.interrupted() : 测试当前线程是否已经中断,执行后具有将状态标志置清除为 false的功能。
(2)this.isInterrupted() : 测试线程是否已经中断,但不清楚状态标志。
能停止的线程——异常法
public class MyThread11 extends Thread{
@Override
public void run(){
super.run();
try{
for(int i = 0; i < 500000; i++){
if(this.interrupted()){
System.out.println("已经是停止状态了!我要退出了!");
throw new InterruptedException();
}
System.out.println("i="+ (i+1));
}
System.out.println("我在for下面");
}catch (InterruptedException e){
System.out.println("进MyThread.java类run方法中的catch了!");
e.printStackTrace();
}
}
}
运行类:
public class Run13 {
public static void main(String[] args){
try {
MyThread11 a = new MyThread11();
a.start();
a.sleep(1000);
a.interrupt();
//System.out.println("是否停止1? ="+a.interrupted());
//System.out.println("是否停止2? ="+a.interrupted());
System.out.println("是否停止1? ="+a.isInterrupted());
System.out.println("是否停止2? ="+a.isInterrupted());
}catch (InterruptedException e){
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
}
}
能停止的线程——在沉睡中停止
public class MyThread12 extends Thread{
@Override
public void run(){
super.run();
try {
for(int i=0;i<100000;i++){
System.out.println("i="+(i+1));
}
System.out.println("run begin");
Thread.sleep(200000);
System.out.println("run end");
}catch (InterruptedException e){
System.out.println("在沉睡中被停止!进入catch!"+this.isInterrupted());
e.printStackTrace();
}
}
}
运行类:
public class Run14 {
public static void main(String[] args){
MyThread12 a = new MyThread12();
a.start();
a.interrupt();
System.out.println("end!");
}
}
能停止的线程——暴力停止
使用stop()方法停止线程则是非常
public class Mythread13 extends Thread{
private int i = 0;
@Override
public void run(){
try{
while(true){
i++;
System.out.println("i=" + i);
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行类:
public class Run15 {
public static void main(String[] args){
try {
Mythread13 a = new Mythread13();
a.start();
a.sleep(8000);
a.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
调用stop()方法时会抛出java.lang.ThreadDeath异常,但在通常情况下,此异常不需要显式地捕捉。
public class MyThread14 extends Thread{
@Override
public void run(){
try {
this.stop();
}catch (ThreadDeath e){
System.out.println("进入了catch()方法!");
}
}
}
运行类:
public class Run16 {
public static void main(String[] args){
MyThread14 a = new MyThread14();
a.start();
}
}
方法stop()已经被作废了,因为如果强制让线程停止则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了”解锁”,导致数据得不到同步的处理,出现数据不一致的问题。
使用stop()释放锁将会给数据造成不一致的结果。如果出现这样的情况,程序处理数据就有可能遭到破坏,最终导致程序执行的流程错误,一定要特别注意。
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();
}
}
}
线程类:
public class MyThread15 extends Thread{
private SynchronizedObject synchronizedObject;
public MyThread15(SynchronizedObject synchronizedObject){
super();
this.synchronizedObject = synchronizedObject;
}
@Override
public void run(){
synchronizedObject.printString("b", "bb");
}
}
运行类:
public class Run17 {
public static void main(String[] args){
try {
SynchronizedObject synchronizedObject = new SynchronizedObject();
MyThread15 a = new MyThread15(synchronizedObject);
a.start();
a.sleep(500);
a.stop();
System.out.println(synchronizedObject.getUsername() + " " + synchronizedObject.getPassword());
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
由于stop()方法已经在JDK中被标明是”作废/过期”的方法,显然它在功能上具有缺陷,所以不建议在程序中使用stop()方法。
使用return停止线程
将方法interrupt()与return结合使用也能实现停止线程的效果。
public class MyThread16 extends Thread{
@Override
public void run(){
while (true){
if(this.isInterrupted()){
System.out.println("停止了!");
return;
}
System.out.println("timer=" + System.currentTimeMillis());
}
}
}
运行类:
public class Run18 {
public static void main(String[] args) throws InterruptedException {
MyThread16 a = new MyThread16();
a.start();
a.sleep(2000);
a.interrupt();
}
}
不过还是建议使用”抛异常”的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。
暂停线程
暂停线程意味着此线程还可以恢复运行。在Java多线程中,可以使用suspend()方法暂停线程,使用resume()方法恢复线程的执行。
例子:
public class MyThread17 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++;
}
}
}
运行类:
public class Run19 {
public static void main(String[] args){
try{
MyThread17 a = new MyThread17();
a.start();
a.sleep(5000);
a.suspend();
System.out.println("A= "+System.currentTimeMillis() + " i=" +a.getI());
a.sleep(5000);
System.out.println("A= "+System.currentTimeMillis() + " i=" +a.getI());
a.resume();
a.sleep(5000);
a.suspend();
System.out.println("B= "+System.currentTimeMillis() + " i=" +a.getI());
a.sleep(5000);
System.out.println("B= "+System.currentTimeMillis() + " i=" +a.getI());
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
suspend与resume方法的缺点——独占
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");
}
}
运行类:
public class Run17 {
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()方法!只打印一个");
System.out.println("因为printString()方法被a线程锁定并且永远suspend暂停了!");
object.printString();
}
};
thread2.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
suspend与resume方法的缺点——不同步
在使用suspend与resume方法时也容易出现因为线程的暂停而导致数据不同步的情况。
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);
}
}
运行类:
public class Run22 {
public static void main(String[] args) throws InterruptedException{
final MyObject object = new MyObject();
Thread thread1 = new Thread(){
public void run(){
object.setValue("a", "aa");
}
};
thread1.setName("a");
thread1.start();
Thread.sleep(500);
Thread thread2 = new Thread(){
public void run(){
object.printUsernamePassword();
}
};
thread2.start();
}
}
程序运行的结果出现值不同步的情况,所以在程序中使用suspend()方法要格外注意。
yield方法
yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
public class MyThread18 extends Thread{
@Override
public void run(){
long beginTime = System.currentTimeMillis();
int count = 0;
for(int i = 0; i < 50000000; i++){
Thread.yield();
count = count + (i+1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时: " + (endTime - beginTime) + " 毫秒! ");
}
}
运行类:
public class Run20 {
public static void main(String[] args){
MyThread18 a = new MyThread18();
a.start();
}
}
线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程的优先级使用setPriority()方法。
在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw 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是一样的。
优先级具有规则性
CPU尽量将执行资源让给优先级比较高的线程。
优先级具有随机性
不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程并不一定每一次都先执行完run()方法中的任务。
守护线程
在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,它的特性有”陪伴”的含义,当进程中不存在非守护进程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程。
public class MyThread21 extends Thread{
private int i = 0;
@Override
public void run(){
try{
while (true){
i++;
System.out.println("i="+(i));
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
运行类:
public class Run23 {
public static void main(String[] args){
try {
MyThread21 thread21 = new MyThread21();
thread21.setDaemon(true);
thread21.start();
Thread.sleep(5000);
System.out.println("我离开thread对象也不再打印了,也就是停止了!");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}