12.0 多线程

1.多线程概述

多线程是提升程序性能非常重要的一种方式,也是Java编程中的一项重要技术。在程序设计中,多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程,它们会交替执行,彼此间可以进行通信。本章将针对Java中的多线程知识进行详细地讲解等。

1.1.进程

进程是计算机中程序的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。虽说进程在程序执行时产生,但进程并不是程序。程序是“死”的,进程是“活”的,程序是指编译好的二进制文件,它存放在磁盘上,不占用系统资源,是具体的;而进程存在于内存中,占用系统资源,是抽象的。当一次程序执行结束之后,进程随之消失,进程所用的资源被系统回收。

1.2. 线程

每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线程。每一个进程中都至少存在一个线程。代码按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。

1.3. 多线程

多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行。多线程程序的执行过程如图所示。

图中所示的多条线程,看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,因此给人同时执行的感觉。

2.线程的创建

Java提供了多种种多线程的创建方式,此处介绍2种:

(1)继承javal.ang包中的Thread类,重写 Thread类的run()方法,在run()方法中实现多线程代码。

(2)实现javal.ang.Runnable接口,在run()方法中实现多线程代码。

2.1. 继承Thread类创建多线程

为了实现多线程,Java提供了一个线程类Thread,通过继承Thread类,并重写Thread类中的run()方法便可实现多线程。在Thread类中提供了一个start()方法用于启动新线程,新线程启动后,JVM会自动调用run()方法,如果子类重写了run()方法便会执行子类中的run()方法。

  • MyThread 类

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + i);
        }
    }
}
  • 测试类

public class Test {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        my1.setName("线程A");
        MyThread my2 = new MyThread();
        my2.setName("线程B");
        my1.start();
        my2.start();
    }
}

2.2. 实现Runnable接口创建多线程

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

  • MyRunnable 类

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
  • 测试类

public class Test {
    public static void main(String[] args) {
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my,"线程A");
        t1.start();
        Thread t2 = new Thread(my,"线程B");
        t2.start();
    }
}

3.线程的生命周期及状态转换

在线程整个生命周期中,基本状态一共有6种,分别是新建状态(New)、可运行(Runnable)、锁阻塞(Blocked)、无限等待(Waiting)、计时等待(Timed_Waiting) 、被终止(Teminated),线程的不同状态表明了线程当前正在进行的活动。

3.1. 线程生命周期中的基本状态

(1)新建状态

创建一个线程对象后,该线程对象就处于新建状态。此时还没调用start()方法进行启动,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。

(2)可运行状态

可运行状态也称为就绪状态。当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。

(3)锁阻塞状态

如果处于可运行的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入锁阻塞状态;当该线程持有锁时,该线程将变成可运行状态。

(4)无限等待状态

一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入无限等待状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

(5)计时等待状态

计时等待状态是具有指定等待时间的等待线程的线程状态。线程由于调用了计时等待的方法(Thread.sleep() 、Object.wait()、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil()),并且指定了等待时间而处于计时等待状态。这一状态将一直保持到超时期满或者接收到唤醒通知。

(6)被终止状态

被终止状态是终止线程的线程状态。线程因为run()方法正常退出而死亡,或者因为没有捕获的异常终止了run()方法而完成执行。

3.2. 线程状态的改变

在程序中,通过一些操作,可以使线程在不同状态之间转换,如图所示。

4.线程操作的相关方法

4.1. 线程的优先级

在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数表示,数字越大优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。

Thread类的优先级常量

功能描述

static int MAX_PRIORITY

表示线程的最高优先级,值为10

static int MIN_PRIORITY

表示线程的最低优先级,值为1

static int NORM_PRIORITY

表示线程的默认优先级,值为5

注意:虽然Java提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,操作系统中的线程优先级不会和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段。

4.2. 线程休眠

线程休眠指让当前线程暂停执行,从运行状态进入阻塞状态,将CPU资源让给其他线程的一种调度方式,可以调用线程的操作方法sleep()来实现线程休眠,sleep()方法是java.lang.Thread类中定义的静态方法。使用sleep()方法时需要指定当前线程休眠的时间,传入一个long类型的数据作为休眠时间,单位为毫秒,并且任意一个线程的实例化对象都可以调用该方法。

