阶段一:Java多线程基础知识
1、线程介绍
进程、线程、单核CPU和多核、协程
2、创建并启动线程
我们现在想一个场景:这里我们尝试并发的做一些事情,读数据库的时候写磁盘,当然我们这里不会真正的读数据库和写磁盘,只是模拟。
package com.wangwenjun.concurrency.chapter1; /** * 这里我们尝试并发的做一些事情 * 读数据库的时候写磁盘,当然我们这里不会真正的读数据库和写磁盘,只是模拟 **/ public class TryConcurrency01 { public static void main(String[] args) { readFromDataBase(); writeDataToFile(); } private static void readFromDataBase() { // read data from database and handle it. try { println("Begin read data from db"); Thread.sleep(1000 * 10L); println("Read data done and start handle it."); } catch (InterruptedException e) { e.printStackTrace(); } println("The data handle finish and successfully."); } private static void writeDataToFile() { // read data from database and handle it. try { println("Begin write data to file"); Thread.sleep(2000 * 10L); println("Write data done and start handle it."); } catch (InterruptedException e) { e.printStackTrace(); } println("The data handle finish and successfully."); } private static void println(String message) { System.out.println(message); } }
我们发现上面的代码是无法进行并行操作的。
我们再举一个例子:
package com.wangwenjun.concurrency.chapter1; public class TryConcurrency01 { public static void main(String[] args) { // 第二个例子(这两个for循环也是顺序执行,不会交替执行,同时执行) for (int i = 0; i < 100; i++) { println("Tash 1=>" + i); } for (int j = 0; j < 100; j++) { println("Tash 2=>" + j); } } private static void println(String message) { System.out.println(message); } }
同样地:这两行for循环代码也无法同时输出。
那我们怎么用并行来处理上面的问题呢?就要用到线程。我们这里先用Thread类来创建。
Thread类如下:public class Thread implements Runnable
这里又两种方式:用匿名内部类;新建一个类继承Thread并重写run方法。
对于上面的并发两个for循环,如下:
用匿名内部类实现(此时这里面有两个线程:Custom-Thread和Main线程)
package com.wangwenjun.concurrency.chapter1; public class TryConcurrency01 { public static void main(String[] args) { Thread t1 = new Thread("Custom-Thread") { @Override public void run() { for (int i = 0; i < 100; i++) { println("Tash 1=>" + i); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t1.start(); for (int j = 0; j < 100; j++) { println("Tash 2=>" + j); try { Thread.sleep(1000 *10L); } catch (InterruptedException e) { e.printStackTrace(); } } } private static void println(String message) { System.out.println(message); } }
用新建类实现(继承Thread并重写run方法)
package com.wangwenjun.concurrency.chapter1; public class TryConcurrency01 { public static void main(String[] args) { new MyThread01().start(); for (int j = 0; j < 100; j++) { println("Tash 2=>" + j); try { Thread.sleep(1000 *10L); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void println(String message) { System.out.println(message); } } class MyThread01 extends Thread { @Override public void run() { for (int j = 0; j < 100; j++) { TryConcurrency01.println("Tash 1=>" + j); try { Thread.sleep(1000 *10L); } catch (InterruptedException e) { e.printStackTrace(); } } } }
那么我们现在用并行实现下数据从数据库读和写入磁盘(此时里面是三个线程,包含Main线程)
package com.wangwenjun.concurrency.chapter1; /** * 这里我们尝试并发的做一些事情 * 读数据库的时候写磁盘,当然我们这里不会真正的读数据库和写磁盘,只是模拟 **/ public class TryConcurrency { public static void main(String[] args) { new Thread("READ-THREAD") { @Override public void run() { readFromDataBase(); } }.start(); new Thread("READ-THREAD") { @Override public void run() { writeDataToFile(); } }.start(); } private static void readFromDataBase() { // read data from database and handle it. try { println("Begin read data from db"); Thread.sleep(1000 * 10L); println("Read data done and start handle it."); } catch (InterruptedException e) { e.printStackTrace(); } println("The data handle finish and successfully."); } private static void writeDataToFile() { // read data from database and handle it. try { println("Begin write data to file"); Thread.sleep(2000 * 10L); println("Write data done and start handle it."); } catch (InterruptedException e) { e.printStackTrace(); } println("The data handle finish and successfully."); } private static void println(String message) { System.out.println(message); } }
3、线程的生命周期
其实我们在上面的案例中用java自带的线程监控界面(命令是cmd下jps查看进程ID和jconsole打开监控界面)看过,线程是会消亡的。
线程的生命周期如下图所示:
当线程创建时,即new Thread时,就进入了new状态;当启用start()方法时,就进入了runnable状态,即可运行状态;此时CPU就看着赏你资源,拿到CPU资源就运行是Running状态;在Running状态可能因为Sleep等原因进入阻塞状态,也有可能因为CPU资源被收回,变回runnable状态,也有可挂了,进入terminated状态;在block状态等阻塞结束会进入runnable状态,注意阻塞结束是不会直接到runnable状态的,其实我在想万一正好给他资源呢??哈哈,不用纠结这个,也可能挂了,进入terminated状态;此外,runnable状态,也可能进入terminated状态。
对于线程周期上面已经说完了,我们来想一个其它问题,我们启动线程为什么调用start方法,而不是run方法??。
https://www.nonelonely.com/article/1559282468236,看下这个链接,笔记我暂时不整理了。
package com.wangwenjun.concurrency.chapter1; /** * @author YCKJ-GaoJT * @create 2020-11-20 9:33 **/ public class TemplateMethod { public final void print(String message) { System.out.println("#############"); wrapPrint(message); System.out.println("#############"); } protected void wrapPrint(String args) { } public static void main(String[] args) { TemplateMethod t1 = new TemplateMethod() { @Override protected void wrapPrint(String message) { System.out.println("*" + message + "*"); } }; t1.print("Hello Thread"); TemplateMethod t2 = new TemplateMethod() { @Override protected void wrapPrint(String message) { System.out.println("+" + message + "+"); } }; t2.print("Hello Thread"); } }
正常情况下:我们会让print声明为final,因为不想让子类继承,继承即可修改里面的代码,可能就不调用wrapPrint方法了;我们会把waroPrint方法声明为protected,这样就只是暴露给子类。
那为什么start和run方法不这么声明,暂时不得而知。
4、Runnable接口介绍
我们现在实现一个场景:银行叫号。假设有三个柜台,票号从1-50,来的人取号排队,超过50就从1重新记号(最后这句话我们就不实现了)。
package com.wangwenjun.concurrency.chapter2; public class TicketWindow extends Thread { private final String name; private final int MAX =50; private int index = 1; public TicketWindow(String name) { this.name = name; } @Override public void run() { while (index <= MAX) { System.out.println("柜台"+ name +"当前的号码是:"+ (index++)); } } }
上面是Thread代码,现在启动线程:
package com.wangwenjun.concurrency.chapter2; public class Bank { public static void main(String[] args) { TicketWindow ticketWindow1 = new TicketWindow("一号柜台"); ticketWindow1.start(); TicketWindow ticketWindow2 = new TicketWindow("二号柜台"); ticketWindow2.start(); TicketWindow ticketWindow3 = new TicketWindow("三号柜台"); ticketWindow3.start(); } }
但是此时你运行会发现一个非常尴尬的事情,就是三个窗口,每个输出都是1-50号票。这和我们的初衷不一致的。那该怎么办??如果想怎么办?要想为什么出现了这样的问题,我们每new一个TicketWindow,里面就会有自己的一份变量index和常量MAX;我们这里创建了三个TicketWindow,那么就是三份,各自在各自的空间执行,互不干扰。那我们处理的关键就是仅此一份,怎么办呢?这两个变量都用static声明,类加载时只此一份,和实例的操作范围已无关。
但是其实上面用static声明后还是有一个问题:线程安全,后面讲解;因为操作了公共数据;表现为可能打出两个1号票。
但是上面用static声明,会有性能问题,因为声明周期,其是根据类的加载而存在,JVM退出(类消亡),而消失。
那还有其他方法吗?想其它方法,就要想上面的问题具体是什么?上面的问题根本是声明线程(这里是声明的子类)和逻辑数据(变量和操作这些变量的run方法)是在一起的。我们要把他们分开,怎么分开呢??你是想不到的,其实就是用runnable接口。
package com.wangwenjun.concurrency.chapter2; public class TicketWindowRunnable implements Runnable { private int index = 0; private final static int MAX = 50; @Override public void run() { while (index <= MAX) { System.out.println(Thread.currentThread() + "的号码是" + (index++)); } } }
下面是启动
package com.wangwenjun.concurrency.chapter2; //逻辑和线程分离,逻辑和线程在不同的object,不同的class里面。 //runnable接口的作用就是将线程单元和逻辑单元分离,这是面向对象思想的一个很好的体现 //这个思想和哪个设计模式很像??后面讲 public class BankWindow2 { public static void main(String[] args) { TicketWindowRunnable ticketWindow = new TicketWindowRunnable(); Thread windowThread1 = new Thread(ticketWindow, "一号窗口"); Thread windowThread2 = new Thread(ticketWindow, "二号窗口"); Thread windowThread3 = new Thread(ticketWindow, "三号窗口"); windowThread1.start(); windowThread2.start(); windowThread3.start(); } }
那么上面这个Runnable采用了什么设计模式呢??
package com.wangwenjun.concurrency.chapter2; /** * @author YCKJ-GaoJT * @create 2020-11-20 10:59 **/ public class TaxCalaculator { private final double salary; private final double bonus; public TaxCalaculator(double salary, double bonus) { this.salary = salary; this.bonus = bonus; } protected double calcTax() { return 0.0d; } public double calculate() { return this.calcTax(); } public double getSalary() { return salary; } public double getBonus() { return bonus; } }
调用:
package com.wangwenjun.concurrency.chapter2; /** * @author YCKJ-GaoJT * @create 2020-11-20 11:01 **/ public class TaxCalculatorMain { public static void main(String[] args) { TaxCalaculator calaculator = new TaxCalaculator(10000d, 2000d) { @Override protected double calcTax() { return getSalary() * 0.1 + getBonus() * 0.15; } }; double tax = calaculator.calcTax(); System.out.println(tax); } }
上面这个其实就是相当于用Thread类重写run方法,并没有用到runnable接口。
下面我们开始用
package com.wangwenjun.concurrency.chapter2; /** * @author YCKJ-GaoJT * @create 2020-11-20 10:59 **/ public class TaxCalaculator { private final double salary; private final double bonus; private CalculatorStrategy calculatorStrategy; public TaxCalaculator(double salary, double bonus) { this.salary = salary; this.bonus = bonus; } protected double calcTax() { return calculatorStrategy.calculate(salary,bonus); } public double calculate() { return this.calcTax(); } public double getSalary() { return salary; } public double getBonus() { return bonus; } public void setCalculatorStrategy(CalculatorStrategy calculatorStrategy) { this.calculatorStrategy = calculatorStrategy; } }
接口定义:
package com.wangwenjun.concurrency.chapter2; @FunctionalInterface public interface CalculatorStrategy { double calculate(double salary, double bonus); }
接口实现:
package com.wangwenjun.concurrency.chapter2; public class SimpleCalculatorStrategy implements CalculatorStrategy { private static final double SALARY_PATE = 0.1; private static final double BONUS_RATE = 0.1; @Override public double calculate(double salary, double bonus) { return salary * SALARY_PATE + bonus * BONUS_RATE; } }
调用:
package com.wangwenjun.concurrency.chapter2; public class TaxCalculatorMain { public static void main(String[] args) { TaxCalaculator calaculator = new TaxCalaculator(10000d, 2000d); SimpleCalculatorStrategy strategy = new SimpleCalculatorStrategy(); calaculator.setCalculatorStrategy(strategy); System.out.println(calaculator.calculate()); } }
其实上面我们可以用lamda简化,还可以进一步简化,在第一个类中,不用设置set接口字段,直接在构造方法中传入,此时调用时再配合lambda表达式就更加方便了。
5、Thread API详细介绍
6、线程同步,锁技术
7、如何优雅的停止线程
8、线程间通讯
9、线程组详细介绍
10、线程池原理以及实现一个简单的线程池
11、线程异常捕获以及线程堆栈信息详细讲解
12、FIFO队列以及线程环境下的运行
13、BoolenLock锁实现
14、常用设计模式在多线程环境下的使用
15、查缺补漏