Java基础(二十一)——多线程和Lambda表达式
同步方法
同步方法:使用synchonized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面等待着,排队。
格式:
public synchonized void method(){ //可能会产生线程安全问题的代码 }
备注:同步锁是谁?
对于非static方法,同步锁就是this
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)
同步方法代码示例如下:
1 public synchronized void saleTicket() { 2 synchronized(this){ 3 try { 4 Thread.sleep(100); 5 } catch (InterruptedException e) { 6 e.printStackTrace(); 7 } 8 // 票存在,买出第ticket张票 9 if (ticket > 0) { 10 System.out.println(Thread.currentThread().getName() + "----->正在售卖第" + ticket-- + "张票"); 11 } 12 } 13 } 14 public static synchronized void saleTicket() { 15 16 synchronized(RunnableImpl.class) { 17 try { 18 Thread.sleep(100); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 // 票存在,买出第ticket张票 23 if (ticket > 0) { 24 System.out.println(Thread.currentThread().getName() + "----->正在售卖第" + ticket-- + "张票"); 25 } 26 } 27 28 }
Lock锁
java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized同步方法更加广泛的锁操作。
同步代码块/同步方法具有的功能,Lock都有,除此之外更强大,更能体现出面向对象特征。
Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:
- public void lock():加同步锁
- public void unlock():释放同步锁
备注:锁是控制多个线程对共享资源进行访问的工具。通常,所提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。
实例代码如下:
1 public class RunnableImpl implements Runnable { 2 // 定义一个多线程共享的资源 3 private int ticket = 100; 4 //1.在成员的位置创建一个ReentrantLock对象 5 Lock Lock = new ReentrantLock(); 6 // 设置线程的任务:卖票 此时窗口--->线程 7 @Override 8 public void run() { 9 // 先判断票是否存在 10 while (ticket > 0) { 11 // 票存在,买出第ticket张票 12 //2.在可能会引发线程安全问题的代码前调用Lock接口中的lock方法获取锁 13 Lock.lock(); 14 try { 15 if (ticket > 0) { 16 Thread.sleep(100); 17 System.out.println(Thread.currentThread().getName() + "----->正在售卖第" + ticket-- + "张票"); 18 } 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } finally { 22 //无论程序出现异常,此时都会释放锁 23 //在finally语句块中一般用于资源的释放,关闭IO流,释放lock锁,关闭数据库连接等等 24 //3.在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁 25 Lock.unlock(); 26 } 27 } 28 } 29 }
线程状态
线程状态概述
当线程被创建并启动之后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态。
在JavaAPI帮助文档中java.lang.Thread.State这个枚举给出了线程的6种状态。
导致状态发生条件 | |
---|---|
NEW(新建) | 线程刚被创建,但是还没有启动,还没有调用start方法 |
RUNNABLE(可运行) | 线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决于操作系统处理器 |
BLOCKED(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程就进入到Runnable状态 |
WAITING(无限等待) | 一个线程在等待另一个线程执行一个动作(新建)时,该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒 |
TIMED_WAITING(计时等待) | 同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知。带有超时参数的常用方法有Thread.sleep(),Object.wait(). |
TERMINATED(被终止) |
六种状态切换描述:
Timed Waiting(计时等待)
Time Waiting在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
其实当我们调用了sleep方法治好,当前正在执行的线程就进入到了计时等待状态。
练习:实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。
1 public class MyThread extends Thread { 2 @Override 3 public void run() { 4 for (int i = 1;i <= 100 ; i ++) { 5 if (i % 10 == 0) { 6 System.out.println("------------------>" + i); 7 } 8 System.out.println(i); 9 // 在每个数字之间暂停1秒 10 try{ 11 Thread.sleep(1000); 12 } catch (Exception e) { 13 e.printStackTrace(); 14 } 15 } 16 } 17 // 准备一个main函数 18 public static void main(String[] args) { 19 new MyThread().start(); 20 } 21 }
备注:
1.进入到Timed Waiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系;
2.为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行规程中会睡眠;
3.sleep与所无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行。
Blocked锁阻塞状态
Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态,反之线程B就进入到Blocked锁阻塞状态。
Waiting无限等待状态
Waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
一个调用了某个对象的Object.wait()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法
其实waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。
等待唤醒机制
线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务),却又不相同。
比如说,线程A用来生产一个娃哈哈饮料,线程B用来消费娃哈哈饮料,娃哈哈饮料可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
为什么要处理线程之间的通信:
多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮助我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。
而这种手段就是----->等待唤醒机制。
等待唤醒机制
什么是等待唤醒机制呢?
这是多个线程间的一种协作机制。
就是一个线程进行了规定操作后,就进入到了等待状态(wait()),等待其他线程执行完他们的指定代码后,再将其唤醒(notify());
在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。
wait/notify就是线程间的一种协作机制。
等待唤醒中的方法:
等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法有三个如下:
- wait():线程不在活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不再去竞争锁,这时的线程状态就是WAITING。它还要等着别的线程执行一个特别的动作,就是唤醒通知(notify)在这个对象上等待的线程从wait set钟师傅出来,重新进入到调度队列(ready queue)中。
- notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等候就餐最久的顾客最先入座。
- notifyAll():释放所通知对象的wait set中的全部线程。
备注:
哪怕只通知了一个等待线程,被通知的线程也不能立即回复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁了,所以它需要再次尝试着去获取锁(很可能面临着其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。
总结如下:
如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态
否则,从wait set中出来,又进入set中,线程就从WAITING状态转变成BLOCKED状态。
调用wait和notify方法的注意细节:
-
wait方法与notify方法必须由同一个锁对象调用。因为,对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
-
wait方法与notify方法是属于Object类的方法的。因为,锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
-
生产者与消费者问题
举一个例子:生产包子与消费包子来描述等待唤醒机制如何有效的利用资源:
代码示例:
1 /* 2 * 资源类:包子类 3 * 设置包子的属性 4 * 皮 5 * 馅 6 * 包子的状态 有 true 没有 false 7 */ 8 public class Baozi { 9 // 皮 10 String pi; 11 // 馅 12 String xian; 13 // 包子的状态 有 true 没有 false,设置初始值为false,没有包子 14 boolean flag = false; 15 16 } 17 // 包子铺 18 /* 19 * 生产者(包子铺):是一个线程类,继承Thread类 20 * 设置线程的任务:生产包子 21 * true:有包子 22 * 包子铺调用wait方法进入等待状态 23 * false:没有包子 24 * 增加一些难度:交替生产两种包子 25 * 有两种状态:(i % 2 == 0) 26 * 包子铺生产包子 27 * 修改包子的状态为true 28 * 唤醒吃货线程,让吃货去吃包子 29 * 30 * 注意: 31 * 包子铺线程和吃货线程关系---->通信(互斥) 32 * 必须使用同步技术保证两个线程只能有一个线程在执行 33 * 锁对象必须保证唯一,可以使用包子对象作为锁对象 34 * 包子铺线程和吃货线程的类需要把包子对象作为参数传递进来 35 * 1.需要在成员的位置上创建一个包子变量 36 * 2.使用带参构造,为这个包子变量赋值 37 */ 38 public class Costs extends Thread{ 39 //1.需要在成员的位置上创建一个包子变量 40 private Baozi baozi; 41 42 //2.使用带参构造,为这个包子变量赋值 43 public Costs(Baozi baozi) { 44 this.baozi = baozi; 45 } 46 47 // 重写run方法 48 @Override 49 public void run() { 50 // 设置线程任务:生产包子 51 // 定义一个变量 52 int count = 0; 53 // 让包子铺一直生产包子 54 while(true) { 55 // 必须保证两个线程只能有一个线程在执行 56 synchronized (baozi) { 57 // 进行包子状态的判断 58 if (baozi.flag) { 59 //包子铺有包子,包子铺需要调用wait方法进入等待状态 60 try { 61 baozi.wait(); 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 } 65 } 66 // 包子铺没有包子,被唤醒之后,包子铺生产包子 67 // 增加一些难度:交替生产两种类型的包子 68 if (count % 2 == 0) { 69 //生产 三鲜馅的包子,皮是薄皮 70 baozi.pi = "薄皮"; 71 baozi.xian= "三鲜馅"; 72 } else { 73 // 生产 猪肉大葱馅 冰皮 74 baozi.pi = "冰皮"; 75 baozi.xian = "猪肉大葱馅"; 76 } 77 count++; 78 System.out.println("包子铺正在生产:" + baozi.pi + baozi.xian + "包子"); 79 // 生产包子需要有一个过程:等待3秒钟 80 try { 81 Thread.sleep(3000); 82 } catch (InterruptedException e) { 83 e.printStackTrace(); 84 } 85 // 包子铺生产好了包子 86 // 修改包子的状态为true 有 87 baozi.flag = true; 88 // 唤醒吃货线程,让吃货线程去吃包子 89 baozi.notify(); 90 System.out.println("包子铺已经生产好了:" + baozi.pi + baozi.xian + "包子,吃货可以开始吃了。。"); 91 } 92 } 93 } 94 } 95 /* 96 * 消费者(吃货)类:是一个线程类 extends Thread 97 * 设置线程的任务:吃包子 98 * 对包子的状态进行判断 99 * true:有包子 100 * 吃货吃包子 101 * 吃货吃完包子 102 * 修改包子的状态味false:没有包子 103 * 吃货唤醒包子铺线程,生产包子 104 * false:没有包子 105 * 吃货调用wait方法,进入到等待状态 106 */ 107 public class Foodie extends Thread{ 108 // 1. 需要在成员的位置上定义一个包子变量 109 private Baozi baozi; 110 111 //2.使用带参构造,为这个包子变量赋值 112 public Foodie(Baozi baozi) { 113 this.baozi = baozi; 114 } 115 116 //3. 重写run方法 117 @Override 118 public void run() { 119 // 设置线程任务:吃包子 120 // 使用死循环,让吃货一直吃包子 121 while(true) { 122 // 使用同步技术保证两个线程只有一个线程在执行 123 synchronized (baozi) { 124 // 对包子的状态进行判断 125 if (baozi.flag == false) { 126 // 让吃货线程进入到等待状态 127 try { 128 baozi.wait(); 129 } catch (InterruptedException e) { 130 e.printStackTrace(); 131 } 132 } 133 // 被唤醒后执行吃包子 134 System.out.println("吃货正在吃:" + baozi.pi + baozi.xian + "包子"); 135 // 吃货吃完包子 136 // 修改包子的状态为false 没有 137 baozi.flag = false; 138 // 吃货线程唤醒包子铺线程--->生产包子 139 baozi.notify(); 140 System.out.println("吃货已经把" + baozi.pi + baozi.xian + "的包子"); 141 System.out.println("------------------------------------------"); 142 } 143 } 144 } 145 } 146 public class TestChihuoAndBaoziPuDemo { 147 148 public static void main(String[] args) { 149 // 创建包子对象 150 Baozi baozi = new Baozi(); 151 // 创建包子铺线程对象 152 new Costs(baozi).start(); 153 // 创建吃货线程对象 154 new Foodie(baozi).start(); 155 } 156 157 }
线程池
线程池的概念
-
线程池:其实就是一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁的创建线程对象的操作,无需反复创建线程而消耗过多的系统资源。
由于线程池中有很多操作都是与优化系统资源有关的,线程池的工作原理如下:
合理利用线程池能够带来什么样的好处:
-
-
提高了响应速度。当任务到达时,任务可以不需要等到线程的创建就能立即执行。
-
提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器的宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,死机的风险也就更高)。
线程池的使用
java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义讲,Executor它并不是一个线程池,它只是执行线程的一个工具,真正的线程池接口是java.util.concurrent.ExecutorService
。
因此在java.util.concurrent.Executors
线程工程类提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors来创建线程池对象。
Executors有创建线程池的方法如下:
-
public static ExecutorService newFixedThreadPool(int nThreads):返回的就是线程池对象。(创建的是有界的线程池,也就是池中的线程个数可以指定最大数量)。
获取到了一个线程池ExecutorService对象,在该类中定义了一个使用线程池对象的方法如下:
-
public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。
Future接口:用来记录线程任务执行完毕后产生的结果。线程的创建与使用。
-
创建线程池对象
-
创建Runnable接口子类对象。(task)
-
提交Runnable接口子类对象。 (take task)
-
Lambda表达式
y = x + 1,在数学中,函数就是有输入量,输出量的一套计算方案;也就是“拿什么东西,做什么事情”。相对而言,面向对象过程过分强调"必须通过对象的形式来做事情",而函数式编程思想则尽量忽略面向对象的复杂语法---强调做什么,而不是以什么方式来做。
面向对象的思想:
做一件事情,找一个能解决这个事情的对象,调用对象的方法来完成事情。
函数式编程的思想:
只要能获得这个事情的结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。
冗余的Runnable代码
当需要启动一个线程去完成一项任务时,通常会通过Runnable接口来定义任务内容,并且使用Thread类来启动线程。
1 /* 2 lambda表达式的标准格式: 3 有三部分组成: 4 a:一些参数0,1,...n 5 b:一个箭头 6 c:一段代码 7 格式: 8 (参数列表) -> {一些重写run方法的代码} 9 格式说明: 10 ():接口中抽象方法,参数列表可以没有参数,就空着;有参数就写出参数,多个参数使用逗号隔开 11 ->:传递的意思,把方法中的参数传递给方法体{} 12 {}:重写接口的抽象方法的方法体。 13 */ 14 public class Demo02Lambda { 15 16 public static void main(String[] args) { 17 new Thread(new Runnable() { 18 @Override 19 public void run() { 20 System.out.println(Thread.currentThread().getName() + "----->新线程被创建了"); 21 } 22 }).start(); 23 24 // (参数列表) -> {一些重写run方法的代码} 25 // 使用lambda表达式实现多线程编程 26 new Thread(() -> { 27 System.out.println(Thread.currentThread().getName() + "----->新线程被创建了"); 28 } 29 ).start(); 30 31 //优化省略Lambda 32 //省略模式需要三项一起省 33 new Thread(() -> System.out.println(Thread.currentThread().getName() + "----->新线程被创建了")).start(); 34 } 35 36 }