《Java多线程编程核心技术》--第1章--Java多线程技能

本章介绍Thread类中的核心方法。重点掌握如下关键技术点:

  1. 线程的启动
  2. 如何使线程暂停
  3. 如何使线程停止
  4. 线程的优先级
  5. 线程安全相关的问题
这5点时本章学习的重点与思路。

1.1 进程和多线程的概念及线程的优点

 讲到线程,我们要先介绍进程

 我们可以将一个正在操作系统中运行的exe程序理解成一个“进程”。进程是受操作系统管理的基本运行单元。

 线程可以理解成是在进程中独立运行的 子任务。比如,QQ.exe运行时就有很多的子任务在同时运行,比如:传文件、听音乐、发送表情等功能都有对象的线程在后台默默地运行。

 使用多线程有什么优点呢? 我们知道“多任务操作系统”。使用多任务操作系统,可以最大限度地利用CPU的空闲时间来处理其他的任务,比如一边让操作系统处理正在打印机打印的数据,一边使用Word编辑文档。而CPU在这些任务之间不停地切换,由于切换的速度非常快,给使用者的感受就是这些任务似乎在同时运行。所以,使用多线程技术后,可以在同一时间内运行更多不同种类的任务。

为了更加有效地理解多线程的优势,看一下如图1-3所示的单任务的模型图,理解一下单任务的缺点。

在图1-3中,任务1和任务2是两个完全独立、互不相关的任务,任务1是在等待远程服务器返回数据,一边进行后期的处理,这时CPU一直处于等待状态,一直在“空运行”。如果任务2是在10秒之后被运行,虽然执行任务2用的时间非常短,仅仅是1秒,但也必须在任务1运行结束之后才可以运行任务2。本程序是运行在单任务环境中,所以任务2有非常长的等待时间,系统运行效率答复降低。单任务的特点就是排队执行,也就是同步,就像在cmd中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令一样。这就是单任务环境的缺点,即CPU利用率大幅降低。

而多任务的环境如图1-4所示。

《Java多线程编程核心技术》--第1章--Java多线程技能_第1张图片

多任务,cpu可以在任务1和任务2之间来回切换,使任务2不必等到10秒再运行,系统的运行效率大大得到提升。这就是要使用多线程技术、要学习多线程的原因。这是多线程技术的优点,使用多线程就是在使用异步

1.2 使用多线程

一个进程在运行时至少会有1个线程在运行。多任务,cpu可以在任务1和任务2之间来回切换,使任务2等到10秒再运行,系统的运行效率大大得到提升。使用多线程就是在使用异步。

1.2.1 继承Thread类

在Java的JDk开发包中,已经自带了对多线程技术的支持。实现多线程编程的方式主要有两种,继承Thread类&实现Runnable接口。

我们看看Thread的源码:

《Java多线程编程核心技术》--第1章--Java多线程技能_第2张图片

Thread类实现了Runnable接口,它们之间具有多态关系。

其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承。用Thread和Runnable两种方式创建的线程在工作时的性质是一样的,没有本质区别。

创建自定义线程类MyThread.java。重写run方法。在run方法中,写线程要执行的任务的代码:

public class MyThread extends Thread {
	@Override
	public void run() {
		super.run();
		System.out.println("MyThread");
	}
}
测试类:
public class Client {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		myThread.start();
		System.out.println("运行结束!");
	}
}
运行结果:

《Java多线程编程核心技术》--第1章--Java多线程技能_第3张图片
运行结果说明,在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。

线程是一个子任务,cpu以不确定的方式,或者说是以随机的时间来调用线程中的run方法。

Thread.java类中的start()方法的作用是,通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。
执行start()方法的顺序不代表线程启动的顺序。

1.2.2 实现Runnable接口

创建一个实现Runnable接口的类MyRunnable
public class MyRunnable implements Runnable {
	@Override
	public void run() {
		System.out.println("运行中!");
	}
}
如何使用这个MyRunnable呢?我们看一下Thread.java的构造函数:
《Java多线程编程核心技术》--第1章--Java多线程技能_第4张图片
有两个构造函数可以传递Runnable接口,说明构造函数支持传入一个Runnable接口的对象。
运行类:
public class Client {
	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.2.3 实例变量与线程安全

共享数据的情况就是多个线程可以访问同一个变量。比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数。

演示数据共享的情况:

public class MyThread extends Thread {
	private int count = 10;

	@Override
	public void run() {
		super.run();
		count--;
		System.out.println("由 " + this.currentThread().getName() + ",计算,count=" + count);
	}
}
public class Client {
	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");
		Thread f = new Thread(myThread, "F");
		Thread g = new Thread(myThread, "G");
		Thread h = new Thread(myThread, "H");
		Thread i = new Thread(myThread, "I");
		Thread j = new Thread(myThread, "J");

		a.start();
		b.start();
		c.start();
		d.start();
		e.start();
		f.start();
		g.start();
		h.start();
		i.start();
		j.start();
	}
}
《Java多线程编程核心技术》--第1章--Java多线程技能_第5张图片
线程A和C打印出的count值是一样的,说明A和C同时对count进行处理,产生了“非线程安全”问题。而我们想要得到的打印结果却不是重复的,而是依次递减的。

某些JVM中,i--的操作要分成如下3步:

  1. 取得原有i值
  2. 计算i-1
  3. 对i进行赋值
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。

其实这个示例就是典型的销售场景:5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品数上继续减1操作。这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作。

更改代码如下:

public class MyThread extends Thread {
	private int count = 10;

	@Override
	synchronized public void run() {
		super.run();
		count--;
		System.out.println("由 " + this.currentThread().getName() + ",计算,count=" + count);
	}
}
在run()方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。这样也就实现了排队调用run方法的目的,也就达到了按顺序对count变量减1的效果了。Synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronized里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。
非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。

