多线程编程

多线程编程

进程

一般可以在同一时间内执行多个程序的操作系统都 有进程的概念。一个进程就是一个执行中的程序, 而每一个进程都有自己独立的一块内存空间、一组 系统资源。在进程的概念中,每一个进程的内部数 据和状态都是完全独立的。

在Windows操作系统中一个进程就是一个exe或者dll 程序,它们相互独立,互相也可以通信,在 Android操作系统中进程间的通信应用也是很多 的。

线程

线程与进程相似,是一段完成某个特定功能的代 码,是程序中单个顺序控制的流程,但与进程不同 的是,同类的多个线程是共享一块内存空间和一组 系统资源。所以系统在各个线程之间切换时,开销 要比进程小的多,正因如此,线程被称为轻量级进程。一个进程中可以包含多个线程。

主线程

Java程序至少会有一个线程,这就是主线程,程序 启动后是由JVM创建主线程,程序结束时由JVM停 止主线程。主线程它负责管理子线程,即子线程的 启动、挂起、停止等等操作。下图所示是进程、 主线程和子线程的关系,其中主线程负责管理子线 程,即子线程的启动、挂起、停止等操作

[图] 进程、主线程和子线程关系

获取主线程示例代码如下:

// 获取主线程

ThreadmainThread=Thread.currentThread();

System.out.println("主线程名:"+mainThread.getName());

创建子线程

Java中创建一个子线程涉及到:java.lang.Thread类 和java.lang.Runnable接口。Thread是线程类,创建一个Thread对象就会产生一个新的线程。而线程执行的程序代码是在实现Runnable接口对象的run()方法中编写的,实现Runnable接口对象是线程执行对 象。线程执行对象实现Runnable接口的run()方法,run() 方法是线程执行的入口,该线程要执行程序代码都 在此编写的,run()方法称为线程体。

提示 主线程中执行入口是main(String[] args) 方法,这里可以控制程序的流程,管理其他的 子线程等。子线程执行入口是线程执行对象 (实现Runnable接口对象)的run()方法,在这个方法可以编写子线程相关处理代码。

实现Runnable接口

创建线程Thread对象时,可以将线程执行对象传递 给它,这需要是使用Thread类如下两个构造方法:

Thread(Runnable target, String name):target是线程执行对象,实现Runnable接口。name为线程 指定一个名字。

Thread(Runnable target):target是线程执行对 象,实现Runnable接口。线程名字是由JVM分配的。

下面看一个具体示例,实现Runnable接口的线程执 行对象Runner代码如下:

publicclassRunnerimplementsRunnable{

​@Override

publicvoidrun() {

for(inti=0;i<10;i++) {

//打印次数和线程名称

System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());

​longsleepTime=(long) (1000*Math.random());

try{

//线程休眠

Thread.sleep(sleepTime);

}catch(InterruptedExceptione) {

e.printStackTrace();

           }

       }

//线程执行结束

System.out.println("执行完成! "+Thread.currentThread().getName());

   }

}

代码Thread.sleep(sleepTime)是休 眠当前线程, sleep是静态方法它有两个版本:

static void sleep(long millis):在指定的毫秒数内 让当前正在执行的线程休眠。

static void sleep(long millis, int nanos) 在指定的毫 秒数加指定的纳秒数内让当前正在执行的线程 休眠。

测试程序代码如下:

publicclassRunnerTest{

publicstaticvoidmain(String[]args) {

//创建线程t1,参数时一个线程执行对象Runner

Threadt1=newThread(newRunner());

//开始线程t1

t1.start();

//创建线程t2,参数时一个线程执行对象Runner

Threadt2=newThread(newRunner(),"这里指定线程名称");

//开始线程t2

t2.start();

   }

}

提示 仔细分析一下运行结果,会发现两个线 程是交错运行的,感觉就像是两个线程在同时 运行。但是实际上一台PC通常就只有一颗 CPU,在某个时刻只能是一个线程在运行,而 Java语言在设计时就充分考虑到线程的并发调 度执行。对于程序员来说,在编程时要注意给 每个线程执行的时间和机会,主要是通过让线 程休眠的办法(调用sleep()方法)来让当前线 程暂停执行,然后由其他线程来争夺执行的机

