Java高级特性 第7节 多线程
Posted 小余上岸
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高级特性 第7节 多线程相关的知识,希望对你有一定的参考价值。
一、进程与线程的概念
1. 进程
进程是应用程序的执行实例,有独立的内存空间和系统资源。
如上图,标红色的是一个Office Word进程。
进程的特点:
- 动态性:进程是动态的创建和消亡;
- 并发性:操作系统利用时间片轮转或其他策略让多个应用程序交替执行;
- 独立性:所有的进程都是独立运行的,系统会分别为这些线程分配资源和实施调度
2. 线程
CPU调度和分派的基本单位,进程中执行运算的最小单位,可完成一个独立的顺序控制流程。
如上图迅雷可以创建多个下载任务(每个下载任务可看做是一个线程),多个下载任务可以并发执行。
线程的特点:
- 一个进程可以包含多个线程,一个线程至少要有一个父进程;
- 线程可以有自己的堆栈、程序计数器和局部变量;
- 线程与父进程的其他线程共享进程的所有资源;
- 独立运行,采用抢占方式;
- 一个线程可以创建和删除另一个线程;
- 同一个进程中的线程可以并发执行;
- 线程的调度管理是由进程来完成的;
3. 线程的分类
- 系统级线程:又称核心级线程,负责调度管理不同进程之间的多个线程,由操作系统直接管理;
- 应用级线程:仅存在于用户空间,在应用程序中控制其创建、执行和消亡。
4. 多线程开发
如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程”。多个线程交替占用CPU资源,而非真正的并行执行;
多线程开发的优势:
- 充分利用CPU资源
- 简化编程模型
- 带来良好的用户体验
二、使用线程
1. Thread类
Java中提供了java.lang.Thread支持多线程编程。
2. 主线程
package cn.MyThreadDemo01; /* * * @author yutianbao * @显示当前线程 * @date 2019/4/6 15:05 * @return * @Motto: good good study,day day up */ public class ThreadDemo { public static void main(String args[]) { Thread t= Thread.currentThread(); System.out.println("当前线程是: "+t.getName()); t.setName("MyJavaThread"); System.out.println("当前线程名是: "+t.getName()); } }
3.Java中创建线程的2种方式
- 继承java.lang.Thread类
package cn.MyThreadDemo01; public class MyThread extends Thread{ //重写run()方法 public void run(){ for(int i=1;i<=10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } }
package cn.MyThreadDemo01; public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); MyThread thread2 = new MyThread(); //创建新线程thread2 thread.start(); thread2.start(); //启动thread2 } }
可以看出:多个线程交替执行,并不是真正的并行;线程每次执行时长由分配的CPU时间片长度决定;
- 启动线程直接调用run()方法和start()方法的区别
package cn.MyThreadDemo01; public class Test { public static void main(String[] args) { MyThread thread = new MyThread(); MyThread thread2 = new MyThread(); //创建新线程thread2 //thread.start(); //thread2.start(); //启动thread2 //直接调用run()方法 thread.run(); thread2.run(); } }
可以看出,直接调用run()方法不是多线程,只是顺序执行;而start()方法才是启动线程的正确方式。
- 实现java.lang.Runnable接口
- 2种线程创建方式的优缺点
三、线程的4种状态
线程从创建、运行到结束总是处于下面4个状态之一:新生状态、可运行状态、阻塞状态及死亡状态。
1.新生状态
创建线程对象后,尚未调用其start()方法之前,这个线程就有了生命,此时线程仅仅是一个空对象,系统没有为其分配资源。此时只能启动和终止线程,任何其他操作都会引发异常。
2.可运行状态
当调用线程的start()方法启动线程之后,系统为该线程分配除CPU之外的所需资源,这个线程就获得了运行的机会,线程处于可运行状态。可运行状态是说该线程对象可能正在运行,也可能尚未运行。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。
- 锁池里的线程拿到对象锁后,进入可运行状态。
对于只有一个CPU的机器而言,任何时刻只能有一个处于可运行状态的线程占用处理机,获得CPU资源,此时系统真正运行线程的run()方法。
- 可运行状态(runnable)的线程获得了cpu 时间片(timeslice),执行程序代码。
- 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
3.阻塞状态
一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态,这是一种“不可运行”的状态,而处于这种状态下的线程在得到一个特定的事件之后会转回可运行状态。
导致一个线程被阻塞的原因可能是:
- 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)方法或其他线程调用join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
4.死亡状态
- 当线程的run()方法完成、stop()方法被调用、运行过程中出现未捕获的异常或者主线程的main()方法完成时,线程进入死亡状态。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。
- 在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
四、线程调度
同一时刻有多个线程处于可运行状态,他们需要排队等待CPU资源,每个线程会自动获得一个优先级,优先级的高低反应线程的重要程度或紧急程度。可运行状态的线程按优先级排队,线程调用依据优先级基础上的“先到先服务”原则。
线程调度管理器负责线程排队和CPU在线程间的分配,并按照线程调度算法进行调度,当线程调度管理器选中,某个线程时,该线程获得CPU资源进入运行状态。
线程调度是“抢占式调度”,即在当前线程执行过程中如果有一个更高优先级的线程进入可运行状态,则这个更高优先级的线程立即被调度执行。
1. 线程的优先级
setPriority():线程对象. setPriority(优先级值)
- 线程优先级的范围可能根据操作系统的而不同,通常情况下,优先级的值最大为10,最小为1,默认为5;
- 默认情况下,一个线程继承其父类的优先级;
- 优先级表示为一个整数值;
- 优先级越高,被执行的机会越大,反之,被执行的机会越小;
- 线程的优先级与线程执行的效率没有必然联系;
package cn.MyThreadDemo01; /** * 设置线程优先级 * */ public class Thread_setPriority implements Runnable{ public void run(){ for(int i=1;i<100;i++){ System.out.println(Thread.currentThread().getName()+"正在运行:"+i); } } public static void main(String[] args) { Thread t1 = new Thread(new MyThread(),"线程A");//通过构造方法指定线程名 Thread t2 = new Thread(new MyThread(),"线程B"); //设置线程的优先级(对比演示设置前后设置后的结果) t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY); System.out.println("****线程的优先级****"); System.out.println("线程A的优先级:"+t1.getPriority()); System.out.println("线程B的优先级:"+t2.getPriority()); System.out.println("****************"); t1.start(); t2.start(); } }
2. join()方法
- 阻塞指定的线程,等到另一个线程执行完成后在继续执行;
package cn.MyThreadDemo01; /* * 使用join()方法 */ public class Thread_join extends Thread{ //构造方法 public Thread_join(String name){ super(name); } //重写run()方法 public void run(){ for (int i = 0; i < 5; i++) { System.out.println(getName()+":"+i); } } public static void main(String[] args) { for (int i = 0; i < 10; i++) { if(i==5){ Thread_join thread_join = new Thread_join("半路加入的线程"); try{ thread_join.start(); thread_join.join(); //此处阻塞的指定线程是主线程 }catch (InterruptedException e){ e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+""+i); } } }
3. sleep()方法
- 语法:Thread.sleep(long millis)
package cn.MyThreadDemo01; /** * 线程休眠 */ class Wait { public static void bySec(long s) { for (int i = 0; i < s; i++) { System.out.println(i + 1 + "秒"); try { Thread.sleep(1000); // 睡眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } } } } class Thread_sleep01 extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(getName()+":"+i); } } } public class Thread_ThreadSleep { public static void main(String[] args) { System.out.println("Wait"); Wait.bySec(5); // 让主线程等待5秒种再执行 System.out.println("start"); Thread_sleep01 thread_sleep01 = new Thread_sleep01(); Wait.bySec(3); // 让线程thread_sleep01等待3秒种再执行 thread_sleep01.start(); } }
例子:线程的强制执行
package cn.MyThreadDemo01; class MyThreadJoin implements Runnable{ public void run(){ for(int i=0;i<5;i++){ try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //输出当前线程的信息 System.out.println(Thread.currentThread().getName()+"运行:"+i); } } } public class Thread_joinDemo { public static void main(String[] args) { System.out.println("*****线程强制执行******"); //创建子线程 Thread temp = new Thread(new MyThreadJoin()); temp.start(); for(int i=0;i<10;i++){ if(i==4){ try { //temp.start(); //阻塞主线程,子线程强制执行 temp.join(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(1000); //主线程休眠1s,进入阻塞状态 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"运行:"+i); } } }
4. yield()方法
package cn.MyThreadDemo01; class MyThreadYield implements Runnable{ public void run(){ for(int i=0;i<7;i++){ System.out.println(Thread.currentThread().getName()+"正在运行:"+i); if(i==3){ System.out.print("线程礼让:"); Thread.yield(); } } } } public class Thread_yield { public static void main(String[] args) { System.out.println("*****线程的礼让*****"); MyThreadYield my = new MyThreadYield(); Thread t1 = new Thread(my,"线程A"); Thread t2 = new Thread(my,"线程B"); t1.start(); t2.start(); } }
5. sleep()方法和yield()方法比较
- sleep()方法使当前线程转为被阻塞状态,yield()方法使当前线程转为可运行状态;
- sleep()方法总是强制当前线程停止执行,而yield()方法则不一定;
- sleep()方法会让其他等待运行的线程具有同样的执行机会,而yield()方法只使相同或更高优先级的线程获得执行机会;
6. setDaemon()方法
- 语法:线程对象.setDaemon(Boolean b)
将指定线程设置为后台线程。
- 新创建的线程,默认都是前台线程 ,只能在线程启动之前将此线程设置为后台线程,而不是启动之后;
- 后台线程从属于创建它的线程,随着创建它的线程的消亡而随之消亡;
- 所有的线程中,除了后台线程其它都是用户线程(也称作前台线程),这些用户线程拥有自己的生命周期,不依赖于创建它的父线程,而是当创建它的父线程结束后它才能执行,例如主线程也是用户线程;
- 由后台线程创建的线程是后台线程;
package cn.MyThreadDemo01;
public class Thread_setDaemon implements Runnable{
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+"--->is running");
}
}
public static void main(String[] args) {
System.out.println("main线程是后台线程吗?"+Thread.currentThread().isDaemon());
// 创建一个Thread_setDaemon对象dt
// 创建线程t,共享dt资源
Thread_setDaemon dt=new Thread_setDaemon();
Thread t=new Thread(dt,"后台线程");
System.out.println("t线程默认是后台线程吗?"+t.isDaemon());
// 将线程t,设置为后台线程
t.setDaemon(true);
// 调用start()方法,开启线程t
t.start();
for(int i=0;i<5;i++){
System.out.println(i);
}
}
}
上面例子演示了一个后台线程结束的过程 ,当开启线程t之后,会执行死循环中的打印语句,我们将线程t设置为后台线程后,当前台线程(main线程)死亡后,JVM会通知后台线程,但后台线程从接受指令到做出响应,需要一定的时间,因此,打印了几次后台线程—is running语句后,后台线程也结束了,这也说明后台线程在运行时,进程就结束了。
所以,对于Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中,只有后台线程在运行,这个进程就会结束。
五、线程同步
1.线程同步的必要性
前面介绍的线程都是独立的,而且是异步执行的,也就是说每个线程都包含了运行时所需的数据或方法,而不需要外部资源或方法,也不必关心其他线程的状态或行为。但是经常有一些同时运行的线程需要共享数据,此时就必须考虑其他线程的状态和行为,否则不能保证运行结果的正确性。
package cn.SynchronizationDemo; /* * 银行账户类 */ public class Account { private int money = 1000; //余额 public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } //取款方法 public void withDraw(int amount){ money -= amount; } }
package cn.SynchronizationDemo; import java.util.Random; /* * 取款的线程类:模拟2人同时取款的过程 */ public class ThreadWithDraw implements Runnable{ //所有用ThreadWithDraw()对象创建的线程共享同一个账户对象 private Account acct = new Account(); //重写run()方法 public void run(){ //取款次数 int WithdrawCount = new Random().nextInt(8)+1; int WithdrawMoney = (new Random().nextInt(10)+1)*20; for (int i = 0; i < WithdrawCount; i++) { makeWithDraw(WithdrawMoney,i); if(acct.getMoney()<0){ System.out.println("您的账户透支了!"); } } } public void makeWithDraw(int amount,int i){ if(acct.getMoney()>=amount){ System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次准备取款........\\n"+"输入取款金额:"+amount); try{ Thread.sleep(1000); //模拟网络延时,1秒后实现取款 }catch (InterruptedException e){ e.printStackTrace(); } //若余额足够则取款 acct.withDraw(amount); System.out.println(Thread.currentThread().getName()+"完成取款,余额为"+acct.getMoney()); }else{ //余额不足给出提示 System.out.println("余额不足以支付"+Thread.currentThread().getName()+"的取款,余额为"+acct.getMoney()); } } }
package cn.SynchronizationDemo; /* * 测试类 */ public class AccountTest { public static void main(String[] args) { ThreadWithDraw threadWithDraw = new ThreadWithDraw(); Thread thread1 = new Thread(threadWithDraw,"李四"); Thread thread2 = new Thread(threadWithDraw,"李四的妻子"); thread1.start(); thread2.start(); } }
由结果可以看出,虽然在程序中对余额做了判断,但仍然出现了透支的情况,原因就是在取款方法中,有可能在查询余额之后取款的一小段时间里(网络延时的1秒),另外一个人已经完成了一次取款,因而此时的余额发生了变化,但是当前线程却还以为余额是足够的。为避免发生这种情况,就需要使用线程同步。
2.线程同步的实现
当2个线程或多个线程需要访问同一资源时,需要以某种顺序来确保该资源在某一时刻只能被一个线程使用的方式成为线程同步。
采用同步来控制线程的执行有2种方式:
- 同步方法
在方法声明中加入synchronization关键字来声明同步方法。
使用synchronization修饰的方法控制对类成员变量的访问。每个类实例对应一把锁,方法一旦执行,就独占该锁,一直到方法返回时采释放锁,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对应每一个实例,其所有生命为synchronization的方法只能有一个处于可执行状态,从而有效的避免了类成员变量的访问冲突。
语法:访问修饰符 synchronization 返回类型 方法名(参数列表){ }
同步方法的缺陷:如果将一个运行时间比较长的方法声明称synchronization将会影响效率。
- 同步代码块
语法:synchronized(syncObject){ }
synchronized块中代码必须获得对象syncObject的锁才能执行,具体实现机制与同步方法一样。由于可针对任意代码块,且可任意指定上锁的对象,故灵活性比较高。
public synchronized void makeWithDraw(int amount,int i){ //synchronized (acct) { if (acct.getMoney() >= amount) { System.out.println(Thread.currentThread().getName() + "第" + (i + 1) + "次准备取款........\\n" + "输入取款金额:" + amount); try { Thread.sleep(1000); //模拟网络延时,1秒后实现取款 } catch (InterruptedException e) { e.printStackTrace(); } //若余额足够则取款 acct.withDraw(amount); System.out.println(Thread.currentThread().getName() + "完成取款,余额为" + acct.getMoney()); } else { //余额不足给出提示 System.out.println("余额不足以支付" + Thread.currentThread().getName() + "的取款,余额为" + acct.getMoney()); } // } }
3. 死锁
多线程在使用同步机制时,存在“死锁”的潜在风险。若果多个线程都处于等待状态而无法唤醒时,就构成了死锁,此时处于等待状态的多个线程占用系统资源,但无法执行,因此不会释放自身的资源。
例如有两个方法A,B,调用两个方法时都需要用到锁,若果X先调用A,再调用B;Y先调用B,再调用A。则X在调用A要调用B的时候,B被Y使用无法获取锁,而Y在调用B的时候调用A,A被X使用,则双方都无法获取所需要的锁,这么X,Y一直处于僵持状态,造成死锁。
避免死锁的有效方法:
- 线程因某个条件未满足而受阻,不能让其继续占有资源;
- 如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁;
六、线程间通信
1.线程间通信的必要性
前面介绍了线程使用同步机制的重要性,这些线程之间都是相互独立的,并不存在任何的依赖关系,他们各自竞争CPU资源,互不相让,并且无条件地组织其他线程对共享资源的异步访问。然而,很多现实问题要求不仅需要同步地访问同一共享资源,而且线程间彼此牵制,相互通信。
在经典的生产者与消费者问题(这显然是一个线程同步问题)中,生产者与消费者共享同一个资源,并且生产者与消费者之间是相互依赖的。使用线程同步可以阻止并发更新同一个共享资源,但是不能用来实现不同线程之间的消息传递,要解决这个问题,就要用到线程通信。
2.实现线程通信
Java中提供3个方法实现线程间通信,这3个方法都是Object类中的final方法,被所有的类继承但不能重写。并且这3个方法只能在在同步代码块或同步方法中使用,否则会抛出异常。
- wait(): 调用wait()方法会挂起当前线程,并释放共享资源的锁;
- notify():调用任意对象的notify()方法会在因调用该对象的wait()方法而阻塞的线程中随机选择一个线程解除阻塞,但要等到获得锁后才能真正执行;
- notifyAll():调用了notifyAll()方法会将因调用该对象的wait()方法而阻塞的线程一次性全部解除阻塞。
/* * 线程间通信 */ public class CommunicateThread implements Runnable{ //同步run()方法 @Override public synchronized void run() { for (int i = 0; i < 6; i++) { System.out.println(Thread.currentThread().getName()+i); if(i==2){ try{ wait(); //退出运行状态,放弃资源锁,进入到等待队列 }catch (InterruptedException e){ e.printStackTrace(); } } if(i==1){ notify(); //从等待队列中唤起一个线程 } if(i==4){ notifyAll(); } } } public static void main(String[] args) { CommunicateThread cm = new CommunicateThread(); new Thread(cm,"线程a").start(); new Thread(cm,"线程b").start(); } }
执行过程分析:
1)在main()方法中启动线程a,线程b
2)由于run()方法加了同步,线程a先执行run()方法,执行for循环输出3条数据
3)当i==2时,执行wait()方法,挂起当前线程,并释放共享资源的锁
4)线程b开始运行,执行for循环输出数据
5)当i==1时,调用notify()方法,从等待队列中唤起一个线程(这里即唤起线程a)
6)线程a等待线程b释放锁,当i==2时,线程b输出完3条数据后执行wait()方法,挂起线程,释放对象锁
7)线程a获得对象锁继续执行输出操作
8)当i==4时,调用notifyAll()方法唤起所有挂起的线程(这里即唤起线程b)
9)当线程a执行完run()方法后释放对象锁,线程b获得对象锁继续执行打印操作至结束
3. 生产者与消费者问题
1)定义共享资源类
package cn.yu.MyThread; /* * 共享资源类 */ public class ShareData { private char c; //产品类别 private boolean isProduced = false; //信号量,标志产品生产或消费状态.true:已生产或未消费 //同步方法putShareChar() public synchronized void putShareChar(char c){ //如果产品还未消费,则生产者等待 if(isProduced){ try{ System.out.println("消费者还未消费,所以生产者停止生产"); wait(); //生产者等待 }catch (InterruptedException e){ e.printStackTrace(); } } //生产产品c并放入仓库 this.c = c; isProduced = true; //标志已经生产 notify(); //通知消费者已经生产,可以消费 System.out.println("生产者生产了产品"+c+",通知消费者消费..."); } //同步方法getShareChar() public synchronized char getShareChar(){ //如果产品还未生产,则消费者等待 if(!isProduced){ try{ System.out.println("生产者还未生产,所以消费者停止消费"); wait(); //消费者等待 }catch (InterruptedException e){ e.printStackTrace(); } } //消费产品c isProduced = false; //标志已经消费 notify(); //通知生产者要生产 System.out.println("消费者消费了产品"+c+",通知生产者生产..."); return this.c; } }
2)定义生产者线程类
package cn.yu.MyThread; /* * 生产者线程类 */ public class Producer extends Thread{ private ShareData s; Producer (ShareData s){ this.s = s; } @Override public void run(){ for (char ch = \'A\'; ch <= \'D\'; ch++) { try{ Thread.sleep((int)Math.random()*3000); //模拟生产产品时间 }catch (InterruptedException e){ e.printStackTrace(); } s.putShareChar(ch); //将产品放入仓库 } } }
3)定义消费者线程类
package cn.yu.MyThread; /* * 消费者线程类 */ public class Consumer extends Thread{ private ShareData s; Consumer (ShareData s){ this.s = s; } @Override public void run(){ char ch; do{ try{ Thread.sleep((int)Math.random()*3000); //模拟生产产品时间 }catch (InterruptedException e){ e.printStackTrace(); } ch = s.getShareChar(); //从仓库取出产品 }while (ch!=\'D\'); } }
4)定义测试类
package cn.yu.MyThread; /* * 测试类 */ public class CommunicationTest { public static void main(String[] args) { //共享同一个资源 ShareData shareData = new ShareData(); //消费者线程 new Consumer(shareData).start(); //生产者线程 new Producer(shareData).start(); } }
在本例中,首先启动的时消费者线程,此时生产者线程还未启动,也就是消费者没有产品可以消费,所以消费者只能等待。在生产者生产了产品A后就通知消费者过来消费,消费者线程停止等待,到仓库中领取产品进行消费。而当生产者如果发现消费者还没有把上次生产的产品消费完时它就停止生产,并通知消费者消费。当消费者消费了产品后便通知生产者继续生产。
以上是关于Java高级特性 第7节 多线程的主要内容,如果未能解决你的问题,请参考以下文章
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_4_解决线程安全问题_同步代码块
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第5节 线程池_2_线程池的代码实现
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第1节 异常_9_finally代码块
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_3_线程安全问题产生的原理
阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第4节 等待唤醒机制_8_等待唤醒机制代码实现_包子类&包子铺类