Java EE初阶--多线程 一.为何使用线程?在过去年代的计算机大部分采用了并发编程一个服务器可以服务多个客户端每给一个客户端服务就会创建一个进程服务完之后就会销毁进程。一个进程的创建和销毁开销比较大频繁的创建和销毁严重浪费资源所以就引入了线程的概念。二.线程和进程的区别1. 一个进程至少包含一个线程一个线程即是一段代码指令。2. 进程和进程不共享空间线程和进程共享空间3. 一个进程挂了不会影响其他进程一个线程挂了可能会导致整个进程崩溃4.进程调度和线程调度调度机制一样具体实现不一样如调度周期和调度单位进程是操作系统分配资源的基本单位线程是系统调度的基本单位5.进程和线程的开销代价不一样创建一个新进程✅ 需要创建一个全新的、独立的地址空间一大块新地皮和新厂房。✅ 需要将父进程的代码和数据复制或映射到这个新地址空间中。✅ 需要创建进程控制块PCB这个结构比TCB更复杂。✅ 需要分配独立的文件描述符表等。✅同时它内部第一个线程的创建成本TCB、栈等也同样存在。总结进程创建是“从零开始建一个新工厂”开销巨大。创建一个新线程✅ 需要分配一个独立的栈一小块工作台。✅ 需要创建一个线程控制块TCB一个工牌和日志。❌不需要创建新的地址空间、复制代码数据等。总结线程创建是“在现有工厂里增加一条生产线”开销极小。三.Java的线程和操作系统之间的关系线程是操作系统内的概念操作系统内核实现了该机制并提供了一些api供用户使用来操控线程api介绍又名为应用程序编程接口就是一个实现一些功能的封装拿过来就可以用。Java 标准库中 java.lang默认导入不需要手动导入包 中的Thread类可以视为是对操作系统提供的API进⾏了进⼀步的抽象和封装.四.线程的创建和使用五.创建线程的不同形式六.低耦合高内聚在程序的过程中都是采用低耦合高内聚的方式写代码低耦合即降低模块和模块之间的关系,一个模块变化尽量不影响其他模块,不会因一个变化牵动全身高内聚即一个模块之类把有关联的东西放在一块到时好找到对应的东西对比继承Thread类实现Runnable接口达到了低耦合高内聚的效果原因是Thread将任务逻辑run方法与线程执行机制Thread类紧密绑定七.Thread类的常用方法及其使用1.构造方法2.普通方法2.1 getId()作用获取线程ID,不同线程有不同的ID.2.2 getName作用获取线程的名字2.3 getPriority()2.4 isDaemon()和setDaemon()作用看是否为后台线程和设置后台线程得在启动线程前设置前台线程进程会等待所有前台线程都执行完毕后才会终止我们创造的线程默认为前台线程后台线程当前台线程都执行完毕进程不会等待后台线程执行完毕会直接结束进程后台线程会强制终止使用2.5 isAlive()作用看线程是否结束2.6 isInterrupt()、interrupted()、interrupt()使用其他办法使用该方法2.7 join()作用哪个线程调用这个并在该线程使用那该线程得等待调用该方法的线程结束才会执行join()下面的类容使用原因如果要控制线程结束的先后顺序用sleep()方法可能达到效果但是由于抢占式调度出现各种可能性该方法就有时不靠谱而该方法可以强制控制线程的先后顺序使用2.8 sleep()作用是休眠且能控制休眠时间这些能控制时间的方法但设置的时间到后不是立即执行而是要进行一些内部操作才去执行其他指令所以要比设置的时间长一些sleep(0): 代表该线程立即放弃cpu资源让优先级更高的线程吃到cpu资源2.9 getState()作用观察某个线程的状态根据这个更好的调试代码。在Thread中的Thread.State是枚举类型以下为枚举常量举例八.多线程带来的安全问题 --- 线程安全重点举例九. 线程安全产生的原因和解决方案1.线程调度机制为抢占式调度系统设定程序员左右不了2.多个线程同时修改同一个变量调正代码结构规避一些线程不安全的代码3.修改操作不是原子的如事务中的原子性一个事务不会执行到一半,而 cpu可能将指令执行到一半就会调到其他指令了可以通过加锁来把一些操作打包成原子的十.synchronized 关键字 - 监视器锁 monitor lock1.synchronized的特性1互斥synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏ 到同⼀个对象synchronized就会阻塞等待使用2可重入十一.死锁1.一个线程一把锁连续加锁俩次在Java 的synchronized 中构成可重入锁在C中会构成死锁2.两个线程两个锁尝试获取对方的锁3.N个线程,M把锁举例哲学家就餐模型描述5个哲学家线程共有5个筷子锁在一个桌子上吃面想实现的逻辑哲学家要么思考要么拿到俩个筷子就可以吃面条吃完面条就放下筷子。如果别人拿到筷子只能阻塞等待到他放下筷子才有机会拿到筷子问题当每个哲学家同时拿到一根筷子每个哲学家就会拿不到其他人的筷子就会产生死锁即哲学家1拿到 锁1等其他哲学家都拿到自己的锁后尝试获取锁2哲学家2拿到 锁2等其他哲学家都拿到自己的锁后尝试获取锁3哲学家3拿到 锁3等其他哲学家都拿到自己的锁后尝试获取锁4哲学家4拿到 锁4等其他哲学家都拿到自己的锁后尝试获取锁5哲学家5拿到 锁5等其他哲学家都拿到自己的锁后尝试获取锁1注意这里必须是拿到第一个锁之后不释放的前提下去获取对方的锁十二. 如何避免出现死锁呢构成死锁的四个必要条件重点。1.锁是互斥的 2.锁是不可剥夺的锁的基本的性质一个线程拿到锁后其他想获取锁就得阻塞等待而且不能剥夺该锁解决方案自己实现个锁3.保持和请求一个线程在已经获取锁1的情况下不提前释放锁1获取锁2解决方案避免嵌套锁在释放该线程的锁后才去获取别的锁4.循环等待线程A等线程B,线程B等线程C,线程C等线程A.解决方案约定加锁的顺序如哲学家就餐问题哲学家1拿到 锁1持有锁1尝试获取哲学家2的锁2哲学家2拿到 锁2持有锁2尝试获取哲学家3的锁3哲学家3拿到 锁3持有锁3尝试获取哲学家4的锁4哲学家4拿到锁4持有锁4尝试获取哲学家5的锁5哲学家5 尝试拿锁1阻塞等待最后拿锁5由于哲学家5没有持有锁5哲学家4就可以吃到面条同时释放锁4哲学家3就可以拿到锁4吃到面条依次类推最后哲学家5吃到面条结束。十三.Java标准库中的线程安全类线程不安全的类• ArrayList • LinkedList • HashMap • TreeMap • HashSet • TreeSet • StringBuilder线程安全的类十四.内存可见性造成线程安全可见性的原因之一原因编译器优化即在逻辑不变的情况下提升性能但在多线程的情况下可能出现失误导致优化后的逻辑和优化前的逻辑出现误差解决方案注意volatile解决的是内存可见性的问题不可以解决原子性问题十五.java内存模型Java Memory Model (JMM)