1 多线程的运行轨迹
由于线程一旦开始执行,很难通过其他方式控制线程的轨迹,多个线程抢占CPU导致线程的运行轨迹不确定。
我们可以通过一个案例来了解线程的运行轨迹。
案例:火车站卖票
窗口A卖出一张票,还剩4张票
窗口B卖出一张票,还剩3张票
窗口B卖出一张票,还剩0张票
窗口C卖出一张票,还剩1张票
窗口A卖出一张票,还剩2张票
1 窗口A抢占到CPU,执行run,count=5 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); 8 } 9 } 10 }
1 窗口B抢占到CPU,count=4,执行run 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); 8 } 9 } 10 }
1 窗口A到抢占CPU,count=3,执行run 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; => count=2 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票");//窗口A在此挂起。count=>2准备输出的字符串:窗口A卖出一张票,还剩2张 8 } 9 } 10 }
1 窗口C到抢占CPU,count=2,执行run 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; => count=1 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票");//窗口C挂起,count=1准备输出的字符串:窗口C卖出一张票,还剩1张 8 } 9 } 10 }
1 窗口B抢占CPU,count=1,从上次挂起的位置开始执行run 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); => count=0 //窗口B挂起,count=0 8 } 9 } 10 }
1 窗口C抢占到CPU,count=0,从上次挂起位置开始执行run 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; => count=1 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); //窗口C从此次开始执行,count=1(挂起时) 8 } 9 } 10 }
1 窗口A抢占到CPU,count=0,从上次挂起的位置开始执行run 2 public void run() { 3 // 模拟一个窗口5个人 4 for (int i = 0; i < 5; i++) { 5 if (count > 0) { 6 count--; => count=2 7 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); //窗口A在此继续执行。count=>2(挂起时) 8 } 9 } 10 }
卖票案例
继承Thread和实现Runnable接口实现多线程的优缺点
[1] 继承Thread的线程类不能再继承其他类,实现Runnable接口的类还可以继承其他类。
[2] 实现Runnable接口的线程类,可以让多个线程共享线程实现类的资源。
总结:
多线程提高了cpu利用率,但程序的复杂度也随之增加。一旦线程开始执行,很难通过其他方式控制线程的轨迹。
多个线程抢占CPU导致线程的运行轨迹不确定。
结论
[1]多线程抢占CPU执行,可能在任意位置被切换出去(挂起)。
[2]多线程抢占到CPU后,从上次挂起的位置开始执行(先恢复上次的执行堆栈)。
[3]多线程都可以独立运行,相互不干扰,多个线程都可以能访问共享资源,很容易导致数据错乱!!!
2 线程的生命周期
新生状态
用new关键字建立一个线程后,该线程对象就处于新生状态。
处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。
就绪状态
处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。
当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为“CPU调度”。
运行状态
在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任何而死亡。
如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行。
死亡状态
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个,一个是正常运行
的线程完成了它的全部工作;另一个是线程被强制性地终止,如通过stop方法来终止一个线程【不推荐使用】,推荐使用interrupt;三是线程抛出未捕获的异常。
可以用下图表示
3 线程的常用方法
【1】 线程优先级
1 public static void main(String[] args) { 2 3 System.out.println(Thread.MIN_PRIORITY); 4 System.out.println(Thread.MAX_PRIORITY); 5 System.out.println(Thread.NORM_PRIORITY); 6 7 //主线程的优先级(默认优先级) 8 System.out.println(Thread.currentThread().getPriority()); 9 10 11 Thread01 t1 = new Thread01(); 12 // 设置线程的优先级 13 t1.setPriority(Thread.MAX_PRIORITY); 14 t1.start(); 15 16 17 Thread01 t2 = new Thread01(); 18 // 设置线程的优先级 19 t2.setPriority(Thread.MIN_PRIORITY); 20 t2.start(); 21 22 23 }
线程优先级高,被cpu调度的概率大,不表示一定先运行。
【2】isAlive
判断线程是否处于活动状态。
1 Thread01 t1 = new Thread01(); 2 System.out.println(t1.isAlive()); 3 // 设置线程的优先级 4 t1.setPriority(Thread.MAX_PRIORITY); 5 t1.start(); 6 System.out.println(t1.isAlive());
线程调用start之后就处于活动状态。
【3】join
调用该方法的线程强制执行,其它线程处于阻塞状态,该线程执行完毕后,其它线程再执行
join称为线程的强制执行,有可能被外界中断产生InterruptedException 中断异常。
(中断异常)
1 public class Test02 { 2 public static void main(String[] args){ 3 4 Thread02 t = new Thread02("线程A"); 5 t.start(); 6 7 for (int i = 0; i < 5; i++) { 8 if(i == 2) { 9 try { 10 t.join(); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 } 15 16 System.out.println(Thread.currentThread().getName() + "->" + i); 17 } 18 } 19 }
【4】sleep
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。休眠的线程进入阻塞状态。
1 public static void main(String[] args) { 2 3 Thread03 t = new Thread03("线程A"); 4 t.start(); 5 6 Thread mainThread = Thread.currentThread(); 7 System.out.println(mainThread.getName()+"即将进入休眠"); 8 try { 9 Thread.sleep(5000); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 14 // 中断线程 15 t.interrupt(); 16 17 System.out.println(mainThread.getName()+"休眠完成"); 18 }
【5】yield
1 public static void main(String[] args) { 2 3 Thread mainThread = Thread.currentThread(); 4 5 Thread04 t = new Thread04("线程A"); 6 t.start(); 7 8 9 for (int i = 0; i < 5; i++) { 10 if (i == 2) { 11 // yield 使当前礼让一次 12 //回到就绪状态,cpu再次从就绪列表里调度,可能会再次调度到,原来礼让的线程 13 Thread.yield(); 14 } 15 System.out.println(mainThread.getName() + "->" + i); 16 } 17 }
A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.
当前线程给cpu调度器一个暗示,暗示其想礼让一次其拥有的cpu,CPU调度者也可以忽略这次暗示。此时当前线程进入就绪状态。
[6] 线程的终止
目前而言,不推荐使用stop直接终止线程。用interrupt()方法去中断正在执行的线程,而在线程内部一定要写捕获中断的异常。通过异常处理机制正常结束线程。
4 线程的安全问题(线程同步)
线程在执行过程中,通过cpu的调度,执行轨迹不确定,对共享资源的访问很容易造成数据的错误。我们称这个错乱称为线程安全问题。
4.1 同步概念
原子性操作:一个操作要么一次性做完,要么根本不开始,不存在中间状态。
案例:ATM取现操作
同步就是让操作保持原子性!java提供两种方式实现同步。
4.2 同步代码块
把所有的同步操作放到同步代码块中,
synchronized (mutex) { // .. . }
mutex 称为互斥锁/同步锁。对共享资源进行加锁实现同步。一般用共享资源作为同步锁,也称同步监视器。
1 public class MyRun implements Runnable { 2 3 // 共享资源(为int类型时,不可以作为同步监视器,此时用this)) 4 private int count = 5; 5 6 @Override 7 public void run() { 8 // 模拟一个窗口5个人 9 for (int i = 0; i < 5; i++) { 10 // 同步代码块 11 // mutex 互斥锁 12 synchronized (this) { 13 if (count > 0) { 14 15 try { 16 Thread.sleep(3000); 17 count--; 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 22 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); 23 } 24 } 25 } 26 } 27 }
总结
synchronized(obj){}中的obj称为同步监视器
同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
4.3 同步方法
如果同步代码(原子性)很多,可以考虑使用同步方法。
把普通方法用 synchronized 修饰,同步方法的同步监视器是this。
1 public class MyRun implements Runnable { 2 3 // 共享资源 4 private int count = 5; 5 6 @Override 7 public void run() { 8 // 模拟一个窗口5个人 9 for (int i = 0; i < 5; i++) { 10 11 this.saleTicket(); 12 13 } 14 } 15 16 // 同步方法默认对this加锁 17 private synchronized void saleTicket() { 18 if (count > 0) { 19 20 try { 21 Thread.sleep(3000); 22 count--; 23 } catch (InterruptedException e) { 24 e.printStackTrace(); 25 } 26 27 System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩" + count + "张票"); 28 } 29 } 30 }
5 死锁(C)
线程t1,拥有A资源,再次申请B资源,线程t2,拥有B资源,再申请A资源,t1因为没有申请到B资源而进入阻塞;t2因为没有申请到A资源进入阻塞。此时两个线程都处于阻塞状态而不能正常结束,而此时cpu空转,这种情况称为死锁。
6 线程间通信
多进程和多线程是系统执行多任务机制的重要手段,多任务同时进行自然少不了相互之间的通信工作。下面先将线程间的通信方式总结一下,便于大家对比学习。
首先来说线程间的通信。因为多个线程是共享进程的空间的,所以线程之间的通信比较简单,主要是利用全局变量的方法。全局变量对进程内的的所有线程都是可见的,所以多个线程可以通过操作全局变量达到相互通信的效果。但是这也存在一个问题,就是“资源”的竞争。
这里所说的资源指的就是全局变量,正是因为这种竞争(因为多线程是同时运行的,而我们往往不会去控制线程运行的顺序,不然也不会用多线程了),导致可一些我们不愿见到的结果,所以我们每个线程对全局变量的操作都希望是原子性的。
为了解决这个问题在线程见引入了三种同步互斥机制,分别是信号量,互斥锁,条件变量。
多线程编程中,如果每个线程之间互相独立,那么将会使多线程带来的优势不能够很好地发挥出来。使用线程间通信,可以使得原先的互相独立的多个线程之间,能够很好地互相协作,使得系统之间的交互性得到提升,大大提高了CPU利用率,从而完成一些复杂的多线程功能模块。
多线程间的通信一般采取等待/通知机制进行实现