当前位置: 首页 > news >正文

管城网站建设wordpress 扒皮

管城网站建设,wordpress 扒皮,宜宾市规划建设局网站,广州有什么好玩的室内文章目录 1. 认识线程1.1 什么是进程1.2 什么是线程1.2.1. 线程是怎么做到的呢#xff1f;1.2.2. 进程和线程的关系 1.3 多线程编程1.3.1. 第一个多线程程序1.3.2. 使用 jconsole 命令查看线程1.3.3. 实现 Runnable 接口#xff0c;重写 run1.3.4. 继承 Thread 重写 run… 文章目录 1. 认识线程1.1 什么是进程1.2 什么是线程1.2.1. 线程是怎么做到的呢1.2.2. 进程和线程的关系 1.3 多线程编程1.3.1. 第一个多线程程序1.3.2. 使用 jconsole 命令查看线程1.3.3. 实现 Runnable 接口重写 run1.3.4. 继承 Thread 重写 run并使用匿名内部类1.3.5. 实现 Runnable重写 run匿名内部类1.3.6. 【推荐/常用】使用 lambda 表达式 2. Thread 类及常见方法2.1 Thread 的常见构造方法2.2 Thread 的几个常见属性2.3 启动⼀个线程 - start()关于start 经典面试题 2.4 中断⼀个线程用自定义变量作为标志位使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 2.5 等待⼀个线程 - join()join 的几个方法 2.6 获取当前线程引用2.7 休眠当前线程 3. 线程的状态3.1 观察线程的所有状态 4. 多线程带来的的风险 - 线程安全4.1 线程安全的概念4.2 线程不安全的例子4.3 线程不安全的原因4.4 加锁 - synchronized4.4.1 用 synchronized 关键字进行加锁4.4.2 synchronized 的特性 在Java 开发中我们并不鼓励“多线程编译”因此线程就更加重要了 1. 认识线程 1.1 什么是进程 在多任务操作系统中我们希望程序能够同时巡行多个程序 不过如果是单任务的程序完全不涉及进程也不需要调度 本质上来说进程解决“并发编程”这样的问题的 事实上进程是可以很好的解决并发编程这样的问题的 再一些特定的情况下进程的表现不是尽人意的 比如有些场景下需要频繁的创建和销毁进程的过程此时使用多进程编程系统开销就会很大其中最关键的原因就是资源的申请和释放 进程是资源CPU硬盘内存网络带宽…分配的基本单位 一个进程更高启动的时候吗首当其冲的就是内存资源进程需要把依赖的代码和数据从磁盘加载到内存中 1.2 什么是线程 线程 就是解决上述问题的方案 线程也可以称为“轻量级进程”在进程的基础上做出了改进 即保持了独立调度执行这样的“并发支持”同时省去“分配资源”“释放资源”带来的额外开销 1.2.1. 线程是怎么做到的呢 前面介绍了会使用PCB 来描述一个进程 现在也是用PCB来描述一个线程 PCB中有个属性是内存指针 多线程的PCB 的内存指针指向的是同一个内存空间 这样就意味着只是创建第一个线程的时候需要从系统分配资源后续的线程就不必分配直接共用前面的那份资源就可以了 除了内存之外文件描述表操作硬盘这个东西也是多个线程共用一份的 操作系统进行“多任务调度”本质上是在调度PCB线程在系统中的调度规则就和之前的进程是一样的 但是也不是随便搞两个线程节能资源共享 把能够资源共享的这些线程分成组称为“线程组”。换句话将线程组也就是进程的一部分 每个进程都可与包含一个线程或者多个线程 1.2.2. 进程和线程的关系 有线程之前进程需要扮演两个角色资源分配的基本单位也是调度执行的基本单位 有了线程之后就把这两个角色分开了 进程专注于资源分配 线程扶着调度执行 在创建进程资源就分配了只不过一个进程中至少包含一个线程创建第一个线程的同时进程也就出来了 进程和线程的关系/区别是非常经典、非常高频的面试题 进程是包含线程的每个线程也是一个独立的执行流可以只想一些代码并且单独的参与到cpu 调度中状态、上下文、优先级、记账信息每个线程有自己的一份每个进程有自己的资源进程中的线程共用这一份资源内存空间 和 文件描述符表 进程是资源分配的基本单位线程是调度执行的基本单位 进程和进程之间不会相互影响。如果同一个进程中的某个线程抛出异常是可能会影响到其他线程会把整个进程中的所有线程都异常终止 同一个进程中的线程之间可能会相互干扰引起线程安全问题 线程也不是越多越好要能够合适。如果能够线程太多了调度开销可能非常明显 1.3 多线程编程 写代码的时候可以使用多进程进行并发编程也可以使用多线程并发编程 但是在java 中并不推荐多进程开发很多和多进程编程相关的api 在 java 标准库中都没有提供 可是系统提供了多线程编程的api java 变转库把这些api封装了在代码中就可以使用了 1.3.1. 第一个多线程程序 Java 提供了 apiThread这样的类 第一种方法就是继承 Thread 重写 run package thread;//1、创建一个自己的类继承自这个 Thread class MyThread extends Thread {//这个类好像直接可以使用不需要导入包//Java 标准库中有一个特殊的包java.lang 这个可以直接使用//class 前面不可以加public 因为一个java文件中只能有一个public 的类//这个类如果没有public 包级作用域就只能在当前包里被其他的类使用Override//方法重写本质上是让你能够对现有的类进行扩展/*** 目前需要写一个线程肯定需要让这个线程执行一些代码* Thread 类本身会带有一个run入口方法* 很明显标准库自带的run是不知道你的需求 业务是什么样的 必须手动指定* 因此就可以针对原有的Thread进行扩展* (把一些能复用的进行了重用需要扩展的进行扩展)* Thread会有很多属性方法大部分内容都复用即可* 只是把需要扩展的这个进行扩展即可*/public void run() {//run 方法就是该线程的入口方法//就类似于main方法是一个java进程(程序)的入口方法/*** 在以后的学习中一般把跑起来的程序称为进程没有运行起来的程序(exe)称为”可执行文件“* 一个进程中至少会有一个线程* 这个进程中的第一个线程也就称为“主线程”* main方法也就是主线程的入口方法*///此处的run方法不需要程序员手动调用会在合适的时机(线程创建好了之后)被jvm 自动调用/*** 这种风格的函数称为“回调函数”(callback)* 回调函数是编程中非常重要的函数* 优先级队列 PriorityQueue* 指定比较规则* Comparable 和 Comparator* 自己和别人比 你妈妈拿你和别人家的孩子比* 如果使用Comparable意味着你这个类只能有一种比较规则毕竟一个类只能实现一次Comparable。这种写法对类侵入性比较强* 使用Comparator意味着可以有对重比较规则** compareTo 和 compare 这俩方法就属于“回调函数”*/System.out.println(hello world);} }public class ThreadDemo1 {public static void main(String[] args) {//2、根据刚在线程的类创建出实例(线程实例才是真正的线程)Thread myThread new MyThread();//3、调用 Thread 的 start 方法才会真正调用系统api在系统内核中创建出线程myThread.start();//虽然没有手动调用run但是run还是执行了这就是jvm自动调用了//虽然看起来跟以前没有什么区别但是当引入多线程之后代码就可以同时具备多个执行流了} }多线程 class MyThread2 extends Thread {Overridepublic void run() {while (true) {System.out.println(hello world);}} }public class TreadDemo2 {public static void main(String[] args) {Thread thread new MyThread2();thread.start();/*** 此处调用start 创建线程之后 兵分两路* 一路沿着 main 方法继续执行打印 hello main* 另一路进入线程的 run 方法打印 hello world** 注意* 当有多个线程的时候 这些线程执行的先后顺序是不确定的* 这一点是因为操作系统内核中有一个“调度器模块这个模块的视线方式是一种类似于”随机调度“效果** 什么叫做“随机调度”* 1.一个线程什么时候被调度到cpu上执行时机是不确定的* 2.一个线程什么时候从cpu上下来给别人让位时机也是不确定的* 这叫做“抢占式执行” 当前的主流操作系统都是抢占式执行**/while (true) {System.out.println(hello main);}} }真正运行程序可以看到两个程序都在执行 由于程序死循环对电脑cpu功耗太大 可以在循环中加上sleep 进行休眠 class MyThread2 extends Thread {Overridepublic void run() {while (true) {System.out.println(hello world);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} }public class TreadDemo2 {public static void main(String[] args) throws InterruptedException {Thread thread new MyThread2();thread.start();while (true) {System.out.println(hello main);Thread.sleep(1000);}} }1.3.2. 使用 jconsole 命令查看线程 第一个main 就是main方法对应的主线程 Thread-0 就是自己写的代码创建的thread 线程 其余的线程都是JVM自带的线程来完成垃圾回收监控统计各种指标把统计指标通过网络的方式传输给其他程序 线程的调用栈线程里当前执行到了那个方法的第几行代码了这个方法是如何一层一层调用过去的 1.3.3. 实现 Runnable 接口重写 run package thread;class MyThread3 implements Runnable {/*** Runnable 可以解释成“可执行的”* 通过这个接口就可以抽象出一段可以被其他实体来执行的代码* 这个Runnable 不仅仅可以搭配线程来执行*/Overridepublic void run() {while (true) {System.out.println(hello runnable);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} }public class ThreadDemo3 {public static void main(String[] args) {Runnable runnable new MyThread3();/*** 只是一段可以执行的代码* 还是需要Thread 类才能真正在系统中创建出线程* 这种写法 其实就是把 线程和要执行的任务 进行了 解耦合*/Thread t new Thread(runnable);t.start();while (true) {System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} } 1.3.4. 继承 Thread 重写 run并使用匿名内部类 匿名内部类是在一个类里面定义的类 package thread;public class ThreadDemo4 {public static void main(String[] args) {Thread t new Thread(){/*** 写{} 意思是要定义一个类 于此同时这个新的类继承与Thread* 与此同时这个新的类继承自Thread* 此处{} 中可以定义子类的属性和方法* 此处最重要的目的就是重写run方法*//*** 此处的 t 并非单纯的Thread* 而是Thread 的子类* 与此同时这个代码还创建了子类的实例*/Overridepublic void run() {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();while (true) {System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} }1.3.5. 实现 Runnable重写 run匿名内部类 package thread;public class ThreadDome5 {public static void main(String[] args) {Thread t new Thread(new Runnable() {//Thread 构造方法的参数填写了Runnable 的匿名内部类的实例Overridepublic void run() {while (true) {System.out.println(hello runnable);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t.start();while (true) {System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} }匿名内部类这种写法非常常见 这里主要的目的是描述这个方法设置回调函数 方法不能脱离类单独存在 这就导致为了设置回调函数就不得不上一层类了 1.3.6. 【推荐/常用】使用 lambda 表达式 由于上几个代码非常麻烦这里就引入了 lambda 表达式 lambda 表达式在主流语言中都有只不过在其他语言不一定叫做 lambda 在 C、Python是叫做 lambda JS、Go直接叫做匿名函数 lambda 表达式/匿名内部 是可以访问到外面定义的局部变量的变量捕获语法规则 lambda 表达式 打破了类比喻和方法绑定在一起的形式函数式接口属于 lambda 背后的实现相当于 java 在没有破坏原有的规则的基础上给了lambda一个合理的解释 package thread;public class ThreadDemo6 {public static void main(String[] args) {//常用/推荐使用 lambda表达式Thread t new Thread(() - {//形参列表这里可以带参数现成的入口不需要参数比如lambda 代替 Comparator可以带上两个参数while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();} } 2. Thread 类及常见方法 Thread 类是 JVM ⽤来管理线程的⼀个类换句话说每个线程都有⼀个唯⼀的 Thread 对象与之关联 2.1 Thread 的常见构造方法 Thread t1 new Thread(); Thread t2 new Thread(new MyRunnable()); Thread t3 new Thread(这是我的名字); Thread t4 new Thread(new MyRunnable(), 这是我的名字);注意 线程之间的名字是可以重复的 同一个工作需要多个线程完成都可以起一样的名字 但是名字不要乱起最好还是要有一定的描述性 2.2 Thread 的几个常见属性 getId() JVM自动分配的身份标识会保证唯一性 getMane()这个线程的名称 getState()进程有状态就绪状态堵塞状态 线程也有状态 Java 中对线程的状态又进行了进一步的区分比系统原生的状态更丰富一些 getPriority()线程的优先级 在 java 中设置优先级效果不明显对内核调度器的调试过程产生一些影响 isDaemon()daemon 守护 意思是是否是守护线程可以叫做是否是“后台线程” 和后台线程相对的还有前台线程 public class ThreadDemo7 {public static void main(String[] args) {Thread t new Thread(new Runnable() {Overridepublic void run() {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}},这是我的线程);t.setDaemon(true);//设为true 是后台 不设就是前台/*** 设为 true 是后台 (后台是躲在背后的人你感知不到) 后套不会阻止进程结束* 不设为 true 是前台(前台是明面上的人你能感知到) 前台会阻止进程的结束*/t.start();} } 在代码创建的线程默认是前台线程会阻止进程的结束。只要前台线程没执行完线程就不会结束即使main已经执行结束 加上 t.setDaemon(true); 之后 再次执行就会发现进程结束 isAlive()表示了内核中的线程PCB是否还存在 java 代码中定义的线程对象Thread实例虽然表示一个线程这个对象本身的生命周期和内核中的pcb生命周期是不完全一样的 这个时候t 对象有了但是内核 pcb 还没有isAlive 就是 false 真正在内核中创建出这个 pub此时 isAlive 就是 true 了 当线程 run 执行完了此时 内核中的线程就结束了内核 pcb 就释放了 但是此时 t 变量可能还存在浴室 isAlive 也是 false 2.3 启动⼀个线程 - start() Thread 类使用 start 方法启动一个线程 但是要记住对于同一个 Thread 对象来说start 只能调用一次 public class ThreadDemo9 {public static void main(String[] args) {Thread t new Thread(() - {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();//第二次调用 start 就会出现问题t.start();} } 在这里可以看到用两个start 会导致非法的线程状态异常 public static void main(String[] args) {Thread t new Thread(() - {System.out.println(hello);});t.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//第二次调用 start 就会出现问题t.start();}这个时候第一个线程虽然已经结束了但是依然不可能用start 进入第二个线程会导致线程状态异常 如果想要启动更多的线程就得创建新的对象 public class ThreadDemo10 {public static void main(String[] args) {Thread t new Thread(() - {System.out.println(hello1);});Thread t2 new Thread(() - {System.out.println(hello2);});t.start();t2.start();} }调用 start 可以创建出新的线程 本质上是 start 会调用系统的 api来完成创建线程的操作 关于start 经典面试题 start 和 run 区别 start 和 run 其实互不相干 class MyThread4 extends Thread {Overridepublic void run() {System.out.println(hello);} }public class ThreadDemo11 {public static void main(String[] args) {Thread t new MyThread4();//t.start();t.run();} } 在这里调用 t.start(); 和 调用 t.run(); 都是打印出来的 hello 看起来执行结果是一样的 所以就会有人陷入的疑惑start 和 run 是不是一样的呢 答案是否定的通过run 执行并没有创建新的线程还是在main 主线程中打印的 hello start 则是创建了新的线程由新的线程来去执行 hello 这里我们变换一下代码仔细看一看这两者之间的区别 class MyThread4 extends Thread {Overridepublic void run() {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} }public class ThreadDemo11 {public static void main(String[] args) throws InterruptedException {Thread t new MyThread4();//t.start();t.run();while (true) {System.out.println(hello main);Thread.sleep(1000);}} } 如果调用的是run那么就会进入到run 方法里面这样并不会执行下面的代码 class MyThread4 extends Thread {Overridepublic void run() {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}} }public class ThreadDemo11 {public static void main(String[] args) throws InterruptedException {Thread t new MyThread4();t.start();//t.run();while (true) {System.out.println(hello main);Thread.sleep(1000);}} }然而用 start 本质上是创建了新的线程新的线程来执行这里的run原有的 main 线程继续执行后续的循环 牢记start 的使命就是立即的在内核中创建出一个新的线程新的线程和之前的线程是“并发执行”的关系 2.4 中断⼀个线程 换一种说法就是终止一个线程 对于中断这个词是有多种含义的在操作系统底层也有中断概念CPU与其他各种设备上也有中断的概念 那怎么终止一个线程呢其实也就是让线程 run 方法入口方法执行完毕 那我们如何让线程提前终止呢 其核心就是让 run 方法能够提前结束这也取决于具体代码的实现方式 用自定义变量作为标志位 为了让线程结束我们引入了标志位 public class ThreadDemo12 {private static boolean isQuit false;public static void main(String[] args) {Thread t new Thread(() - {while (!isQuit) {System.out.println(我是一个线程工作中);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//由于当前是个死循环给了错误提示System.out.println(线程工作完毕);});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(让 t 线程结束);isQuit true;} } 通过上述代码就可以让线程结束掉 具体线程啥时候结束取决于在另一个线程中何时修改 isQuit 的值 main 线程要想让 t 线程结束大前提一定是 t 线程的代码对这样的逻辑有所支持而不是 t 里面的代码让其结束 如果代码没有配合 main 无法让 t 提前结束 并且谨记run 方法和 main 方法是两个线程这两个线程的执行顺序是不确定的 但是如果我想把代码稍微改一下 public class ThreadDemo12 {private static boolean isQuit false;public static void main(String[] args) {Thread t new Thread(() - {while (!isQuit) {System.out.println(我是一个线程工作中);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//由于当前是个死循环给了错误提示System.out.println(线程工作完毕);});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}isQuit true;System.out.println(让 t 线程结束);} }这里的 isQuit 是 作为全局变量如果作为 main 方法中的局部变量是否可行呢 很明显是错误的 在 lambda 表达式/匿名内部类中是可以访问到外面定义的局部变量的 但是捕获的变量必须是 final 或者 “事实final”虽然没写final但是没有修改 但是如果写了final 后面就没有办法修改因此不能写局部变量是行不通的 因此我们就必须写成成员变量 lambda 表达式本质上是“函数式接口”也就是匿名内部类 内部类访问外部类的成员这个事情本身就是可以的这个事情本身就不受到变量捕获的影响 那什么 java 中对变量捕获有final 的限制 isQuit 是局部变量的时候是属于 main 方法的栈帧中但是 Thread lambda 是有自己独立的栈帧的这个时候两个栈帧的声明周期是一致的 这就会导致main 方法执行完了栈帧销毁了同时 Thread 的栈帧还在还想继续使用 isQuit 所以在java 中的做法就非常简单了变量捕获本质上就是传参换句话说就是让 lambda 表达式在自己的栈帧中创建一个 新的 isQuit 并且把外面的 isQuit 值拷贝过来为了避免里外的 isQuit 的值不同步java干脆就不让你 isQuit 修改 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() Thread.currentThread() 获取当前实例在代码中那个线程调用就得到的是哪个线程的实例雷诗雨 this public class ThreadDemo13 {public static void main(String[] args) {Thread t new Thread(() - {while (!Thread.currentThread().isInterrupted()) {System.out.println(我是一个线程工作中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(线程执行完毕!);});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}//使用一个 interrupt 方法来改变刚才标志位的值System.out.println(让 t 线程结束);t.interrupt();} } 这里的代码就是把上面的 判定isQuit 改成判定 isInterrupted 这个代码本质上就是使用 Thread 实例内部自带的标志位来替代刚才手动创建的 isQuit 变量 但是运行完之后会发现事实上跟预想的并不一样 这里 t 线程并没有真的结束 刚刚写的 interrupt 导致 sleep 出现了异常 如果没有 sleepinterrupt 可以让线程顺利结束但是有了sleep 却引起了变数 我们在执行 sleep 的工程中调用了interrupt大概率的情况下sleep 休眠时间还没到就被提前唤醒了 提前唤醒会出现两种情况 抛出 InterruptedException 异常紧接着就会被 catch 获取到清除 Thread 对象的 isInterrupted 标志位 意思就是刚刚已经通过 interrupt 方法把标志位设为了 true但是 sleep 提前唤醒操作就把标志位又设回了 false因此循环回继续执行 其实要想要线程结束也很简单在catch 中加入break 就可以了 public class ThreadDemo13 {public static void main(String[] args) {Thread t new Thread(() - {while (!Thread.currentThread().isInterrupted()) {System.out.println(我是一个线程工作中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();//加上 break 此时抛出异常以后直接跳出break;}}System.out.println(线程执行完毕!);});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}//使用一个 interrupt 方法来改变刚才标志位的值System.out.println(让 t 线程结束);t.interrupt();} } 这里就也可以结束线程了 那么下面的那一串红色的是什么呢 那个其实是日志如果把 e.printStackTrace() 注释掉就不会存在了 sleep 为什么要清空标志位呢 其实这里是为了给程序员更多的“可操作性空间” 在前一个代码写的是 sleep(1000)结果现在 1000 还没到就要终止线程这就相当于两个前后矛盾的操作。此时是希望有更多的代码对这种情况进行操作 此时我们就可以在catch 语句中加入一些代码来做一些处理 让线程立刻结束 加上break让线程不结束继续执行 不加break让线程执行一些逻辑之后再结束 写一些其它代码再break 对于异常的处理我们有以下几种方法 尝试自动恢复 能自动恢复就尽量自动恢复比如出现一个网络通信相关的异常就可以在 catch 尝试重连网络记录日志异常心心记录到文件中 有些情况并非很严重的问题只需要把这个问题记录下来即可并不需要立即解决发出警报 针对一些比较严重的问题包括并不限于发邮件、打电话、发短信、发微信也有少数的正常的业务逻辑会一来到 catch 比如文件操作中有的犯法就是要通过 catch 来结束循环之类的 在java 中线程的终止是一种“软性”操作 必须要对应的线程配合才能把终止落实下去 相比之下系统原生的 api其实还提供了前置终止线程的操作 无论代码是否愿意配合无论线程执行到哪个代码都能强行让这个线程终止 但是这种操作在java 的api 中没有提供上述的做法如果强行终止一个线程可能线程执行到一半就可能出现一些残留的临时性质的“错误”数据其实是弊大于利的 2.5 等待⼀个线程 - join() 在操作系统中多个线程的执行顺序是不确定的随机调度抢占式执行有时我们需要等待⼀个线程完成它的⼯作后才能进⾏⾃⼰的下⼀步⼯作 虽然线程底层的调度是无需的但是可以在应用程序中通过一些api来影响线程执行的顺序 join 就是之中方式可以影响此线程结束的先后顺序 接下来我们来写一段代码看看join 的作用 public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {for (int i 0; i 5; i) {System.out.println(我是一个线程正在工作...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(线程执行结束);});t.start();System.out.println(这个是主线程期望这个日志在t结束后打印);}在这段代码中很明显我们想要的是线程最后一个一行打印是在最后的但是由于 t.start 让线程兵分两路让一个接着main 函数执行一个进入到了 Thread 中所以没有办法按照预期执行 由于 run 方法中的内容执行时间无法预期使用 join 就可以很好的解决问题 public class ThreadDemo14 {public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {Random random new Random();int n 5;for (int i 0; i n; i) {System.out.println(我是一个线程正在工作...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(线程执行结束);});t.start();//这个操作就是线程等待t.join();System.out.println(这个是主线程期望这个日志在t结束后打印);} } 在 main 线程中调用 t.ioin() 就是让 main 线程 等待 t 线程结束 在哪个线程调用就是等待的一方哪个实例被调用就是被等待的一方 执行 join 的时候没就看 t 线程是否正在运行如果 t 运行中mian 线程就会阻塞main 线程就展示不去 cpu 执行了如果 t 运行结束main 线程就会从阻塞中恢复过来并且继续往下执行 阻塞使这两个线程的结束时间产生了先后关系 上述线程结束顺序的先后在代码中是通过 api 控制的 让 main 线程主动放弃了去调度器中调度t 现车个虽然也可能是和其他线程共同进行调度的由于主线程一直等待即使 t 中间也经历多次 cpu 的切换不影响最终 t 也能正确先执行完毕 在任何一个线程都可以调用 join哪个线程调用 join 哪个线程就阻塞等待 创建一个新线程进行运算 public class ThreadDemo15 {//t 线程吧计算的结果放到 result 中private static int result 0;public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {for (int i 1; i 1000; i) {result i;}});t.start();//主要是不知道 t 此案成要执行多久//Thread.sleep(1000);//使用 join就会严格按照 t 线程结束来作为等待的条件//什么时候 t 运行结束(计算完毕)什么时候join 就结束等待t.join();//如果主线程直接就打印 result此时得到的结果是什么是无法预期的//由于主线程之间的执行顺序是不确定的主线程打印的 result 可能是还没有开始计算的初始值 0//也可能是计算过程中的证件结果也可能是 t 线程计算完之后的最终结果System.out.println(result result);} } 上面是使用一个线程来进行计算如果运算量足够的大就可以来用多个线程进行计算 public class ThreadDemo15 {//t 线程吧计算的结果放到 result 中private static int result 0;public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {for (long i 1; i 100_0000_0000L; i) {result i;}});t.start();long beg System.currentTimeMillis();t.join();long end System.currentTimeMillis();System.out.println(result result);System.out.println(time: (end - beg) ms);} } 这个时候就会溢出导致出错 所以我们就可以选择用多个线程进行计算 由此可以看出来多线程是能够有效的提高程序的运行效率的 这个时候主线程继续执行 join 主色 t 线程执行 t 线程的逻辑负责计算钱50亿的数据 t2 线程执行 t2 线程的逻辑 这算个线程兵分三路并发执行并发 并行 并发 t 和 t2 可能在两个不同的核心上同时执行并行 t 和 t2 也可能在同一个核心上分时复用并发 具体执行过程宏观上感知不到但是总的来说在cpu 不太繁忙的情况下大概率还是并行执行的过程更多一点 记住多线程代码是变幻莫测的稍微一调整逻辑都可能截然不同 join 的几个方法 public void join() 等待线程结束死等这个是不科学的如果代码因为死等导致代码卡住无法执行后面的逻辑就会导致很严重的bug public void join(long millis) 等待线程结束最多等millis 毫秒带有超时时间的等等有一个时间的上限的超时时间 public void join(long millis,int nanos) 同理可以更高精度设置一个ns 级别的时间实际的用处不大系统时间也没法精确到 ns 2.6 获取当前线程引用 public static Thread currentThread(); 返回当前线程对象的引用class MyThread5 extends Thread {Overridepublic void run() {//这个代码中如果想获取到线程的引用直接使用 this 即可System.out.println(this.getId() , this.getName());} }public class ThreadDemo16 {public static void main(String[] args) throws InterruptedException {MyThread5 t1 new MyThread5();MyThread5 t2 new MyThread5();t1.start();t2.start();Thread.sleep(1000);System.out.println(t1.getId() , t1.getName());System.out.println(t2.getId() , t2.getName());} } 这个方法也是可以获取到当前的引用对象的 如果是继承 Thread直接使用 this 拿到咸亨实例 如果是 Runnable 或者 lambda 的方式this 就无能为力了超时 this 已经不再指向 Thread 对象了 就只能使用 Thread.currentThread(); public class ThreadDemo17 {public static void main(String[] args) {Thread t1 new Thread(() - {Thread t Thread.currentThread();System.out.println(t.getName());});Thread t2 new Thread(() - {Thread t Thread.currentThread();System.out.println(t.getName());});t1.start();t2.start();} } 2.7 休眠当前线程 也是我们⽐较熟悉⼀组⽅法有⼀点要记得因为线程的调度是不可控的 所以这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的 3. 线程的状态 3.1 观察线程的所有状态 就绪这个线程随时可以去 cpu 上执行也包含正在 cpu 上执行 阻塞这个线程暂时不方便去 cpu 上执行。在java中针对阻塞状态又做了进一步的细分 java 中线程有以下之中状态 • NEW: Thread 对象创建好了但是还没有调用 start 防范在系统中创建线程 • RUNNABLE: 就绪状态表示这个线程正在 cpu 上执行或者准备就绪随时可以去 cpu 多行执行 • BLOCKED: 由于锁竞争引起的阻塞 • WAITING: 不带时间的阻塞死等必须要满足一定的条件才会解除阻塞。join 或者 wait 都会进入WAITING • TIMED_WAITING: 指定时间的阻塞到大一定时间之后自动解除阻塞。使用 sleep 会进入这个状态使用带有超时时间的 join 也会 • TERMINATED: Thread 对象仍然纯在但是系统内部的线程已经执行完毕了 学习线程的状态最大的作用就是调试多线程 bug 的时候最为重要的参考依据 public static void main(String[] args) throws InterruptedException {Thread t new Thread(() - {for (int i 0; i 5; i) {System.out.println(线程运行中...);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});//线程启动之前状态就是 NEWSystem.out.println(t.getState());t.start();t.join();//线程运行完毕之后状态就是 TERMINATEDSystem.out.println(t.getState());}一个 Thread 对象只能 start 一次 这和线程状态密切相关只有处于 NEW 状态 才能 start 使用 jconsole 查看线程 如果在代码的运行中发现某个进程卡住了就可以使用 jconsole 这样的工具查看这个进程中的一些重要线程的状态和调用栈 通过状态们就可以判定此线程是否阻塞以及什么原因阻塞 4. 多线程带来的的风险 - 线程安全 引入多线程目的是为了能够实现“并发编程” 实现“并发编程”也不仅仅只能依靠多线程 相比之下多线程属于一种比较原始也比较朴素的方案问题和注意事项就是比较多的 4.1 线程安全的概念 一段代码无论是在单线程下执行还是多个线程下执行都不会产生 bug这个情况就称为“线程安全” 如果这段代码在单线程下执行没有问题多线程之下出现问题这个情况就称为“线程不安全”或者“存在线程安全问题” 4.2 线程不安全的例子 public class ThreadDemo19 {private static int count 0;public static void main(String[] args) throws InterruptedException {//创建两个线程每个线程都针对上述 count 变量循环自增 5w 次Thread t1 new Thread(() - {for (int i 0; i 50000; i) {count;}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {count;}});t1.start();t2.start();t1.join();t2.join();//打印 count 的结果System.out.println(count count);} } 这段代码按理说一个代码自增 5w 次两个线程一共自增 10w 次最终结果应该是 10w 但是结果不符合预期说明上述代码出现了问题循环自增的代码就属于“存在线程安全问题”的代码 count 相当于 1 这个count 其实是三个cpu指令构成的 1load 把内存中读取数据到 cpu 的寄存器 2add 把寄存器中的值 1 3save 把寄存器的值歇会到内存中 如果是一个线程执行上述的三个指令是没有问题的 如果是两个线程并发的执行上述操作此时就会存在变数线程之间的顺序是不确定的 一共有无数种情况但是正确的情况也就只有两种 由于这两个线程是并行执行还是并发执行也不知道但是即使是并发执行在一个cpu 和欣赏两个线程有各自的上下文各自一套寄存器的值不会相互影响 这种顺序运行是正确的 这个就出现了问题 最关键的问题在于得确保第一个线程 save 了之后第二个线程在 load这个时候第二个线程 load才是第一个线程自增的后果 否则的话第二个线程 load到的就是第一个线程自增前的结果了 4.3 线程不安全的原因 【根本原因】操作系统上的线程是“抢占式执行”“随机调度” 由于随机调度就会给线程之间执行的顺序带来很多变数代码结构 代码中多个线程同时修改同一个变量会导致线程不安全 1一个线程修改一个变量没有问题 2多个线程读取同一个变量没有问题 说明如果只是读取变量的内容变量本身是固定不变的 3多个线程修改不同的变量没问题 如果是两个不同的变量彼此之间就不会产生相互覆盖的情况了 不过这个原因不够严谨后面会看到一个线程修改一个线程读也可能存在问题【直接原因】上述的多线程修改操作本身不是“原子的” count 这其实是有多个 cpu 指令构成的一个线程这行这些指令执行到一半就可能会被调度走从而给其他线程“可乘之机” 每个 cpu 指令都是“原子”的要么不执行要么执行完内存可见性问题指令重排序问题 在了解到了 导致线程不安全的原因那么我们就可以在这些原因上看看是否能找到解决方法 针对原因一 无法做出任何改变因为系统内部已经实现了 抢占式执行无法干预 针对原因二 分情况讨论有的时候代码结构可以调整有的时候调整不了 针对原因三 乍看起来count生成几个指令好像无法干预 但实际上是有办法的可以通过特殊的手段把这三个指令打包在一起成为“整体” 接下来我们就细说如何让其成为“整体” 4.4 加锁 - synchronized 为了让指令打包在一起我们可以通过“加锁”来做到 锁 具有“互斥”“排他” 这样的特征 当一个房子上了锁外面的人就无法进入只有当房子里面的人出来打开锁外面的人才有可能进入 4.4.1 用 synchronized 关键字进行加锁 在 java 中加锁方式有很多种最主要的方式是 synchronized 关键字并且任何一个对象都可以作为锁对象 加锁的目的就是为了把多个操作打包成一个原子的操作 在加锁的时候需要准备好一个“锁对象” 加锁解锁操作都是依托于这里的“锁对象”来展开 如果一个线程针对一个对象加上锁之后 其他线程也尝试对这个对象加锁就会产生阻塞BLOCKED 一直阻塞到前一个线程释放锁为止 我们把产生阻塞这种情况也叫做 锁冲突/锁竞争 如果两个线程是分别针对不同的对象加锁其实就不会有锁竞争就不会有堵塞 public class ThreadDemo19 {private static int count 0;public static void main(String[] args) throws InterruptedException {Object locker new Object();//创建两个线程每个线程都针对上述 count 变量循环自增 5w 次Thread t1 new Thread(() - {for (int i 0; i 50000; i) {synchronized (locker) {count;}}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {synchronized (locker) {count;}}});t1.start();t2.start();t1.join();t2.join();//打印 count 的结果System.out.println(count count);} }加上锁之后代码就能正常执行 前面说加锁是把 count 这三步操作成原子了但是很明显并非是加锁之后执行三个操作工程中线程就不调度了 但是即使加锁的线程调度走了其他线程也无法“插队执行” 接下来我们思考一个问题如果我拿一个方法对 coun 进行封装然后进行加锁这样会不会是一个线程安全的呢 class Test {public int count 0;public void add() {count;} }public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {Test t new Test();Thread t1 new Thread(() - {for (int i 0; i 50000; i) {t.add();}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {t.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count t.count);} }很明显这也是错误的也会导致线程不安全 接下来我们改一下代码把 count 方法哦一个Test t 对象之通过上述 add 方法来进行修改加锁的时候锁对象写作 this class Test {public int count 0;public void add() {synchronized (this) {count;}} }public class ThreadDemo20 {public static void main(String[] args) throws InterruptedException {Test t new Test();Thread t1 new Thread(() - {for (int i 0; i 50000; i) {t.add();}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {t.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count t.count);} }这个时候我们要看 this 值得是哪个对象是不是对同一个对象进行加锁 synchronized (this)给 this 加锁相当于把 synchronized 加到普通方法上 就和当时我们学的 StringBulider 和 StringBuffer 很像 但是 那我们如果把 this 变成 类对象结果会如何呢 这个时候我们获取到 Test 的类对象在一个 java 进程中一个类的类对象都是只有一个 因此第一个线程中拿到的类对象和第二个线程中拿到的类对象是同一个对象 因此锁竞争依然存在还是可以保证线性安全的 synchronized 修饰静态方法相当于给类对象加锁 扩充类对象 4.4.2 synchronized 的特性 1、互斥性 加锁的效果也可以称为“互斥性” 2、可重入 public class ThreadDemo21 {public static void main(String[] args) {Object locker new Object();Thread t new Thread(() - {synchronized (locker) {synchronized (locker) {System.out.println(hello);}}});t.start();} } 有的人看到两个 synchronized会想这样直观看起来好像是有锁冲突的为什么会正确运行呢 针对 locker 进行加锁这里的加锁应该是可以顺利获取到的 但是第二个 synchronized直观感受上应该是不能成功的呀此时 locker 对象处于已经加锁的状态这个时候如果再尝试对 locker 加锁不会出现“阻塞”情况吗 为什么最终没有出现阻塞呢 最关键的问题在于说这两次假说其实是在同一个线程中进行的 当前由于是同一个线程此时锁对象就知道了第二次加锁的线程就是持有锁的线程第二次操作就可以直接通过不会出现堵塞 这哥特性称为“可重入”
http://www.zqtcl.cn/news/190551/

