网站建设类书籍,深圳网站公司哪家好,阿里云和wordpress,扬州市住房和建设局网站线程与进程为了实现多个任务并发执行的效果#xff0c;人们引进了进程。何谓进程#xff1f;我们电脑上跑起来的每个程序都是进程。每一个进程启动#xff0c;系统会为其分配内存空间以及在文件描述符表上有所记录等。进程是操作系统进行资源分配的最小单位#xff0c;这意…线程与进程为了实现多个任务并发执行的效果人们引进了进程。何谓进程我们电脑上跑起来的每个程序都是进程。每一个进程启动系统会为其分配内存空间以及在文件描述符表上有所记录等。进程是操作系统进行资源分配的最小单位这意味着各个进程互相之间是无法感受到对方存 在的这就是操作系统抽象出进程这一概念的初衷这样便使得进程之间互相具备”隔离性 Isolation“。然而进程存在很大的问题那就是频繁的创建或销毁进程时间成本与空间成本都会比较高。于是人们又引进了更加轻量级的线程。进程包含线程一个进程里可以有一个线程或是多个线程这些线程共用同一份资源。每个线程都是一个独立的执行流多个线程之间也是并发的。当然了多个线程可以是在多个CPU核心上并发运行也可以在同一个CPU核心上通过快速的交替调度看起来是同时运行的。基于此创建线程比创建进程快销毁线程比销毁进程快以及调用线程比调用进程快。虽然线程比进程更加轻量级但不是说多线程就能完全替代多线程相反多线程与多进程在电脑中是同时存在的。系统中自带的任务管理器看不到线程只能看到进程级别的需要使用其他第三方工具才能看到 线程比如winDbg。 线程的创建class MyThread extends Thread {Overridepublic void run() {while (true) {System.out.println(hello t);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class ThreadDemo1 {public static void main(String[] args) {Thread t new MyThread();// start 会创建新的线程t.start();// run 不会创建新的线程. run 是在 main 线程中执行的~~// t.run();while (true) {System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}输出hello mainhello thello thello mainhello mainhello thello main.......启动一个线程之前我们已经看到了如何通过覆写 run 方法创建一个线程对象但线程对象被创建出来并不意味着线程 就开始运行了。还需要调用start方法,而这才真的在操作系统的底层创建出一个线程。所以真正启动一个线程之前有三步1. main方法是主线程在主线程中创建线程对象。2. 覆写run方法覆写 run 方法是提供给线程要做的事情的指令清单。3. 调用start方法线程开始独立执行。系统会在这里调用run方法而不是靠程序员去调用为了加深同学们的理解我对上述情况做了个类比张三是项目的负责人主线程但要干的活太多了他一个人忙不过来所以他又叫了王五过来帮忙创建线程对象并给他讲解具体要做什么事情覆写run方法最后随着张三一声令下“大家开干吧”所有人都开始干活了调用start方法。4. 中断一个线程王五一旦进到工作状态他就会按照老板的指示去进行工作不完成是不会结束的。但有时我们 需要增加一些机制例如老板突然来电话了说转账的对方是个骗子需要赶紧停止转账那张三该如 何通知王五停止呢这就涉及到线程中断的方式了。 中断一个线程就是让一个线程停下来或是说让线程终止。对程序来说那就是让run方法执行完毕。目前常见的有以下两种方式 1. 给线程设定一个结束标志位 2. 调用 interrupt() 方法来通知 4.1 使用自定义的变量来作为标志位 public class ThreadDemo1 {public static boolean isQuit false;public static void main(String[] args) {Thread t new Thread(() - {while(!isQuit){//currentThread 是获取到当前线程实例.//此处的线程实例是 tSystem.out.println(Thread.currentThread().getName() 给某客户转账中);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() 立马停止转账并长吁一口气\好险好险\);},王五);t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}//此处的线程实例是 main System.out.println(Thread.currentThread().getName() 说 \坏了遇到骗子了叫王五停止转转\);isQuit true;}
}这里问一个问题我们可不可以把isQuit变量写到main函数里面答案是不可以的。因为lambda表达式访问外面的局部变量时使用的是变量捕获其语法规定捕获的变量必须是final修饰的或者是一个实际final即该变量并没有用final修饰但在实际过程中变量没有修改。而我们看到为了让run方法终止isQuit的确修改了那么这就会报错4.2 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位 Thread类中内置了一个标志位通过调用interrupt方法实现方法功能public void interrupt()设置标志位为true如果该线程正在阻塞中如执行sleep则会通过抛异常的方式让sleep立即结束Thread 内部还包含了一个 boolean 类型的变量作为线程是否被中断的标记。方法功能public static boolean interrupted()判断当前线程的中断标志位是否设置调用后清除标志位 public static boolean isInterrupted()判断当前线程的中断标志位是否设置调用后不清除标志位 在这里同学们不免有个疑问interrupted调用后清除标志位与isInterrupted调用后不清除标志位有什么区别呢下面给出两个对比例子 public class ThreadDemo2 {public static void main(String[] args) {Thread t new Thread(()-{for(int i 0; i 3; i){System.out.println(Thread.interrupted());}});t.start();//把 t 内部的标志位给设置成 truet.interrupt();}
}输出 true false false除了第一个是true之外其余的都是false因为标志位被清除了。public class ThreadDemo3 {public static void main(String[] args) {Thread t new Thread(()-{for(int i 0; i 3; i){System.out.println(Thread.currentThread().isInterrupted());}});t.start();t.interrupt();}
}输出 true true true全是true因为标志位没有被清除但是如果interrupt与isInterrupted遇到了像sleep这样的会让线程阻塞的方法会发生什么??public class ThreadDemo3 {public static void main(String[] args) {Thread t new Thread(() - {while(!Thread.currentThread().isInterrupted()){System.out.println(Thread.currentThread().getName()给某位客人转账中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName()停止交易);},王五);t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()说:\通知王五对方是骗子\);t.interrupt();}
}按照前面的讲解同学们一定会很自然的觉得肯定能打印停止交易这句话但实际运行结果大部分情况是下面这样子的要知道多线程的代码执行顺序并不是以往熟知的从上到下而是并发的。上述代码中自t.start()之后便兵分两路线程与主线程交替执行。当执行到t.interrupt()时设置标志位为true。这里又会遇到两种情况一是此时的线程t刚好来到while的判断语句因为是取反此时为false跳出循环打印王五停止交易线程t结束并且由于主线程后面没有代码主线程也结束。但发生这种情况的概率十分低因为执行sleep占据了大部分时间所以大部分的情况是线程t已经进入到while当中了。那么sleep执行过程中遇到标志位为true它又会干两件事一是立刻抛出异常。二是自动把isInterrupted的标志位给清空true-flase——这就导致while死循环了。当然了如果执行sleep时标志位为flase它就继续执行什么也不会做。上述的情况是执行t.interrupt()之后t线程大部分情况下依旧在跑。下面再看其他的情况执行t.interrupt()之后t线程立马结束public class ThreadDemo3 {public static void main(String[] args) {Thread t new Thread(() - {while(!Thread.currentThread().isInterrupted()){System.out.println(Thread.currentThread().getName()给某位客人转账中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}System.out.println(Thread.currentThread().getName()停止交易);},王五);t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()说:\通知王五对方是骗子\);t.interrupt();}
}执行t.interrupt()之后t线程等一会再结束public class ThreadDemo3 {public static void main(String[] args) {Thread t new Thread(() - {while(!Thread.currentThread().isInterrupted()){System.out.println(Thread.currentThread().getName()给某位客人转账中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();try {Thread.sleep(10000);} catch (InterruptedException ex) {ex.printStackTrace();}break;}}System.out.println(Thread.currentThread().getName()停止交易);},王五);t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()说:\通知王五对方是骗子\);t.interrupt();}
}到这里同学们应该可以发现t.interrupt()更像是个通知而不是个命令。interrupt的效果不是让t线程马上结束而是给它结束的通知至于是否结束、立即结束还是等会结束都有赖于代码是如何写的。5. 等待一个线程线程之间是并发的操作系统对于线程的调度是随机的因此无法判断两个线程谁先结束谁后结束。有时我们需要等待一个线程结束之后再启动另一个线程那么这时我们就需要一个方法明确等待线程的结束——join()。public class ThreadDemo4 {public static void main(String[] args) throws InterruptedException{Thread t new Thread(() -{for(int i 0; i 6; i){System.out.println(hello t);}});t.start();t.join();System.out.println(hello main!!);}
}上述过程可以描述为执行t.join()的时候线程t还没有完成main只能等t结束了才能往下执行此时main发生阻塞不再参与cpu的调度此时只有线程t在运行。而如果是下面这种情况呢public class ThreadDemo4 {public static void main(String[] args) throws InterruptedException{Thread t new Thread(() -{for(int i 0; i 6; i){System.out.println(hello t);}});t.start();Thread.sleep(3000);t.join();System.out.println(hello main!!);}
}上述代码中在t.start() 和 t.join() 之间加了个sleep这就意味着执行 t.join() 时线程t已经结束了那么此时main线程无需再多花时间等待直接就可以往下执行那如果线程t完成的时间很长难道只能一直“死等”下去吗其实不是join还有另外的重载方法是有参数的可以指定最大等待时间如果超过了main线程就不等了直接往下执行。public class ThreadDemo4 {public static void main(String[] args) throws InterruptedException{Thread t new Thread(() -{for(int i 0; i 1000; i){System.out.println(hello t);}});t.start();t.join(1);//等待t线程执行1毫秒System.out.println(hello main!!);System.out.println(hello main!!);System.out.println(hello main!!);}
}输出..................hello thello thello thello thello main!!hello main!!hello main!!hello thello thello thello t..................6. 线程的状态public class ThreadDemo5 {public static void main(String[] args) throws InterruptedException{Thread t new Thread(() - {while(true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println(t.getState());t.start();System.out.println(t.getState());}
}输出 NEW RUNNABLEpublic class ThreadDemo5 {public static void main(String[] args) throws InterruptedException{Thread t new Thread(() - {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});t.start();Thread.sleep(2000);System.out.println(t.getState());}
}输出 TERMINATED系统中线程已经执行完毕但t还在。了解线程的状态有利于后序对多线程代码进行调试。7. 线程的安全导致线程不安全的原因有以下几点1. 线程的调度是抢占式执行的2. 多个线程修改同一个变量对比另外三种线程安全的情况一个线程修改同一个变量多个线程分别修改不同的变量多个线程读取同一个变量。3. 修改操作不是原子性的即某个操作对应单个cpu指令的就是原子性。4. 内存的可见性引起的线程不安全5. 指令重排序引起的线程不安全7.1 修改操作不是原子性的导致线程的不安全由于线程之间的调度顺序是不确定的某些代码在多线程环境下运行可能会出现bug。比如下面这段代码两个线程针对同一个变量自增5w次。class Counter{private int count 0;public void add(){count;}public int get(){return count;}
}public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException{Counter counter new Counter();Thread t1 new Thread(() - {for (int i 0; i 50000; i) {counter.add();}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.get());}
}同学们猜一猜输出结果是什么?大家肯定都是一拍脑门脱口而出一个答案那就是100000但实际情况是每次输出都是一个不一样的数字86186、75384、100000、99839......这里其实就是发生了线程不安全问题。count操作由三个cpu指令构成load把内存中的数据读取到cpu中的寄存器中add把寄存器中的值进行1计算save把寄存器中的值写会内存中。由于多线程的调度顺序是不确定的实际执行过程中两个线程的1操作的实际指令顺序就有多种可能下面画图示意实际情况是各种可能都会出现比如会出现一个线程多次执行、一个线程count的操作只执行了部分又执行另一个线程就是因为顺序执行与交错执行随机出现导致最后自增次数不一定是100000。在这里可以发现t1和t2对count分别自增一次但由于调度的无序性使得自增的操作交叉了最后内存上的count为2也就是本应自增两次的变成了一次。甚至还可能出现t1 load了之后t2执行多次最后t1再add跟save那么t2自增那么多次的结果就会让t1自增一次的结果给覆盖这就是修改操作非原子性导致的线程不安全问题。那我们应该如何解决这样的问题呢答案就是加锁通过加锁的方式让count这一操作变成原子性的。如果两个线程针对同一个对象进行加锁就会出现锁竞争一个线程抢占式执行抢到了锁之后另一个线程如果也执行到同一个地方它不能立马加锁得阻塞等待那个抢到锁的线程放锁之后才能成功加锁。如果两个线程针对不同对象进行加锁就不会出现锁竞争各自获取各自的锁即可。下面介绍加锁的方式一. 直接在定义方法时使用 synchronized 关键字相当于以this为锁对象class Counter{private static int count 0;synchronized public void add(){count;}public int get(){return count;}
}class Counter{private static int count 0;public void add(){synchronized (this){count;}}public int get(){return count;}
}锁有两个核心操作加锁和解锁。上面的代码块表示进入synchronized修饰的代码块时会加锁而出了synchronized代码块时会解锁。this是针对具体哪个对象加锁。二. synchronized修饰静态方法相当于给类对象加锁class Counter{private static int count 0;synchronized public static void add(){count;}public static int get(){return count;}
}
public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException{Thread t1 new Thread(() -{for (int i 0; i 50000; i) {Counter.add();}});Thread t2 new Thread(() -{for (int i 0; i 50000; i) {Counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(Counter.get());}
}等同于以下方式class Counter{private static int count 0;public static void add(){synchronized (Counter.class){count;}}public static int get(){return count;}
}三. 更常见的是手动指定一个锁对象class Counter{private static int count 0;private Object locker new Object();public void add(){synchronized (locker){count;}}public int get(){return count;}
}此时就能保证t2的load是在t1的save之后的。这样一来计算结果就能保证是线程安全的了。加锁本质上就是让并发的变成串行的。这里又有同学要问了那加锁跟join有什么区别吗还是有很大区别的。join是让两个线程完整的串行而加锁只是让两个线程的某个一小部分串行而其他依旧是并发的。这样就能在保证线程安全的情况下让代码跑的更快一些更好的利用多核cpu。总而言之加锁可能导致阻塞这对程序的效率势必会造成影响。加了锁的肯定比不加锁的慢但又比join的完整串行要快更重要的是加了锁的一定比不加锁的算出来的结果更准确7.2 内存的可见性引起的线程不安全可见性: 一个线程对共享变量值的修改能够及时地被其他线程看到. 为了引出这一情况我们先来看以下的代码import java.util.*;
public class ThreadDemo2 {public static int flag 0;public static void main(String[] args) {Thread t1 new Thread(() - {while(flag 0){}System.out.println(t1 循环结束);});Thread t2 new Thread(() - {Scanner sc new Scanner(System.in);System.out.println(请输入一个整数);flag sc.nextInt();});t1.start();t2.start();}
}对于上述代码预期情况如下t1通过flag0作为条件进入循环t2通过控制台输入一个整数只要这个整数非0t1的循环就会结束此时t1的线程也就结束。但是当我们去运行这段代码时发现输入非0整数之后t1线程并没有结束打开jconsole发现t1线程还在运行为什么会有这样的问题呢且看以下分析while的条件语句flag0此处可拆分成两道cpu指令load从内存中读取数据到cpu寄存器cmp比较寄存器的值是否为0已知读取内存数据的速度比读取硬盘数据要快然而读取寄存器上的数据又比读取内存上的数据要快即。因此load的时间开销就远远的高于cmp。如果编译器需要快速的load并且每次load的结果都一样这对编译器来说是个负担。于是编译器就做了一个非常大胆的操作把load给优化掉了也就是说只有第一次执行load才真正的从内存读取了flag 0后续循环都只cmp而不再执行load相当于复用首次放于寄存器中的flag值。编译器优化就是能够智能的调整程序员的代码执行逻辑保证程序结果不变的前提下通过加减语句或是语句变换等一系列操作让整个程序执行的效率大大提升在单线程环境下编译器对于优化之后程序结果不变的判断是非常准确的但是多线程环境就不一定了会出现调整之后效率提高了但是结果变了。当预期结果跟实际结果不符时就是出现bug了。所以到这里就可以解释清楚什么是内存可见性出现问题也就是在多线程环境下编译器对于代码优化产生了误判从而引起bug。那么该如何解决这个问题呢只要让编译器停止对这个场景优化即可——使用volat关键字被volatile修饰的变量编译器会禁止上述优化能保证每次都是从内存中重新读取数据。使用volatile修饰flag volatile public static int flag 0;当我们把上述代码修改成以下代码时t1线程也能结束public class ThreadDemo2 {public static int flag 0;public static void main(String[] args) {Thread t1 new Thread(() - {while(flag 0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(t1 循环结束啦啦啦);});Thread t2 new Thread(() - {Scanner sc new Scanner(System.in);System.out.println(请输入一个整数);flag sc.nextInt();});t1.start();t2.start();}
}加了sleep循环执行速度就慢下来了此时load操作就不再是负担了编译器就没必要优化了。7.3 指令重排序引起的线程不安全volatile还有另外的一个作用——禁止指令重排序。指令重排序也是编译器优化的策略在保证整体逻辑不变的情况下其调整了代码的执行顺序让程序更高效。写一个伪代码:class Student{ ......}public class Test{ Student s null; t1: s new Student(); t2: if(s ! null){ s.learn(); }}new 一个Student的对象大体可以分为三步操作1. 申请内存空间 2. 调用构造方法初始化内存数据3. 把对象的引用赋值给s内存地址的赋值上述代码就可能会因为指令重排序出现问题假设t1按照 1 3 2 的顺序执行。如果t1执行完1 3 之后t2就开始执行了此时s这个引用就非空了当t2调用s.learn()时由于s指向的内存还未初始化很有可能这里就会产生bug因此解决方法要么是加锁要么就使用volatile修饰8. wait 和 notify 实际开发中有时我们会希望合理协调多个线程之间的执行先后顺序。wait会做的事释放当前锁让当前执行代码的线程阻塞等待满足一定条件被唤醒时重新尝试获取锁由此可知wait要搭配synchronized来使用否则没有锁后面又如何释放锁单独使用wait编译器会直接抛出异常notify会做的事只有一个单线程处于wait状态下notify会将其唤醒并使其重新获取该对象的对象锁如果有多个线程等待cpu调度会随机选一个处于wait状态下的线程唤醒notify也是与synchronized搭配使用调用notify方法后当前线程并不会立马释放锁要等到执行notify方法的线程全部执行完了之后才会释放对象锁另外wait和notify都是Object的方法只要是个类对象不是基本数据类型都可以调用wait和notify。public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException{Object locker new Object();Thread t1 new Thread(() - {try {System.out.println(wait 开始);synchronized (locker) {locker.wait();System.out.println(wait 再次加锁了~~~~~);}System.out.println(wait 结束);} catch (InterruptedException e) {e.printStackTrace();}});t1.start();Thread.sleep(1000);Thread t2 new Thread(() - {synchronized (locker){System.out.println(notify 开始);locker.notify();System.out.println(notify 结束);}});t2.start();}
}输出 wait 开始 notify 开始 notify 结束 wait 再次加锁了~~~~~ wait 结束与notify有一样唤醒功能的还有一个notifyAll。使用notifyAll方法可以一次唤醒所有等待线程。此时所有被唤醒的线程又要开始新一轮的竞争锁9. 多线程案例9.1 单例模式啥是设计模式? 设计模式好比象棋中的 棋谱。软件开发中也有很多常见的 问题场景. 针对这些问题场景, 大佬们总结出了一些固定的套路。按照 这个套路来实现代码, 也不会吃亏。 单例模式是校招中最常考的设计模式之一。单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。单例模式具体的实现方式, 分成 饿汉 和 懒汉 两种。对于这两种模式举一个这样的例子打开硬盘上的文件读取文件内容并显示出来。饿汉把文件所有内容都读到内存中并显示懒汉只读取把当前屏幕填充满的一小部分文件。如果用户要翻页那就再读取剩下内容如果用户不继续看直接就省下了。试想一下如果这个文件非常的大100g。饿汉模式的打开文件就要费半天但懒汉却一下子就打开了。下面给出单线程环境下java实现单例模式的代码//饿汉模式——单线程
class Singleton{//唯一的一个实例private static Singleton instance new Singleton();//获取实例的方法public static Singleton getInstance(){return instance;}//禁止外部 new 实例private Singleton(){}
}
public class ThreadDemo2 {public static void main(String[] args) {Singleton s1 Singleton.getInstance();Singleton s2 Singleton.getInstance();System.out.println(s1 s2);}
}在类内部把实例创建好同时禁止外部创建实例这样就可以保证单例的特性了。上述s1和s2指向同一个对象。而懒汉模式实现单例其核心思想是非必要不创建那么就能先写出以下代码//懒汉模式————实现单例模式
//单线程环境
class SingletonLazy{private static SingletonLazy instance null;public static SingletonLazy getInstance(){if(instance null){instance new SingletonLazy();}return instance;}private SingletonLazy(){}
} 这里提出一个问题上述两个代码是否线程安全在多线程环境下调用getInstance是否会出问题对于饿汉模式线程是安全的因为程序是指单纯的 读 操作没有修改。但是对于懒汉模式多线程下根本无法保证创建对象的唯一性如果对象管理的内存数据太大了比如100gn个线程那就得加载100 * n G到内存中那影响可就大了。怎么解决这个问题呢根据前面所学答案就是加锁了//懒汉模式————实现单例模式
//多线程环境
class SingletonLazy{private static SingletonLazy instance null;public static SingletonLazy getInstance(){synchronized (SingletonLazy.class){if(instance null){instance new SingletonLazy();}}return instance;}private SingletonLazy(){}
}把锁加在能使 判定 与new一个对象是原子性的。但如此一来每一个调用getInstance的线程都得锁竞争这样会让程序效率变得极为低下。其实前面所说的线程不安全只出现在首次创建对象时。一旦对象创建好了后续调用getInstance就只是单纯的 读 操作也就没有线程安全问题了就没必要再加锁了所以最好在进入锁之前再来一次判断//懒汉模式————实现单例模式
//多线程环境
class SingletonLazy{private static SingletonLazy instance null;public static SingletonLazy getInstance(){// 这个条件, 判定是否要加锁. 如果对象已经有了, 就不必加锁了, 此时本身就是线程安全的.if(instance null){synchronized (SingletonLazy.class){if(instance null){instance new SingletonLazy();}}}return instance;}private SingletonLazy(){}
}懒汉模式的代码修改到这就完美无暇了吗NoNoNo!上述代码还可能会发生一个极端的小概率情况——指令重排序要知道new一个对象其实是三步cpu指令申请内存空间调用构造方法初始化变量引用指向内存地址如果两个线程同时调用getInstance方法t1线程发生指令重排序执行了1和3之后系统调度给了t2来到判定条件发现instance非空此时条件不成立直接返回实例的引用。如果t2继续调用而instance所指向的内存空间并未初始化那就会产生其他问题了所以为了保险起见杜绝指令重排序的情况发生最好给instance加上volatilevolatile private static SingletonLazy instance null;9.2 阻塞队列阻塞队列是一种特殊的队列也遵循“先进先出”的原则。阻塞队列也是一种线程安全的数据结构具有以下特性当队列为满的时候继续入队列的操作就会阻塞等待直到其他线程从队列中取走元素当队列为空时继续出队列的操作也会阻塞等到直到其他线程往队列里插入元素。阻塞队列非常有用尤其是在写多线程代码多个线程之间进行数据交互的时候就可以使用阻塞队列来简化代码的编写阻塞队列的一个典型应用场景就是“生产者消费者模型”。这是一种非常典型的开发模型。9.2.1 介绍生产者消费者模型在介绍生产者消费者模型之前先了解以下两个概念耦合描述两个模块之间的关联性关联性越强耦合越高关联性越差耦合越低。写代码追求低耦合避免代码牵一发而动全身内聚高内聚指的是将代码分门别类相关联的放在一起想找就会特别容易。生产者消费者模型可以解决很多问题但最主要的是以下两方面可以让上下游模块更好的“解耦合”——高内聚低耦合。考虑以下场景服务器A向服务器B发出请求服务器B给A响应的过程中如果B挂了此时会直接影响AA也就会跟着挂这就是高耦合。如果引入生产者消费者模型耦合就降低了。阻塞队列与业务无关代码不大会变化更加稳定而AB是业务服务器与业务相关需要随时修改以便支持新的功能也因此更容易出问题。如果A与B利用中间的阻塞队列来进行通信那么当B出问题时A完全不受影响。削峰填谷如果服务器A和服务器B是直接通信的那么当A收到了用户的请求峰值B也会同样收到来自A的请求峰值。要知道服务器处理每个请求都需要消耗硬盘资源包括但不限于(CPU、内存、硬盘、带宽......)。如果某个硬盘资源使用达到了上限的同时服务器B的设计并没有考虑到峰值的处理此时服务器B可能就挂了这就给业务的稳定性带来了极大的风险。但如果A与B之间通过阻塞队列来进行通信那么当A收到的请求多了时阻塞队列里的元素也跟着多起来此时B却可以按照平时的速率来接收请求并返回响应。这里就可以看出阻塞队列帮服务器B承担了压力。9.2.2 代码实现1模拟实现一个阻塞队列//循环队列
public class MyBlockingQueue {private int[] nums new int[100];volatile private int head 0;volatile private int tail 0;volatile private int size 0;//入队列synchronized public void put(int elem) throws InterruptedException{//如果队列满了阻塞等待if(size nums.length){this.wait();}//如果队列未满则可以继续添加nums[tail] elem;tail;//检查一下元素添加的位置是否未数组的最后一个if(tail nums.length){tail 0;}size;//如果别的线程要取元素发现数组为空wait阻塞等待了以下代码唤醒//如果并没有别的线程阻塞等待那么以下代码也没有任何副作用this.notify();}//出队列synchronized public int take() throws InterruptedException{//如果队列为空则阻塞等待if(size 0){this.wait();}//如果队列不为空那么就可以放心取元素了int value nums[head];head;//检查head是否已经来到数组的最后一个下标1if(head nums.length){head 0;}size--;this.notify();return value;}
}上述代码的notify是交叉唤醒wait的需要注意的是除了notify()能唤醒wait()之外如果其他线程中调用了interrupt方法就会提前唤醒wait此时代码就会继续往下走那对于空队列来说再取元素size就成了-1了这就造成很大的问题了。所以最好wait被唤醒的时候再判断一次是否满足条件即将if改成while就好了//循环队列
public class MyBlockingQueue {private int[] nums new int[100];volatile private int head 0;volatile private int tail 0;volatile private int size 0;//入队列synchronized public void put(int elem) throws InterruptedException{//如果队列满了阻塞等待while(size nums.length){this.wait();}//如果队列未满则可以继续添加nums[tail] elem;tail;//检查一下元素添加的位置是否未数组的最后一个if(tail nums.length){tail 0;}size;//如果别的线程要取元素发现数组为空wait阻塞等待了以下代码唤醒//如果并没有别的线程阻塞等待那么以下代码也没有任何副作用this.notify();}//出队列synchronized public int take() throws InterruptedException{//如果队列为空则阻塞等待while(size 0){this.wait();}//如果队列不为空那么就可以放心取元素了int value nums[head];head;//检查head是否已经来到数组的最后一个下标1if(head nums.length){head 0;}size--;this.notify();return value;}
}9.2.3 代码实现2模拟实现生产者消费者模型public class ThreadDemo2 {public static void main(String[] args) {MyBlockingQueue queue new MyBlockingQueue();//消费者Thread t1 new Thread(() - {while(true){try {int value queue.take();System.out.println(消费者: value);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});//生产者Thread t2 new Thread(() - {int value 0;while(true){try {System.out.println(生产 value);queue.put(value);Thread.sleep(2000);value;} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();}
}上述代码中生产者的生产速度远比消费者消费速度要慢生产限制消费输出生产0消费者: 0生产1消费者: 1生产2消费者: 2生产3消费者: 3生产4消费者: 4.......如果生产速度远高于消费速度又会怎么样呢输出生产0消费者: 0生产1生产2......生产31消费者: 1生产32......生产63消费者: 2生产64......生产95消费者: 3生产96......生产104消费者: 4生产105消费者: 5生产106消费者: 6.......到后面生产满了就得等消费1个才能生产1个了。9.3 定时器定时器是实际开发中一个非常常用的组件,类似于一个 闹钟,达到一个设定的时间之后, 就执行某个指定 好的代码。比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连。再比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除)。9.3.1 标准库中的定时器标准库中提供了一个 Timer 类Timer 类的核心方法为 schedule。 schedule 包含两个参数第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒)。 import java.util.Timer;
import java.util.TimerTask;public class ThreadDemo1 {public static void main(String[] args) {Timer timer new Timer();timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(hello hello hello hello♥);}},4000);timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(hello hello hello♥);}},3000);timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(hello hello♥);}},2000);timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(hello♥);}},1000);System.out.println(♥);}
}输出♥hello♥hello hello♥hello hello hello♥hello hello hello hello♥此时的程序并没有执行完毕。那是因为Timer内置了线程而且是个前台线程会阻止进程结束9.3.2 模拟实现一个定时器同学们要想对定时器有更加深刻的了解我们最好模拟实现一下定时器。由上一节的代码可以看出定时器内部不仅仅管理一个任务还可以管理多个任务虽然任务有很多但它们的触发时间是不同的。只需要一个/一组工作线程每次找到这些任务中最先到达时间的任务执行完之后再去执行下一个到达时间的任务也可以是阻塞等待下一个到达时间的任务。这也就意味着定时器的核心数据结构是堆如果希望在多线程环境下优先级队列还能线程安全Java集合框架中提供了PriorityBlockingQueue即带优先级的阻塞队列。import java.util.concurrent.PriorityBlockingQueue;class MyTask implements ComparableMyTask{public Runnable runnable;public long time;//构造方法//delay 单位 毫秒public MyTask(Runnable runnable, long delay){this.runnable runnable;time System.currentTimeMillis() delay;}Override//优先级队列存放MyTask的对象是需要比较方法的public int compareTo(MyTask o) {return (int)(this.time - o.time);}
}class MyTimer {private PriorityBlockingQueueMyTask queue new PriorityBlockingQueue();private Object locker new Object();public void schedule(Runnable runnable, long delay){MyTask myTask new MyTask(runnable,delay);queue.put(myTask);//如果新创建的任务等待时间比之前的任何一个任务都要短那么这里唤醒wait线程继续往下走的时候//取的就是这个新加入的任务了、synchronized (locker){locker.notify();}}// 在这里构造线程, 负责执行具体任务了.public MyTimer(){Thread t1 new Thread(() - {while(true){try {synchronized (locker){MyTask myTask queue.take();long currentTime System.currentTimeMillis();if(myTask.time currentTime){myTask.runnable.run();}else{//时间还没到把拿出来的任务再塞回去queue.put(myTask);//由于这是个循环再塞回去之后下一个循环又再取出来但时间依旧还有好久才到//这会让cpu在等待的时间里反复取任务又反复塞回去忙等//所以我们最好让线程在这个时间里不再参与cpu调度locker.wait(myTask.time - currentTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();}
}public class ThreadDemo2 {public static void main(String[] args) {MyTimer myTimer new MyTimer();myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(♥♥♥♥♥);}},4000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(♥♥♥♥);}},3000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(♥♥♥);}},2000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(♥♥);}},1000);System.out.println(♥);}}输出♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥这小节的最后提出这样一个问题:加锁的位置改成这样会出现什么问题呢......public MyTimer(){Thread t1 new Thread(() - {while(true){try {MyTask myTask queue.take();long currentTime System.currentTimeMillis();if(myTask.time currentTime){myTask.runnable.run();}else{//时间还没到把拿出来的任务再塞回去queue.put(myTask);//由于这是个循环再塞回去之后下一个循环又再取出来但时间依旧还有好久才到//这会让cpu在等待的时间里反复取任务又反复塞回去忙等//所以我们最好让线程在这个时间里不再参与cpu调度synchronized (locker){locker.wait(myTask.time - currentTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();}
}
......每一次take拿的的任务一定是所有任务中到达时间最短的。如果把锁加到上述位置就让take与wait不是原子性的。假设take之后拿到了一个任务执行时间1430而当前时间为1400在还没有走到wait之前主线程main又创建了一个新的任务1410分的此时notify就没有对应的wait可以唤醒。新任务创建成功之后之前的线程继续往下走就得干等30分而不是等10分之后去执行1410分的那个任务这就使得最先到达时间的任务不能及时执行。9.4 线程池 线程的创建虽然比进程的创建要轻量但在频繁创建的情况下开销也是不可忽视的 为了进一步减少每次启动、销毁线程的损耗,人们引进了线程池。线程池就是提前把线程创建好后续需要创建线程时不再向系统申请而是直接从线程池中拿最后用完了再把线程还给线程池。为什么线程池就比直接创建线程效率更高呢要知道从系统中创建线程是在内核态里完成的。但对于内核来说创建线程不是它唯一的任务它可能先去完成别的任务再继续创建线程。再者创建线程还涉及到用户态与内核态之间的切换而从线程池里拿一个线程只涉及到了用户态。给一个形象一点的例子为了打印一份文件你来到了打印店。内核就像是打印店里的工作人员你让他帮你打印时他有时空闲能直接帮你打印有时你需要排队等候甚至有时工作人员在忙自己的事情你就得等他忙完了再帮你打印——时间不可控而用户态操作更像是打印店里的自助打印机你来到店里直接打印完事——时间可控。9.4.1 标准库里提供的线程池类public class ThreadDemo2 {public static void main(String[] args) {//并没直接 new ExecutorService 来实例化一个对象//而是通过 Executors 里的静态方法完成对象构造ExecutorService pool Executors.newFixedThreadPool(2);//线程池里有个线程pool.submit(new Runnable() {Overridepublic void run() {while(true){System.out.println(♥♥♥♥♥♥♥♥♥);}}});pool.submit(new Runnable() {Overridepublic void run() {while(true){System.out.println(♥♥⭐♥♥);}}});}
}输出......♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥⭐♥♥♥♥⭐♥♥.......♥♥⭐♥♥♥♥⭐♥♥♥♥⭐♥♥♥♥⭐♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥......9.4.2 模拟实现一个线程池import java.util.*;
import java.util.concurrent.*;class MyThreadPool{//用阻塞队列来存放任务private BlockingDequeRunnable queue new LinkedBlockingDeque();public void summit(Runnable runnable) throws InterruptedException{queue.put(runnable);}public MyThreadPool(int n){for (int i 0; i n; i) {Thread t1 new Thread(() - {while(true){try {Runnable runnable queue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();System.out.println(创建线程 i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException{MyThreadPool pool new MyThreadPool(10);Thread.sleep(12000);for (int i 0; i 1000; i) {int number i;pool.summit(new Runnable() {Overridepublic void run() {System.out.println(hello number);}});}}
}输出创建线程 0创建线程 1创建线程 2创建线程 3创建线程 4创建线程 5创建线程 6创建线程 7创建线程 8创建线程 9hello 0hello 1hello 4hello 5........需要注意的是ThreadDemo1类中匿名内部类遵循变量捕获规则每次循环修改number的值就相当于number没有被修改。10 面试题进程与线程的区别wait和sleep的区别不同点wait需要搭配synchronized使用而sleep不需要wait是Object的方法而sleep是Thread的静态方法相同点都可以让线程放弃执行一段时间多线程环境下懒汉模式如何保证线程安全加锁把if和new变成原子操作双重if减少不必要的加锁操作使用volatile禁止指令重排序保证后续线程一定拿到完整的对象
文章转载自: http://www.morning.kbkcl.cn.gov.cn.kbkcl.cn http://www.morning.zqwp.cn.gov.cn.zqwp.cn http://www.morning.ltpmy.cn.gov.cn.ltpmy.cn http://www.morning.sqmbb.cn.gov.cn.sqmbb.cn http://www.morning.xylxm.cn.gov.cn.xylxm.cn http://www.morning.iiunion.com.gov.cn.iiunion.com http://www.morning.gqbtw.cn.gov.cn.gqbtw.cn http://www.morning.hcbky.cn.gov.cn.hcbky.cn http://www.morning.zbtfz.cn.gov.cn.zbtfz.cn http://www.morning.ghcfx.cn.gov.cn.ghcfx.cn http://www.morning.xdqrz.cn.gov.cn.xdqrz.cn http://www.morning.mcjp.cn.gov.cn.mcjp.cn http://www.morning.kqlrl.cn.gov.cn.kqlrl.cn http://www.morning.btcgq.cn.gov.cn.btcgq.cn http://www.morning.lznfl.cn.gov.cn.lznfl.cn http://www.morning.hdrsr.cn.gov.cn.hdrsr.cn http://www.morning.nwzcf.cn.gov.cn.nwzcf.cn http://www.morning.gfqj.cn.gov.cn.gfqj.cn http://www.morning.lkthj.cn.gov.cn.lkthj.cn http://www.morning.pjyrl.cn.gov.cn.pjyrl.cn http://www.morning.kzslk.cn.gov.cn.kzslk.cn http://www.morning.gnbtp.cn.gov.cn.gnbtp.cn http://www.morning.qpmmg.cn.gov.cn.qpmmg.cn http://www.morning.qxlxs.cn.gov.cn.qxlxs.cn http://www.morning.nxbsq.cn.gov.cn.nxbsq.cn http://www.morning.rbzd.cn.gov.cn.rbzd.cn http://www.morning.lgtcg.cn.gov.cn.lgtcg.cn http://www.morning.etsaf.com.gov.cn.etsaf.com http://www.morning.bxfy.cn.gov.cn.bxfy.cn http://www.morning.gpryk.cn.gov.cn.gpryk.cn http://www.morning.zffps.cn.gov.cn.zffps.cn http://www.morning.rpkl.cn.gov.cn.rpkl.cn http://www.morning.ggrzk.cn.gov.cn.ggrzk.cn http://www.morning.rkdhh.cn.gov.cn.rkdhh.cn http://www.morning.xnhnl.cn.gov.cn.xnhnl.cn http://www.morning.xpgwz.cn.gov.cn.xpgwz.cn http://www.morning.qbdqc.cn.gov.cn.qbdqc.cn http://www.morning.xpzrx.cn.gov.cn.xpzrx.cn http://www.morning.qbdqc.cn.gov.cn.qbdqc.cn http://www.morning.qmwzz.cn.gov.cn.qmwzz.cn http://www.morning.nbnpb.cn.gov.cn.nbnpb.cn http://www.morning.klzdy.cn.gov.cn.klzdy.cn http://www.morning.kgfsz.cn.gov.cn.kgfsz.cn http://www.morning.nfzzf.cn.gov.cn.nfzzf.cn http://www.morning.clybn.cn.gov.cn.clybn.cn http://www.morning.tnbas.com.gov.cn.tnbas.com http://www.morning.qrpx.cn.gov.cn.qrpx.cn http://www.morning.guanszz.com.gov.cn.guanszz.com http://www.morning.yrgb.cn.gov.cn.yrgb.cn http://www.morning.gbrdx.cn.gov.cn.gbrdx.cn http://www.morning.dpdr.cn.gov.cn.dpdr.cn http://www.morning.zxqxx.cn.gov.cn.zxqxx.cn http://www.morning.sbpt.cn.gov.cn.sbpt.cn http://www.morning.hwxxh.cn.gov.cn.hwxxh.cn http://www.morning.trtdg.cn.gov.cn.trtdg.cn http://www.morning.twmp.cn.gov.cn.twmp.cn http://www.morning.jwxmn.cn.gov.cn.jwxmn.cn http://www.morning.rzbgn.cn.gov.cn.rzbgn.cn http://www.morning.tdzxy.cn.gov.cn.tdzxy.cn http://www.morning.yxwrr.cn.gov.cn.yxwrr.cn http://www.morning.mnbcj.cn.gov.cn.mnbcj.cn http://www.morning.qqhfc.cn.gov.cn.qqhfc.cn http://www.morning.lgznf.cn.gov.cn.lgznf.cn http://www.morning.ntgsg.cn.gov.cn.ntgsg.cn http://www.morning.fnhxp.cn.gov.cn.fnhxp.cn http://www.morning.jwfkk.cn.gov.cn.jwfkk.cn http://www.morning.hhnhb.cn.gov.cn.hhnhb.cn http://www.morning.qnbck.cn.gov.cn.qnbck.cn http://www.morning.lwrks.cn.gov.cn.lwrks.cn http://www.morning.klwxh.cn.gov.cn.klwxh.cn http://www.morning.tkqzr.cn.gov.cn.tkqzr.cn http://www.morning.nrjr.cn.gov.cn.nrjr.cn http://www.morning.rftk.cn.gov.cn.rftk.cn http://www.morning.ynjhk.cn.gov.cn.ynjhk.cn http://www.morning.xyjlh.cn.gov.cn.xyjlh.cn http://www.morning.jypqx.cn.gov.cn.jypqx.cn http://www.morning.zgqysw.cn.gov.cn.zgqysw.cn http://www.morning.jqswf.cn.gov.cn.jqswf.cn http://www.morning.rfwgg.cn.gov.cn.rfwgg.cn http://www.morning.bxrlt.cn.gov.cn.bxrlt.cn