会。如果上面的程序中没有用到sleep()方法, 则就是第一个线程先执行完毕,然后第二个线 程再执行完毕。所以用活sleep()方法是多线程 编程的关键。

继承Thread线程类

Thread类也实现了Runnable接口,所以 Thread类也可以作为线程执行对象,这需要继承 Thread类,覆盖run()方法。

采用继承Thread类重新实现Runner示例,自定义 线程类MyThread代码如下:

publicclassMyThreadextendsThread{

publicMyThread() {

super();

   }

publicMyThread(Stringname) {

super(name);

   }

//程序执行代码

@Override

publicvoidrun() {

for(inti=0;i<10;i++) {

//打印次数和线程名称

System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());

longsleepTime=(long) (1000*Math.random());

try{

//线程休眠

Thread.sleep(sleepTime);

}catch(InterruptedExceptione) {

e.printStackTrace();

           }

       }

//线程执行结束

System.out.println("执行完成! "+getName());

   }

}

通 过super调用父类Thread构造方法,这两个Thread类 构造方法:

Thread(String name):name为线程指定一个名 字。代码第④行调用都就是此构造方法。

Thread():线程名字是JVM分配的。代码第②行 调用都就是此构造方法。

测试程序代码如下:

//通过继承Thread线程类来创建线程

//创建线程t1,参数时一个线程执行对象Runner

Threadt3=newMyThread();

//开始线程t1

t3.start();

//创建线程t2,参数时一个线程执行对象Runner

Threadt4=newMyThread("这里指定线程名称");

//开始线程t4

t4.start();

提示 由于Java只支持单重继承,继承Thread 类的方式不能再继承其他父类。当开发一些图 形界面的应用时,需要一个类既是一个窗口 (继承JFrame)又是一个线程体,那么只能采 用实现Runnable接口方式。

使用匿名内部类和Lambda表达式实现线程体

如果线程体使用的地方不是很多,可以不用单独定 义一个类。可以使用匿名内部类或Lambda表达式直 接实现Runnable接口。Runnable中只有一个方法是 函数式接口,可以使用Lambda表达式。

代码如下:

//使用匿名内部类和Lambda表达式实现线 程体

//创建线程t5,参数是实现Runnable接口的匿名内部类

Threadt5=newThread(newRunnable() {

@Override

publicvoidrun() {

for(inti=0;i<10;i++) {

//打印次数和线程名称

System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());

longsleepTime=(long) (1000*Math.random());

try{

//线程休眠

Thread.sleep(sleepTime);

}catch(InterruptedExceptione) {

e.printStackTrace();

                   }

               }

//线程执行结束

System.out.println("执行完成! "+Thread.currentThread().getName());

           }

       });

// 开始线程t5

t5.start();


//创建线程t6,参数是,参数是实现Runnable接口的Lambda表达式

Threadt6=newThread(()->{

for(inti=0;i<10;i++) {

//打印次数和线程名称

System.out.printf("第%d次执行 - %s\n",i,Thread.currentThread().getName());

longsleepTime=(long) (1000*Math.random());

try{

//线程休眠

Thread.sleep(sleepTime);

}catch(InterruptedExceptione) {

e.printStackTrace();

                   }

               }

//线程执行结束

System.out.println("执行完成! "+Thread.currentThread().getName());

},"Lambda表达式");

// 开始线程t6

t6.start();

上述代码第①行采用匿名内部类实现Runnable接 口,覆盖run()方法。这里使用的是Thread(Runnable target)构造方法。代码第②行采用Lambda表达式实

现Runnable接口,覆盖run()方法。这里使用的是 Thread(Runnable target, String name)构造方法, Lambda表达式是它的第一个参数。匿名内部类和 Lambda表达式代码虽然很多,但是它只是一个参 数,实现了Runnable接口线程执行对象,

