Java的多线程(Java基础复习归纳系列)

目录

一、线程概述

1.进程

2.线程

二、线程的创建

1.Thread类

2.两种创建多线程的方法

1)继承Thread类创建多线程

2)实现Runnable接口创建多线程

3)两种实现多线程方式的对比分析

3.后台线程

三、线程的生命周期

四、线程调度与优先级

1.线程的优先级

2.线程休眠(sleep())

3.线程让步(yield())

4.线程插队(join())

五、多线程同步

1.同步代码块

2.同步方法

六、多线程通信

1.线程间通信方法

2.生产者消费者问题


一、线程概述

1.进程

  • 在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。

  • 进程和程序

A  process  =  a  program  in  execution

一个进程应该包括:程序的代码;程序的数据;CPU寄存器的值,如PC,用来指示下一条将运行的指令、通用寄存器等;堆、栈;一组系统资源(如地址空间、打开的文件)

总之,进程包含了正在运行的一个程序的所有状态信息

进程≠程序

  • A program is C statements or commands----静态的;
  • A process is program + running context ----动态的.

  • 目前大部分计算机上安装的都是多任务操作系统,即能够同时执行多个应用程序。
  • 在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。

2.线程

  • 每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看做程序执行的一条条路径,被称为线程

  • 操作系统中的每一个进程中都至少存在一个线程。当一个Java程序启动时,就会产生了一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

  • 如果只是一个 cpu,它怎么能够同时执行多段程序呢?这是从宏观上来看的,cpu 一会执行 a 线索,一会执行 b 线索,切换时间很快,给人的感觉是 a,b 在同时执行(时间片轮转)。

  • 单线程和多线程程序

    1. 代码是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序

    2. 如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。

  • 线程与进程

  1. 进程是除CPU以外的资源分配的基本单位,线程是CPU的基本调度单位;
  2. 进程  =  线程 + 资源平台。进程拥有一个完整的资源平台,进程和进程之间不能共享资源。而线程共享所在进程的地址空间和其它资源,同时独享必不可少的资源,如寄存器和栈;
  3. 线程同样具有就绪、阻塞和执行三种基本状态,同样具有状态之间的转换关系;
  4. 线程 = 轻量级进程(lightweight  process),线程相对于进程能减少并发执行的时间和空间开销;

二、线程的创建

       Java中提供了两中创建新执行线程的方法:

1.Thread类

Thread类构造方法

Thread()

构造一个新的线程对象

Thread(Runnable target)

分配新的 Thread 对象,以便将 target 作为其运行对象。Target是一个实现Runnable接口的类的对象

Thread(String name)

构造一个新的线程对象,并指定线程名

Thread(Runnable target,String name)

分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称

常用方法:

static Thread

currentThread()

返回当前正在运行的线程对象

所有可能异常:

 

InterruptedException - 如果任何线程中断了当前线程。当抛出该异常时,当前线程的中断状态被清除。

   IllegalThreadStateException - 如果线程已经启动。

 

SecurityException - 如果当前线程无法修改该线程

    IllegalArgumentException - 如果优先级不在 MIN_PRIORITY 到 MAX_PRIORITY 范围内。

 

static void

yield()

使当前线程对象暂停,允许别的线程开始运行

void

join()

等待该线程终止。

 void

join(long millis)

等待该线程终止的时间最长为 millis 毫秒。若millis 毫秒内被join的线程还没有终止,则不再等待。

static void

sleep(long millis)

使当前线程暂停运行指定毫秒数

void

start()

启动线程,加入就绪队列

void

run()

Thread的子类应重写此方法,内容应为该线程应执行的任务

void

interrupt()

中断此线程

 long

getId()

返回该线程的标识符。

 String

getName()

返回该线程的名称。

 int

getPriority()

返回线程的优先级。

void

setPriority(int newPriority)

设置线程优先级

void

setDaemon(Boolean on)

设置是否为后台线程(on为 true时是)。该方法必须在启动线程前调用

boolean

isDaemon()

测试该线程是否为守护线程。

boolean

isAlive()

判断线程是否处于活动状态

2.两种创建多线程的方法

