线程概述
多任务操作系统,即能够同时执行多个应用程序,最常见的有 Windows、Linux、UNIX等。
在一个操作系统中,每个独立运行的程序都可以称为一个进程,即“正在运行的程序”。
在多任务操作系统中,例如可以一边听音乐一边聊天,但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由 CPU 执行的,对于一个 CPU 而言,在某个时间点只能运行一个程序,即只能执行一个进程。由于 CPU 运行速度很快,能在极短的时间内,在不同的进程之间进行切换,所以给人以同时运行多个程序的感觉。
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序运行的一条条线索,被称为线程。
操作系统中的每一个进程中都至少存在一个线程。当一个 Java 程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程上会运行 main() 方法中的代码。
单线程程序:代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果。
多线程程序:多段程序代码交替运行的效果。
多线程和进程一样,也是由 CPU 轮流执行的,只不过 CPU 运行速度很快,所以给人同时执行的感觉。
线程的创建
第一种方式:继承 java.lang 包下的 Thread 类,覆写 Thread 类的 run() 方法,在 run() 方法中实现运行在线程上的代码
示例代码如下:
class MyThread extends Thread {
public void run() {
while(true) {
System.out.println("MyThread类的 run() 方法在运行");
}
}
}
public class Example {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
while() {
System.out.println("main() 方法在运行");
}
}
}
第二种方式:实现 java.lang.Runnable 接口,同样是在 run() 方法中实现运行在线程上的代码
class MyThread implements Runnable {
public void run() { //线程的代码段,当调用 start() 方法时,线程从此处开始执行
while(true) {
System.out.println("main() 方法在运行");
}
}
}
public class Example {
public static void main(String[] args) {
MyThread myThread = new MyThread(); //创建 MyThread 的实例对象
Thread thread = new Thread(myThread); //创建线程对象
thread.start(); //开启线程,执行线程中的 run() 方法
while(true) {
System.out.println("main() 方法正在运行");
}
}
}
实现 Runnable 接口相对于继承 Thread 类来说,有以下优点:
- 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好地体现了面向对象的设计思想。
- 可以避免由于 Java 的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,只能采用实现 Runnable 接口的方式
- 事实上,大部分的应用程序都会采用第二种方式来创建多线程,即实现 Runnable 接口。
前台线程与后台线程
前台线程:新创建的线程默认都是前台线程。
后台线程:若某个线程对象在启动之前(即调用 start() 方法之前)调用了 setDaemon(true) 语句,这个线程就是一个后台线程。
注意:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说 setDaemon() 方法必须在 start() 方法之前,否则会引发 IllegalThreadStateException 异常。
对 Java 程序来说,只要还有一个前台线程在运行,这个进程就不会结束。若一个进程中只有后台线程运行,这个进程就会结束。
示例代码如下:
演示当程序只有后台线程时就会结束的情况
class DamonThread implements Runnable {
public void run() {
while(true) {
System.out.println(Thread.currentThread().getName() + "-----is running.");
}
}
}
public class Example {
Public static void main(String[] args) {
System.out.println("main 线程就是后台线程么?" + Thread.currentThread().isDaemon());
DamonThread dt = new DamonThread(); //创建一个 DamonThread 对象 dt
Thread t = new Thread(dt, "后台线程"); //创建线程 t 共享 dt 资源
System.out.println("t 线程默认是后台线程么?" + t.isDaemon()); //判断是否为后台线程
t.setDaemon(true);
t.start();
for(int i=0; i<10; i++) {
System.out.println(i);
}
}
}
该程序说明,当开启线程 t 后,会执行死循环中的打印语句,但我们将线程 t 设置为后台线程后,当前台线程死亡后,JVM 会通知后台线程。由于后台线程从接受指令,到做出响应,需要一定的时间。因此,打印了几次 “后台线程—is running” 语句后,后台也结束了。所以当只有后台线程运行时,进程就会结束。
线程的调度
线程的调度:程序中的多个线程是并发执行的,某个线程若想被执行必须要得到 CPU 的使用权,Java 虚拟机会按照特定的机制为程序中的每个线程分配 CPU 的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,即分时调度模型和抢占式调度模型。Java 虚拟机默认采用抢占式调度模型。
分时调度模型:是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片。
抢占式调度模型:是指让可运行池中优先级高的线程优先占用 CPU,而对于优先级相同的线程,随机选择一个线程使其占用 CPU,当它失去了 CPU 的使用权后,再随机选择其他线程获取 CPU 使用权。
线程的优先级
在应用程序中,若要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得 CPU 执行的机会越大。线程的优先级用 1~10 之间的整数来表示,数字越大优先级越高。除了数字表示线程的优先级,还可以使用 Thread 类中提供的三个静态常量表示线程的优先级,如下表所示:
Thread 类的静态常量 | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,相当于值 10 |
static int MIN_PRIORITY | 表示线程的最低优先级,相当于值 1 |
static int NORM_PRIORITY | 表示线程的普通优先级,相当于值 5 |
示例代码如下:
//定义类 MaxPriority 实现 Runnable 接口
class MaxPriority implements Runnable {
public void run() {
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
}
}
}
//定义类 MinPriority 实现 Runnable 接口
class MinPriority implements Runnable {
public void run(){
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
}
}
}
public class Example {
public static void main(String[] args){
//创建两个线程
Thread minPriority = new Thread(new MinPriority, "优先级较低的线程");
Thread maxPriority = new Thread(new MaxPriority, "优先级较高的线程");
minPriority.setPriority(Thread.MIN_PRIORITY); //设置线程的优先级为 1
maxPriority.setPriority(10); //设置线程的优先级为 10
//开启两个线程
maxPriority.start();
minPriority.start();
}
}
注意:虽然 Java 中提供了 10 个线程优先级,但不同的操作系统对优先级的支持是不一样的,不能很好地和 Java 中线程优先级一一对应。因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,只能把线程优先级作为一种提高程序效率的手段
线程休眠
若要人为地控制线程,使正在执行的线程暂停,将 CPU 让给别的线程,可以使用静态方法 sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用 sleep(long millis) 方法后,在指定时间内是不会执行的,这样其他的线程就可以得到执行的机会了。
注意:第一点:sleep(long millis) 方法声明抛出 InterruptedException 异常,因此在调用该方法时,应该捕获异常,或者声明抛出该异常。
第二点:sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。
示例代码如下:
class SleepThread implements Runnable{
public void run() {
for(int i =1; i<10; i++){
if(i==3){
try{
Thread.sleep(2000); //当前线程休眠 2 秒
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("线程一直在输出:" + i);
try{
Thread.sleep(500); //当前线程休眠 500 毫秒
}catch(Exception e){
e.printStackTrace();
}
}
}
}
public class Example {
public static void main(String[] args) throws Exception{
//创建一个线程
new Thread(new SleepThread()).start();
for(int i=1; i<=10; i++){
if(i==5){
Thread.sleep(20000); //当前线程休眠 2 秒
}
System.out.println("主线程正在输出:" + i);
Thread.sleep(500); //当前线程休眠 500 毫秒
}
}
}
线程让步
在校园中,经常会看到同学互相抢篮球,当某个同学抢到篮球后就可以拍一会,之后他会把篮球让出来,大家重新开始抢篮球,这个过程就相当于 Java 程序中的线程让步。
线程让步可以通过 yield() 方法来实现,该方法和 sleep() 方法有点相似,都可以让当前正在运行的线程暂停,区别在于 yield() 方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用 yield() 方法后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
示例代码如下:
//定义 YieldThread 类继承 Thread 类
class YieldThread extends Threads {
//定义一个有参的构造方法
public YieldThread(String name){
super(name); //调用父类的构造方法
}
public void run(){
for(int i=0; i<5; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
if(i==3){
System.out.print("线程让步:");
Thread.yield(); //线程运行到此,做出让步
}
}
}
}
public class Example{
public static void main(String[] args){
//创建两个线程
Thread t1 = new YieldThread("线程 A");
Thread t2 = new YieldThread("线程 B");
//开启两个线程
t1.start();
t2.start();
}
}
线程插队
现实生活中经常碰到“插队”的情况,同样,在 Thread 类中也提供了一个 join() 方法来实现这个“功能”。
当在某个线程中调用其他线程的 join() 方法时,调用的线程将被阻塞,知道被 join() 方法加入的线程执行完成后,它才会继续运行。
示例代码如下:
class EmergencyThread implements Runnable{
public void run(){
for(int i=1; i<6; i++){
System.out.println(Thread.currentThread().getName() + "输入:" + i);
try{
Thread.sleep(500); //线程休眠 500 毫秒
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public class Example{
public static void main(String[] args) throws Exception{
Thread t = new Thread(new EmergencyThread(), "线程一");
t.start(); //开启线程
for(int i=1; i<6; i++){
System.out.println(Thread.currentThread().getName() + "输入:" + i);
if(i==2){
t.join(); //调用 join() 方法
}
Thread.sleep(500); //线程休眠 500 毫秒
}
}
}
多线程同步
线程安全问题是由多个线程同时处理共享资源所导致的。因此必须能保证共享资源在任何时刻只能有一个线程访问。
为了实现这种限制,Java 中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用 synchronized 关键字来修饰,被称作同步代码块,其语法结构如下:
synchronized(lock){
操作共享资源代码块
}
其中,lock 是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查所对象的标志位,默认情况下标志位为 1,此时线程会执行同步代码块,同时将锁对象的标志位置为 0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为 0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为 1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程类似于一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。
示例代码如下:
//定义 Ticket 类实现 Runnable 接口
class Ticket implements Runnable{
private int ticket = 10; //定义变量 tickets,并赋值 10
object lock = new Object(); //定义任意一个对象,用作同步代码块的锁
public void run(){
while(true){
synchronized(lock){ //定义同步代码块
try{
Thread.sleep(10); //经过的线程休眠 10 毫秒
}catch(InterruptedException e){
e.printStackTrace();
}
if(tickets>0){
System.out.println(Thread.currentThread().getName()
+ "---卖出的票" + tickets--);
}else{
break;
}
}
}
}
}
public class Example{
public static void main(String[] args){
Ticket ticket = new Ticket(); //创建 Ticket 对象
//创建并开启四个线程
new Thread(ticket, "线程一").start();
new Thread(ticket, "线程二").start();
new Thread(ticket, "线程三").start();
new Thread(ticket, "线程四").start();
}
}
注意:同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。还有一点,锁对象的创建代码不能放到 run() 方法中,否则每个线程运行到 run() 方法都会创建一个新对象,这样每个线程都有一个不同的锁,每个锁都有自己的标志位。线程之间便不能产生同步的效果。
同步方法
在方法前,使用 synchronized 关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
synchronized 返回值类型 方法名 ([参数1, 参数2, ...]){}
注意:被 synchronized 修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。
示例代码如下:
//定义 Ticket 类实现 Runnable 接口
class Ticket implements Runnable{
private int ticket = 10;
public void run(){
while(true){
saleTicket();
if(ticket<=0){ //调用售票方法
break;
}
}
}
//定义一个同步方法 saleTicket()
if(ticket>0){
try{
Thread.sleep(10); //经过的线程休眠 10 毫秒
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName()
+ "---卖出的票" + tickets--
);
}
}
public class Example{
public static void main(String[] args){
Ticket ticket = new Ticket(); //创建 Ticket 对象
//创建并开启四个线程
new Thread(ticket, "线程一").start();
new Thread(ticket, "线程二").start();
new Thread(ticket, "线程三").start();
new Thread(ticket, "线程四").start();
}
}
多线程同步小结:
同步代码块的锁是自己定义的任意类型的对象
同步方法的锁是当前调用该方法的对象,也就是 this 指向的对象。
多线程通信
为了更好地理解线程间通信,可以模拟这样一种应用场景,假设有两个线程同时去操作同一个存储空间,其中一个线程负责向存储空间中存入数据,另一个线程负责取出数据。
示例代码如下:
class Storage{
private int[] cells = new int[10]; //数据存储数组
private int[] inPos, outPos; //inPos存入时数组下标,outPos取出时数组下标
private int count; //存入或者取出数据的数量
public synchronized void put(int num){
try{
//如果放入数据等于 cells 的长度,此线程等待
while(count==cells.length){
this.wait();
}
cells[inPos] = num; //向数组中放入数据
System.out.println("在 cells["+inPos+"]中放入数据---" + cells[inPos]);
inPos++; //存完元素让位置加 1
this.notify();
}catch(Exception e){
e.printStackTrace();
}
}
public synchronized void get(){
try{
while(count==0){ //如果 count 为 0,此线程等待
this.wait();
}
int data = cells[outPos]; //从数组中取出数据
System.out.println("从 cells["+outPos+"] 中取出数据" + data);
cells[outPos] = 0; //取出后,当前位置的数据置 0
outPos++; //取完元素让位置加 1
if(outPos==cells.length){ //当从 cells[9] 取完数据后再从 cells[0] 开始
outPos = 0;
count--;
this.notify();
}catch(Exception e){
e.printStackTrace();
}
}
}
//讲解待续