4-5 多线程
线程与进程
进程
-
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程
-
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
-
线程实际上是进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
线程调度
分时调度
-
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间
抢占式调度
-
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度
-
CPU使用抢占式调度模式在多个线程之间进行着高速的切换。 对于CPU的一个核心而言 ,某个时刻,只能执行一个线程,而CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高
-
同时开始一千个连接由一千个人操作他 是操作8个脑子排着队去执行快 还是 8个脑子交替执行快 答案:排着队快 省去了切换的时间
同步与异步
同步:排队执行,效率低但安全
异步:同时执行,效率高但不安全
Thread类
常用方法:
-
getName():获取线程名
-
currentThread():获取当前正在执行的线程对象
-
sleep(int millis , int nanos) / sleep(int millis):一个暂停毫秒加纳秒 一个暂停毫秒
-
interrupt():中断此线程(不一定中断)
-
setDaemon(true):设置线程为守护线程
线程的中断:一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定 如果由外部决定线程何时结束,很有可能会导致程序资源还没来得及释放,一直占用内存,从而产生无法回收的内存垃圾,也有可能导致硬件资源无法释放从而使硬件无法正常运行 线程结束问题:不能用stop()方法,已过时,因为如果在线程还没完全结束的时候调用了额stop()方法会出现异常 结束的方法:可以在线程的开始定义一个变量,然后在线程执行过程中一直观察这个变量,如果变量改变了,就直接return 结束该方法; 使用interrupt()方法 interrupt()方法:当调用interrupt()方法时,是为了告知线程该结束了,线程内部的处理方式是产生异常进入catch块,程序员通过代码来决定这个线程是否死亡 (直接 return; 结束run()方法 线程结束) 线程阻塞:可以粗略地理解为,所有消耗时间的操作,比如常见的文件读取 会导致程序停止直到文件读完
守护线程
线程分为守护线程和用户线程 用户线程:当一个进程不包含任何存活的用户线程时,进程结束 守护线程:守护用户线程的线程,当最后一个用户线程结束时,所有守护线程自动死亡 setDaemon(true)
线程安全问题
解决的方法 | 格式 | 描述 |
---|---|---|
同步代码块(关键字) | synchronized(锁对象){} | 隐式锁,多个线程的锁对象必须唯一 |
同步方法(修饰符) | synchronized 返回类型 方法名(){} | 隐式锁,谁调用该方法谁就是锁对象 |
显式锁 | ReentrantLock类的lock() / unlock()方法 | 显式锁,由程序员决定在哪开启/关闭锁 |
解决线程不安全问的的方法 1.同步代码块:synchronized 格式: synchronized( 锁对象(任何对象都能作为锁对象) ){} 多个线程在使用同步代码块时都把自己的线程对象当做锁对象是不合理的(也就是把锁对象写在run方法里面,导致每次创建线程对象时都会创建一个针对自己线程的锁对象,每次都不用排队) 要多个线程看同一把锁! 2.同步方法:吧synchronized 关键字修饰到方法里面 这个方法 非静态的情况下,锁是this,就是调用的那个方法,如果是静态的的话,锁就是类名.class 注意:当一个类里面有多个方法都是使用synchronized 关键字修饰方法的话,当其中一个方法在执行的都是其他方法都不会执行 3.显式锁Lock Lock l = new ReentrantLock(); 在需要派对执行的代码前加上 l.lock(); 执行完毕后加上 l.unlock();
显式锁和隐式锁的区别
所谓的隐式锁和显示锁就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作
一、构成不同
-
sync:Java中的关键字,是由JVM来维护的,是JVM层面的锁
-
sync:底层是通过monitorenter进行加锁(地刺鞥是通过monitor对象来完成的,其中的wait/notify等方法也是依赖于monitor对象的。只有在同步代码块或者同步方法中才可以调用wait / notify等方法。因为只有在同步代码块或者是同步方法中,JVM才会调用monitory对象);通过monitorexit来退出锁
-
Lock:是JDK5以后才出现的类。使用Lock是调用对应的API方法来获取锁和释放锁,是API层面的锁。
二、使用方法不同
-
sync:程序能够自动获取锁和释放锁。因为当sync代码块执行完成之后,系统会自动让程序释放占用的锁。sync是由系统维护的,如果非逻辑问题的话,是不会出现死锁的。
-
Lock:手动获取锁(lock())和释放锁(unlock()),不然可能导致出现死锁的问题
三、等待是否可中断
-
sync是不可中断的,除非是抛出异常或者正常运行完成
-
Lock是可以中断的
-
调用设置超时方法 tryLock(long timeout , timeUnit unit)
-
嗲用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
-
四、加锁的时候是否公平
-
sync:非公平锁。即排队抢锁
-
Lock:可以设置为公平锁。默认的是非公平锁 在构造方法传入Boolean值
-
Lock l = new ReentrantLock(true / false)
-
true:公平锁
-
false:默认值 非公平锁
-
-
五、锁绑定多个条件来condition
-
sync:要么随机唤醒一个线程,要么是唤醒所有等待的线程
-
Lock:用来实现分组唤醒需要唤醒的线程,可以精确地唤醒,而不是像sync那样,不能精确唤醒线程
六、性能比较
-
sync:sync是托管给JVM执行的。在JDK5中,前者因为需要调用接口操作,可能加锁等消耗时间更长。但在JDK6后,sync进行很多优化,有适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,性能也不比lock差
-
之前的sync使用的是悲观锁机制,即线程独占锁,其他线程只能依靠阻塞来等待线程释放的锁,而线程阻塞时会引起线程的上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低
-
-
lock:Lock是java写的控制锁的代码。对象性能更高点。
-
Lock使用乐观锁的方式。乐观锁就是:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁的实现方式就是CAS机制(Compare And Swap),调用的是CPU提供的底层指令。
-
线程死锁
概念: 线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其他线程是获取不到这个锁的,而且会一直等待下去,因此编造成了死锁 产生条件: 1.互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其他线程均是无法获取到这个锁的 2.占用且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,及时获取不到也不会释放已获得的锁 3.不可剥夺性:任何一个线程都无法强制获取别的线程已经占有的锁 4.循环等待性:线程A拿着线程B的锁,线程B拿着线程A的锁。。 如何避免死锁 1.加锁顺序:线程按照相同的顺序加锁 2.加锁时限,线程获取锁的过程中限制一定的时间,如果给定时间内获取不到,就算了,别勉强。这需要用到Lock的一些API
多线程通信问题
同一个类里的两个方法产生了线程冲突 加flag(条件),
线程的中断
一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定 如果由外部决定线程何时结束,很有可能会导致程序资源还没来得及释放,一直占用内存,从而产生无法回收的内存垃圾,也有可能导致硬件资源无法释放从而使硬件无法正常运行
继承Thread
-
继承Thread后要重写run方法,run方法就是线程要执行的任务方法
-
在run方法里写好代码。这些代码就是一条执行路径
-
但是这个执行路径的触发方式,不是调用run方法。而是通过thread对象调用start()方法来启动任务
-
每个县城都有自己的栈空间,共用一份堆内存
-
在整个执行过程中,子线程任务调用的方法都在子线程中运行
Runnable接口
-
实现接口,实现run方法
-
Runnable的使用
-
class MyRunnable implement Runnable{} main方法: //1. 创建一个任务对象 MyRunnable r = new MyRunnable(); //2. 创建一个线程,并为其分配任务 Thread t = new Thread(r)
-
实现Runnable与集成Thread相比
-
通过创建任务,给线程分配任务的方式来实现多线程,这种方式更适合多个线程同时执行相同任务的情况
-
可以避免单继承所带来的的局限性
-
任务与线程本身是分离的,提高了程序的健壮性
-
后续学习的线程池技术,接受Runnable类型的任务,而不接收Thread类型的线程
-
Callable
Callable使用步骤
1.编写类实现Callable接口,实现call方法 class xxx implements Callable<T> { @Override public <T> call() throws Excpetion{ return T; } } 2.创建FutureTask对象,并传入第一步编写的Callable类对象 FutureTask<Integer> future = new FutureTask<>(callable); 3.通过Thread,启动线程 new Thread(future).start();
Runnable与Callable的相同点
- 都是接口 - 都可以编写多线程程序 - 都采用Thread.start()启动线程
Runnable与Callable的不同点
-
Runnable没有返回值;Callable可以返回执行结果
-
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值
Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
线程的六种状态
1. NEW 1. 尚未启动的线程处于此状态 2. RUNNABLE 1. 在JAVA虚拟机中执行的线程处于此状态 3. BLOCKED 1. 被阻塞等待监视器锁定的线程处于此状态 4. WAITING 1. 无限期等待另一个线程执行特定操作的线程处于此状态 5. TIMED_WAITING 1. 正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态 6. TERMINATED 1. 已退出的线程处于此状态 线程在给定实现点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。
线程池
线程池概述
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源
线程池的好处
- 降低资源消耗 - 提高影响速度 - 提高线程的可管理性
缓存线程池
缓存线程池:ExecutorService pool = Executors.newCachedThreadPool(); (长度无限制) 任务加入后的执行流程 1.判断线程是否存在空闲线程 2.存在则使用 3.不存在,则创建线程 并放入线程池,然后使用 ExecutorService pool = Executors.newCachedThreadPool(); //指挥线程池执行新的任务 pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); 不休眠的运行结果: 线程的名称:pool-1-thread-1 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-3 休眠的运行结果: 线程的名称:pool-1-thread-1 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-3 线程的名称:pool-1-thread-3 证明了线程实现了重复使用
定长线程池
定长线程池:ExecutorService pool = Executors.newFixedThreadPool(2); (长度是指定的数值) 执行流程: 1.判断线程池是否存在空闲线程 2.存在则使用 3.不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池,然后使用 4.不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 ExecutorService pool = Executors.newFixedThreadPool(2); //向线程池中加入新的任务 pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); 运行结果: 线程的名称:pool-1-thread-1 线程的名称:pool-1-thread-2 等待一段时间后 线程的名称:pool-1-thread-2
单线程线程池
单线程线程池(相当于定长线程池输入1) 执行流程: 1.判断线程池的线程是否空闲 2.空闲则使用 3.不空闲,则等待池中的单个线程空闲后使用 ExecutorService pool = Executors.newSingleThreadExecutor(); //向线程池中加入新的任务 pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); pool.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); 运行结果: 线程的名称:pool-1-thread-1 (间隔2s) 线程的名称:pool-1-thread-1 (间隔2s) 线程的名称:pool-1-thread-1 每个执行结果之间都间隔2s,且都由一个线程执行
周期定长线程池
周期任务 定长线程池 执行流程: 1.判断线程池是否存在空闲线程 2.存在则使用 3.不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用 4.不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 周期性任务执行时: 定时执行,当某个时机触发时,自动执行某任务 //定义一个周期性定长线程池 ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); /** * 定时执行 * 参数1.runnable类型的任务 * 参数2.时长数字 * 参数3.时长数字的单位 */ pool.schedule(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } },5, TimeUnit.SECONDS); 执行结果: 等待5s后 线程的名称:pool-1-thread-1 //定义一个周期性定长线程池 ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); /** * 周期执行 * 参数1.runnable类型的任务 * 参数2.时长数字(延迟执行的时长) * 参数3.周期时长(每次执行的间隔时间) * 参数4.时长数字的单位 */ pool.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } },5,1,TimeUnit.SECONDS); 执行结果: (等待5s后) 线程的名称:pool-1-thread-1 线程的名称:pool-1-thread-1 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-2 线程的名称:pool-1-thread-2 每个输出之间间隔1s,两个线程抢占执行。
Lambda表达式
由 Thread t = new Thread(new Runnable() { @Override public void run() { 代码 } }); t.start(); 变为 Thread t = new Thread(() -> 代码); t.start(); 注意:使用Lambda表达式的时候,需要实现方法的接口一定只有一个方法