提示 匿名内部类和Lambda表达式不需要定 义一个线程类文件,使用起来很方便。特别是 Lambda表达式使代码变得非常简洁。但是客 观上匿名内部类和Lambda表达式会使代码可 读性变差,对于初学者不容易理解。

线程的状态

在线程的生命周期中,线程会有几种状态,如图 23-5所示,线程有5种状态。下面分别介绍一下。

新建状态

新建状态(New)是通过new等方式创建线程对 象,它仅仅是一个空的线程对象

就绪状态

当主线程调用新建线程的start()方法后,它就进 入就绪状态(Runnable)。此时的线程尚未真正 开始执行run()方法,它必须等待CPU的调度。

运行状态

CPU的调度就绪状态的线程,线程进入运行状 态(Running),处于运行状态的线程独占 CPU,执行run()方法。

阻塞状态因为某种原因运行状态的线程会进入不可运行 状态,即阻塞状态(Blocked),处于阻塞状态的线程JVM系统不能执行该线程,即使CPU空 闲,也不能执行该线程。如下几个原因会导致 线程进入阻塞状态:

当前线程调用sleep()方法,进入休眠状态。

被其他线程调用了join()方法,等待其他线程 结束。

发出I/O请求,等待I/O操作完成期间。

当前线程调用wait()方法。

处于阻塞状态可以重新回到就绪状态,如:休 眠结束、其他线程加入、I/O操作完成和调用 notify或notifyAll唤醒wait线程。

死亡状态

线程退出run()方法后,就会进入死亡状态 (Dead),线程进入死亡状态有可以是正常实 现完成run()方法进入,也可能是由于发生异常 而进入的。

线程管理

线程优先级

线程的调度程序根据线程决定每次线程应当何时运 行,Java提供了10种优先级,分别用1~10整数表 示,最高优先级是10用常量MAX_PRIORITY表 示;最低优先级是1用常量MIN_PRIORITY;默认 优先级是5用常量NORM_PRIORITY表示。

Thread类提供了setPriority(int newPriority)方法可以 设置线程优先级,通过getPriority()方法获得线程优 先级。

设置线程优先级示例代码如下:

//通过实现Runnable接口来创建线程

//创建线程t1,参数时一个线程执行对象Runner

Threadt1=newThread(newRunner());

//设置线程优先级10

t1.setPriority(Thread.MAX_PRIORITY);

//开始线程t1

t1.start();

//创建线程t2,参数时一个线程执行对象Runner

Threadt2=newThread(newRunner(),"这里指定线程名称");

//设置线程优先级1

t1.setPriority(Thread.MIN_PRIORITY);

//开始线程t2

t2.start();

提示 多次运行上面的示例会发现,t1线程经 常先运行,但是偶尔t2线程也会先运行。这些 现象说明了:影响线程获得CPU时间的因素, 除了受到的线程优先级外,还与操作系统有 关。

等待线程结束

在介绍现在状态时提到过join()方法,当前线程调用 t1线程的join()方法,则阻塞当前线程,等待t1线程 结束,如果t1线程结束或等待超时,则当前线程回到就绪状态。

Thread类提供了多个版本的join(),它们定义如下:

void join():等待该线程结束。

void join(long millis):等待该线程结束的时间最 长为millis毫秒。如果超时为0意味着要一直等下 去。

void join(long millis, int nanos):等待该线程结束 的时间最长为millis毫秒加nanos纳秒。

使用join()方法示例代码如下:

publicclassJoin{

staticintvalue=0;

publicstaticvoidmain(String[]args)throwsInterruptedException{

System.out.println("主线程开始");

//创建线程t,参数是一个线程执行对象Runner

Threadt1=newThread(newRunnable() {

@Override

publicvoidrun() {

System.out.println("子开始");

for(inti=0;i<3;i++) {

System.out.println("子执行");

value++;

               }

System.out.println("子结束");

           }

},"ThreadA");

//开始t线程

t1.start();

//主线程被阻塞,等待t1线程结束

t1.join();

System.out.println("value = "+value);

System.out.println("主线程结束");

   }

}

