人们在日常生活中,很多事情可以同时进行。计算机这种能够同时完成多项任务的技术,就是多线程技术。java是支持多线程的语言之一,它内置了对多线程技术的支持,可以使程序同时执行多个执行片段。
在知道线程之前,需要先了解一下什么是进程。在一个操作系统中,每一个独立执行的程序都可以称之为一个进程,也就是“正在运行的程序”。目前大部分计算机上安装的都是多任务操作系统,即能够同时执行多个应用程序,最常见的有windows系统,linux和unix等。在windows操作系统下,可以鼠标右键单击任务栏,选择【启动任务管理器】选项可以打开任务管理器面板,在窗口中的【进程】选项卡中可以看到当前正在运行的程序,也就是系统所有的进程。
在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天。但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由cpu执行的,对于一个cpu而言,在某个时间只能运行一个程序,也就是说只能执行一个进程。操作系统会为每个进程分配一段有限的cpu使用时间,cpu在这段时间中执行某个进程,然后能在极短的时间内在不同的进程之间切换,所以给人以同时执行多个程序的感觉。
从上面我们可以知道,每个运行的程序都是一个进程,在一个进程中还可以执行多个单元同时运行,这些执行单元可以看做程序执行一条条线索,被称作线程。操作系统中每一个进程都至少存在一个线程。例如当一个java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
在上面所了解过的程序中,代码都是按照调用顺序依次往下执行,没有出现两端程序代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替执行的效果,则需要创建多个线程,即多线程程序。所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,他们可以并发执行。
在java中,提供了两种多线程实现方式,一种是继承java.lang包下的thread类,覆写thread类的run方法,在run方法中实现运行在线程上的代码;另一种是实现java.lang.Runnable接口,同样是在run方法中实现运行在线程上的代码。接下来就对创建的多线程的两种方式进行讲解和比较。
/*
* 单线程程序
*/
public class Example01 {
public static void main(String[] args) {
//创建MyThread类对象
MyThread myThread = new MyThread();
myThread.run();//调用run方法
while(true) {
System.out.println("main方法在运行");
}
}
}
class MyThread{
public void run() {
while(true) {//该循环对应的是一个死循环,打印输出语句
System.out.println("MyThread类的run()方法在运行");
}
}
}
/*
* 继承Thread类的方式来实现多线程
*/
public class Example02 {
public static void main(String[] args) {
MyThread myThread = new MyThread();//创建线程MyThread的线程对象
myThread.start();//开启线程
while(true) {
System.out.println("main方法在运行");
}
}
}
class MyThread extends Thread{
public void run() {
while(true) {
System.out.println("MyThread类的run方法正在运行");
}
}
}
单线程的程序在运行时,会按照代码的调用顺序执行,而在多线程中,main方法和MyThread类的run方法却可以同时运行,互不影响,这正是单线程和多线程的区别。
在上面通过Thread类实现了多线程,但是这种方式有一定的局限性。因为java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类。
为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需要为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中run方法作为运行代码,而不需要调用Thread类中的run方法。
/*
* 通过实现Runnable接口方式来创建多线程
*/
public class Example03 {
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正在运行");
}
}
}
//线程的任务类
class MyThread implements Runnable{
@Override
public void run() {//线程对象调用start方法时,线程会从此处进行执行
while(true) {
System.out.println("MyThread类中的run方法正在运行");
}
}
}
既然直接继承Thread类和实现Runable接口都能实现多线程,那么两种实现多线程的方式在实际应用中又有是没区别呢?接下来我们分析一下应用场景。
假设售票厅有四个可发售某日某次列车的100张车票,这时,100张车票可以被看做共享资源,四个售票窗口需要创建四个线程。为了更加直观的显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法可以获取到线程的名称。
/*
* 售票程序,通过继承Thread类的方式来实现多线程的创建
*/
public class Eaxmple04 {
public static void main(String[] args) {
new TicketWindow().start();//创建第一个线程对象TicketWindow,并开启线程
new TicketWindow().start();//创建第二个线程对象TicketWindow,并开启线程
new TicketWindow().start();//创建第三个线程对象TicketWindow,并开启线程
new TicketWindow().start();//创建第四个线程对象TicketWindow,并开启线程
}
}
class TicketWindow extends Thread{
private int tickets = 100;
public void run() {//重写thread的run方法
while(true) {
if(tickets>0) {
Thread th = Thread.currentThread();//获取run方法的线程
String th_name = th.getName();//获取当前线程的名字
System.out.println(th_name+"正在发售第"+ tickets-- +"张票;");
}
}
}
}
以下是运行的部分结果:
Thread-0正在发售第100张票;
Thread-0正在发售第99张票;
...
Thread-0正在发售第73张票;
...
Thread-0正在发售第1张票;
Thread-3正在发售第100张票;
Thread-3正在发售第99张票;
Thread-3正在发售第98张票;
可以发现,这是每个窗口都卖100张的票,与我们的初衷不符,再来看看用Runnable的方法
/*
* 售票程序,通过实现Runnable接口的方式来创建多线程
*/
public class Example05 {
public static void main(String[] args) {
//创建线程的任务类对象
TicketWindows task = new TicketWindows();
new Thread(task,"窗口1").start();//创建线程并命名为窗口1,开启线程
new Thread(task,"窗口2").start();//创建线程并命名为窗口2,开启线程
new Thread(task,"窗口3").start();//创建线程并命名为窗口3,开启线程
new Thread(task,"窗口4").start();//创建线程并命名为窗口4,开启线程
}
}
//线程任务类
class TicketWindows implements Runnable{
private int tickets = 100;
@Override
public void run() {
while(true) {
if(tickets>0) {
Thread th = Thread.currentThread();//获取当前运行run方法的程序
String th_name = th.getName();//获取线程的名称
System.out.println(th_name+"窗口,正在发售第"+tickets--+"张票;");
}
}
}
}
以下是运行的部分结果:
窗口2窗口,正在发售第2张票;
窗口3窗口,正在发售第71张票;
窗口4窗口,正在发售第89张票;
窗口1窗口,正在发售第96张票;
窗口2窗口,正在发售第1张票;
通过上面的两个例子可以看出,实现Runnable接口相对于继承Thread类来说,有如下明显的好处:
在java中,任何对象都有生命周期,线程也不例外,它有自己的生命周期。当Thread对象创建完成时,线程的生命周期就开始了。当run方法中代码正常执行完毕或者线程抛出一个未捕获对的异常或者错误时,线程的什么周期变回结束。线程整个生命周期可以分为5个阶段,分别是新建状态、就绪状态、运行状态、阻塞状态和死亡状态。线程的不同状态表名了咸亨当前正在进行的活动。在程序中,通过一些操作,可以使线程在不同状态之间转换。
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他java对象一样,仅仅由java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
当线程对象调用了start方法后,该线程就进入了就绪状态。处于就绪状态的线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
如果处于就绪状态的线程获得了cpu的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完全系统分配的时间后,系统就会剥夺该线程占用的cpu资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才能转换到运行状态。
一个正在执行的线程在某些特殊的情况下,如被认为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
以下是线程由运行状态转化成阻塞状态的原因,以及如何从阻塞状态转化成就绪状态。
当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须获得到其他线程所持有的锁。
当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须等到这个阻塞的IO方法返回。
当线程调用了某个对象的wait方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify方法唤醒该线程。
当线程调用了Thread的sleep()方法时,也会使线程进入阻塞状态,在这种情况下,只需要等到线程睡眠的时间到了之后,线程就自动进入就绪状态。
当在一个线程中调用了另一个线程的join方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。
需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
当线程调用stop方法或run方法正常执行完毕后,或者线程抛出一个未捕获的异常、错误,线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转化其他状态。#
之前说过,程序中多个线程是并发执行的,某个线程弱项被执行必须要得到CPU的使用权。java虚拟机会按照特点定的机制为程序中的每个线程分配CPU使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型时让所有的线程轮流获得CPU的使用权,并且平均分配每一个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级最高的线程有限占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他的线程获取CPU使用权。java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定需求下需要改变这种模式,由程序自己来控制cpu的调度。
在应用程序中,如果要对线程进行调度,最直接的方式就是社会组线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量来表示线程的优先级,如表所示:
Thread类的静态常量 | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,值为10 |
static int MINPRIORITY | 表示线程的最低优先级,值为1 |
static int NORM_PRIORITY | 表示线程的普通优先级,值为5 |
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority方法对其进行设置,该方法中的参数newPriority接受的是1~10之间的整数或者Thread类的三个静态常量。
/*
* 不同的优先级的连个线程在层序中运行情况
*/
public class Example06 {
public static void main(String[] args) {
//创建两个线程的操作
Thread minPriority = new Thread(new Task(),"优先级较低的线程");
Thread maxPriority = new Thread(new Task(),"优先级较高的线程");
//设置线程的优先级
minPriority.setPriority(Thread.MIN_PRIORITY);//设置线程的优先级为1
maxPriority.setPriority(Thread.MAX_PRIORITY);//设置线程的优先级为10
//开启两个线程
minPriority.start();
maxPriority.start();
}
}
//定义一个线程的任务类
class Task implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"正在输出"+i);
}
}
}
我们知道,优先级高的程序会先执行,而优先级低的程序会后执行。如果希望人为地控制线程,使正在执行的线程暂停,将cpu让给别的线程,这是可以使用静态方法sleep,该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep方法后,在指定时间内该线程不会执行的,这样其他线程就可以得到执行的机会了。
sleep方法声明会抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。
/*
* sleep方法在程序中的使用
*/
public class Example07 {
public static void main(String[] args) throws InterruptedException {
new Thread(new Task()).start();
for (int i = 1; i <= 10; i++) {
if(i==5) {
Thread.sleep(2000);
}else {
Thread.sleep(500);
}
System.out.println("Main线程正在输出"+i);
}
}
}
class Task implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
try {
if(i==3) {
Thread.sleep(2000);//当前程序休眠2秒
}else {
Thread.sleep(500);
}
System.out.println("线程一正在输出"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在篮球比赛中,我们经常会看到两对选手互相抢篮球,当某个选手抢到篮球后就可以拍一会,之后他会把篮球让出来,其他选手重新开始抢篮球,这个过程就相当于java程序中的线程让步。所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行。
线程让步可以通过yield()方法来实现,该方法和sleep方法有些相似,都可以让当前正在运行的线程暂停,区别在于yield方法不会阻塞该线程,它只是将线程转化成就绪状态,让系统的调取器重新调度一次。当某个线程调用yield方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
/*
* 线程让步,yield方法
*/
public class Example08 {
public static void main(String[] args) {
//创建两个线程
Thread t1 = new Yield("线程A");
Thread t2 = new Yield("线程B");
//开启线程
t1.start();
t2.start();
}
}
class Yield extends Thread{
//构造方法
public Yield(String name) {
super(name);//调用父类的构造方法,完成线程名称的指定
}
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
if(i==3) {
System.out.println(Thread.currentThread().getName()+"让步");
Thread.yield();//线程运行到此处,做出让步
}
}
}
}
现实生活中经常能看到“插队”的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。当某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到join()方法加入线程执行完成后才会继续运行。
/*
* 线程插队。join()方法实现
*/
public class Example09 {
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread t = new Thread(new Task(),"线程1");
//启动线程
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毫秒
}
}
}
class Task implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 6; i++) {
System.out.println(Thread.currentThread().getName()+"输出"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
前面讲到过多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
我们将之前买票代码进行修改,在卖出一张票之前进行休眠10秒。
/*
* 售票程序,通过实现Runnable接口的方式来创建多线程
*/
public class Example10 {
public static void main(String[] args) {
//创建线程的任务类对象
TicketWindows task = new TicketWindows();
new Thread(task,"窗口1").start();//创建线程并命名为窗口1,开启线程
new Thread(task,"窗口2").start();//创建线程并命名为窗口2,开启线程
new Thread(task,"窗口3").start();//创建线程并命名为窗口3,开启线程
new Thread(task,"窗口4").start();//创建线程并命名为窗口4,开启线程
}
}
//线程任务类
class TicketWindows implements Runnable{
private int tickets = 10;//10张票
@Override
public void run() {
while(tickets>0) {
try {
Thread.sleep(10);//线程休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--卖出的票" + tickets--);
}
}
}
在运行之后,会发现有时候一个窗口在卖同一张票,也有的时候出现了不存在的票,接下来我们看看如何修改。
上面我们了解到线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决上面代码的线程安全问题,必须得保证下面用于处理共享资源的代码在任何时刻都只能有一个线程访问。
为了实现这种限制,java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键词来修饰的艾玛中,这个代码块被称作同步代码块,其语法格式如下:
synchronized(lock){
操作共享资源代码块
}
上面的代码中,lock是一个锁对象,它是同步代码块的关键。当某一个县城执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。
/*
* 售票程序,通过实现Runnable接口的方式来创建多线程
*/
public class Example11 {
public static void main(String[] args) {
//创建线程的任务类对象
TicketWindows task = new TicketWindows();
new Thread(task,"窗口1").start();//创建线程并命名为窗口1,开启线程
new Thread(task,"窗口2").start();//创建线程并命名为窗口2,开启线程
new Thread(task,"窗口3").start();//创建线程并命名为窗口3,开启线程
new Thread(task,"窗口4").start();//创建线程并命名为窗口4,开启线程
}
}
//线程任务类
class TicketWindows implements Runnable{
private int tickets = 10;//10张票
Object lock= new Object();//定义任意一个对象,用作同步代码块的锁
@Override
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;
}
}
}
}
}
上面直到同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为了这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体方法如下:
synchronized 返回值类型 方法名({参数1},....){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,知道当前线程访问完毕后,其他线程才会有机会执行该方法。
/*
* 同步方法
*/
public class Example12 {
public static void main(String[] args) {
//创建线程的任务类对象
TicketWindows task = new TicketWindows();
new Thread(task,"窗口1").start();//创建线程并命名为窗口1,开启线程
new Thread(task,"窗口2").start();//创建线程并命名为窗口2,开启线程
new Thread(task,"窗口3").start();//创建线程并命名为窗口3,开启线程
new Thread(task,"窗口4").start();//创建线程并命名为窗口4,开启线程
}
}
//线程任务类
class TicketWindows implements Runnable{
private int tickets = 10;//10张票
@Override
public void run() {
while(true) {
sendTicket();
}
}
//定义售票方法
public synchronized void sendTicket() {
try {
Thread.sleep(10);//线程休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets>0) {
System.out.println(Thread.currentThread().getName() + "--卖出的票" + tickets--);
} else {
System.exit(0);
}
}
}
思考:
如果两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象成为死锁。
/*
* 死锁问题
*/
public class Example13 {
public static void main(String[] args) {
//2个创建任务对象
Task task1 = new Task(true);
Task task2 = new Task(false);
//创建两个线程对象
Thread t1 = new Thread(task1,"Chinese");
Thread t2 = new Thread(task2,"American");
//启动线程
t1.start();
t2.start();
}
}
class Task implements Runnable{
//Object chopsticks = new Object();//定义object类型的锁对象 chopsticks筷子
//Object knife = new Object();//定义object类型的锁对象knife 刀
//这样的代码还是不会出现任何问题,为了让多个线程使用同一把锁,我们需要在当前对象加上静态修饰,这样这个对象在内存中就是独一份。
static Object chopsticks = new Object();//定义object类型的锁对象 chopsticks筷子
static Object knife = new Object();//定义object类型的锁对象knife 刀
private boolean flag;
//构造方法
public Task(boolean flag) {
this.flag=flag;
}
@Override
public void run() {
if(flag) {
while(true) {
synchronized (chopsticks) {//chopsitcks锁对象的同步代码块
System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
synchronized (knife) {//knife锁对象的同步代码块
System.out.println(Thread.currentThread().getName()+"---if---knife");
}
}
}
} else {
while(true) {
synchronized (knife) {//knife锁对象的同步代码块
System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
synchronized (chopsticks) {//chopsitcks锁对象的同步代码块
System.out.println(Thread.currentThread().getName()+"---if---knife");
}
}
}
}
}
}