- 同一时间点网站访问巨大,被称为高并发
多线程
-
进程 —> 在操作系统 ( OS ) 中正在执行的应用程序
-
多进程 —> 支持多个应用程序同时执行
-
多线程并发执行原理
- 微观上串行 —> cpu将时间片分配给哪一个进程,哪一个进程执行对应的任务 —> 进程一个一个被执行
- 宏观上并行 —> 多个线程一起执行
-
线程 —> 又称轻量级进程
- 在进程中执行的每个任务,一个进程中同时可以执行多个任务,即多线程
- 线程是进程的任务执行单元( 单位 )
- main函数为主线程,以main函数的开始为开始,以main函数的结束为结束。
-
线程组成
- CPU:获取cpu时间片 —> 多线程并发执行原理也是微观上串行,宏观上并行
- 数据 —> 堆空间共享,栈空间独立 —> 一个线程一个栈空间,相互独立,但是堆空间只有一个,是共享的;只要能获取线程中某一个对象的首地址,就可以对此线程进行操作;但是每一个线程都有自己独一份的栈空间,其中数据不共享
- 代码:实现多线程
代码实现多线程
-
借助java.lang.Thread类
- 定义一个类继承Thread类
- 重写public void run(){ }方法,将线程任务定义在run方法中
- 创建线程对象 —> 用定义的类创建即可
- 开启线程 —> jvm默认执行调用线程中run方法 —> 一个线程对象不能被多次开启 —> 直接对象.方法名只是普通的方法调用,与线程无关
-
借助java.lang.Runnable接口
-
定义一个类实现Runnable接口,同时实现run方法
-
创建目标(任务)对象 —> 用定义的类创建
-
创建线程对象,借助JDK中提供的线程类java.lang.Thread,并将目标对象作为参数进行传递
Thread t2 = new Thread(m);//m为实现了Runnable类型的对象
-
开启线程:t2.start(); —> jvm虚拟机默认执行run方法
-
-
线程池
- 一个线程对象的存在只为特定的一个任务而存在,任务执行完毕则线程被销毁,导致频繁的创建线程和频繁的销毁线程,降低JVM性能,从而使用线程池
- 线程池 —> 线程容器,将预先创建好的线程对象存放池中,而且线程池中线程对象可以被反复使用,避免频繁创建线程和销毁线程,从而提高jvm的性能
- 常用接口 —> java.util.concurrent包中 —> 简称juc,并发包
- Executor —> 线程池的顶级接口
- ExecutorService —> 子接口,线程池核心接口
- submit(Runnable task) —> 将Runnable类型任务提交给线程池,线程池会分配空闲线程执行对应的线程任务,如果线程池暂时没有空闲线程对象,则该任务需要等待,等待有空闲线程对象
- submit ( Callable c ) :将Callable类型的任务提交给线程池
- void shutdown() —> 关闭线程池,同时将线程池中的线程对象销毁(将提交的任务全部执行完之后进行销毁线程)
- 常用类 —> java.util.concurrent包中 —> 简称juc,并发包
- Executors —> 获取线程池对象的工具类,定义了大量的静态方法,可通过类名直接调用即可
- public static ExecutorService newFixedThreadPool(int n):获取固定数量线程对象的线程池,参数 n 代表指定线程池中预先创建对象个数
- public static ExecutorService newCachedThreadPool():获取不固定个数线程对象的线程池;当提交任务时,如果线程池中没有空闲线程,则创建新的线程对象;若长时间没有接收任务的线程对象,会被自动清理销毁
- Executors —> 获取线程池对象的工具类,定义了大量的静态方法,可通过类名直接调用即可
-
借助Callable接口 + 线程池技术 —> java.util.concurrent 包中,JDK5.0版本提出
- 目前线程任务定义在run方法中,但run方法有两个缺点
- 方法没有返回值
- run方法内部出现异常时只能积极处理异常,无法消极声明异常
- Callable应用等价于Runnable,用于定义线程任务,只是有返回值,可以消极处理异常
- Callable是一个泛型接口,接口泛型类型决定方法的返回值类型
- V call( ) throws Exception:带有返回值并可以消极处理
- Future —> 位于java.util.concurrent包中
- Future用于接收submit提交任务的返回值
- Future是一个泛型接口,泛型类型取决于submit提交的Callable泛型的类型
- V get ( ):从Future获取任务执行之后的结果,返回值类型取决于Future的泛型类型
- 目前线程任务定义在run方法中,但run方法有两个缺点
线程同步
-
临界资源 —> 多线程并发时,同时被多个线程共享的同一个对象 —> 一个对象同时被多个线程共享
-
原子操作 —> 不可分割的多步操作,被视作一个整体,其步骤和顺序不能被破坏
-
线程同步 —> 保证临界资源的正确的一种技术手段 —> 多线程并发时,为了保证临界资源的正确性的,而不能破坏临界资源中的原子操作
-
线程同步方式
-
同步代码块 —> 对临界资源进行加锁 —> 定义在方法内部 —> synchronized ( 临界资源 ){//原子操作}
- 当线程获取锁标记时,如果锁标记没有被其他线程占用,则成功获取锁标记,获取锁标记之后才能执行同步代码块{ }中内容;当{ }中所有内容全部执行完,会自动释放当前线程占用的锁标记;如果锁标记被其他线程占用,则当前线程进入阻塞状态(Blocked状态:等待锁标记),直到被占用的锁标记释放才结束阻塞状态,从而进入就绪状态,同时获取cpu时间片才可以执行同步代码块中内容
-
同步方法 —> 被synchronized修饰的方法称为同步方法
? —> 修饰符 synchronized 方法返回值类型 方法名 ( 形参 ){ //原子操作 }
-
同步方法等价于同步代码块 —> synchronized(this){ //原子操作 }
-
每一个对象都具有单一的锁标记(监视器)
-
线程同步时,必须保证多个线程共用同一个对象的锁标记,才能保证共享资源的正确性,达到互斥作用,从而保证线程同步(保证临界资源的正确性)
ArrayList Vector 线程不安全,内部为非同步方法,但是无法保证数据的安全性 线程安全,内部方法为同步方法 并发效率高 并发效率相对低
-
-
Lock(锁) —> 灵活的控制加锁和释放锁
- jdk5.0提供的线程同步一种加锁策略,位于java.util.concurrent.locks包中的接口
- void lock() —> 获取锁 —> 如果锁被其他线程占用,当前线程进入堵塞状态
- void unlock() —> 释放锁
- 实现类 —> ReentrantLock
-
sleep():形参为毫秒 | wait() |
---|---|
位于Thread类中静态方法,使当前线程进入休眠(有限期的等待状态),释放时间片,但是不释放锁标记 | 位于Object类中成员方法,使当前线程进入等待状态,释放cpu时间片,同时释放锁标记 |
线程的状态 —> 线程的生命周期
-
一个时间点上,一个线程只可能处于其中的一种状态
-
初始状态(New) —> 线程对象被创建,即为初始状态。只在堆中开辟内存,与常规对象无异
-
JDK5.0之后就绪状态、运行状态统称为Runnable
- 就绪状态(Ready) —> 调用start()之后,进入就绪状态。等待OS选中,并分配时间片
- 运行状态(Running) —> 获得时间片之后,进入运行状态,如果时间片到期,则回到就绪状态
-
计时等待(Timed Waiting) —> 当前线程Running状态时可以进入,但醒了之后回到Ready状态重新等待时间片
- static void sleep(long ms) —> 让当前运行中的线程进入休眠状态(有限期等待状态),时间单位ms,处于休眠状态下的线程释放cpu时间片资源,不释放拥有的锁标记 —> 可以直接通过Thread.sleep();使用即可
-
无计时等待(Waiting) —> 在A线程中,调用join的线程不结束,则A线程一直等待。A线程会等待在A中所有调用join方法的线程,若有两个线程调用join,则这两个线程继续抢时间片
- final void join() —> 在一个线程中利用某一个线程对象调用其join方法,则这个线程让步与调用join方法的线程
-
阻塞状态(Blocked) —> synchronized锁定对象,在运行状态时进入堵塞状态,结束时返回到就绪状态
- 当线程获取锁标记时,锁标记被其他线程占用
-
终止状态(Terminated) —> 主线程main()或独立线程run()结束,代表线程的任务结束;进入终止状态,并释放持有的时间片
死锁
- 两个线程或是多个线程占用着对方想用的锁资源,都不进行释放,导致相互等待对方释放锁资源,从而产生一种无限期的一种等待现象
- 一旦出现死锁现象,不会抛出异常,也没有任何提示,所有线程进入阻塞状态;如果没有外力干预,程序将无法继续进行下去
- 解决方案
- 尽可能调整加锁方案
- 尽可能的避免出现同步嵌套使用
- 试图使用线程间的通信 等待 - 通知解决
- 线程间的通信 —> 等待 - 通知
- 等待 —> public final void wait() throws InterruptedException —> 位于java.lang.Object类中方法
- 在一个线程中调用wait方法,则当前线程会进入等待状态
- 可以使用任意对象调用wait() 方法,但是必须处理调用它的对象所有的同步代码块中
- 选择使用临界资源调用wait() 方法
- wait() 方法会让当前线程释放cpu,同时释放锁标记
- 通知(唤醒) —> notify() / notifyAll() —> 位于java.lang.Object类中
- notify() —> 通知一个处于等待状态的线程结束等待
- notifyAll() —> 通知所有的线程从等待状态结束
- 可以使用任意对象调用notify() 方法,但是必须使用在调用它的对象所在的同步代码块中
- 选择使用临界资源调用notify() 方法
- notify() 方法只是起到通知作用,不会使当前线程释放锁标记和cpu
- 等待 —> public final void wait() throws InterruptedException —> 位于java.lang.Object类中方法
集合的扩充
-
目前电脑为多核cpu,从微观意义上已经实现并行
-
写操作 —> 会对集合中数据进行改变 —> 加锁
-
读操作 —> 不会对集合中数据进行改变 —> 不加锁
-
CopyOnWriteArrayList —> 位于java.util.concurrent包中一个集合类 —> List实现类
-
此类中所有读操作没有加锁(没有synchronized和Lock的使用),写操作加锁(Lock)
-
多线程并发时,读与读之间不阻塞,写于写之间阻塞
-
在进行写操作时,会在原有文本的基础上拷贝一个新的数组,在新数组上进行写操作,写操作结束之后,会用新文本替换旧文本
-
牺牲写的操作效率提高读的操作效率,所以通常在读操作次数远远大于写操作次数时使用,线程安全
ArrayList CopyOnWriteArrayList Vector 线程不安全,读操作和写操作为非同步方法 线程安全,写操作加锁,读操作不加锁,牺牲写操作的效率提高读操作的效率 线程安全,读操作和写操作都为同步方法 并发效率高 在读操作次数远远大于写操作时,并发效率仅次于ArrayList 并发效率较低
-
-
ConcurrentHashMap —> 位于java.util.concurrent包中
-
线程安全,并发效率相对高效
-
采用***分段锁***,缩小锁定对象范围,减少锁冲突的可能性,提高并发效率
-
在全局操作不频繁的情况下,并发效率相对较高
-
jdk8.0及以上采用CAS ( 比较交换算法,三个参数控制比较) 和 synchronized实现并发高效
HashMap ConcurrentHashMap Hashtable 线程不安全 线程安全 线程安全 并发效率高 并发效率相对较高 并发效率较低
-
-
Queue接口 —> 位于java.util包中 —> 队列 —> Collection子接口,模拟队列存储结构 —> 先进先出,后进后出 —> First In First Out —> FIFO
-
LinkedList —> Queue实现类
- boolean add(E e):将指定的元素插入此队列
- ***boolean offer(E e)***:往队列中添加一个元素,成功true,反之
- E peek():获取但不移除此队列的头,队列为空返回null
- ***E poll()***:获取并移除此队列的头,队列为空返回null
- remove():获取并移除此队列的头
-
BlockingQueue —> Queue子接口,阻塞队列,队列中添加了两个线程状态为无限期等待的方法
- 常用方法
- void put(Object o) —> 往队列中存储元素,没有可用空间则等待
- E take() —> 从队列中获取队头元素,没有可取元素则等待
- 实现类
- ArrayBlockingQueue —> 数组实现,需要手动设置容量大小
- LinkedBlockingQueue —> 链表实现,可以手动设置容量大小,不指定则默认容量为Integer类型最大值
- 常用方法
-
ConcurrentLinkedQueue —> 位于java.util.concurrent包中,是Queue的实现类
- 链表实现
- 没有任何锁但是线程安全,采用无锁比较交换算法 ( CAS ) —> 三个参数控制比较