16.多线程
当程序同时完成多件事情时,就是所谓的多线程程序。使用多线程程序可以创建窗口程序、网络程序等。
16.1线程简介
并发,Java中一种同时进行多种活动的思想。
线程,被并发完成的每一件活动。
多线程,在程序中执行多个线程,每个线程完成一个功能,多个线程并发执行。
以Windows系统为例:
该系统为多任务操作系统,以进程为单位。一个进程是一个包含有自身地址的程序,每个独立执行的程序都被称为进程。系统分给每个进程一段CPU时间片,由于CPU转换较快,使每个每个进程看起来像同时执行。
一个线程是进程中的执行流程,一个进程可以同时包括多个线程,每个线程也可以得到一小段程序的执行时间,如此一个线程就可以具有多个并发执行的线程。
16.2实现线程的两种方式
Java中有两种实现线程的方式,分别是继承java.lang.Thread类和实现java.lang.Runable接口。
16.2.1继承Thread类
程序启动一个新线程需要建立Thread实例,Thread类中常用的两个构造方法如下:
public Thread():创建一个新的线程对象。
public Thread(String threadName):创建一个名称为threadName的线程对象。
继承类Thread类创建一个新的线程语法如下:
public class ThreadTest extends Thread{
}
一个类继承Thread类后,需要通过start()方法调用run()方法启动线程,具体实现线程功能需要覆盖run()方法。run()方法语法格式如下:
public void run(){
}
如果start()方法调用的是一个已经启动的方法,系统将抛出IllegalThreadException异常。
执行一个线程程序,将自动产生一个线程。主方法也是在线程上运行。不启动其他线程,该程序为单线程程序。
主方法由Java虚拟机启动,自定义线程需要由程序员启动。启动自定义线程代码如下:
public static void main(Stirng[] args){
new ThreadTest().start();
}
以下是一个继承Thread类的实例。
public class ThreadTest extends Thread {
private int count =10;
public void run(){
while(true){
System.out.print(count+" ");
if(--count==0)return;
}
}
//通常在run()使用无限循环使线程一直运行下去,有条件的return()跳出循环
public static void main(String[] args) {
new ThreadTest().start();
}
}
16.2.2实现Runnable()接口
如果程序需要继承非Thread类,又需要实现多线程,可以通过Runable接口来实现。
查询JDK文档,可知Thread类实现了Runable接口,其中run()方法正是对Runable()接口中run()方法的具体实现。
实现Runable接口的程序会创建要给Thread对象,并将Runable对象与Thread对象相关联。Thread()类构造方法如下:
public Thread(Runnable target)
public Thread(Tunnable target,String name)
使用Runable接口启动新的线程步骤如下:
(1)编写一个实现了Runnable()接口的类,实例化该类以建立Runnable对象
(2)使用参数为Runnable对象的构造方法创建Thread实例
(3)通过实例调用start()方法启动线程
16.3线程的生命周期
线程共有7种生命周期:
1.出生状态:进程被创建后,被start()方法调用之前的状态。
2.就绪状态:当用户调用start()方法后,线程处于就绪状态(可执行状态)。
3.运行状态:就绪状态的线程得到资源后进入运行状态。
4.等待状态:运行状态的线程调用Thread类中的wait()方法时,该线程便进入等待状态,处于等待状态的线程必须调用Thread类的notify()方法唤醒,notifyAll()方法可以唤醒所有等待状态下的线程。
5.休眠状态:线程调用sleep()方法时会进入休眠状态。
6.阻塞状态:运行状态的线程发出输入/输出请求,该线程将进入阻塞状态,等待结束后进入就绪状态。阻塞的线程即是系统资源空闲,线程依然不能回到运行状态。
7.死亡状态:线程的run()方法执行完毕时线程进入死亡状态。
系统为每个线程分配一小段CPU时间片,一旦时间片结束就会将当前线程换为下一个线程,即使该线程没有结束。
使线程处于就绪状态的几张方法:
调用sleep()方法。
调用wait()方法。
等待输入/输出完成。
处于就绪状态的线程有以下几张发发再次进入运行状态:
线程调用notify()方法。
线程调用notifyAll()方法。
线程调用interrupt()方法。
线程的休眠时间结束。
输入/输出结束。
16.4线程操作方法
16.4.1线程的休眠
sleep()方法可以使线程休眠,参数指定线程休眠时间,单位为毫秒。该方法通常在run()方法的循环中使用。
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
sleep()方法执行中可能抛出InterruptedException异常,所以将sleep()方法调用放在try-catch块中。
使用sleep()方法的线程醒来后,不一定会进入运行状态,也可能是就绪状态。
16.4.2线程的加入
当某线程调用join()方法加入另一线程后,另一线程将等待当前线程执行完毕再继续执行。
16.4.3线程的中断
“`java
public class InterruptedTest implements Runnable{
private boolean inContinue=false;
public void run(){
//...
if(isContinue)
break;
}
}
public void seContinue(){
this.isContinue=true;
}
“`
对于使用了sleep()或者wait()方法进入就绪状态的线程,可以使用Thread类中的inperrupt()方法使线程离开run()方法,同时结束线程。但此时线程会抛出InterruptedException异常,用户可以再处理该异常时完成线程的中断业务处理,如终止while循环。
16.4.4线程的礼让
Thread类的yield()方法告知当前线程可以将资源礼让给其他线程,但不保证当前线程会将资源礼让。
yield()方法使具有同样优先级的线程有进入可执行状态的机会,当前线程放弃执行权后再度进入就绪状态。
支持多任务的操作系统不需要调用yield()方法,操作系统会为线程自动分配CPU时间片。
16.5线程的优先级
如果有很多线程处于就绪状态,系统会根据优先级来决定先使用哪个线程进入运行状态。
低优先级的进程运行概率较小,如垃圾回收进程。
每个新产生的线程都继承了父线程的优先级。
Thread类中包含的成员变量代表了线程的优先级,如Thread.MIN_PRIORITY(常数1)、Thread.MAX_PRIORITY(常数10)、Thread.NORM_PRIORITY(常数5)。默认情况下进程优先级都是Thread.NORM_PRIORITY。
线程的优先级可以用setPriority()方法调整,如果使用 该方法设置的优先级不在1~20之内,将产生IllegalArgumentException异常。
16.6线程同步
在多线程程序中,可能出现多个线程抢占资源问题。同步机制即是为了放置资源访问冲突。
16.6.1线程安全
以下是一个线程冲突导致资源访问错误的实例:
public class ThreadSafeTest implements Runnable{
int num=8;
public void run(){
while(true){
if(num>0){
try {
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
System.out.println("tickets"+num--);
}
}
}
public static void main(String[] args) {
ThreadSafeTest t=new ThreadSafeTest();
Thread TA=new Thread(t);
Thread TB=new Thread(t);
Thread TC=new Thread(t);
Thread TD=new Thread(t);
Thread TE=new Thread(t);
TA.start();
TB.start();
TC.start();
TD.start();
TE.start();
}
}
由于各个线程读取num值和修改num值并不能同时进行,造成访问临界资源冲突。
16.6.2线程同步机制
所有解决多线程资源冲突问题的方法都是采用给定时间只允许一个线程访问共享资源,这时需要给共享资源上一道锁。同步机制使用synchronized关键字。
1.同步块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
同步块也被称为临界区,语法定义如下:
synchronized(Object){
}
Object为任意一个对象,每个对象都存在一个标志位,并且具有两个值,分别为0和1。一个线程运行到同步块时先检查该对象的标志位,为1表示该同步块无对象在运行,则该线程执行同步块代码,将Object对象值设为0,执行完毕后将Object对象值置为1。若该线程检查到同步块对象值为0,则该进程状态改变成就绪状态,知道同步块中线程执行完毕后将对象值重新设置为1。
public class ThreadSafeTest implements Runnable{
int num=8;
public void run(){
while(true) {
synchronized ("") {
if (num > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}
}
public static void main(String[] args) {
ThreadSafeTest t=new ThreadSafeTest();
Thread TA=new Thread(t);
Thread TB=new Thread(t);
Thread TC=new Thread(t);
Thread TD=new Thread(t);
Thread TE=new Thread(t);
TA.start();
TB.start();
TC.start();
TD.start();
TE.start();
}
}
2.同步方法
同步方法就是在方法前加上synchronized关键字的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
参考资料:https://www.cnblogs.com/jiansen/p/7351872.html
举例如下:
public synchronized void run(){
while(true) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}
值得注意的是,同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。