例: 假设售票厅有三个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,三个售票窗口需要创建三个线程分别显示各个窗口的售票情况。

1)继承Thread类创建多线程

  • JDK中提供了一个线程类Thread,通过继承Thread类,并重写Thread类中的run()方法便可实现多线程。
  • 在Thread类中,提供了一个start()方法用于启动新线程,线程启动后,系统会自动调用run()方法,如果子类重写了该方法便会执行子类中的方法。 
public class SellTickets1 {

	public static void main(String[] args) {
		TicketWindow t1 = new TicketWindow("窗口1");
		TicketWindow t2 = new TicketWindow("窗口2");
		TicketWindow t3 = new TicketWindow("窗口3");
		t1.start();t2.start();t3.start();
	}
}

class TicketWindow extends Thread {
	int tickets = 100;
	public TicketWindow(String name) {
		this.setName(name);
	}
	@Override
	public void run() {
		while (tickets > 0) {
			try {
				Thread.sleep(10);
			} catch (Exception e) {
			}
			System.out.println(Thread.currentThread().getName() + " 余票: " + tickets--);
		}
	}
}

 

可以看出,每张票都被打印了三次。出现这样现象的原因是在程序中创建了三个TicketWindow对象,就等于创建了三个售票程序,每个程序中都有100张票,每个线程在独立地处理各自的资源,无法共享实例变量。

 

2)实现Runnable接口创建多线程

通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类

Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target))构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法

例子中,为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行这同一个售票对象的售票方法,简单来说就是三个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式。

public class SellTickets2 {
	public static void main(String[] args) {
		TicketWindow t = new TicketWindow();
		new Thread(t,"窗口1").start();// 使用构造方法Thread(Runnable target, String name)
		new Thread(t,"窗口2").start();
		new Thread(t,"窗口3").start();
	}
}
class TicketWindow implements Runnable {
	int tickets = 100;
	@Override
	public void run() {
		while (tickets > 0) {
			try {
				Thread.sleep(10);
			} catch (Exception e) {
			}
			System.out.println(Thread.currentThread().getName() + " 余票: " + tickets--);
		}
	}
}

 

从上面可以看出通过继承Thread类来获得当前线程对象直接使用this即可;但通过实现Runnable接口来获得当前线程对象,必须使用Thread.currentThread()方法。

3)两种实现多线程方式的对比分析

实现Runnable接口相对于继承Thread类来说,有如下显著好处:

  • 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
  • 可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么就只能采用实现Runnable接口的方式。

3.后台线程

对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

/**
 * @Project laojiu
 * @Title Daemon.java
 * @Description TODO 测试后台线程
 * @Author 15643
 * @Time 2018年8月16日 上午10:53:57
 * @Other
 */
public class Daemon {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Thread t = new Thread(new MyThreadDmn());
		t.setDaemon(true);
		t.start();
		System.out.println("t是后台线程吗? "+t.isDaemon());
	}
}

class MyThreadDmn implements Runnable {
	@Override
	public void run() {
		while (true) {
			System.out.println(Thread.currentThread().getName() + " is running");
		}
	}
}

运行结果:

当开启线程t后,会执行死循环中的打印语句,但实际情况是语句只打印几次就结束了。这是因为我们将线程t设置为后台线程后,当前台线程死亡后,JVM会通知后台线程死亡,由于后台线程从接受指令到作出响应,需要一定的时间,因此,打印了几次“Thread-0 is running.”语句后,后台线程也结束了。由此说明进程中只有后台线程运行时,进程就会结束。


三、线程的生命周期

线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。

1、新建状态(New)

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其它Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。

2、就绪状态(Runnable)

当线程对象调用了start()方法后,该线程就进入就绪状态(也称可运行状态)。处于就绪状态的线程位于可运行池中,JVM会为其创建方法调用栈和程序计数器。此时它只是具备了运行的条件,能否获得CPU的使用权开始运行,还需要等待JVM线程调度器的调度。

3、运行状态(Running)

如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态,若是一个多处理器的机器,将会有多个线程并行执行。当一个线程启动后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就结束了),当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其它线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态