1.2.4 留意i--与System.out.println()的异常

虽然println()方法在内部是同步的,但i—的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率。所以,还是应该继续使用同步方法。

1.3 currentThread()方法

currentThread()方法可返回代码段正在被哪个线程调用的信息。

public class Client {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
	}
}
《Java多线程编程核心技术》--第1章--Java多线程技能_第6张图片
结果说明,main方法被名为main的线程调用。

继续试验,创建MyThread.java类:

public class MyThread extends Thread {
	public MyThread() {
		System.out.println("构造方法的打印:" + Thread.currentThread().getName());
	}

	@Override
	synchronized public void run() {
		System.out.println("run方法的打印:" + Thread.currentThread().getName());
	}
}
运行client:
public class Client {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		myThread.start();
	}
}
《Java多线程编程核心技术》--第1章--Java多线程技能_第7张图片
MyThread.java类的构造函数是被main线程调用的,而run方法时被名称为Thread-0的线程调用的,run方法时自动调用的方法。
修改client代码:
public class Client {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
//		myThread.start();
		myThread.run();
	}
}
《Java多线程编程核心技术》--第1章--Java多线程技能_第8张图片

1.4 isAlive()方法

isAlive()的作用是测试线程是否处于活动状态。

public class MyThread extends Thread {

	@Override
	synchronized public void run() {
		System.out.println("run=" + this.isAlive());
	}
}
public class Client {
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		System.out.println("begin==" + myThread.isAlive());
		myThread.start();
		System.out.println("end==" + myThread.isAlive());

	}
}
《Java多线程编程核心技术》--第1章--Java多线程技能_第9张图片

活动状态就是线程已经启动且尚未终止。线程处于正在运行准备开始运行的状态,就认为线程时“存活”的。

1.5 sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
public class MyThread extends Thread {

	@Override
	public void run() {
		try {
			System.out.println(Thread.currentThread().getName() + " begin");
			Thread.sleep(1000);
			System.out.println(Thread.currentThread().getName() + " end");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

1.6 getId()方法

getId()方法的额作用是取得线程的唯一标识。
public class Client {
	public static void main(String[] args) {
		Thread runThread = Thread.currentThread();
		System.out.println(runThread.getName() + " " + runThread.getId());
	}
}

1.7 停止线程

停止线程时在多线程开发中很重要的技术点。
停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。停止一个线程可以使用Thread.stop()方法,但是这个方法时不安全的,并且已经被废弃了。
大多数停止一个线程的操作使用Thread.interrupt()方法,但是还需要加入一个判断才可以完成线程的停止。
Java中有以下3中方法可以终止正在运行的线程:
  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  • 使用stop方法强行终止线程,但是不推荐使用。因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
  • 使用interrupt方法中断线程

1.7.1 停止不了的线程

调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。
通过一个例子来证明interrupt, 并不是真的停止线程:
public class MyThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 500000; i++) {
			System.out.println("i=" + (i + 1));
		}
	}
}
public class Client {
	public static void main(String[] args) {
		try {
			MyThread thread = new MyThread();
			thread.start();
			Thread.sleep(2000);
			thread.interrupt();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
结果,打印了500000行,说明没有停止线程。

1.7.2 判断线程是否是停止状态

在介绍如何停止线程之前,先看一下如何判断线程的状态是不是停止。
Java的SDK,Thread.java类提供两种方法。
  • this.interrupted(): 测试当前线程是否已经中断。当前线程是指运行this.interrupted()方法的线程。
  • this.isInterrupted(): 测试线程是否已经中断。

1.7.3 能停止的线程——异常法

1.7.4 在沉睡中停止

1.7.5 能停止的线程——暴力停止

1.7.6 方法stop()与java.lang.ThreadDeath异常

1.7.7 释放锁的不良后果

1.7.8 使用return停止线程

1.8 暂停线程

1.8.1 Suspend与resume方法的使用

1.8.2 Suspend与resume方法的确定——独占

1.8.3 Suspend与resume方法的缺点——不同步

1.9 yield方法

yield()方法的作用是放弃当前的cpu资源,将它让给其他的任务去占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。

1.10 线程的优先级

设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法。线程的优先级分为1~10这10个等级。

1.10.1 线程优先级的继承特性

线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
用代码进行验证吧:
public class MyThread1 extends Thread {
	@Override
	public void run() {
		System.out.println("mythread1 priority=" + this.getPriority());
		MyThread2 thread2 = new MyThread2();
		thread2.start();
	}
}
public class MyThread2 extends Thread {
	@Override
	public void run() {
		System.out.println("mythread2 priority=" + this.getPriority());
	}
}
public class Client {
	public static void main(String[] args) {
		MyThread1 thread1 = new MyThread1();
		thread1.setPriority(6);
		thread1.start();
	}
}

1.10.2 优先级具有规则性

高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关。
线程的优先级具有一定的规则性,也就是cpu尽量将执行资源让给优先级比较高的线程。

1.10.3 优先级具有随机性

线程的“随机性”是指,优先级较高的线程不一定每一次都先执行完。

1.11 守护线程

在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。用个比较通俗的比喻来解释一下“守护线程”:任何一个守护线程都是整个jvm中所有非守护线程的“保姆”,只要当前jvm实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随jvm一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC。
来个代码,体会一下吧:
public class MyThread 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 Client {
	public static void main(String[] args) {
		try {
			MyThread thread = new MyThread();
			thread.setDaemon(true);
			thread.start();
			Thread.sleep(5000);
			System.out.println("我离开thread对象也不再打印了,也就是停止了!");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

1.12 本章小结

本章介绍了Thread类的API

你可能感兴趣的:(读书笔记)