5.线程同步

多线程环境下,多个线程需要共享数据资源,因此可能存在多个并发线程同时访问同一资源,这可能引起线程冲突的情况。

5.1. 线程安全

(1)案例说明

假设售票厅有2个窗口可发售某日某次列车的100张车票,这时100张车票可以看做共享资源,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法,简单来说就是2个线程运行同一个售票程序。

(2)案例分析

上述售票案例中,极有可能碰到“意外”情况,例如一张票被打印多次,或者打印出的票号异常。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题。模拟上述所说的“意外”情况。假设2个窗口同时出售100张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠300毫秒。

(3)案例实现

定义SellTicket类并实现Runnable接口;定义私有int类型变量ticket,表示票数,初始值为1;重写run()方法,在run()方法中使用while循环售票;调用sleep()方法使线程休眠300毫秒,用于模拟售票过程中线程的延迟;具体代码如下所示:

  • SellTicket类

public class SellTicket implements Runnable{
    private int ticket = 1;
    @Override
    public void run() {
        while(true) {
            if(ticket<=100) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                };
                System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
                ticket++;
            }
            if(ticket > 100) {
                break;
            }
        }
    }
}
  • 测试类

public class TestDemo {

    public static void main(String[] args) {
        SellTicket st = new SellTicket();
        Thread t1 = new Thread(st,"窗口A");
        Thread t2 = new Thread(st,"窗口B");
        t1.start();
        t2.start();
    }

}

(4)运行结果

(5)结果分析

线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须保证在任何时刻只能有一个线程访问共享资源。

5.2. 同步代码块

为了实现多个线程处理同一个资源,在Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。

(1)同步代码块的语法格式

使用synchronized关键字创建同步代码块的语法格式如下。

synchronized(lock){
    操作共享资源代码块
}

在上面的代码中,lock是一个锁对象,它是同步代码块的关键,相当于为同步代码加锁。当某一个线程执行同步代码块时,其他线程将无法执行,进入阻塞状态。当前线程执行完后,再与其他线程重新抢夺CPU的执行权,抢到CPU执行权的线程将进入同步代码块,执行其中的代码。以此循环往复,直到共享资源被处理完为止。

(2)同步代码块优化售票案例

  • SellTicket 类

public class SellTicket implements Runnable{
    private int ticket = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while(true) {
            synchronized (obj) {
                if(ticket<=100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    };
                    System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
                    ticket++;
                }
                if(ticket > 100) {
                    break;
                }
            }
        }
    }
}
  • 测试类

public class Test {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();
        Thread t1 = new Thread(st,"窗口A");
        Thread t2 = new Thread(st,"窗口B");
        t1.start();
        t2.start();
    }
}

使用同步代码块注意事项:同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的一个。“任意”说的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果。

5.3. 同步方法

除了修饰代码块,sychronized关键字同样可以修饰方法,被synchronized关键字修饰的方法为同步方法。同步方法和同步代码块一样,同一时刻,只允许一个线程调用同步方法。

synchronized关键字修饰方法的具体语法格式如下:

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

同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一个线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。

5.4. Lock锁

并发工具提供了一些更高级的锁。如Lock接口,它提供了可以克服Java内置锁局限性的方法。Lock接口提供了lock()和unlock()方法,这意味着只要保留对某个锁的引用,就可以在程序中的任何位置释放该锁。

(1)Lock锁语法

Lock锁的语法格式如下所示:

private Lock lock = new ReentrantLock();
lock .lock();      //加锁
//需要锁的数据
lock .unlock();    //释放锁

(2)Lock锁线程安全案例

  • SellTicket类

public class SellTicket implements Runnable{
    private int ticket = 1;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true) {
            lock.lock();
            if(ticket <= 100) {
                try {
                    System.out.println(Thread.currentThread().getName()+"正在售卖第"+ticket+"张票");
                    Thread.sleep(100);
                    ticket++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                };
            }
            lock.unlock();
            if(ticket > 100) {
                break;
            }
        }
    }
}
  • 测试类

public class Test {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();
        Thread t1 = new Thread(st,"窗口A");
        Thread t2 = new Thread(st,"窗口B");
        t1.start();
        t2.start();
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WFIT~SKY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值