相关文章:

  • 国家免费技能培训官网白杨seo博客
  • 福州seo网站建设微服务网站
  • 网站宽度 像素长沙电商运营培训
  • 备案上个人网站和企业网站的区别app开发多少钱一个
  • 有限公司网站建设 中企动力佛山培训机构招生方案
  • 扫黄打非网站建设专业的高端网站制作公司
  • 做自媒体发视频用哪些网站江西网站建设哪家好
  • wordpress用户列表南宁百度seo排名优化
  • 做网站时如何写接口文档上海网站设计建设公司
  • 网站小图标怎么制作平面设计素材网站推荐
  • 多元网络兰州网站建设惠州网页建站模板
  • 网站建设中首页模板下载网页制作模板保存
  • 宁夏做网站的江苏网站建设的案例展示
  • 网站功能需求文档如何免费域名注册
  • 推广网站的软件包头移动的网站建设
  • 自己制作音乐的软件免费上海seo怎么优化
  • 学vue可以做pc网站网站站长统计怎么弄
  • 做物流的可以在那些网站找客户大淘客网站建设app
  • 石家庄兼职做网站dedecms做视频网站
  • 优化公司怎么优化网站的网站 意义
  • 唯品会一家专门做特卖的网站手机版招聘网站开发技术维护
  • 做短租哪个网站wordpress 4.7
  • 网站换空间 site网站域没到期不能续费吗
  • 找别人做网站要考虑哪些网站导航条设计欣赏
  • mvc网站开发实例wordpress雪人主题2.0
  • 红色好看的网站中山网站建设工作室
  • 如何做喊单网站flask公司网站开发
  • 简单个人网站制作流程自己怎么做卖服装的网站
  • 网站开发公司创业做洁净的网站
  • 要建一个优惠卷网站怎么做企业开发小程序公司