外围概述:本篇咱们将持续学习 Java 中的多线程,其中有多线程的期待唤醒机制、Condition 接口的应用、Java 中的线程池、Timer 定时器以及 ConcurrentHashMap 的应用。
第一章:期待唤醒机制
1.1- 线程间的通信(理解)
什么是线程之间的通信呢?
就是多个线程在解决同一个资源,然而解决的动作(线程的工作)却不雷同。
比方:线程 A 用来生成包子的,线程 B 用来吃包子的,包子能够了解为同一资源,线程 A 与线程 B 解决的动作,一个是生产,一个是生产,那么线程 A 与线程 B 之间就实现了通信,其实就是一种协作关系。
为什么要解决线程间的通信?
多个线程并发执行时, 在默认状况下 CPU 是随机切换线程的,当咱们须要多个线程来共同完成一件工作,并且咱们 心愿他们有法则的执行, 那么多线程之间须要一些 协调通信,以此来帮咱们达到多线程独特操作一份数据。
如何保障线程间通信无效利用资源?
多个线程在解决同一个资源,并且工作不同时,须要线程通信来帮忙解决线程之间对同一个变量的应用或操作。就是多个线程在操作同一份数据时,防止对同一共享变量的抢夺。也就是咱们须要通过肯定的伎俩使各个线程能无效的利用资源。而这种伎俩即—— 期待唤醒机制。
1.2- 什么是期待唤醒机制(理解)
这是多个线程间的一种合作机制。谈到线程咱们常常想到的是线程间的竞争(race),比方去抢夺锁,但这并不是故事的全副,线程间也会有合作机制。就好比在公司里你和你的共事们,你们可能存在在降职时的竞争,但更多时候你们更多是一起单干以实现某些工作。
就是在一个线程进行了规定操作后,就进入期待状态(wait()),期待其余线程执行完他们的指定代码过后 再将其唤醒(notify()); 在有多个线程进行期待时,如果须要,能够应用 notifyAll()来唤醒所有的期待线程。
wait/notify 就是线程间的一种合作机制。
1.3- 期待唤醒相干办法(重要)
线程期待和唤醒的办法定义在 java.lang.Object
类中。
wait 办法
当调用 wait 办法后,线程不再流动,不再参加调度,进入 wait set 中,因而不会节约 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特地的动作,也即是“告诉(notify)”在这个对象上期待的线程从 wait set 中释放出来,从新进入到调度队列(ready queue)中。
notify 办法
当调用 notify 办法后,则选取所告诉对象的 wait set 中的一个线程开释;例如,餐馆有空地位后,等待就餐最久的顾客最先入座。
notifyAll 办法
当调用 notifyAll 办法后,则开释所告诉对象的 wait set 上的全副线程。
注意事项
注意事项 1 :
哪怕只告诉了一个期待的线程,被告诉线程也不能立刻复原执行,因为它当初中断的中央是在同步块内,而此刻它曾经不持有锁,所以她须要再次尝试去获取锁(很可能面临其它线程的竞争),胜利后能力在当初调用 wait 办法之后的中央复原执行。
总而言之,如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;否则,从 wait set 进去,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
注意事项 2 :
- wait 办法与 notify 办法必须要由同一个锁对象调用。因为:对应的锁对象能够通过 notify 唤醒应用同一个锁对象调用的 wait 办法后的线程。
- wait 办法与 notify 办法是属于 Object 类的办法的。因为:锁对象能够是任意对象,而任意对象的所属类都是继承了 Object 类的。
- wait 办法与 notify 办法必须要在同步代码块或者是同步函数中应用。因为:必须要通过锁对象调用这 2 个方 法。
1.4- 案例(练习)
期待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子生产包子来说期待唤醒机制如何无效利用资源
需要
定义一个变量,包子铺线程实现生产包子,包子进行 ++ 操作;吃货线程实现购买包子,包子变量打印进去。
- 当包子没有时(包子状态为 false),吃货线程期待。
- 包子铺线程生产包子(即包子状态为 true),并告诉吃货线程(解除吃货的期待状态)。
- 保障线程平安,必须生产一个生产一个,不能同时生产或者生产多个。
代码
包子铺类
public class BaoZiPu {
private int baoZiCount;
// 标记位变量
// 当包子没有时(包子状态为 false),吃货线程期待。// 包子铺线程生产包子(即包子状态为 true),并告诉吃货线程(解除吃货的期待状态)。private boolean flag;
public void setFlag(boolean flag){this.flag = flag;}
public boolean getFlag(){return flag;}
// 消费者调用办法,变量输入
public void get(){System.out.println("生产第"+baoZiCount+"个包子");
}
// 生产者调用办法,变量 ++
public void set(){
baoZiCount++;
System.out.println("生产第"+baoZiCount+"个包子");
}
}
生产者类
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu){this.baoZiPu = baoZiPu;}
@Override
public void run() {while (true){synchronized (baoZiPu) {
// 生产者线程判断标记位变量,==true,曾经生产还没有生产
if(baoZiPu.getFlag() == true){
try {
// 线程期待
wait();} catch (InterruptedException e) {e.printStackTrace();
}
}
// 生产一个
baoZiPu.set();
// 批改标记位
baoZiPu.setFlag(true);
// 唤醒对方线程
notify();}
}
}
}
消费者类
public class Customer implements Runnable {
private BaoZiPu baoZiPu;
public Customer(BaoZiPu baoZiPu){this.baoZiPu = baoZiPu;}
@Override
public void run() {while (true){synchronized (baoZiPu) {
// 消费者线程判断标记位,==false,没有生产
if(baoZiPu.getFlag()==false) {
try {
// 线程期待
wait();} catch (InterruptedException ex) {}}
// 调用生产办法
baoZiPu.get();
// 批改标记位
baoZiPu.setFlag(false);
// 唤醒对方线程
notify();}
}
}
}
测试类
public class Test{public static void main(String[] args) {BaoZiPu baoZiPu = new BaoZiPu();
Product product = new Product(baoZiPu);
Customer customer = new Customer(baoZiPu);
new Thread(product).start();
new Thread(customer).start();}
}
执行后果
异样剖析
- 程序呈现有效的监视器状态异样。
-
wait()或者 notify()办法会抛出此异样。
- 程序中,wait()或者 notify()办法的调用者是 this 对象。
- 而 this 对象在同步中并不是锁对象,只有作为锁的对象能力调用 wait()或者 notify()办法。
- 而锁对象是生产者和消费者共享的包子铺对象。
代码革新
生产者类
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu){this.baoZiPu = baoZiPu;}
@Override
public void run() {while (true){synchronized (baoZiPu) {
// 生产者线程判断标记位变量,==true,曾经生产还没有生产
if(baoZiPu.getFlag() == true){
try {
// 线程期待
baoZiPu.wait();} catch (InterruptedException e) {e.printStackTrace();
}
}
// 生产一个
baoZiPu.set();
// 批改标记位
baoZiPu.setFlag(true);
// 唤醒对方线程
baoZiPu.notify();}
}
}
}
消费者类
public class Customer implements Runnable {
private BaoZiPu baoZiPu;
public Customer(BaoZiPu baoZiPu){this.baoZiPu = baoZiPu;}
@Override
public void run() {while (true){synchronized (baoZiPu) {
// 消费者线程判断标记位,==false,没有生产
if(baoZiPu.getFlag()==false) {
try {
// 线程期待
baoZiPu.wait();} catch (InterruptedException ex) {}}
// 调用生产办法
baoZiPu.get();
// 批改标记位
baoZiPu.setFlag(false);
// 唤醒对方线程
baoZiPu.notify();}
}
}
}
代码优化
通过线程期待与唤醒,实现了生产者与消费者案例,然而代码维护性差,浏览性差,应用同步办法进行代码的优化。在包子铺类中的 get(),set()办法进行同步办法的改良。
留神:一旦办法同步后,this 就是锁对象。
包子铺类:变量 flag 只在类中应用,因而能够去掉 get/set 办法。
包子铺类
public class BaoZiPu {
private int baoZiCount;
// 标记位变量
// 当包子没有时(包子状态为 false),吃货线程期待。// 包子铺线程生产包子(即包子状态为 true),并告诉吃货线程(解除吃货的期待状态)。private boolean flag;
// 消费者调用办法,应用同步
public synchronized void get(){
// 判断标记位 ==false,没有生产,线程期待
if (flag == false)
try {this.wait();
}catch (InterruptedException ex){}
System.out.println("生产第"+baoZiCount+"个包子");
// 批改标记位
flag = false;
// 唤醒对方线程
this.notify();}
// 生产者调用办法,变量 ++,应用同步
public synchronized void set(){
// 判断标记位,==true,没有生产,线程期待
if(flag == true)
try {this.wait();
}catch (InterruptedException ex){}
baoZiCount++;
System.out.println("生产第"+baoZiCount+"个包子");
// 批改标记位
flag = true;
// 唤醒对方线程
this.notify();}
}
生产者类
public class Product implements Runnable{
private BaoZiPu baoZiPu;
public Product(BaoZiPu baoZiPu){this.baoZiPu = baoZiPu;}
@Override
public void run() {while (true){baoZiPu.set();
}
}
}
消费者类
public class Customer implements Runnable {
private BaoZiPu baoZiPu;
public Customer(BaoZiPu baoZiPu){this.baoZiPu = baoZiPu;}
@Override
public void run() {while (true){baoZiPu.get();
}
}
}
1.5-sleep()办法和 wait()办法的区别(理解)
- sleep()是 Thread 类静态方法,不须要对象锁。
- wait()办法是 Object 类的办法,被锁对象调用,而且只能呈现在同步中。
- 执行 sleep()办法的线程不会开释同步锁。
- 执行 wait()办法的线程要开释同步锁,被唤醒后还需获取锁能力执行。
1.6- 多生产者多消费者(理解)
概述
上一练习中,咱们实现了生产者和消费者案例,然而如果咱们开启多个生产者线程和多个生产者线程会产生什么景象呢,线程还会平安吗?
线程平安起因剖析
当开启了多个线程后,数据呈现了平安问题。问题就呈现在期待和唤醒环节。咱们将线程分成了生产者和消费者两个局部,须要生产者线程唤醒消费者线程,而消费者线程要唤醒生产者线程。然而线程的唤醒是依照队列模式进行,先期待的会先被唤醒。很可能呈现生产者线程又唤醒了生产者线程,消费者线程唤醒了消费者线程。因而咱们须要将线程全副唤醒,应用 notifyAll()办法。
全副唤醒后,线程仍然不平安,是因为线程判断完标记位后就会期待,当被唤醒后,就不会再判断标记位了,咱们必须让线程在唤醒后,还要持续判断标记位,容许生存能力生产,不运行生产就要持续期待。
革新代码实现多生产和多生产
包子铺类
public class BaoZiPu {
private int baoZiCount;
// 标记位变量
// 当包子没有时(包子状态为 false),吃货线程期待。// 包子铺线程生产包子(即包子状态为 true),并告诉吃货线程(解除吃货的期待状态)。private boolean flag;
// 消费者调用办法,应用同步
public synchronized void get(){
// 判断标记位 ==false,没有生产,线程期待
while (flag == false)
try {this.wait();
}catch (InterruptedException ex){}
System.out.println("生产第"+baoZiCount+"个包子");
// 批改标记位
flag = false;
// 唤醒对方线程
this.notifyAll();}
// 生产者调用办法,变量 ++,应用同步
public synchronized void set(){
// 判断标记位,==true,没有生产,线程期待
while(flag == true)
try {this.wait();
}catch (InterruptedException ex){}
baoZiCount++;
System.out.println("生产第"+baoZiCount+"个包子");
// 批改标记位
flag = true;
// 唤醒对方线程
this.notifyAll();}
}
第二章:Condition 接口
2.1- 期待唤醒的弊病(理解)
多生产与多生产案例中,咱们应用了线程通信的相干办法 wait()和 notify(),notifyAll()。
public final native void wait(long timeout) throws InterruptedException
public final native void notify()
public final native void notifyAll()
以上三个办法都是本地办法,要和操作系统进行交互,因而线程期待唤醒须要耗费系统资源,程序效率升高。另外咱们一次唤醒所有的线程,也会节约很多资源,为了解决这些弊病,JDK1.5 版本的时候呈现了 Lock 接口和 Condition 接口。
2.2-Condition 接口(重点)
介绍
Condition
将 Object
监视器办法(wait
、notify
和 notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意 Lock
实现组合应用,为每个对象提供多个期待 set(wait-set)。其中,Lock
代替了synchronized
办法和语句的应用,Condition
代替了 Object
监视器办法的应用。
获取 Condition 对象
Lock 接口的办法 newCondition()获取
public Condition newCondition()
Condition 对象罕用办法
Condition 接口办法和 Object 类办法比拟
-
Condition 能够和任意的 Lock 组合,实现治理线程的阻塞队列(间接在内存重操作)。
- 一个线程的案例中,能够应用多个 Lock 锁,每个 Lock 锁上能够联合 Condition 对象。
- synchronized 同步中做不到将线程划分到不同的队列中(须要本地办法 [c++ 编写] 和操作系统交互)。
- Object 类 wait()和 notify()都要和操作系统交互,并告诉 CPU 挂起线程,唤醒线程,效率低。
- Condition 接口办法 await()不和操作系统交互,而是让开释锁,并存放到线程队列容器中(内存总),当被 signal()唤醒后,从队列中进去,从新获取锁后在执行。
- 因而应用 Lock 和 Condition 的效率比 Object 要快很多。
生产者与消费者案例改良
包子铺类
public class BaoZiPu {
private int baoZiCount;
// 标记位变量
// 当包子没有时(包子状态为 false),吃货线程期待。// 包子铺线程生产包子(即包子状态为 true),并告诉吃货线程(解除吃货的期待状态)。private boolean flag;
// 创立 Lock 接口实现类,线程平安提供锁定
private Lock lock = new ReentrantLock();
//Condition 对象和生产者锁联合
private Condition productCondition = lock.newCondition();
//Condition 对象和消费者锁联合
private Condition customerCondition = lock.newCondition();
public void setFlag(boolean flag){this.flag = flag;}
public boolean getFlag(){return flag;}
// 消费者调用办法,消费者 Lock 对象锁定
public void get(){lock.lock();
// 判断标记位 ==false,没有生产,线程期待
while (flag == false)
try {customerCondition.await();
}catch (InterruptedException ex){}
System.out.println("生产第"+baoZiCount+"个包子");
// 批改标记位
flag = false;
// 唤醒对方线程
productCondition.signal();
lock.unlock();}
// 生产者调用办法,变量 ++,生产者 Lock 对象锁定
public void set(){lock.lock();
// 判断标记位,==true,没有生产,线程期待
while(flag == true)
try {productCondition.await();
}catch (InterruptedException ex){}
baoZiCount++;
System.out.println("生产第"+baoZiCount+"个包子");
// 批改标记位
flag = true;
// 唤醒对方线程
customerCondition.signal();
lock.unlock();}
}
第三章:线程池
3.1- 概述(理解)
线程池思维
咱们应用线程的时候就去创立一个线程,这样实现起来十分简便,然而就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个工夫很短的工作就完结了,这样 频繁创立线程就会大大降低零碎的效率,因为频繁创立线程和销毁线程须要工夫。
那么有没有一种方法使得线程能够复用,就是执行完一个工作,并不被销毁,而是能够继续执行其余的工作?
在 Java 中能够通过 线程池 来达到这样的成果。
什么是线程池
其实就是一个 包容多个线程的容器,其中的线程能够重复应用,省去了频繁创立线程对象的操作,无需重复创立线程而耗费过多资源。
正当应用线程池的益处
- 升高资源耗费。缩小了创立和销毁线程的次数,每个工作线程都能够被反复利用,可执行多个工作。
- 进步响应速度。当工作达到时,工作能够不须要的等到线程创立就能立刻执行。
- 进步线程的可管理性。能够依据零碎的承受能力,调整线程池中工作线线程的数目,避免因为耗费过多的内存,而把服务器累趴下(每个线程须要大概 1MB 内存,线程开的越多,耗费的内存也就越大,最初死机)。
3.2- 应用线程池(重点)
java.util.concurrent
包中定义了线程池相干的类和接口。
Java 外面线程池的顶级接口是 java.util.concurrent.Executor
,然而严格意义上讲 Executor 并不是一个线程 池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很分明的状况下,很有可能配置的线程池不是较优的,因而在 java.util.concurrent.Executors 线程工厂类外面提供了一些动态工厂,生成一些罕用的线程池。官网倡议应用 Executors 工程类来创立线程池对象。
Executors 类
创立线程池对象的工厂办法,应用此类能够创立线程池对象。
ExecutorService 接口
线程池对象的治理接口,提交线程工作,敞开线程池等性能。
Callable 接口
线程执行的工作接口,相似于 Runnable 接口。
-
接口办法
public V call()throw Exception
- 线程要执行的工作办法
- 比起 run()办法,call()办法具备返回值,能够获取到线程执行的后果。
Future 接口
异步计算结果,就是线程执行实现后的后果。
- 接口办法
public V get()
获取线程执行的后果,就是获取 call()办法返回值。
示例代码
需要:创立有 2 个线程的线程池,别离提交线程执行的工作,一个线程执行字符串切割,一个执行 1 +100 的和。
实现 Callable 接口,字符串切割性能:
public class MyStringCallable implements Callable<String[]> {
private String str;
public MyStringCallable(String str){this.str = str;}
@Override
public String[] call() throws Exception {return str.split("+");
}
}
实现 Callable 接口,1+100 求和:
public class MySumCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int x = 1 ; x<= 100; x++){sum+=x;}
return sum;
}
}
测试类:
public static void main(String[] args) throws Exception {
// 创立有 2 个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 提交执行字符串切割工作
Future<String[]> futureString = executorService.submit(new MyStringCallable("aa bbb cc d e"));
System.out.println(Arrays.toString(futureString.get()));
// 提交执行求和工作
Future<Integer> futureSum = executorService.submit(new MySumCallable());
System.out.println(futureSum.get());
executorService.shutdown();}
第四章:Timer 定时器
4.1- 概述(理解)
Java 中的定时器,能够依据指定的工夫来运行程序。
java.util.Timer
一种工具,线程用其安顿当前在后盾线程中执行的工作。可安顿工作执行一次,或者定期反复执行。定时器是应用新建的线程来执行,这样即便主线程 main 完结了,定时器也仍然会持续工作。
4.2-Timer 定时器的应用
罕用办法
- 构造方法:无参数。
-
定时办法:public void schedule(TimerTask task,Date firstTime,long period)
- TimerTask 是定时器要执行的工作,一个抽象类,咱们须要继承并重写办法 run()
- firstTime 定时器开始执行的工夫
- period 工夫距离,毫秒值
示例
public class Test{public static void main(String[] args) {Timer timer = new Timer();
timer.schedule(new TimerTask() {
int i = 0;
@Override
public void run() {
i++;
System.out.println(i);
}
},new Date(),1000);
}
}
第五章:ConcurrentHashMap
5.1- 概述(理解)
java.util.concurrent.ConcurrentHashMap
反对获取的齐全并发和更新的所冀望可调整并发的哈希表。
此汇合实现 Map 接口,因而 Map 汇合中的所有性能都能够间接应用。
-
ConcurrentHashMap 汇合特点
- 底层是哈希表构造
- 此汇合是线程平安的,然而某些性能不用锁定。比方 get()
-
不会抛出 ConcurrentModificationException 并发批改异样
- 此汇合反对遍历过程中增加,删除元素。
-
ConcurrentHashMap 汇合的锁定特点
- 为了提高效率,不会将整个汇合全副锁定。
- 当增加或者移除元素时,是对链表进行操作,链表存储在数组中,那么就只会针对这个链表进行锁定。
5.2- 迭代中增加元素(测试)
public static void main(String[] args) throws Exception {Map<String,String> map = new ConcurrentHashMap<String, String>();
map.put("1","a");
map.put("2","b");
map.put("3","c");
System.out.println(map);
Set<Map.Entry<String,String>> set = map.entrySet();
Iterator<Map.Entry<String,String>> it = set.iterator();
while (it.hasNext()){map.put("4","4");
Map.Entry<String, String> next = it.next();
System.out.println(next.getKey()+"="+next.getValue());
}
}
5.3- 线程平安测试
public static void main(String[] args) throws Exception {Map<String,Integer> map = new ConcurrentHashMap<String, Integer>();
Map<String,Integer> map = new HashMap<String, Integer>();
// 存储 2000 个键值对
for(int x = 0 ; x < 2000; x++){map.put("count"+x,x);
}
// 开启线程,删除前 500 个
Runnable r1 = new Runnable() {
@Override
public void run() {for(int i = 0 ; i < 500;i++){map.remove("count"+i);
}
}
};
// 开启线程,删除 1000-1500 个
Runnable r2 = new Runnable() {
@Override
public void run() {for(int i = 1000 ; i < 1500;i++){map.remove("count"+i);
}
}
};
new Thread(r1).start();
new Thread(r2).start();
// 期待 2 秒,让 2 个线程全副运行结束
Thread.sleep(2000);
// 打印汇合长度,线程平安汇合应该是 1000
System.out.println(map.size());
}