并发性(concurrency):在同一时刻只能有一条指令在执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

并行性(parallel):在同一时刻,有多条指令在多个处理器上同时执行。

4、阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

  • 当线程试图获取某个对象的同步锁时,如果该锁被其它线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其它线程所持有的锁。

  • 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。

  • 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。

  • 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。

  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

5、死亡状态(Terminated)

线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其它状态(如果对死亡状态的线程再次调用start()方法,将会导致IllegalThreadStateException)。


四、线程调度与优先级

1.线程的优先级

  • 优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。

  • 线程的优先级用1~10之间的整数来表示,数字越大优先级越高。

  • 除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级

static final int MIN_PRIORITY

线程可以具有的最低优先级。相当于1

static final int NORM_PRIORITY

分配给线程的默认优先级。相当于5

static final int MAX_PRIORITY

线程可以具有的最高优先级。相当于10

程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。也可以通过int getPriority()来获取线程的优先级。

2.线程休眠(sleep())

  • 如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

  • 当前线程调用sleep(long millis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其它的线程就可以得到执行的机会了。

例:

import java.util.Date;

public class TestSleep {

	public static void main(String[] args) throws Exception {
		Thread t = new Thread(new ThreadTs());
		t.start();
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + "=======" + new Date());
			Thread.sleep(1000);
		}
	}
}

class ThreadTs implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName() + "===" + new Date());
			try {
				Thread.sleep(1100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

运行结果:

3.线程让步(yield())

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,但是它与sleep()有以下区别:

  1. yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。而sleep()方法会将线程转入阻塞状态,只有达到阻塞时间才会转入就绪状态。

  2. 当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。而sleep()方法不会理会其他线程的优先级。

  3. yield()方法没有声明抛出任何异常,而sleep()方法声明抛出了InterruptedException。

public class YieldPriority {

	public static void main(String[] args) {
		Thread t1 = new Thread(new ThreadYp(), "低优先级线程");
		Thread t2 = new Thread(new ThreadYp(), "高优先级线程");
		t1.setPriority(Thread.NORM_PRIORITY);
		t2.setPriority(10);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + "========> " + i);
			if (i == 10) {
				t1.start();
				t2.start();
				Thread.currentThread().yield();
			}
		}
	}
}

class ThreadYp implements Runnable {

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + "=====> " + i);
		}
	}
}

 

4.线程插队(join())

在Thread类中也提供了一个join()方法来实现这个“功能”。

当在某个线程中调用其它线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。

public class TestJoin {

