本文内容是根据毕晓东老师的视频教程总结而得。包括多线程概述、线程的创建、线程的运行状态、多线程的安全问题、多线程同步代码块同步函数同步锁、多线程死锁、多线程等待唤醒机制、多线程生产者消费者、停止守护join/yield()方法等多线程基础知识。
1.多线程(概述)
本章主要内容进程、线程、多线程存在的意义、线程的创建方式、多线程的特性。
进程:进程就是正在进行中的程序。
如上图,CPU在对多个程序进行执行,但是多个程序间并不是同时执行。在某个时刻CPU只能执行一个程序,因此CPU是在多个程序中不断切换。如迅雷下载,为了提高下载速度,可以同时有多个下载请求,即一个进程中有可能会出现多条执行路径。在进行迅雷下载时的多条路径,如有100兆的数据,迅雷会默认分成5个部分,一个部分一个部分的同时发送下载请求到服务端,这样下载速度就会比较快。5个部分请求都是在同一个进程中完成,每一部分就是一个线程。线程是进程中的内容,每一个应用程序里面至少有一个线程。线程是程序中的执行单元,或者叫做执行路径(一个程序从头到尾执行)。
进程:是一个正在执行的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
进程是做什么的?无论QQ还是迅雷,他们只要启动,都会在内存中分配一片空间,给其分配地址。而进程其实就是用于定义和标识这个空间的,用于封装里面的控制单元。
线程:就是进程中一个独立的控制单元,线程在控制着进程的执行。一个进程中至少有一个线程。
java有两个进程。一个是编译进程java.exe;一个是运行进程javaw.exe。java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
扩展:其实java虚拟机启动不止一个线程,还有负责垃圾回收机制的线程。java虚拟机启动的时候就是多线程的。如,java主线程执行时会new很多对象,当对象使用完后,主线程会继续执行,但是同时负责垃圾回收的线程会对其进行垃圾回收。
多线程存在的意义:让程序中的每个部分具有同时执行效果;提高程序执行效率。
2.创建线程—继承Thread类
进程和线程都是由系统(Windows或Linux系统)创建的,所以java虚拟机依赖于系统后,只需要调用系统中创建线程的功能即可。
以下是java API中关于线程Thead类的描述:
创建线程的第一种方法:继承Thread类
步骤:
- 定义类继承Thread类
- 复写Thread类中的run方法
- 调用线程的start方法(该方法两个作用:使该线程开始执行;java虚拟机调用该线程的run方法)
run():如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回。Thread的子类应该重写该方法。
执行多次后,发现运行结果每一次都不同,因为多个线程都有权获取CPU的执行权,CPU执行到谁,谁就运行,但是,在某一时刻,只能有一个程序在执行(多核除外),CPU在做着快速的切换,已达到看上去是同时运行的效果。我们可以把多线程的运行认为是在互相抢夺CPU的执行权,这就是多线程的特性,随机性。谁抢到谁执行,至于执行多长时间由CPU决定。
CPU中不能同时有多个进程同时执行,而真正操作程序执行的是线程。所以,CPU其实是在不断切换进程中的线程。即多个线程在抢夺CPU的资源。
3.创建线程—run和start特点
为什么覆盖run方法?
Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。也就是说Thread类中的run方法是用于存储线程要运行的代码。(主线程要运行的代码存储在main方法中,由虚拟机定义)。
为什么不直接创建Thread类的实例(Thread t = new Thread; t.start())再开启(为什么要继承Thread类和复写Thread类中的run方法)?
因为start调用的是Thread里的run方法,但是run方法中没有任何内容;而且开启线程是为了执行指定的自定义代码,但是父类Thread中并没有任何实际功能,所以才要继承Thread类,复写掉run方法而去执行自定义的代码。
如上图执行结果,发现每次都是先执行主线程中的hello world,再执行自定义的线程类中的Demo run。因为自定义线程类只是创建了但是并没有启动(调用start方法),所以会先执行完main方法中的,再去调用自定义类中的内容,而此时执行自定义类中内容的形式和普通调用类没有区别,而不是多线程去执行。
- start():开启线程并执行该线程的run方法;
- run():仅仅是对象调用方法,而线程创建了但并未执行
4.线程练习
需求:创建两个线程和主线程交替执行
package com.vnb.javabase.thread;
public class TwoThread extends Thread{
private String name;
public TwoThread(String name){
this.name = name;
}
public void run(){
for (int i = 0; i < 60; i++) {
System.out.println(name+" TwoThread run..."+i);
}
}
}
package com.vnb.javabase.thread;
public class ThreadDemo {
public static void main(String[] args) {
TwoThread t1 = new TwoThread("one");
TwoThread t2 = new TwoThread("two");
t1.start();
t2.start();
for (int i = 0; i < 60; i++) {
System.out.println("main run..."+i);
}
}
}
运行结果:
如果都使用run方法:
package com.vnb.javabase.thread;
public class ThreadDemo {
public static void main(String[] args) {
TwoThread t1 = new TwoThread("one");
TwoThread t2 = new TwoThread("two");
//t1.start();
//t2.start();
t1.run();
t2.run();
for (int i = 0; i < 60; i++) {
System.out.println("main run..."+i);
}
}
}
执行结果:先执行完t1 one所有的,再执行t2 two所有的,最后执行main方法的。因为是直接调用run()方法,所以相当于实例的调用。而不是线程调用。
5.线程运行状态
线程的五种状态:
被创建:new Thread或者new Thread子类。
运行状态:调用start()后
冻结:分为睡眠和等待。sleep(time)调用后即冻结,时间到后自动启动;wait()调用后进程并未结束,由notify()唤醒后,线程重新返回到临时状态准备运行。
消亡:stop()方法调用后直接将线程挂掉,线程为消亡状态;run方法结束,线程也会消亡。
阻塞(临时状态):线程被start()不一定会运行,CPU在某一时刻只能运行一个线程,当同时启动多个线程时,只能运行一个线程,其他的线程都在等待CPU的执行权。此时,具备运行资格,但没有执行权。冻结状态也一样,当一个线程被sleep()或wait()后,线程放弃了执行资格,直到sleep()时间过了或者wait()被notify()唤醒后,会先从冻结状态回到临时状态,等待执行。
6.获取线程对象及名称
线程都有自己默认的名称:Thread-编号(该编号从0开始)
package com.vnb.javabase.thread;
public class TwoThread extends Thread{
public void run(){
for (int i = 0; i < 60; i++) {
System.out.println(this.getName()+" TwoThread run..."+i);
}
}
}
package com.vnb.javabase.thread;
public class ThreadDemo {
public static void main(String[] args) {
TwoThread t1 = new TwoThread();
TwoThread t2 = new TwoThread();
t1.start();
t2.start();
for (int i = 0; i < 60; i++) {
System.out.println("main run..."+i);
}
}
}
输出结果:
从Thread构造函数可知线程初始化就可以初始化名字。且发现此时父类的Thread类已经初始化完成,所以子类只需要调用super()方法即可。
package com.vnb.javabase.thread;
public class TwoThread extends Thread{
public TwoThread(String name){
super(name);
}
public void run(){
for (int i = 0; i < 60; i++) {
System.out.println(this.getName()+" TwoThread run..."+i);
}
}
}
package com.vnb.javabase.thread;
public class ThreadDemo {
public static void main(String[] args) {
TwoThread t1 = new TwoThread("one");
TwoThread t2 = new TwoThread("two");
t1.start();
t2.start();
for (int i = 0; i < 60; i++) {
System.out.println("main run..."+i);
}
}
}
输出结果:
返回当前正在执行的线程对象的引用:
- static Thread currentThread()
- 获取线程名称:getName()
- 设置线程名称:setName()或者构造函数
例:
package com.vnb.javabase.thread;
public class TwoThread extends Thread{
public TwoThread(String name){
super(name);
}
public void run(){
for (int i = 0; i < 60; i++) {
System.out.println(Thread.currentThread().getName()+" TwoThread run..."+i);
}
}
}
执行结果:
发现this调用和Thread.currentThread()方法线程调用方法结果是一致的。但是Thread.currentThread()是标准调用方式。
在循环中,当线程同时进来时,使用的不是同一个变量i,线程0运行时,栈内存中就会给线程0分配一个空间,这个空间里面就有run()方法,run方法里就有一个i变量;线程1进来运行时也会在栈内存中分配给线程1一个空间,这个空间上同样有一个run()方法,这个run()方法中也有一个i变量。且局部变量在每一个线程中都有独立的一份空间。
7.多线程—售票的例子
需求:简单的卖票例子,有多个窗口同时卖票。
package com.vnb.javabase.thread;
public class TicketThread extends Thread {
private int tick = 100;
public void run(){
while(true){
if(tick>0){
System.out.println(currentThread().getName()+" sale:"+tick--);
}
}
}
}
package com.vnb.javabase.thread;
public class TicketThreadDemo {
public static void main(String[] args) {
TicketThread t1 = new TicketThread();
TicketThread t2 = new TicketThread();
TicketThread t3 = new TicketThread();
TicketThread t4 = new TicketThread();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
由以下执行结果发现问题:即4个线程同时在卖一张票。
Thread-1 sale:1
Thread-3 sale:1
Thread-0 sale:1
Thread-2 sale:1
解决:让4个线程共享100张票即可。即将变量设置为静态,即可在内存中四个线程共享100张票。如下:
package com.vnb.javabase.thread;
public class TicketThread extends Thread {
private static int tick = 100;
public void run(){
while(true){
if(tick>0){
System.out.println(currentThread().getName()+" sale:"+tick--);
}
}
}
}
结果正确。
但是又发现以下问题:虽然static可以共享数据,但是static声明周期太长。在对象使用完后,会出现垃圾未被回收。
解决:使用一个对象执行4次。如下:
package com.vnb.javabase.thread;
public class TicketThreadDemo {
public static void main(String[] args) {
TicketThread t1 = new TicketThread();
t1.start();
t1.start();
t1.start();
t1.start();
}
}
结果:发现只有一个线程在执行。且报错。
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.vnb.javabase.thread.TicketThreadDemo.main(TicketThreadDemo.java:11)
Thread-0 sale:100
Thread-0 sale:99
Thread-0 sale:98
Thread-0 sale:97
分析:无效,线程状态异常。原因:当第一个start()执行后,已经从创建状态到运行状态了,当再次调用start()方法启动这个线程就没有意义了。因此这种创建线程方式不能解决了。见下一种创建线程方式:实现Runnable接口。
8.创建线程第二种方法—实现Runnable接口
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private static int tick = 100;
@Override
public void run(){
while(true){
if(tick>0){
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
}
package com.vnb.javabase.thread;
public class TicketRunnableDemo {
public static void main(String[] args) {
TicketRunnable t = new TicketRunnable();
new Thread(t).start();
}
}
解析:TicketRunnable类并不是线程类(没有创建Thread类或者其子类)。若直接写Thread tt = new Thread();tt.start();的话,执行的是Thread类中的run()方法,但是现在这个方法并没有被某个子类继承,而是只是实现了Runnable接口的run(),那么要怎样用Thread类去调用Runnable接口中的run()方法呢?
而且Runnable接口中的run()方法是非静态的需要被对象调用,而对象就是TicketRunnable对象,所以TicketRunnable对象要执行就必须得和Thread联系起来。
另外,创建线程的目的是为了让其执行指定的代码,而现在要运行的代码在TicketRunnable中,那么怎么让TicketRunnable对象和Thread类有关系呢?解决:在创建线程对象时就要明确自己要运行什么代码。由Thread类的API可知,Thread类有个构造方法Thread(Runnable target)可以将Runnable接口传给Thread类。即new Thread(t).start();或如下:
package com.vnb.javabase.thread;
public class TicketRunnableDemo {
public static void main(String[] args) {
TicketRunnable t = new TicketRunnable();
//new Thread(t).start();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果:发现100张票被共享,即每张票只会被卖一次。
创建线程的第二种方式:实现Runnable接口步骤:
- 定义类实现Runnable接口
- 覆盖Runnable接口中的run()方法(将线程要运行的代码存放在该run方法中)
- 通过Thread类建立线程对象
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数(为什么要将Runnable接口的子类对象传递给Thread的构造函数?因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属对象)
- 调用Thread类的start()方法开启线程并调用Runnable接口子类的run()方法
实现(Runnable)和继承(new Thread)创建线程的两种方式有什么区别?**
如上图,Student类中需要多线程执行时要继承Thread,当Student向上抽取后,有了一个父类Person类要继承,但是发现这样就不能继承Thread类了。因此就有了Runnable接口,当Student继承Person类后,仍然可以实现Runnable接口。
实现Runnable接口好处:避免了单继承的局限性,在定义线程时建议使用实现方式。
两种方式的区别:最大的区别在线程代码存放的位置不同
继承Thread:线程代码存放在Thread子类run方法中
实现Runnable:线程代码存放在接口的子类的run方法中
9.多线程的安全问题
如上图,当0,1,2,3四个线程同时进来时,假设0线程首先获得线程执行权,但是执行到if(tick>0)后,0线程卧倒了(线程具备执行资格但是被其他线程抢走);假设1线程获得执行权,但是1线程判断完if(tick>0)后也卧倒了,随后,2和3线程也相继因此卧倒。假设4个线程都卧倒之后CPU再次执行到0线程,0线程开始输出线程并打印结果为1(假设执行到此),而tick--变为0,而1线程再次获得执行权后,发现tick为0,tick--则变为-1。由此,卖票为-1是不可能存在的,因此出现了安全性问题。
通过sleep()方法,模拟线程睡眠后出现安全问题:
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private int tick = 100;
@Override
public void run(){
while(true){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
//暂不处理
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
}
结果:
通过分析发现打印出了0,-1,-2等错票,多线程的运行出现了安全问题。只建立一个Ticket对象,tick变量在堆内存中也是共享的。为什么还会出现安全问题?
问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程又参与进来执行。导致共享数据的错误。
解决办法:对多条操作共享数据的语句只能让一个线程先执行完,再执行其他线程。在某个线程执行过程中,其他线程不可以参与执行。因此便有了下一节的多线程同步代码块。
10.多线程同步代码块
java对多线程的安全问题提供了专业的解决方式,即同步代码块:
synchronized(对象){
需要被同步的代码
}
哪些代码需要同步,就看哪些代码在操作共享数据。如下例:发现判断tick是否大于0后都在操作共享数据。
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private int tick = 100;
Object obj = new Object();
@Override
public void run(){
while(true){
synchronized (obj) {//这个只要是一个对象即可
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
}
}
结果:发现不再有错误的票卖出。
原理:
如上图,对象obj一存在后,默认会有两个标志位(能否执行的标志,如1可执行,0不可执行)。当0线程获取标志位为1时该线程获得执行权,然后0线程就进入同步代码块了,进入同步代码块后,不会先去执行if判断,而是会先将标志位改为0,然后进行判断,0线程执行到sleep(),执行权就释放了,0线程处于卧倒状态。假设1线程此时获得了执行权,会读取到标志位为0,则进入不到代码块,其他线程也执行不了。直到0线程再次执行(sleep时间到),执行完代码将tick--后会执行出同步代码块,然后将标志位改为1。然后0线程就执行完成。当1线程再次获得执行权并执行到同步代码块时,标志位已经变为1,然后进入同步代码块进行判断,继续上述过程。
对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取CPU的执行权,也执行不了,因为没有获取锁。
同步的前提:
- 必须要有两个或者两个以上的线程
- 必须是多个线程使用同一个锁
- 必须保证同步中只有一个线程正在执行
好处:解决了多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源。
11.多线程—同步函数
需求:银行有一个金库。有两个储户分别存300元,每次存100,存3次。
目的:探讨该程序是否有安全问题,如果有,如何解决?
简单例子:
package com.vnb.javabase.thread;
public class Bank {
private int sum;
public void add(int mon){
sum = sum + mon;
System.out.println("sum :"+sum);
}
}
package com.vnb.javabase.thread;
public class Customer implements Runnable {
private Bank b = new Bank();
@Override
public void run() {
for (int i = 0; i < 3; i++) {
b.add(100);
}
}
}
package com.vnb.javabase.thread;
public class BankDemo {
public static void main(String[] args) {
Customer c = new Customer();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
如何找问题?
- 明确哪些代码是多线程运行代码
- 明确哪些数据是共享数据
- 明确多线程运行代码中哪些语句是操作共享数据的
首先,找到需要多线程的运行代码,bank中:
public void add(int mon){
sum = sum + mon;
System.out.println("sum :"+sum);
}
Customer类中:
for (int i = 0; i < 3; i++) {
b.add(100);
}
其次,哪些数据是需要共享数据:sum和b引用。
最后,哪些语句操作共享数据:
Customer中 b.add(100);
bank中
sum = sum + mon;
System.out.println("sum :"+sum);
其中在sum = sum+mon;执行后就可能线程挂掉,则会出现线程问题。
如下,在sum = sum+mon;后进行sleep()模拟线程暂时睡眠后:
package com.vnb.javabase.thread;
public class Bank {
private int sum;
public void add(int mon){
sum = sum + mon;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sum :"+sum);
}
}
结果:发现出问题了。
加上同步代码块后:
package com.vnb.javabase.thread;
public class Bank {
private int sum;
Object obj = new Object();
public void add(int mon){
synchronized (obj) {
sum = sum + mon;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sum :"+sum);
}
}
}
结果:发现不再出现问题。
那么,同步代码块应该放在哪?
此例中,Customer类中的for循环也可以放在同步代码块,但是发现会等一个线程执行完后,另一个线程才开始执行。是不合理的。函数的调用是不需要同步的b.add(100);只需要函数里面的多条语句。
同步代码块是用于封装代码的,而函数也是用于封装代码的,两者唯一的区别是,同步代码块具有同步的特性。那么如果函数具有同步的特性也能解决问题。将synchronized关键字放在函数上即可。如下:
package com.vnb.javabase.thread;
public class Bank {
private int sum;
//Object obj = new Object();
public synchronized void add(int mon){
//synchronized (obj) {
sum = sum + mon;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sum :"+sum);
//}
}
}
12.多线程—同步函数的锁是this
将售票的例子同步代码块修改为同步方法,即为:
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private int tick = 100;
//Object obj = new Object();
@Override
public void run(){
while(true){
sale();
}
}
public synchronized void sale(){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
但是,发现Object obj = new Object();标志锁的对象已经被注释了,那么同步方法的锁究竟是什么?同步函数用的是哪一个锁呢?
sale方法一定会被对象调用,而调用时其实是this.sale();省略了this。而函数是需要被对象调用,函数都有一个本类的所属对象引用。就是this。所有同步函数使用的锁是this。
验证:使用两个线程来卖票,一个线程在同步代码块中,一个线程在同步函数中,同时执行卖票行为。通过boolean flag标记执行哪个方法。
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private int tick = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run(){
if(flag){
synchronized (obj) {
while(true){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" code:"+tick--);
}
}
}
}else{
while(true){
sale();
}
}
}
public synchronized void sale(){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
package com.vnb.javabase.thread;
public class TicketRunnableDemo {
public static void main(String[] args) {
TicketRunnable t = new TicketRunnable();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t.flag = false;
t2.start();
}
}
结果:发现所有的都是执行的sale方法,并没有执行同步代码块
原因:以上代码中存在3个线程,主线程和两个创建的线程,当主线程起来后,可能会瞬间就执行完main()方法中的线程启动代码和设置flag的代码将flag设置为false了,而此时t1还没有开始执行,等到执行时两个创建的线程的flag都已经为false了。所以都执行了一个方法,所以只要要主函数中再加一个sleep让主函数不要那么快设置flag为false即可,如下:
package com.vnb.javabase.thread;
public class TicketRunnableDemo {
public static void main(String[] args) {
TicketRunnable t = new TicketRunnable();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
结果:发现两个同步方式都有执行到,但是却仍然有错票出现。又出现了不安全的现象。
分析:为什么都加了同步还出现安全问题?查看同步安全的前提发现同步用的是两个同步方法,但是同步里的同步锁不是一个锁。同步方法中使用的是this,而同步块中使用的是obj。所以将同步块和同步方法中的锁改为一致即可。synchronized (this)。
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private int tick = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run(){
if(flag){
synchronized (this) {
while(true){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" code:"+tick--);
}
}
}
}else{
while(true){
sale();
}
}
}
public synchronized void sale(){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
结果:发现使用的是同一个锁,安全问题也解决了。
13.多线程-静态同步函数的锁是Class对象
售票的例子:在同步函数上加上静态后锁仍使用this,会出现不安全问题
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private static int tick = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run(){
if(flag){
while(true){
synchronized (this) {
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" code:"+tick--);
}
}
}
}else{
while(true){
sale();
}
}
}
public static synchronized void sale(){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
package com.vnb.javabase.thread;
public class TicketRunnableDemo {
public static void main(String[] args) {
TicketRunnable t = new TicketRunnable();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
结果:
由于同步函数sale()上加了静态方法,所以tick变量也要加上静态。
分析:发现静态同步方法sale()用的锁不是this。如果同步函数被静态修饰后,使用的同步锁不再是this。
解决:静态方法中不可以将锁定义成this,因为静态进内存时没有本类对象,但是有该类对应的字节码文件对象,类名.class。该对象的类型是Class。(类进内存时有对象,类进内存时会先封装成Class的对象,即字节码文件对象,而Ticket先进内存然后变成字节码对象)。
修改为本类对象后,正常:
package com.vnb.javabase.thread;
public class TicketRunnable implements Runnable {
private static int tick = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run(){
if(flag){
while(true){
synchronized (TicketRunnable.class) {
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" code:"+tick--);
}
}
}
}else{
while(true){
sale();
}
}
}
public static synchronized void sale(){
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" sale:"+tick--);
}
}
}
结果:不再出现安全问题
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。即,类名.class。类名.class是字节码文件,是唯一的。
14.多线程-单例设计模式-懒汉式
饿汉式:
class Single{
private static final Single s = new Single();
private Single(){}
public static Single getInstance(){
return s;
}
}
懒汉式:
问题:每次都要判断一次锁。效率较低。
解决:使用同步代码块。
package com.vnb.javabase.base;
public class Single {
private static Single instance = null;
private Single(){
}
public static Single getInstance(){
if(instance == null){
synchronized (Single.class) {
if(instance == null){
instance = new Single();
}
}
}
return instance;
}
}
15.多线程-死锁
同步会出现弊端。
死锁:同步中嵌套同步。如下例:
package com.vnb.javabase.thread;
public class TicketRunnable2 implements Runnable {
private int tick = 100;
Object obj = new Object();
boolean flag = true;
@Override
public void run(){
if(flag){
while(true){
synchronized (obj) {
sale();//obj里有同步this锁(sale同步方法)
}
}
}else{
while(true){
sale();
}
}
}
public synchronized void sale(){
//this锁里有Object锁
synchronized (obj) {
if(tick>0){
try{
Thread.sleep(10);
}catch(Exception e){
}
System.out.println(Thread.currentThread().getName()+" code:"+tick--);
}
}
}
}
package com.vnb.javabase.thread;
public class TicketRunnableDemo2 {
public static void main(String[] args) {
TicketRunnable2 t = new TicketRunnable2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
结果:发现执行到66后就锁住了不再继续执行了。
死锁例子:
package com.vnb.javabase.thread.deadlock;
public class MyLock {
static Object locka = new Object();
static Object lockb = new Object();
}
package com.vnb.javabase.thread.deadlock;
public class DeadLock implements Runnable {
boolean flag = true;
public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (MyLock.locka) {
System.out.println("if MyLock.locka");
synchronized (MyLock.lockb) {
System.out.println("if MyLock.lockb");
}
}
}else{
synchronized (MyLock.lockb) {
System.out.println("else MyLock.lockb");
synchronized (MyLock.locka) {
System.out.println("else MyLock.locka");
}
}
}
}
}
package com.vnb.javabase.thread.deadlock;
public class DeadLockTest {
public static void main(String[] args) {
Thread t1 = new Thread(new DeadLock(true));
Thread t2 = new Thread(new DeadLock(false));
t1.start();
t2.start();
}
}
结果:发现会出现死锁,且每次执行的结果不一样。
16.线程间通信-示例代码
思考1:wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?
- 这些方法存在于同步中
- 使用这些方法必须要标识所属的同步锁
- 锁可以是任意对象,所以任意对象的调用的方法一定定义Object类中。
思考2:wait(),sleep()有什么区别?
- wait()释放资源,释放锁
- sleep()释放资源,不释放锁
以上讲解的多线程都是在每个线程运行的代码都一致的情况下。以下讲解的是多个线程,部分线程用于存(输入数据),部分线程用于取(获取数据)。如下图:
针对这个图,将描述三部分,输入数据、资源、输出数据。
线程间通讯:其实就是多个线程在操作同一个资源,但是操作的动作不同。
示例:
package com.vnb.javabase.thread.threadcommunication;
public class Res {
String name;
String sex;
}
package com.vnb.javabase.thread.threadcommunication;
public class Input implements Runnable {
private Res res;
Input(Res res){
this.res = res;
}
@Override
public void run() {
int x = 0;
while(true){
if(x==0){
res.name = "lili....";
res.sex = "female....";
}else{
res.name = "丽丽....";
res.sex = "女....";
}
x = (x+1)%2;//不断交替赋值
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class Output implements Runnable {
private Res res;
Output(Res res){
this.res = res;
}
@Override
public void run() {
while(true){
System.out.println(res.name+"*****"+res.sex);
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class InputOutputDemo {
public static void main(String[] args) {
Res res = new Res();
Input in = new Input(res);
Output out = new Output(res);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
分析:示例中有一个资源,和两个线程,一个线程负责输入数据,一个线程负责输出数据。输出结果如下:
由上图可见,线程间的数据共享出错了。如丽丽….female,lili……女女等数据。错误原因:一个线程赋值了name,但是没有赋值性别,就被另一个线程输出了。解决方案见下一节。
17.线程间通信-解决安全问题
加同步锁即可解决安全问题,但是要区分哪些需要哪些不需要同步。考虑前提:是不是多个线程?是不是共用一把锁?锁是不是共用一个对象?
即要同步输入数据,也要同步输出数据。
使用同一个本类对象。该示例总共四个类,每个类都是唯一,所以用哪个类都行,但是共享的是res资源所以用Res类会好一点。
package com.vnb.javabase.thread.threadcommunication;
public class Input implements Runnable {
private Res res;
Input(Res res){
this.res = res;
}
@Override
public void run() {
int x = 0;
while(true){
synchronized (res) {
if(x==0){
res.name = "lili....";
res.sex = "female....";
}else{
res.name = "丽丽....";
res.sex = "女....";
}
x = (x+1)%2;//不断交替赋值
}
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class Output implements Runnable {
private Res res;
Output(Res res){
this.res = res;
}
@Override
public void run() {
while(true){
synchronized (res) {
System.out.println(res.name+"*****"+res.sex);
}
}
}
}
结果:发现数据输出正确
18.线程间通信-等待唤醒机制
输入线程如果获得CPU执行权,存完丽丽 女后,Input和Output都有可能抢到执行权,如果Input再次抢到执行权,再次将lili female输入到内存中,前面的值就会被覆盖掉,当执行到某一时刻,output抢到执行权,就会出现一个值打印多个的情况。但是我们现在需要的是,输入一个打印一个。
解决:定义一个标记,input获取到执行权后,存入数据并将flag改给真,当input线程再次获取到执行权时,发现flag标记为真,则会睡眠(wait/sleep),等待output输出,输出数据后,当Output再次拿到执行权后,判断flag为false时,在放弃执行权之前将标记改为false,然后进入等待,并唤醒input线程。input获取执行权后,判断flag为false,则输入数据,修改标记为true,然后唤醒output线程。示例:
package com.vnb.javabase.thread.threadcommunication;
public class Res {
String name;
String sex;
boolean flag = false;
}
package com.vnb.javabase.thread.threadcommunication;
public class Input implements Runnable {
private Res res;
Input(Res res){
this.res = res;
}
@Override
public void run() {
int x = 0;
while(true){
synchronized (res) {
if(res.flag){
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(x==0){
res.name = "lili....";
res.sex = "female....";
}else{
res.name = "丽丽....";
res.sex = "女....";
}
x = (x+1)%2;//不断交替赋值
res.flag = true;
res.notify();
}
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class Output implements Runnable {
private Res res;
Output(Res res){
this.res = res;
}
@Override
public void run() {
while(true){
synchronized (res) {
if(!res.flag){
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(res.name+"*****"+res.sex);
res.flag = false;
res.notify();
}
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class InputOutputDemo {
public static void main(String[] args) {
Res res = new Res();
Input in = new Input(res);
Output out = new Output(res);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
线程运行中,线程中会建立线程池,等待线程都存在线程池中。当notify后都是唤醒的线程池中的线程,线程池中有很多线程等待时,通常唤醒第一个被等待的线程。
wait(),notify(),notifyAll()等方法都是定义在Object基类中,在线程中使用wait(),notify(),notifyAll()必须要有该线程的监视器(锁),所以wait(),notify(),notifyAll()这些方法只能用在同步锁中。而且在同步里使用wait(),notify(),notifyAll()时,必须要标识出wait()所属线程里的持有锁(因为同步会出现嵌套)。
总结:wait(),notify(),notifyAll()都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。为什么wait(),notify(),notifyAll()定义在Object类中?因为这些方法在操作同步中线程时都必须要标识它们所操作线程持有的锁,只有同一个锁上的被等待线程,才可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒,也就是说等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object中。
19.线程间通信-代码优化
资源中数据私有化,然后提供方法供其他类调用即可。
package com.vnb.javabase.thread.threadcommunication;
public class Res {
private String name;
private String sex;
boolean flag = false;
public synchronized void set(String name,String sex){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out(){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"........"+sex);
flag = false;
this.notify();
}
}
package com.vnb.javabase.thread.threadcommunication;
public class Input implements Runnable {
private Res res;
Input(Res res){
this.res = res;
}
@Override
public void run() {
int x = 0;
while(true){
if(x==0){
res.set("lili", "female");
}else{
res.set("丽丽", "女");
}
x = (x+1)%2;//不断交替赋值
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class Output implements Runnable {
private Res res;
Output(Res res){
this.res = res;
}
@Override
public void run() {
while(true){
res.out();
}
}
}
package com.vnb.javabase.thread.threadcommunication;
public class InputOutputDemo {
public static void main(String[] args) {
Res res = new Res();
new Thread(new Input(res)).start();
new Thread(new Output(res)).start();
}
}
结果:
20.线程间通信-生产者消费者
生产一个消费一个。按照上节中的例子更改成生产者消费者,示例如下:
package com.vnb.javabase.thread.producterconsumer;
public class Resource {
private String name;
private int num = 0;
boolean flag = false;
public synchronized void set(String name){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "..." + num++;
System.out.println(Thread.currentThread()+"...生产者..."+this.name);
flag = true;
this.notify();
}
public synchronized void out(){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread()+"...消费者..."+this.name);
flag = false;
this.notify();
}
}
package com.vnb.javabase.thread.producterconsumer;
public class Producter implements Runnable {
private Resource res;
Producter(Resource res){
this.res = res;
}
@Override
public void run() {
while(true){
res.set("商品");
}
}
}
package com.vnb.javabase.thread.producterconsumer;
public class Consumer implements Runnable {
private Resource res;
Consumer(Resource res){
this.res = res;
}
@Override
public void run() {
while(true){
res.out();
}
}
}
package com.vnb.javabase.thread.producterconsumer;
public class ProducterConsumer {
public static void main(String[] args) {
Resource res = new Resource();
Producter pro = new Producter(res);
Consumer con = new Consumer(res);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
结果:如预期一样,生产一个消费一个。
如上例,是使用两个线程来进行示例,但是正在开发时,是多个线程生产多个线程消费。如果直接增加线程会出现以下问题:会同时生产多个或者同时消费多个的情况。
package com.vnb.javabase.thread.producterconsumer;
public class ProducterConsumer {
public static void main(String[] args) {
Resource res = new Resource();
Producter pro = new Producter(res);
Consumer con = new Consumer(res);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(pro);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果如下:
解析:如上,假设生产者先获取到CPU的执行权,而生产者有两个t1和t2,假设t1获取到了,t1判断为假,就进入判断,进行赋值,然后将flag设置为真;当再次进来判断时,flag为真,即t1 wait()等待放弃执行资格,接下来,t2/t3/t4都有可能抢到执行资格,假设t2抢到了执行资格,判断为真,也等待放弃执行资格;剩下t3非真为假则进行正常消费,然后将flag设置为false,然后唤醒t1(因为t1最先进入等待),t1就获取资格但不一定有执行权,此时执行权还在t3,再次在out()方法里判断为真就调用wait()进入等待,t4也如上将会进入判断;然后只剩下t1有资格,t1执行时赋值并放弃资格后,调用唤醒t2,但是t2不会再进行判断,直接生产(赋值),因此前一个就被覆盖了。
分析原因:因为t2没有再判断过flag标记,如果换成while每次循环都会重新进入判断循环。但是发现用了while循环后,所有线程都进入等待。解决:将所有notify()唤醒方法改为notifyAll()。
package com.vnb.javabase.thread.producterconsumer;
public class Resource {
private String name;
private int num = 0;
boolean flag = false;
public synchronized void set(String name){
while(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + "..." + num++;
System.out.println(Thread.currentThread()+"...生产者..."+this.name);
flag = true;
this.notifyAll();
}
public synchronized void out(){
while(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread()+"...消费者.........."+this.name);
flag = false;
this.notifyAll();
}
}
结果:结果正常
对于多个生产者和消费者,为什么要定义while判断标记?
原因:让被唤醒的线程再一次判断标记。
为什么定义notifyAll()?
原因:因为需要唤醒对方线程。因为只用notify,容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待。
所以当有多个生产者和多个消费者时,必须要while循环,且要使用notifyAll()方法唤醒所有等待的线程。
21.线程间通信-生产者消费者JDK5.0升级版
JDK1.5提供了java.util.concurrent.Locks类,提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。Condition将Object监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待set(wait-set)。其中Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。其中有lock()和unlock()加锁和解锁方法。Condition中方法await()将当前线程置于中断且处于等待状态,singnal()唤醒一个线程,signalAll()唤醒所有等待线程。
JDK中Lock示例:
由上例发现,notifyAll()唤醒的是所有(包括对方线程)的线程,对方的线程也唤醒了。
需求:只唤醒对方线程。
package com.vnb.javabase.thread.producterconsumer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Resource {
private String name;
private int num = 0;
boolean flag = false;
private ReentrantLock lock = new ReentrantLock();
//支持多个相关的Condition对象
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
public void set(String name) throws InterruptedException{
lock.lock();
try{
while(flag){
condition_pro.await();
}
this.name = name + "..." + num++;
System.out.println(Thread.currentThread()+"...生产者..."+this.name);
flag = true;
condition_con.signalAll();
}finally{
lock.unlock();
}
}
public void out() throws InterruptedException{
lock.lock();
try{
while(!flag){
condition_con.await();
}
System.out.println(Thread.currentThread()+"...消费者.........."+this.name);
flag = false;
condition_pro.signalAll();
}finally{
lock.unlock();
}
}
}
package com.vnb.javabase.thread.producterconsumer;
public class Producter implements Runnable {
private Resource res;
Producter(Resource res){
this.res = res;
}
@Override
public void run() {
while(true){
try{
res.set("商品");
}catch(InterruptedException e){
}
}
}
}
package com.vnb.javabase.thread.producterconsumer;
public class Consumer implements Runnable {
private Resource res;
Consumer(Resource res){
this.res = res;
}
@Override
public void run() {
while(true){
try {
res.out();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
本来wait、notify和notifyAll这些方法都必须存在在同步里,但是使用Lock及Condition后,没有了同步,就需要从Lock中获得Condition,可以通过Lock的newCondition()方法获得。释放锁的操作一定要放在finally中。
JDK1.5中提供了多线程升级解决方案。将同步synchronized替换成显示的Lock操作,将Object中的wait,notity,nitifyAll替换成了Condition对象,该对象可以用Lock锁进行获取。在该示例中实现了本方只唤醒对方被等待的线程的操作。
22.停止线程
停止线程:
- 定义循环结束标记(因为线程运行代码一般都是循环,只要控制了循环即可停止线程)
- 使用interrupt(中断)方法(该方法是结束线程的冻结状态,使线程回到运行状态中来)
stop方法已经过时,suspend方法也已经过时(会导致死锁),所以停止线程只有一种方法,即run方法结束。开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
示例:
package com.vnb.javabase.thread.stopthread;
public class StopThread implements Runnable {
boolean flag = true;
@Override
public void run() {
while(flag){
System.out.println(Thread.currentThread().getName()+".........run");
}
}
public void changeFlag(){
flag = false;
}
}
package com.vnb.javabase.thread.stopthread;
public class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while(true){
if(num++==60){
st.changeFlag();
break;
}
System.out.println(Thread.currentThread().getName()+"........."+num);
}
}
}
结果:在main方法中控制程序使得run方法结束。
但是,当程序中使用wait后,发现程序一直处于wait状态停不下来。如下例:
package com.vnb.javabase.thread.stopthread;
public class StopThread implements Runnable {
boolean flag = true;
@Override
public synchronized void run() {
while(flag){
try {
this.wait();
} catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+".........Exception");
}
System.out.println(Thread.currentThread().getName()+".........run");
}
}
public void changeFlag(){
flag = false;
}
}
结果:程序停不下来
分析:无论哪个线程过来都会改变标记,但是线程都会执行同步方法中的wait方法,从而都处于等待状态,但是主线程已经执行完了。当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。
解决:可以强制结束,在Thread中提供了interrupt方法结束线程。
interrupt():中断线程。如果当前线程没有中断它自己,则该线程的checkAccess方法就会被调用,这可能会抛出SecutiryException。如果线程在调用Object类的wait(),wait(long),或wait(long,int)方法,或者该类的join(),join(long),join(long,int),sleep(long),sleep(long,int)方法过程中,则其中断状态将被清除,它将收到一个InterruptedException。
当进入冻结状态全部挂起时,使用interrupt()方法强制将其恢复到运行状态。但是会出现异常。如下:
package com.vnb.javabase.thread.stopthread;
public class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while(true){
if(num++==60){
//st.changeFlag();
t1.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"........."+num);
}
}
}
结果:t1被中断所以会有异常。但因为是catch处理了异常,所以会继续往下执行。而且程序并没有结束。
但是interrupt清除异常后,flag还是ture,所以还是会进入wait等待状态。所以t1,t2都要清除,既然能清除也能结束,所以只要在处理异常后将flag设置为false即可结束线程。如下:
package com.vnb.javabase.thread.stopthread;
public class StopThread implements Runnable {
boolean flag = true;
@Override
public synchronized void run() {
while(flag){
try {
this.wait();
} catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+".........Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName()+".........run");
}
}
public void changeFlag(){
flag = false;
}
}
package com.vnb.javabase.thread.stopthread;
public class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while(true){
if(num++==60){
//st.changeFlag();
t1.interrupt();
t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"........."+num);
}
}
}
结果:强制恢复了等待的线程,而且程序正常结束。
当没有指定的方式让冻结的线程恢复到运行状态时,需要对冻结的线程进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。Thread类中提供该方法interrupt();(sleep,wait,join都能被interrupt()。
23.守护线程
以下,介绍一些Thread线程类中的其他方法。
setDaemo(Boolean on):将线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,java虚拟机退出。该方法必须在启动线程前调用。如下:
package com.vnb.javabase.thread.stopthread;
public class StopThreadDemo {
public static void main(String[] args) {
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
int num = 0;
while(true){
if(num++==60){
//st.changeFlag();
//t1.interrupt();
//t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"........."+num);
}
}
}
结果:当线程启动前不使用setDaemon(true)时,线程会处于等待状态;当线程启动前使用了setDaemon(true)后,线程正常停止。
守护线程,即为后台线程,我们所看到的都是前台线程,当把某些线程标记成后台线程后,开启后和前台线程共同抢夺CPU的执行权,开启和运行都没有区别,只是结束有区别,当所有前台线程都结束后,后台线程自动结束(后台依赖前台)。而本程序有三个线程,main和创建的两个线程,主线程和其他两个线程共同抢资源,而主线程是在其他两个线程结束后,自动结束。
24.Join方法
join():等待该线程终止。首先抢夺CPU执行权。
示例:
package com.vnb.javabase.thread.jointhread;
public class JoinThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 70; i++) {
System.out.println(Thread.currentThread().getName()+"......"+i);
}
}
}
package com.vnb.javabase.thread.jointhread;
public class JoinThreadDemo {
public static void main(String[] args) {
JoinThread jt = new JoinThread();
Thread t1 = new Thread(jt);
Thread t2 = new Thread(jt);
t1.start();
t2.start();
for (int i = 0; i < 80; i++) {
System.out.println("mian ......."+i);
}
System.out.println("mian over.......");
}
}
结果:上例没有使用join()。主函数和其他线程是互相交替执行的,而且谁先执行由CPU决定。
当加入join后:
package com.vnb.javabase.thread.jointhread;
public class JoinThreadDemo {
public static void main(String[] args) throws Exception {
JoinThread jt = new JoinThread();
Thread t1 = new Thread(jt);
Thread t2 = new Thread(jt);
t1.start();
t1.join();
t2.start();
for (int i = 0; i < 80; i++) {
System.out.println("mian ......."+i);
}
System.out.println("mian over.......");
}
}
结果:发现t1即Thread-0的完全执行完成后,才开始交替执行t2和主函数。
Thread-0......0
Thread-0......1
Thread-0......2
Thread-0......3
Thread-0......4
Thread-0......5
Thread-0......6
Thread-0......7
Thread-0......8
Thread-0......9
Thread-0......10
Thread-0......11
Thread-0......12
Thread-0......13
Thread-0......14
Thread-0......15
Thread-0......16
Thread-0......17
Thread-0......18
Thread-0......19
Thread-0......20
Thread-0......21
Thread-0......22
Thread-0......23
Thread-0......24
Thread-0......25
Thread-0......26
Thread-0......27
Thread-0......28
Thread-0......29
Thread-0......30
Thread-0......31
Thread-0......32
Thread-0......33
Thread-0......34
Thread-0......35
Thread-0......36
Thread-0......37
Thread-0......38
Thread-0......39
Thread-0......40
Thread-0......41
Thread-0......42
Thread-0......43
Thread-0......44
Thread-0......45
Thread-0......46
Thread-0......47
Thread-0......48
Thread-0......49
Thread-0......50
Thread-0......51
Thread-0......52
Thread-0......53
Thread-0......54
Thread-0......55
Thread-0......56
Thread-0......57
Thread-0......58
Thread-0......59
Thread-0......60
Thread-0......61
Thread-0......62
Thread-0......63
Thread-0......64
Thread-0......65
Thread-0......66
Thread-0......67
Thread-0......68
Thread-0......69
mian .......0
mian .......1
mian .......36
mian .......37
mian .......38
mian .......39
Thread-1......0
Thread-1......1
Thread-1......2
Thread-1......3
Thread-1......4
Thread-1......5
Thread-1......36
mian .......40
Thread-1......37
mian .......41
Thread-1......38
mian .......42
mian .......43
mian .......77
mian .......78
mian .......79
mian over.......
Thread-1......39
Thread-1......40
Thread-1......41
Thread-1......42
t1.join():t1申请加入到运行中来,即t1要CPU执行权,join即抢夺CPU执行权。t1.join()后,主线程让出CPU执行权,且处于冻结状态,t1拿到执行权执行结束后,主线程才回到运行状态。
如果程序修改成以下:t1.join()在t1.start()和t2.start();后
package com.vnb.javabase.thread.jointhread;
public class JoinThreadDemo {
public static void main(String[] args) throws Exception {
JoinThread jt = new JoinThread();
Thread t1 = new Thread(jt);
Thread t2 = new Thread(jt);
t1.start();
t2.start();
t1.join();
for (int i = 0; i < 80; i++) {
System.out.println("main ......."+i);
}
System.out.println("main over.......");
}
}
分析:主线程在执行了t1.start();t2.start();会启动两个线程,当执行到t1.join();后主线程会释放资源,让出执行权,而此时t1和t2会互相抢夺CPU执行权交替执行,而主函数会等待t1执行完成后开始执行,不管t2有没有执行完成。如果t1执行完了,t2还没有执行完成,主线程会和t2线程交替执行。一旦t1wait后,主线程也会挂掉。所以用了interrupt后,t1wait了,主线程还是能继续执行,只是会抛出异常。
join特点:当A线程执行到了B线程的join方法时,A线程就会等B线程都执行完后,A线程才会执行。join可以用来临时加入线程执行。
25.优先级&yield方法
通过Thread中的toString()方法查看执行线程的线程名、优先级、线程组情况。例如:
package com.vnb.javabase.thread.yieldthread;
public class YieldThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 70; i++) {
System.out.println(Thread.currentThread().toString()+"......"+i);
}
}
}
package com.vnb.javabase.thread.yieldthread;
public class YieldThreadDemo {
public static void main(String[] args) throws Exception {
YieldThread jt = new YieldThread();
Thread t1 = new Thread(jt);
Thread t2 = new Thread(jt);
t1.start();
t2.start();
for (int i = 0; i < 80; i++) {
System.out.println("mian ......."+i);
}
System.out.println("mian over.......");
}
}
结果:
Thread[Thread-1,5,main]......0
Thread[Thread-0,5,main]......0
mian .......0
Thread[Thread-0,5,main]......1
Thread[Thread-1,5,main]......1
Thread[Thread-0,5,main]......2
- Thread-1标识线程名
- 5标识线程的优先级
- main标识Thread-1线程所属的组
线程组:一般情况下,谁开启的就属于哪个组,由主线程开启的就是属于主线程组。如果要使用其他组,可以new一个ThreadGroup对象,把线程放进去即可。开发时使用较少。
优先级:代表抢资源的频率。setPriority(int new Priority)可以设置优先级。所有优先级的默认优先级都是5。范围为1~10级。1,5,10级变化会比较明显,所以会用MIN_PRIORITY,MORM_PRIORITY,MAX_PRIORITY表示。
package com.vnb.javabase.thread.yieldthread;
public class YieldThreadDemo {
public static void main(String[] args) throws Exception {
YieldThread jt = new YieldThread();
Thread t1 = new Thread(jt);
Thread t2 = new Thread(jt);
t1.start();
t1.setPriority(Thread.MAX_PRIORITY);
t2.start();
for (int i = 0; i < 80; i++) {
System.out.println("mian ......."+i);
}
System.out.println("mian over.......");
}
}
结果:
Thread[Thread-0,10,main]......0
Thread[Thread-0,10,main]......1
mian .......0
mian .......1
Thread[Thread-1,5,main]......0
Thread[Thread-1,5,main]......1
mian .......20
Thread[Thread-0,10,main]......2
Thread[Thread-0,10,main]......3
发现t1的运行频率会高一些,但是并不代表会先将t1全部执行完后再执行其他线程。
yield():暂停当前正在执行的线程对象,并执行其他线程。即临时停止。如:
package com.vnb.javabase.thread.yieldthread;
public class YieldThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 70; i++) {
System.out.println(Thread.currentThread().toString()+"......"+i);
Thread.yield();
}
}
}
结果:发现t1在执行了0后会暂时释放执行资源,减缓线程执行,然后再交替执行,t2进来后也可能会暂时停一下。
开发时怎么使用线程?
如下例,存在多个循环,如果不使用多线程,要先执行完第一个循环再执行其他循环,会存在其他循环一致等待情况,效率会很低。但是,如果使用多线程,当某些代码需要同时被执行时,就使用单独的线程进行封装(此处可以封装三个类),也可以简单的直接封装在线程Thread的子类对象或Runnable子类对象即匿名内部类中。
修改后:
new Thread(){};是Thread类的子类对象。
如上例:就有了三个线程(两种不同线程创建方式),循环间也不需要安找先后执行。或者单独封装类执行: