Java多线程详解
基本概念
多线程
多线程是指的是这个程序(一个进程) 运行时产生了不止一个线程
并行:
多个cpu实例或者多台机器同时执行一段处理逻辑 是真正的同时
并发
通过cpu调用算法 让用户看上去同时执行 实际上从cpu操作层面不是真正的同时 并发往往在场景中有公用的资源 那么针对这个公用的资源往往产生瓶颈 我们会用TPS或者QPS来反应这个系统的处理能力
线程安全
指在并发的情况下 该代码经过多线程使用 线程的调度顺序不影响任何结果 这个时候使用多线程 我们只需要关注系统的内存 cpu是不是够用即可 反过来 线程不安全就意味着线程的调度顺序会影响最终结果
同步
通过人为的控制和调度 保证共享资源的多线程访问成为线程安全 来保证结果的准确
线程的状态
NEW(新建状态)
当一个线程创建以后 就处于新建状态 那什么时候这个状态会改变呢 只要它调用的start()方法 线程就进入了锁池状态
BLOCKED(锁池)
进入锁池以后就会参与锁的竞争 当它获得锁以后还不能马上运行 因为一个单核cpu在某一时刻 只能执行一个线程 所以他需要操作系统分配给它时间片 才能执行
RUNNABLE(运行状态)
当一个持有对象的锁的线程获得CPU时间片以后 开始执行这个线程 此时叫做运行状态
TIMED_WAITING(定时等待)、WAITING(等待)
处于运行状态的线程还可能调用wait方法 或者带时间参数的wait方法 这时候线程就会将对象锁释放 进入等待队列里面(如果调用wait方法则进入等待状态,如果调用带时间参数的则进入定时等待状态)
TERMINATED(终止、结束)
当线程正常执行完 那么就进入终止(死亡) 状态 系统就会回收这个线程占用的资源
注意:
1.当线程调用sleep方法或当前线程中有其他线程调用了带时间参数的join方法的时候进入定时等待状态
2.当其他线程调用了不带时间参数的join()方法进入等待状态
3.当线程遇到I/O的时候还是运行状态
4.当一个线程调用了suspend方法挂起的时候它还是运行状态
synchronized、Lock
他们是应用于同步问题的人工线程调度工具 wait/notify必须存在于synchronized块中 并且 这三个关键字针对的是同一个监视器(某对象的监视器) 意味着wait之后 其他线程可以进入同步块执行
同步原理
JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步 但两者的实现细节不一样 代码块同步是使用monitorenter和monitorexit指令实现 而方法同步是使用另外一种方式实现的 细节在JVM规范里并没有详细说明 但是方法的同步同样可以使用这两个指令来实现 monitorenter指令是在编译后插入到同步代码块的开始位置 而monitorexit是插入到方法结束处和异常处 JVM要保证每个monitorenter必须有对应的monitorexit与之配对 任何对象都有一个monitor与之关联 当且一个monitor被持有后 它处于锁定状态 线程执行到monitorenter指令时 将会尝试获取对象所对应的monitor的所有权 即尝试获得对象的锁
synchronized的使用
锁的本质是对象实例 对于非静态方法来说: Synchronized有两种呈现形式,Synchronized方法体和Synchronized语句块 两种呈现形式本质上的锁都是对象实例。
锁定实例
public class SynchronizeDemo{public void doSth1(){/ * 锁对象实例 synchronizeDemo */ synchronized(synchronizeDemo){ try{System.out.println("正在执行方法");Thread.sleep(10000);System.out.println("正在退出方法");}catch(InterruptedException e){e.printStackTrace();} }}public void doSh2(){/ * 锁对象实例 this等同于synchronizeDemo * */ synchronized(this){ try{ System.out.println("正在执行方法"); Thread.sleep(10000); System.out.println("正在退出方法"); }catch(){ e.printStackTrace(); } }}}
synchronized块中的方法获取了lock实例的monitor 如果实例相同 那么只有一个线程能执行该块内容
直接用于方法
public synchronized void doSth3(){/ * 表面呈现是锁方法体 实际上是synchronized(this) 等价于上面 */try{System.out.println("正在执行方法");Thread.sleep(10000);System.out.println("正在退出方法");}catch(InterruptedException e){e.printStackTrace();}}
相当于上面代码中用lock锁定的效果 实际获取的是Thread类的monitor 更进一步 如果修饰的是static方法 则锁定Synchronized的calss对象
Lock的使用
Lock 在java.util.concurrent包内 共有三个实现:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
lock的主要目的是和Synchronized一样 但是lock更灵活
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁;而非公平锁则不保证这一点 在锁被释放时 任何一个等待锁的线程有机会获得锁。Synchronized的锁是非公平的 ReentrantLock默认情况下也是非公平的 但可以通过带boolean值的构造函数要求使用公平锁;
锁绑定多个条件是指一个ReentranLock对象可以同时绑定多个Condition对象 而在Synchronized中 锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件 如果要多于一个条件关联的时候 就不得不额外添加一个锁 而ReentranLock无需这样做 只需要多次调用newCondition方法即可
ReentrantLock
import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/ 1. 有界阻塞队列 2. 当队列为空 队列将会阻塞删除并获取操作的线程 直到队列中有新元素 3. 当队列已满 队列会阻塞添加操作的过程 直到队列有空位置 */public class BoundedQueue<T>{private Object[] items;//添加的下标 删除的下标和数组当前数量private int addIndex,removeIndex,count;private Lock lock = new ReentrantLock();private Condition notEmpty = lock.newCondition();private Condition notFull = lock.newCondition();public BoundedQueue(){items = new Object[5];}public BoundedQueue(int size){items= new Object[size];}/ * 添加一个元素 数据满则添加线程进入等待状态 */ public void add(T t) throws InterruptedException{ lock.lock(); try{while(items.length==count){System.out.println("添加队列--陷入等待");notFull.await();}items[addIndex] = t;addIndex = ++addIndex == items.length ? 0 : addIndex;count++;notEmpty.signal();}finally{lock.unlock();} } / * 删除并获取一个元素 数组空则进入等待 */ public T remove() throws InterruptedException{lock.lock();try{while(count==0){System.out.println("删除队列---陷入等待");notEmpty.await();}Object tmp = items[removeIndex];items[removeIndex] = null;removeIndex = ++removeIndex == items.length ? 0 : removeIndex;count--;notFull.signal();return (T) tmp;}finally{lock.unlock();} } public Object[] getItems(){ return items; } public void setItems(Object[] items){this.items=items; } public int getAddIndex() { return addIndex; }public void setAddIndex(int addIndex) {this.addIndex = addIndex;}public int getRemoveIndex() {return removeIndex;}public void setRemoveIndex(int removeIndex) {this.removeIndex = removeIndex;}public int getCount() {return count;}public void setCount(int count) {this.count = count;}}
ReentranReadWriteLock
允许多个读线程同时访问 但不允许写线程和读线程、写线程和写线程同时访问 相对于排他锁 提高了并发性在实际应用中 大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentranReadWriteLock能够提供比排他锁更好的并发性和吞吐量
进程切换导致的系统开销
Java的线程是直接映射到操作系统线程之上的 线程的挂起、阻塞唤醒等都需要操作系统的参与 因此在线程切换的过程中是有一定的系统开销的 在多线程环境下调用Synchronized方法 有可能需要多次线程状态切换 因此可以说Synchronized是在Java语言中一个重量级操作
虽然如此 JDK1.6版本后还是对Synchronized关键字做了相对优化 加入锁自旋特性减少系统线程切换导致的开销 几乎与ReentrantLock的性能不相上下 因此建议在能满足业务需求的前提下 优先使用sychronized
volatile与synchronized的区别
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值不确定的 需要从主存中读取;synchronized则是锁定当前变量 只有当前线程可以访问该变量 其他线程被阻塞住
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.volatile仅能实现变量的修改可见性 不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile不会造成线程的阻塞synchronized可能会造成线程的阻塞
5.volatile标记的变量不会被编译器优化 synchronized标记的变量可以被编译器优化
常用方法
yield()
当前线程可转让cpu控制权 让别的就绪状态运行(切换)但是不一定是别的线程运行 看cpu心情 有可能还是当前线程
sleep()
暂停一段时间
join()
在一个线程中调用other.join() 将等待other执行完后才继续本线程。 类似于插队
interrupt()
中断线程
中断是一个状态 interrupet方法只是将这个状态设置为true而已 所有说正常运行的程序不会检测状态 就不会终止 而wait等阻塞方法会检查并抛出异常 如果在正常运行的程序中添加while(!Thread.interrupted()) 则同样可以在中断后离开代码体
future模式
使用步骤
- 创建Callable接口的实现类 并实现call()方法 该call()方法将作为线程执行体 并且有返回值
- 创建Callable实现类的实例 使用FutureTask类来包装Callable对象 该FutureTask对象封装了该Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象的targer创建并启动新线程。
- 调用FutureTask对象的get方法来获得子线程执行结束的返回值
多线程控制类
ThreadLocal类
作用
保存线程的独立变量 对一个线程类
当使用ThreadLocal维护变量时 ThreadLocal为每个使用该变量的线程提供独立的变量副本 所以每一个线程都可以独立地改变自己的副本 而不会影响其他线程所对应的副本 常用于用户登录控制 如记录session信息
原理
每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样 区别是桶里放的是entry而不是entry的链表 功能还是一个map) 以本身为key 以目标为value
主要方法是get()和set(T a) set之后在map里维护一个threadLocal -> a get时将a 返回
ThreadLocal是一个特殊的容器
代码解析
ThreadLocal类提供的几个方法
public T get(){}public void set(T value){}public void remove(){}protected T initialValue(){}
get方法是用来获取ThreadLocal在当前线程中保存的变量副本 set方法用来设置当前线程中变量的副本 remove用来移除当前线程中变量的副本 initialValue是一个protected方法 一般是用来在使用时进行重写 他是一个延迟加载方法
get方式的实现:
public T get(){Thread t = new Thread.currentThread();TrheadLoaclMap map = getMap(t);if(map!=null){ThreadLocalMap.Entry e = map.getEntry(this);if(e!=null){@SuppressWarings("unchecked")T result = (T) e.value;return result;}}return setInitalValue():}
第一句是取得当前线程 然后通过getMap(t)方法获取到一个map map的类型为ThreadLocalMap 然后接着下面获取到键值对 注意这里获取键值对传进去的是this 而不是当前线程t。
如果获取成功 则返回value值
如果map为空 则调用setInitialValue方法返回value
getMap方法
ThreadLocalMap getMap(Thread t){return t.threadLocals;}
在getMap中 是调用当前线程t 返回当前线程t中的一个成员变量threadLocals ThreadLocal就是我们之前提到的每个Thread都持有一个TreadLocalMap类型的变量
容器类
BlockingQueue
阻塞队列 该类是java.utilconcurrent包下的重要类 通过对Queue的学习可以得知这个queue是单向队列 可以在队列头添加元素和在对尾删除或去取出元素
除了传统的queue功能 之外 还提供了阻塞接口put和take 带超时功能的阻塞接口offer和poll put会在队列满的时候阻塞 直到有空间时被唤醒 take在队列空的时候阻塞 直到有东西拿的时候才被唤醒 用于生产者-消费者模型尤其好用
//常见的阻塞队列还有:ArrayListBlockingQueueLinkedListBlockingQueueDelayQueueSynchronousQueue
ConcurrentHashMap
高效的线程安全哈希map
信号量
信号量是一个非负整数(车位数) 所有通过它的线程都会将该整数减一 当该整数值为零时 所有视图通过它的线程都会将处于等待状态 在信号量上我们定义两种操作 wait 和 release 当一个线程调用wait操作时 它要么通过然后将信号量减一 要么一直等下去 直到信号量大于一或超时 Release实际上是在信号量上执行加操作 对应于车辆离开停车场 该操作之所以叫做“释放”时因为加实际操作上释放了由信号量守护的资源