结果:

提示 使用join()方法的场景是,一个线程依赖 于另外一个线程的运行结果,所以调用另一个 线程的join()方法等它运行完成。

线程让步

线程类Thread还提供一个静态方法yield(),调用 yield()方法能够使当前线程给其他线程让步。它类 似于sleep()方法,能够使运行状态的线程放弃CPU 使用权,暂停片刻,然后重新回到就绪状态。与 sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级 高低都有机会运行。而yield()方法只给相同优先级 或更高优先级线程机会。

示例代码如下:

Thread.yield();

提示 yield()方法只能给相同优先级或更高优 先级的线程让步,yield()方法在实际开发中很

少使用,大多都使用sleep()方法,sleep()方法 可以控制时间,而yield()方法不能。

线程停止

线程体中的run()方法结束,线程进入死亡状态,线 程就停止了。但是有些业务比较复杂,例如想开发 一个下载程序,每隔一段执行一次下载任务,下载 任务一般会在由子线程执行的,休眠一段时间再执 行。这个下载子线程中会有一个死循环,但是为了 能够停止子线程,设置一个结束变量。

示例下面如下:

publicclassStopThread{

privatestaticStringcommand="";

publicstaticvoidmain(String[]args) {

//创建线程t1,参数是一个线程执行对象Runner

Threadt1=newThread(newRunnable() {

@Override

publicvoidrun() {

// 一直循环,直到满足条件在停止线程

while(!"exit".equalsIgnoreCase(command)) {

// 线程开始工作

System.out.println("下载中...");

try{

//线程休眠

Thread.sleep(1000);

}catch(InterruptedExceptione) {

e.printStackTrace();

                   }

               }

//线程结束

System.out.println("执行完成");

           }

       });

//开始线程t1

t1.start();

//输入

//BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

try(BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in))){

command=br.readLine();

}catch(IOExceptione) {

e.printStackTrace();

       }

   }

}

运行结果:

提示 控制线程的停止有人会想到使用Thread 提供的stop()方法,这个方法已经不推荐使用 了,这个方法有时会引发严重的系统故障,类似还是有suspend()和resume()挂起方法。Java现在推荐的做法就是采用本例的结束变量方式。

线程安全

多一个线程同时运行,有时线程之间需要共享数 据,一个线程需要其他线程的数据,否则就不能保 证程序运行结果的正确性。

例如有一个航空公司的机票销售,每一天机票数量 是有限的,很多售票点同时销售这些机票。下面是 一个模拟销售机票系统,示例代码如下