	public static void main(String[] args) {
		Thread t1 = new Thread(new ThreadJp(), "被join的线程,有时间限制");
		Thread t2 = new Thread(new ThreadJp(), "被join的线程");
		for (int i = 0; i < 50; i++) {
			System.out.println(Thread.currentThread().getName() + "========> " + i);
			if (i == 19) {
				t1.start();
				t2.start();
				try {
					t1.join(1);
					t2.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

class ThreadJp implements Runnable {

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + "=====> " + i);
		}
	}
}

 

本例中,当i=20的时候,主线程会始终在t2执行完成之后执行,而t1可能在主线程之前执行也可能在其之后执行,主要是看其在1ms之内能否结束线程。


五、多线程同步

1.同步代码块

在二、2.两种创建多线程的方法中的两个例子,运行后可能会出现如图所示的“意外”情况:

剩余的票变成了负数,这是由多线程操作共享资源tickets所导致的线程安全问题。

要想解决线程安全问题,必须得保证下面用于处理共享资源的代码在任何时刻只能有一个线程访问。为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块,其语法格式如下:

synchronized(lock){
    同步代码块
}

lock是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0(加锁)。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后(修改),锁对象的标志位被置为1(释放锁),新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这种做法符合“加锁-修改-释放锁”的逻辑,通过这种方式,保证并发线程在同一时刻只有一个线程可以进入修改共享资源的代码区(也称为临界区)。

将原来例子中的TicketWindow类做如下修改:

class TicketWindow implements Runnable {
	int tickets = 100;

	@Override
	public void run() {
		while (true) {
			synchronized (this) {// 定义同步代码块
				if (tickets > 0) {
					try {
						Thread.sleep(30);
					} catch (Exception e) {
					}
					System.out.println(Thread.currentThread().getName() + " 余票: " + tickets--);
				}
			}
		}
	}
}

运行结果:

2.同步方法

在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:

synchronized 返回值类型 方法名([参数1,…]){
              方法体
}

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其它线程都会发生阻塞,直到当前线程访问完毕后,其它线程才有机会执行方法。

注意:

  • 同步方法的lock锁对象是this,也就是该对象本身。

  • synchronized关键字可以修饰方法、代码块,但不能修饰构造器、属性等。

public class SyncMethod {
	public static void main(String[] args) {
		Ticket t = new Ticket();
		new Thread(t, "窗口1").start();
		new Thread(t, "窗口2").start();
		new Thread(t, "窗口3").start();
	}

}

class Ticket implements Runnable {
	int tickets = 100;

	@Override
	public void run() {
		while (tickets > 0)
			sale();
	}

	public synchronized void sale() {// 同步方法
		if (tickets > 0) {
			try {
				Thread.sleep(30);
			} catch (Exception e) {
			}
			System.out.println(Thread.currentThread().getName() + " 余票: " + tickets--);
		}
	}
}

六、多线程通信

1.线程间通信方法

如果想解决上述问题,就需要控制多个线程按照一定的顺序轮流执行,此时需要让线程间进行通信。在Object类中提供了wait()、notify()、notifyAll()方法用于解决线程间的通信问题,由于Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。

wait()

使当前线程放弃同步锁并进入等待状态,

notify()

唤醒一个在此同步锁上处于等待状态(调用wait())的线程,然后本线程继续执行

notifyAll()

唤醒在此同步锁上所有处于等待状态的线程,本线程继续执行

但这三个方法必须由上了同步锁的对象调用,可以分成以下两种情况:

  • 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步锁对象,所以可以在同步方法中直接调用这3个方法。

  • 对于使用synchronized修饰的同步代码块,同步锁对象是synchronized后括号里的对象,所以必须使用该对象调用这3个方法。  

2.生产者消费者问题

设有一个缓冲区buffer,用一个数组来盛放数据。生产者线程(Producer)不断产生数据,送buffer,消费者线程(Consumer)从buffer中取出数据打印。如不加控制,会出现多种打印结果,这取决于这两个线程运行的相对速度。在这众多的打印结果中,只有这两个线程的运行刚好匹配的一种是正确的,其它均为错误。

public class ProducerConsumer{

	public static void main(String[] args) {
		Buffer b = new Buffer();
		Thread t1 = new Thread(new Producer(b));
		Thread t2 = new Thread(new Consumer(b));
		t1.start();
		t2.start();
	}
}

class Buffer {
	int[] data;// 盛放数据
	int index = 0;// 记录数组中元素的下标
	int id = 0;// 记录数组中元素的ID

	public Buffer() {
		data = new int[10];
	}

	// 生产方法
	public synchronized void produce() {
		data[index] = id;
		System.out.println("input: " + index + ", " + data[index]);
		index++;
		id++;
		while (index == data.length) {// 这里使用while而不用if,防止catch到InterruptedException时无视条件,继续往下执行
			this.notify();
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			index = 0;// 操作完数组的最后一个元素时索引置0,重新从数组的第一个位置操作。
		}
	}

	// 消费方法
	public synchronized void consume() {
		if (index > 0) {
			index--;
			System.out.println("output: " + index + ", " + data[index]);
		}
		while (index <= 0) {
			this.notify();
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

class Producer implements Runnable {
	private Buffer buffer;

	public Producer(Buffer b) {// 构造方法用来接收Buffer对象
		buffer = b;
	}

	@Override
	public void run() {
		while (true) {
			buffer.produce();
		}
	}
}

class Consumer implements Runnable {
	private Buffer buffer;

	public Consumer(Buffer b) {
		buffer = b;
	}

	@Override
	public void run() {
		while (true) {
			buffer.consume();
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

运行结果:

本文部分内容来自于THU 谌卫军老师教学PPT,特此感谢。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值