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();
}
}