publicclassTicketDB{

//机票数量

privateintticketCount=5;

//获取当前机票数量

publicintgetTicketCount(){

returnticketCount;

   }

//销售机票

publicvoidsellTicket(){

//等待用户付款

//线程休眠

try{

Thread.sleep(1000);

}catch(InterruptedExceptione) {

e.printStackTrace();

       }

System.out.printf("第%d号票已经售出\n",ticketCount);

ticketCount--;

   }

调用代码如下:

publicclassTicketTest{

publicstaticvoidmain(String[]args) {

Runnablerun=newTicket();

// 创建线程t1

Threadt1=newThread(run);

t1.start();

Threadt2=newThread(run);

t2.start();

   }

staticclassTicketimplementsRunnable{

TicketDBdb=newTicketDB();

@Override

publicvoidrun() {

while(true){

intcurrentTC=db.getTicketCount();

//查询是否有票

if(currentTC>0) {

db.sellTicket();

}else{

//无票退出

break;

               }


           }

       }

   }

}

一次运行结果如下:

虽然可以能每次运行的结果都不一样,但是从结果 看还是能发现一些问题:同一张票重复销售、出现 第0号票和5张票卖了6次。这些问题的根本原因是 多个线程间共享的数据导致数据的不一致性。

提示 多个线程间共享的数据称为共享资源或 临界资源,由于是CPU负责线程的调度,程序

员无法精确控制多线程的交替顺序。这种情况 下,多线程对临界资源的访问有时会导致数据 的不一致性。

多线程同步

为了防止多线程对临界资源的访问有时会导致数据 的不一致性,Java提供了“互斥”机制,可以为这些 资源对象加上一把“互斥锁”,在任一时刻只能由一 个线程访问,即使该线程出现阻塞,该对象的被锁 定状态也不会解除,其他线程仍不能访问该对象, 这就多线程同步。线程同步保证线程安全的重要手 段,但是线程同步客观上会导致性能下降。可以通过两种方式实现线程同步,两种方式都涉及 到使用synchronized关键字,一种是synchronized方 法,使用synchronized关键字修饰方法,对方法进 行同步;另一种是synchronized语句,使用 synchronized关键字放在对象前面限制一段代码的 执行。

synchronized方法

synchronized关键字修饰方法实现线程同步,方 法所在的对象被锁定,修改售票系统示 例, TicketDB.java文件代码如下:

publicclassTicketDB{

//机票数量

privateintticketCount=5;

//获取当前机票数量

publicsynchronizedintgetTicketCount(){

returnticketCount;

   }


//销售机票

publicsynchronizedvoidsellTicket(){

//等待用户付款

//线程休眠

try{

Thread.sleep(1000);

}catch(InterruptedExceptione) {

e.printStackTrace();

       }

System.out.printf("第%d号票已经售出\n",ticketCount);

ticketCount--;

   }

}

上述代码第①行和第②行的方法前都使用了 synchronized关键字,表明这两个方法是同步 的,被锁定的,每一个时刻只能由一个线程访 问。并不是每一个方法都有必要加锁的,要仔 细研究加上的必要性,上述代码第①行加锁可

  以防止出现第0号票情况和5张票卖出6次的情况;代码第②行加锁是防止出现销售两种一样 的票

synchronized语句

synchronized语句方式主要用于第三方类,不方 便修改它的代码情况。同样是售票系统示例,可以不用修改TicketDB.java类,只修改调用代码TicketTest.java实现同步。

代码如下:

publicclassTicketTest{

publicstaticvoidmain(String[]args) {

//        Runnable run = new Ticket();

//        new Thread(run).start();

//        new Thread(run).start();

// 创建线程t1

Runnablerun=newTicket();

Threadt1=newThread(run);

t1.start();

Threadt2=newThread(run);

t2.start();

   }

staticclassTicketimplementsRunnable{

TicketDBdb=newTicketDB();

@Override

publicvoidrun() {

while(true){

//同步代码语句

synchronized(db) {

intcurrentTC=db.getTicketCount();

//查询是否有票

if(currentTC>0) {

db.sellTicket();

}else{

//无票退出

break;

                   }

               }

           }

       }

   }

}

将需要同步的代码用大括号括起来。 synchronized后有小括号,将需要同步的对象括 起来。

线程间通信

上面的示例只是简单地为特定对象或方法加 锁,但有时情况会更加复杂,如果两个线程之间有 依赖关系,线程之间必须进行通信,互相协调才能 完成工作。例如有一个经典的堆栈问题,一个线程生成了一些 数据,将数据压栈;另一个线程消费了这些数据, 将数据出栈。这两个线程互相依赖,当堆栈为空 时,消费线程无法取出数据时,应该通知生成线程 添加数据;当堆栈已满时,生产线程无法添加数据 时,应该通知消费线程取出数据。为了实现线程间通信,需要使用Object类中声明的5 个方法:

void wait():使当前线程释放对象锁,然后当前 线程处于对象等待队列中阻塞状态,如下图所 示,等待其他线程唤醒。

void wait(long timeout):同wait()方法,等待 timeout毫秒时间。

void wait(long timeout, int nanos):同wait()方法,等待timeout毫秒加nanos纳秒时间。

void notify():当前线程唤醒此对象等待队列中 的一个线程,如下图所示该线程将进入就绪状 态。

void notifyAll():当前线程唤醒此对象等待队列 中的所有线程,如下图所示这些线程将进入就 绪状态

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342