一、线程间的通信
-
定义
- 多个线程并发执行时,默认情况下CPU是随机切换线程的
- 如果我们希望他们有规律的执行,就可以使用通信
- 线程之间也不是直接干预某个线程,也是随机的
-
常用方法
- wait() 让当前线程处于等待状态,并释放锁
- notify() 唤醒某个等待中的线程
- notifyAll() 唤醒所有等待中的线程
-
演示(两个线程之间的通信)
public static void main(String[] args) { new Thread(){ private String str; @Override public void run() { synchronized (Class.class) { while (true) { Class.class.notify(); System.out.println("线程一"); try { Class.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }.start(); new Thread(){ @Override public void run() { synchronized (Class.class) { while (true) { Class.class.notify(); System.out.println("线程二"); try { Class.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }.start(); }
-
演示(多个线程之间的通信)
public static void main(String[] args) { new Thread(){ @Override public void run() { synchronized (Class.class) { while (true) { Class.class.notifyAll(); System.out.println("线程一"); try { Class.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }.start(); new Thread(){ @Override public void run() { synchronized (Class.class) { while (true) { Class.class.notifyAll(); System.out.println("线程二"); try { Class.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }.start(); new Thread(){ @Override public void run() { synchronized (Class.class) { while (true) { Class.class.notifyAll(); System.out.println("线程三"); try { Class.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }.start(); }
-
注意事项
- 线程间的所有通信行为都必须在同步代码块中执行
- 这些行为都是锁调用的
- 当一个线程陷入等待,线程会释放掉锁,并且该线程无法动弹,即使被唤醒了,也仅仅表示有了获取锁的机会,只有当真正获取到锁的时候才能继续运行
- wait方法还有重载的方法,可以传入毫秒值,表示多少毫秒之后当前线程自动唤醒
- 一个锁只能唤醒被自己锁住的线程
- 无法在当前同步代码块内操作别的锁
-
测试题
- 需求:小明同学上大学,父亲每次给1000块钱,小明每次花100元,当钱花完了,就打电话给父亲,通知他去银行存钱,编程模拟
- 思考,为什么wait和notify方法定义在Object中?
二、ThreadLocal
-
定义
- ThreadLocal是线程的本地变量,是一个存储变量的容器,存入到这个容器中的变量可以在线程的任何位置取出
- ThreadLocal中的变量是使用线程分离的,别的线程无法使用,保证了变量的安全性
-
演示
static ThreadLocal<String> local = new ThreadLocal<>(); public static void main(String[] args) { new Thread() { @Override public void run() { // 在线程的任意位置设置变量 local.set("你"); method(); } }.start(); new Thread() { @Override public void run() { local.set("好"); method(); } }.start(); ; } public static void method() { //可以在当前线程的任意位置获取变量 System.out.println(local.get()); }
三、互斥锁
-
定义
- 使用ReenTrantLock类代替synchronized关键字,提供了锁定和解锁的方法
- 提供了更多的操作锁的方法
-
常用方法
- lock() 锁定当前线程
- unlock() 解锁
- newCondition() 获取可以操作线程等待和唤醒Condition对象
- await() 让当前线程陷入等待
- signal() 唤醒某个被锁定的线程
-
演示
public static void main(String[] args) { //创建可以锁定代码的对象 ReentrantLock lock = new ReentrantLock(); //获取可以操作线程等待唤醒的Condition对象 Condition condition = lock.newCondition(); new Thread(){ @Override public void run() { lock.lock(); for (int i = 0; i < 10; i++) { //唤醒等待的线程 condition.signal(); System.out.println("线程一"); try { //让当前线程等待 condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } lock.unlock(); } }.start(); new Thread(){ @Override public void run() { lock.lock(); for (int i = 0; i < 10; i++) { //唤醒等待的线程 condition.signal(); System.out.println("线程二"); try { //让当前线程等待 condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } lock.unlock(); } }.start();; }
-
注意事项
- 互斥锁提供了更多的操作线程的方法供我们使用
- Condition对象允许我们在任意锁内唤醒任意线程
- 前提是使用同一把锁(同一个ReentrantLock对象)
四、线程的七种状态
-
图例
线程的状态.jpg
-
状态分析
- 初始状态:线程对象创建完成
- 就绪状态:线程可以被执行
- 运行状态:线程正在运行中
- 堵塞状态:线程被休眠
- 等待队列:线程陷入无限的等待中
- 锁池状态:线程被唤醒,但是没有获取到锁
- 死亡状态:线程执行完毕,被关闭
-
注意事项
- 大部分书籍里都讲线程的状态分为5类:初始,就绪,运行,堵塞,死亡
- 但是我们更愿意将堵塞状态细分出来,因为堵塞,等待队列,锁池完全是不同的性质
五、线程组(了解)
-
定义
- Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分组管理,Java允许程序直接对线程组进行控制
- 提供了一些操作整体的方法,比如设置组中线程的权限,销毁所有线程等
-
演示
public static void main(String[] args) { Thread t1 = new Thread(); Thread t2 = new Thread(); //获取线程的组 System.out.println(t1.getThreadGroup().getName()); System.out.println(t2.getThreadGroup().getName()); //创建新的线程组 ThreadGroup group = new ThreadGroup("新的线程组"); //创建线程的时候确定组,给线程起个新名字 Thread t3 = new Thread(group , "张三"); Thread t4 = new Thread(group , "李四"); System.out.println(t3.getThreadGroup().getName()); System.out.println(t4.getThreadGroup().getName()); }
六、线程池(了解)
-
定义
- 程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中药创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
- 在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
-
演示
public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; //创建线程池 ExecutorService es = Executors.newCachedThreadPool(); //调用线程池中的线程执行逻辑代码 es.submit(runnable); es.submit(runnable); es.shutdown(); }
七、Runtime类
-
定义
- 这是一个单例类,可以运行系统命令
-
演示
public static void main(String[] args) throws IOException { Runtime runtime = Runtime.getRuntime(); runtime.exec("shutdown -s -t 300");//300秒后关机 runtime.exec("shutdown -a"); //取消关机 }
八、Timer类
-
定义
- 定时器,可以在指定时间执行任务,可以重复执行
- 其实就是在指定时间去调用某个方法
- 任务类必须继承TimerTask类,并且重写run()方法
-
常用方法
- schedule(TimerTask task, Date time) 在指定的时间安排指定的任务执行
- schedule(TimerTask task, Date time, long period) 在指定的时间开始执行任务,并按照固定的时间长度重复执行
- schedule(TimerTask task, long period) 在指定的延迟之后安排指定的任务执行
- schedule(TimerTask task, long delay, long period) 在指定的延迟之后开始执行任务,并按照固定时间长度重复执行
-
演示
public static void main(String[] args) { TimerTask task = new TimerTask() { @Override public void run() { System.out.println("赶紧起床了"); } }; Timer timer = new Timer(); timer.schedule(task, 5000); }
总结
- 线程的通信
- 让多个线程之间可以相互影响 多个线程公用的空间
- 我们可以控制线程的等待和唤醒 wait() notify() notifyAll()
- 这几个方法都必须用在锁的范围内,只对同步代码块有效
- 当有多个线程处于等待状态时,notify只能随机唤醒一个
- 锁,只能唤醒被自己锁住的线程
- ThreadLocal
- 线程本地变量
- 可以存储一个值,线程隔离的,当前线程存放的值,其他线程无法获取
- 底层原理:一个以线程唯一标识作为key的map集合
- 互斥锁
- synchronized的升级版,提供了更多丰富的方法
- 目前给我们的直观体现:一个锁可以有多个Condition用来等待唤醒不同的线程
- 比synchronized功能丰富,但相对的,编码复杂度也有所提升
- 线程的状态
- 新建状态:线程的出生,创建Thread对象
- 就绪状态:线程可以被执行,但是还没有获取到CPU的执行权
- 运行状态:线程正在运行中
- 休眠状态:线程处于停止状态,可以自动醒来,不会释放锁
- 等待队列:线程陷入永久的等待(也可以中等待一段时间),释放锁,需要外力才能唤醒
- 锁池状态:等待中的线程被唤醒就会进入锁池状态,这时具备获取锁的资格,在获取锁之前,不能运行
- 消亡状态:线程运行完毕,结束程序
- 线程池
- 在初始化的时候先创建一批线程存储起来,等使用的时候直接从容器中获取
- 优点:使用时节约了创建线程的时间(线程的创建时间远远大于一般的对象)
- 线程池有多种,每种线程池都有其特点
- 线程组
- 多个线程进行分组管理,便于操作
- Runtime和Timer
- Runtime:帮助我们运行操作系统的底层命令
- Timer:计时器和定时器
作业
-
第一题
- 需求: 创建3个线程, 让着三个线程按顺序执行(比如: 线程一, 线程二, 线程三.......)
-
第二题
- 子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100次,如此循环50次,试写出代码
-
扩展题
-
需求: 有四个线程 1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推.........现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:
A:1 2 3 4 1 2....
B:2 3 4 1 2 3....
C:3 4 1 2 3 4....
D:4 1 2 3 4 1....
-
-
扩展题
- 启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5, 然后是线程2打印6,7,8,9,10, 然后是线程3打印11,12,13,14,15. 接着再由线程1打印16,17,18,19,20....以此类推, 直到打印到75.
-
扩展题
- 查阅资料, 制作一个闹铃程序,到设定的时间就播放音乐