------- android培训、java培训、期待与您交流! ----------
1 多线程的概述
1.1 进程
是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个执行单元,一个进程中可以有多个执行单元同时运行。
1.2 线程
就是进程中的一个独立的执行单元,这些执行单元可以看作程序执行的一条条线索,被称为线程,操作系统中的每一个进程都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程中会运行main()方法中的代码。线程控制着进程的执行。
1.3 线程的创建
1.3.1 继承Thread类创建多线程
创建线程的第一种方式:继承Thread类
步骤:
1 定义类继承Thread。
2 复写Thread类中的run方法。
3 调用线程的start方法,
该方法两个作用:启动线程,调用run方法。
创建线程的第二种方式:实现Runable接口
步骤:
1 定义类实现Runnable接口
2 重写Runnable接口中的run方法
3 通过Thread类建立线程对象
4 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
5 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
1.3.2 通过匿名内部类
1.3.3 两种创建方法的对比
下面通过开启多个线程售票的例子来对比两种方法:
1 通过继承Thread方法
运行结果:
发现同一张票三个线程都有售出,说明虽然实现了多线程但是并没有保证资源的共享性。
2 通过实现Runnable接口方法
运行结果:
发现实现Runable接口的方法可以确保三个线程访问的是同一个x变量,这样实现了资源的共享性。
综上所述,我们可以得出实现Runnable接口相对于继承Thread类来说有如下好处:
1 适合多个相同程序代码的线程去处理同一个资源的情况,可以达到资源的共享性。
2 可以避免由于Java的单继承带来的局限性。
事实上,大部分的应用程序都会选用实现Runnable接口的方式创建多线程。
通过以上案例我们还可以发现,程序运行结果每一次都不同,因为多个线程都在获取cpu执行权,cpu执行到谁,谁就运行,明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)这是多线程的一个特性:随机性
1.3.4 获取和设置线程名称
获取线程名称:
1 Thread.currentThread():获取当前线程的对象
2 getName():获取线程名称,线程都有自己默认的名称,Thread-编号,如Thread-1
合起来用:Thread.currentThread().getName();
设置线程名称:
1 自定义线程名称:直接在创建线程的时候给父类构造函数赋值。
2 setName("线程名"):设置线程名称
2 线程的生命周期及状态转换
2.1 线程的状态
线程的整个生命周期可以分为5个状态:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。
1 新建状态
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,没有表现出任何线程的动态特征。
2 就绪状态
当线程对象调用了start()方法后,该线程就进入了就绪状态,处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得CPU的使用权开始执行还需要等待系统的调度。
3 运行状态
如果处于就绪状态的线程获得了CPU的执行权,开始执行run()方法中的代码,则该线程处于运行状态。当一个线程启动后,它不可能一直处于运行状态,当使用完系统分配的时间后系统就会剥夺它的执行权,让其他线程获得执行的机会。需要注意的是只有处于就绪状态的线程才可能转换到运行状态。
4 阻塞状态
一个正在执行的线程在某些特殊情况下会放弃CPU的执行权,进入阻塞状态,线程进入阻塞状态后就不能进入排队队列,只有引起阻塞的原因被消除后,线程才可以转入就绪状态。
需要注意的是,线程从阻塞状态只能进入就绪状态,不能直接进入运行状态。
5 死亡状态
线程的run()方法执行完毕,或者线程抛出一个未捕获的异常、错误,线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
3 线程的调度
Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制称作线程的调度。
3.1 线程的优先级
优先级越高的线程获得CPU的执行机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1-10之间的整数来表示,数字越大优先级越高。通过Thread类的setPriority()方法进行设置,除了用1-10来表示,还可以调用Thread类提供的三个静态常量:
Thread类的静态常量 | 功能描述 |
static int MAX_PRIORITY | 表示最高优先级,相当于10 |
static int NORM_PRIORITY | 表示普通优先级,相当于5 |
static int MIN_PRIORITY | 表示最低优先级,相当于1 |
运行结果:
发现最高优先级的线程比最低优先级的线程要优先被CPU执行。
3.2 线程休眠
如果希望人为地控制线程,使正在执行的线程暂停,把CPU的执行权让给别的线程,可以使用静态方法sleep(),该方法可以让当前正在执行的线程暂停一段时间,进入休眠状态。需要注意的是,sleep()方法会抛出一个InterruptedException异常,因此在使用时注意抛出或处理这个异常。
运行结果:
可以看出在主线程休眠的200ms内,线程一执行完毕,然后才到主线程。
需要注意的是sleep()方法是静态方法,只能控制当前正在运行的线程休眠,不能控制其他线程休眠,当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。
3.3 线程让步
线程让步可以用yield()方法来实现,该方法和sleep()方法有点相似,都可以让正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,只是将线程转换成就绪状态,让CPU再重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
注意:使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
运行结果:
从运行结果可以看出,当线程B输出3以后,会做出让步,线程A执行,同样,线程A输出3以后,也会做出让步,线程B执行。
3.4 线程插队
当在某个线程中调用其它线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后才会继续运行。
运行结果:
可以看出,当主线程运行到x=3时,线程A开始插队,并且线程A全部运行完之后主线程才开始运行。注意join()方法需要抛出或处理InterruptedException异常。
3.5 守护线程
如果所有的线程中只有守护线程,那么守护线程才会停止运行。
运行结果:
可以很清楚的看到当主线程运行完毕后,只剩下了守护线程,所以守护线程也会停止运行。
需要注意的是,要将某个线程设置为守护线程,必须在该线程启动之前,也就是说setDaemon()方法必须在start()方法之前调用,否则会引发异常。
4 线程的同步
4.1 线程的安全问题
根据售票案例,极有可能碰到“意外的情况”,如一张票被打印多次,或者出现票号为0甚至是负数的情况,这是什么原因引起的呢?下面我们通过加入sleep()方法来重现这个错误。
运行结果:
发现出现了0号票甚至是-1号票,这是因为,线程在进入x>0的循环体内有可能不执行打印,但其他线程又进入,所以会导致本来要进行的判断失效,一下多打印了好几次。
4.2 同步代码块
要想解决上述问题,必须保证用于处理共享资源的代码在任何时刻只能有一个线程访问。
为了实现这种限制,Java提供了同步机制,即将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,语法格式:
Synchronized(lock)
{
操作共享资源代码块。
}
lock是一个锁对象,当执行到同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会阻塞,等待当前线程执行完后锁对象的标志位被置为1,新线程才能进入。
同步的前提:
1 必须要有两个或者两个以上的线程。
2 必须是多个线程使用同一个锁。
3 必须把同步代码块放在判断的外面
同步失败的例子:
好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,较为消耗资源
4.3 同步函数
只需把synchronized作为修饰符加到函数上,则此函数具有同步功能,同步函数的锁是this
静态同步函数的锁是class对象,类名.class
4.4 多线程的死锁
如果两个线程都在等待对方的锁,这样就会造成程序的停滞,这种现象称为死锁。
5 线程的通信
多个线程之间需要协同完成工作就需要线程之间进行通信。
模拟一个场景,假设有两个线程同时去操作同一个存储空间,其中一个线程负责存入数据,另一个线程负责取出数据,通过一个案例来实现上述情况。
运行结果:
这样便实现线程间交替执行了
需要注意的是,wait(),notify(),notifyAll(),因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。