多线程
课程内容
- 并发和并行
- 线程和进程 概念,区别
- 线程状态 Thread类
- 线程常用的方法及线程调度 sleep、yield、join、interrupet...
- 线程状态转化
- 关键字 volatile: 特征:可见性、禁止指令重排序 底层实现机制
- 线程同步:Synchronized 使用场景、字节码的语义、底层实现(原子性、可见性、有序性)
- 线程间通信 wait、notify、notifyAll
- 线程间通信的经典使用:生产者、消费者模型
- 锁:乐观锁、悲观锁、重量级锁、偏向锁、自旋锁、重入锁、读写锁
- CAS 原理,ABA问题及解决
- 锁的优化
- 死锁
- JUC:(Java.util.Concurrent) ConcurrentHashMap
- Atom:AtomInteger...
- BlockingQueue(同步阻塞队列):ArrayBlockingQueue...
- 线程池、特殊线程池类
参考书籍
《深入理解Java虚拟机》 第12、13章 《Java 核心技术卷1》 第14章 《java 并发编程艺术》 通读
并发和并行
最大化来提高计算机的使用率
并发和并行的区别
同一台设备限定下:产生并发和并行的概念
并发是指多个线程操作同一个资源,不是同时执行,而是交替执行,单核CPU,只不过因为CPU的时间片很短,速度太快,看起来是同时执行(张三、李四厨师,共用一口锅炒菜,交替执行)
并行才是真正的同时执行,多核CPU。每个线程使用一个单独的CPU的资源来运行(张三、李四厨师、一人一口锅,一起炒菜)
(具体自己电脑的情况可以查看处理器百度)
并发编程:是指允许多个任务在一个时间端内重复的执行的设计结构(串行化?时间片轮转法??)
并行示意图(同步进行)
高并发:我们设计的程序,可以执行海量的任务同时执行
- QPS:每秒能够响应的请求数,QPS并不是并发数
- 吞吐量:单位时间内处理的请求数,QPS和并发数决定的
- 平均响应时间:系统对一个请求作出响应的平均响应时间 QPS=并发数/平均响应时间
- 并发用户数:系统可以承载的最大用户量
互联网系统的架构中们如何提高系统的并发能力?
- 垂直扩展(同一台设备)
- 水平扩展(多态设备之间的协作)
垂直扩展
提升单机的处理能力
1、增强单机的硬件性能:增加CPU的核数、内存升级、磁盘扩容
2、提升系统的架构能力:使用Cache来提高效率(缓存)
水平扩展
集群、分布式都是水平的扩展方案
集群:多个人做同一事(同时多顾几个厨师同时炒菜)
分布式:一个复杂的事情,拆分成几个简单的步骤,分别找不同的人去完成(1、洗菜 2、切菜 3、炒菜)
1、站点层扩容:通过Nginx反向代理,实现高并发的系统,将服务部署在多个服务器上(中间层的优化)
2、服务层扩容:通过RPC框架实现远程调用:Dubbo,Spring Cloud,将业务逻辑分拆成不同的RPC Client,
Clident完成各自的不同的业务,如果并发量比较大,新增加RPC Client(终端的优化)
3:数据层扩容:一台数据库拆分成多态,分库分表,主从复制,读写分离(客户端的优化)
进程和线程
进程和线程的概念
进程是计算机上正在执行的一个独立的应用程序,进程是一个动态概念,必须是进行状态。如果一个应用程序没有启动,那就不是进程:进程是资源的分配的基本单位(内存、进程ID(PID))
线程是组成进程的基本单位,可以完成特定的功能,一个进程是有一个后者多个线程组成的:线程是资源调度的单位
进程和线程的区别:
1、内存空间的区别:
进程是有独立的内存空间,每个进程之间是相互独立的,互不干扰,
线程有共享的内存空间(也有私有的)(一个程序内共享)
2、安全性:进程是相互独立的,一个进程的奔溃不会影响到其他的进程,进程是安全的,
线程存在内存空间的共享,一个线程的奔溃可能会影响到其他的线程的执行。,线程的安全性不如进程
关系:进程是相互独立的,一个进程下可以有一个或者多个线程
总结:进程独立、线程可共享;进程安全,线程可能不安全;进程资源分配,线程资源调度。
Java中很少使用进程的概念,但也可以使用:
Java默认有几个线程呢?
(Main方法启动会对应启动一个JVM实例,Main也是Java的一个进程,也叫做主线程)
Java默认是有两个线程:主线程 和 垃圾回收的线程(Main和GC)
Java本身能否启动线程?
start方法来启动并创建出新线程:底层调用native的start0方法
Java本身是没办法启动线程的,线启动时需要底层操作系统支持的,Java通过调用本地方法,C++编写的动态函数库,由C++去操作底层启动线程,Java是通过间接的调用来启动线程
一:创建线程的三种常用方式:
1.实现Runnable接口
Thread的start方法会调用Callable接口的call方法
2.继承Thread类:
3.实现Runnale接口
创建线程的三种方式的对比:
1.采用实现Runnable、Callable接口的方式创建多线程时
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想(接口可以被多个线程共享,代码和线程独立)。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2.使用继承Thread类的方式创建多线程时
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。(一个类只能继承一个类,可以实现多个接口)
注:java中,每次程序至少启动两个线程,一个main,一个垃圾收集线程。
3.Java中Runnable和Callable有什么不同?
Runnable和Callable都代表那些要在不同的线程中执行的任务,都需要调用Thread.start方法启动线程。
Runnable从JDK1.0开始就有了,Callable是在 JDK1.5增加的;它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,Callable可以返回装载有计算结果的Future对象(get方法),而Runnable的run()方法没有返回值不需要抛出异常;实现Callable接口的线程可以用Future.cancel取消执行,实现Runnable接口的线程不能。
注:Runnable装载当前继承Runnanle接口类的start方法(
Demo1 demo = new Demo1();
new Thread(demo).start(); );
Callable装载当前task对象的实例类实现start方法(
Demo3 demo3 = new Demo3();
FutureTask<Integer> task = new FutureTask<>(demo3);
new Thread(task,"线程").start(); );
二:线程状态及状态转换
线程状态
Java中线程状态有6种
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW:新建状态
用new创建的线程处于新建状态,此时他和其他的Java对象一样,仅仅在堆中分配了内存
Runable:就绪状态
当线程对象被创建后,调用了start方法,线程就处于就绪状态,处于就绪状态的线程,等待获取CPU的使用权
(JVM为线程创建方法和程序计数器,等待线程调度器调度)
Running:运行状态,运行run方法
Blocked:阻塞状态;
调用sleep方法主动放弃处理器资源;在等待某个通知notify;调用suspend方法将该线程挂起(容易导致死锁,尽量避免使用该方法)
Waiting:等待状态
Timed_waiting:超时等待
Terminated:终止状态
run或call方法执行完成,正常结束;线程抛出未捕获的异常或错误;stop方法结束线程(容易死锁,不推荐)
线程状态转换
过线程转换可知:一个线程的生命周期中需要的状态:New(初始作业状态)、Runable(就绪)、Running(运行)、Terminated(终止)四个状态
线程在需要响应的资源时,进入到阻塞状态:阻塞状态包含Waiting,Blocked、Time_waiting状态
线程常用方法介绍
start():启动线程
start方法启动一个新线程,start方法首先调用才能创建子线程,不能重复使用
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); boolean started = false; try { start0(); started = true; } finally { //省略非核心代码 } } private native void start0(); //调用native方法
启动方法是需要底层OS来启动子线程,start是间接启动
注:native方法说明:
JNI:Java native interface,java本地方法接口(JMM中存在本地方法栈),调度系统本身的提供的非Java代码(C/C++代码)
run():子线程的执行体
整个子线程执行的业务逻辑都在run方法中
单独调用run方法,会在当前的线程中执行run()操作,和普通方法调用是一样的,不会启动子线程,run方法可以重复调用
Thread类中的run方法只是判断任务体Runable是否存在,未做其他业务,继承Thread是需要重写
start()和run()方法的区别?
start()方法是用来启动子线程,start方法启动子线程后自动的来调用run方法
run()方法是子线程的业务执行体,不能直接调用run方法(通过调用run方法是不能操作子线程的)
yield():线程让步
是用来暂停当前线程的执行,并且让步于其他相同优先级或更高优先级的线程先执行
yield():方法在thread类中,是Thread类中静态方法
yield方法特点:
1、yield方法让步CPU的资源,让给谁由系统决定的,一般是让给相同优先级或者更高优先级的线程获得执行权,如果没有的话,会执行原来的线程
2、yield让步:会让当前线程由“运行状态”进入到“就绪状态”,等待CPU的调度
3、yield让步CPU资源后,线程不会释放锁
public static native void yield();
就绪状态的线程会按照优先级进行调度
join():线程合并
暂停当前线程执行,等待子线程的执行,也叫做线程合并,join方式是将并行执行的线程合并成串行执行,
例:在线程ta中调用tb.join,会暂停ta的执行,先让tb执行完毕,ta才会执行
方法介绍:
t.join() 允许t线程在当前线程之前执行,待t线程执行结束当前线程在执行
t.join(long millis)(时间单位:毫秒)允许t线程在当前线程之前执行,且最长时间millis毫秒之后,当前线程才能执行
t.join(long millis, int nanos)与t.join(long)一样,只不过可以提供纳秒级的精度
方法特点:
1、join方法是thread类中的方法,会抛出InterruptedException中断异常
2、当前线程ta中tb.join,tb会执行,ta线程会进入WAITING或TIMED_WAITING状态
3、当前线程ta中tb.join,则ta线程会释放当前持有的锁,join方法实现是通过wait/notify线程通信方式来实现的,wait方法的使用会释放锁
//一直等待子线程 public final void join() throws InterruptedException { join(0); } //提供毫秒,纳秒级别的等待时间 public final synchronized void join(long millis, int nanos) throws InterruptedException { if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } //纳秒的处理最终转换为毫秒处理 join(millis); } //提供毫秒级别的等待时间 public final synchronized void join(long millis)throws InterruptedException { if (millis == 0) { //millis=0,会一直判断子线程是否结束,否则会一直等待 while (isAlive()) { //线程等待 wait(0); } } else { //判断子线程是否结束且是否到达指定的毫秒数,否则会等待 while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); } } }
join方法可以使线程进行顺序执行???
练习:假如存在A、B、C三个线程,让三个线程按照C、B、A的顺序执行
/** * 让A、B、C三个线程按照CBA的方式顺序执行 * join方法 */ public class ThreadTest { public static void main(String[] args) { Thread c = new Thread(new Runnable() { @Override public void run() { System.out.println("线程C执行"); } }); Thread b = new Thread(new Runnable() { @Override public void run() { try { c.join();//c执行完,b才能执行 System.out.println("线程B执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread a = new Thread(new Runnable() { @Override public void run() { try { b.join();//b执行完,a才能执行 System.out.println("线程A执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }); System.out.println("线程的执行顺序"); a.start(); b.start(); c.start(); } }
ta和tb线程执行示意图
interrupt():中断线程
用来中断当前线程,终止处于“阻塞状态”的线程
方法介绍:
interrupt():该方法在Thread类中,是一个普通方法,由对象调用该方法
boolean isInterrupted() 判断是否发送了中断操作, true:发生中断操作 false:未发生中断操作
方法特点:
1、如果当前线程是可中断的阻塞状态(join、sleep、wait等方法会导致线程进入阻塞撞状态:WATING / TIMED_WAITING状态),在任意其他的线程中调用interruprt方法,那么会立即抛出抛出InterruptedException来停止的阻塞状态
2、如果当前线程是可运行状态。调用interruprt方法,线程还是会继续执行,直到发生了sleep、join、wait等方法的调用,才会在进入阻塞之后,随后立即抛出InterruptedException,跳出阻塞状态
public void interrupt() { synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag 注意这句 b.interrupt(this); return; } } interrupt0(); } private native void interrupt0(); //JNI方法,中断操作由系统来提供中断的方式
sleep():线程休眠
会让线程休眠,而且那个线程调用,那个线程休眠 ,TimeUtil.sleep(long),Threrd.sleep、或当前的额线程t.sleep,其结果都是当前的线程休眠
方法介绍:
sleep(long millis)
sleep(long millis, int nanos)
都是提供休眠操作,时间单位粒度不同
sleep 是Thread类提供的方法,会抛出InterruptedException异常
方法特点:
1、sleep休眠期间,会让出CPU使用权,但线程任然持有锁
2、sleep休眠时间到了之后,不会立即执行,而是线程由“阻塞状态”进入到“就绪状态”
public static native void sleep(long millis) throws InterruptedException //JNI public static void sleep(long millis, int nanos) throws InterruptedException { if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } sleep(millis); }
问:sleep和wait方法区别?
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
面试题:wait和notify的异同?
相同点:
- 都Object中的方法。
- 调用必须在同步的前提下。(因为该方法是要用锁对象调用,而只有在同步的情况下才有锁,也就是notify方法调用必须放在synchronized方法或synchronized块中)。
不同点:
wait():使得当前线程必须要等待,直到另外一个线程调用notify()或者notifyAll()方法(注意必须调用notify/notifyAll方法才能唤醒)。会导致锁的释放。
notify():可以唤醒当前锁对象下等待的另一个单个线程。优先级较高的优先唤醒,如果优先级一样,则随机唤醒。notifyAll()方法的调用可以唤醒当前锁对象下等待的所有线程。
daemon:守护线程
方法介绍:
setDaemon(boolean on) 设置线程为守护线程 true:表示是守护线程 false:非守护线程 默认false
boolean isDaemon():判断当前线程是否是守护线程 true:守护线程
Java中的线程主要有两种:用户线程和守护线程
守护线程和用户线程是什么?
用户线程一般用户执行的用户级的线程
守护线程:也叫做后台线程,脱离于终端,用来服务于用户线程 例如:GC是一个单独的线程来处理,GC线程就是一个守护线程
守护线程的生命周期?
守护线程的生命周期是依赖于用户线程,当有用户线程存在,守护线程就会存活,当没有用户线程存在,那守护线程也随之消亡,
需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。
public final void setDaemon(boolean on) { checkAccess(); if (isAlive()) { throw new IllegalThreadStateException(); } daemon = on; } public final boolean isDaemon() { return daemon; }
Priority:线程的优先级
线程优先级,顾名思义:就是来指导线程的执行优先级的
方法介绍:
int getPriority() 获取优先级
setPriority(int newPriority) 设置优先级
方法特点:
1、java线程的优先级并不绝对,它所控制的是执行的机会,也就是说,优先级高的线程执行的概率比较大,而优先级低的线程也并不是没有机会,只是执行的概率相对低一些。
2、Java线程一共有10个优先级,分别为1-10,数值越大,表明优先级越高,一个普通的线程,其优先级为5;
线程的优先级具有继承性,如果一个线程B是在另一个线程A中创建的,则B叫做A的子线程,B的初始优先级与A保持一致。
优先级范围:
public final static int MIN_PRIORITY = 1; //最小优先级
public final static int NORM_PRIORITY = 5; //默认优先级
public final static int MAX_PRIORITY = 10; //最大优先级
java 中的线程优先级的范围是1~10.最小值是1,默认的优先级是5,最大值是10.“高优先级线程”会优先于“低优先级线程”执行
public final int getPriority() { return priority; } public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } } private native void setPriority0(int newPriority); //JNI
线程调度
用户级调度
可以提供的调度方式:
1、调整线程优先级:Java线程有优先级,优先级高的线程获得较多的运行机会(运行时间);
static int Max_priority 线程可以具有的最高优先级,值为10;
static int MIN_PRIORIYT 线程可以具有的最低优先级,值为1;
static int NORM_PRIORITY 分配给线程的默认优先级,值为5;
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级;
2、线程睡眠:Thread.sleep(long millins)使线程转到阻塞状态;
3、线程等待:Object.wait()方法,释放线程锁,使线程进入等待状态,
直到被其他线程唤醒
private native void setPriority0(int newPriority); //JNI 和notifyAll());
4、线程让步:Thread.yeild()方法暂停当前正在执行的线程,使其进入等待执行状态,
把执行机会让给相同优先级或更高优先级的线程,如果没有较高优先级或相同优先级的线程,该线程会继续执行;
5、线程加入:join()方法,在当前线程中调用另一个线程的join
private native void setPriority0(int newPriority); //JNI 方法,
则当前线程转入阻塞状态,知道另一个进程运行结束,当前线程再有阻塞状态转为就绪状态;
系统级调度
主要指系统在特定的时机自动进行调度,主要说明一下可运行状态到运行状态的调度,这个是OS的调度,主要涉及的调度算法
实时系统:
FIFO(First Input First Output,先进先出算法),
SJF(Shortest Job First,最短作业优先算法),
SRTF(Shortest Remaining Time First,最短剩余时间优先算法)。
交互式系统:
RR(Round Robin,时间片轮转算法),
HPF(Highest Priority First,最高优先级算法)。
基于时间片的优先级的调度算法
临界资源和临界区
临界资源:一个时刻只允许一个线程访问,一个线程在访问临界资源时,其他线程是不能访问的,临界资源时不可剥夺资源,OS不能阻止资源的独享行为
临界区:是一个线程中访问临界资源的代码片段
临界区的使用规则:“空闲让进,忙则等待,有限等待,让权等待”,分别来解释一下:
线程安全问题?
如果是一个操作的序列,在单线程执行和多线程执行的情况下,最终得到结果永远是相同的,把这个操作序列称之为线程安全的,反之,则是非线程安全的
并发的特征
原子性
如果一个操作是不可分割的,那这就是一个原子操作。相反,如果一个操作是可以分割的,那么他就是非原子操作,(a++是线程安全操作吗?非线程安全操作)
可见性
一个变量被多个线程共享,如果一个线程修改了这个变量的值,其他的线程会立马得知这个修改,我们称这个操作具有可见性
有序性
有序性两方面表现:
1、在一个线程内存观察,所有的操作都是有序来的,所有的执行指令按照”串行“(as-if-serial)
2、在线程间观察,从一个线程观察其他线程,则线程的执行时交替执行,是正序的
内存结构:
线程局部变量表
在Java内存模型中,虚拟机栈(本地方法栈)是线程私有,存储的是局部变量表、动态链接、方法的出口
.dll 动态链接文件 ,虚拟机栈中存在一个个帧栈(对象存在一个个方法)
对象是存在于堆中
局部变量表:主要存储的数据:
基本的数据类型:存储在堆中的数据会拷贝一份到局部变量中 Integer
对象的引用:局部变量表中不存储对象,对象存在堆中,在局部变量中只存在对对象的引用地址
内存模型
堆内存中的对象和基本数据类型的备份,称为主内存(main memory),把上面所说的栈内存中用于存储变量的部分内存,称为本地内存(local memory)(或叫工作内存)
1、Java线程对于变量的操作,都是在自己的工作内存中进行的,线程不会直接读写主内存的变量
2、不同的线程无法访问对方线程工作内存中的变量
3、线程间变量的传递,需要主内存来完成的
主内存存在变量count = 10;
线程A和线程B来操作count,都会拷贝副本到本地内存?
当前线程A 执行count= 14;将结果写到主内存,此时线程B已经读取了count ,此时修无法读取最新值,会存在问题。
Java内存模型中对数据的操作存在8中操作:
volatile关键字
示例代码
统计1秒钟count++ 的次数?
/** * volatile关键字的应用: * 两个线程同时启动,交替执行 */ public class VolatileDemo { private volatile static boolean flag=true; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { int count = 0; while (flag) { count++; } System.out.println("count的次数:" + count); } }); Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("1秒开始"); try { thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } flag = false; System.out.println("1秒钟结束.."); } }); thread.start(); thread1.start(); } }
注:count会出现负数的情况(不明所以);如果不给flag加volatile关键字,会陷入循环,无法输出。
在不加volatile关键时,变量flag修改无法实时同步给另一个线程?
线程堆栈中保存线程运行时的变量值的副本,当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,
也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
注:java中的System.out.println为何会影响内存可见性?
prinltn底层代码:
public void println(String x) { synchronized (this) { print(x); newLine(); } }
使用了synchronized上锁这个操作后会做一下操作:
1.获得同步锁
2.清空工作内存
3.从主内存中拷贝对象副本到本地内存
4.执行代码(打印语句或加加操作)
5.刷新主内存数据
6.释放同步锁
总结:
对于普通的共享变量来讲,比如我们上文中的flag,线程A将其修改为true这个动作发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B缓存了status的初始值false,此时可能没有观测到flag的值被修改了,所以就导致了上述的问题。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,有点炮打蚊子的意思。比较合理的方式其实就是volatile
volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操作会导致其他线程中的缓存无效。
提炼:voliate锁轻量级锁,synchronized重量级..
volatile特征
禁止指令的重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a
public class TestVolatile { int a = 1; boolean status = false; /** * 状态切换为true */ public void changeStatus(){ a = 2;//1 status = true;//2 } /** * 若状态为true,则running。 */ public void run(){ if(status){//3 int b = a+1;//4 System.out.println(b); } } }
假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?
答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远是按照其出现顺序执行的。重排序的依据是happens-before法则
保证内存的可见性
volatile通过一定的机制保证主内存和工作内存中的数据具有实时感知最新的变化
volatile工作原理
在volatile关键字所修饰的变量时,在汇编层代码上,会添加一个lock前缀的指令
Lock前缀指令相当于添加了一个内存屏障,内存屏障提供的功能:
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、它会强制将对缓存的修改操作立即写入主存;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
在上图中,如果是普通变量:
1,变量值从主内存(在堆中)load到本地内存(在当前线程的栈桢中);
2,之后,线程就不再和该变量在主内存中的值由任何关系,而是直接操作在副本变量上(这样速度很快),这时,如果主存中的count或本地内存中的副本发生任何变化,都不会影响到对方,也正是这个原因导致并发情况下出现数据不一致;
3,在修改完的某个时刻(线程退出之前),自动把本地内存中的变量副本值回写到对象在堆中的对应变量。
如果是volatile修饰的变量:
volatile仍然在执行一个从主存加载到工作内存,并且将变更的值写回主存的操作,但是:
1,volatile保证每次读取该变量前,都判断当前值是否已经失效(即是否已经与主存不一致),如果已经失效,则从主存load最新的变量;
2,volatile保证每次对该变量做出修改时,都立即写入主存;
注意:volatile保证共享数据的可见性,有序性,却无法保证数据的原子性,
volatile的应用场景
1、一般用来修饰Boolean类型的共享状态标志位
2、单例模式下的双重校验锁
3、修饰单个的变量
总结:
简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。关于原子操作类,会在后续的文章进行介绍。
Atomic
Java 中Atomic操作,比如AtomicInteger等原子操作,提供了i++等原子操作
如果读取操作,等将value声明为volatile的,保证在没锁的情况下,数据是同步的
private volatile int value;
public final int get() {
return value;
}
涉及到数据变更:AtomicInteger的++i操作:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而这里的comparAndSet(current,next)就是前面介绍CAS的时候所说的依赖JNI实现的乐观锁做法:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
除了基本数据类型的原子操作类,JUC还提供了一下的Atomic类
提供原子类的目的是为了解决基本类型操作的费原子性导致在多线程并发情况下引发的问题
非原子类操作:
原子类操作:
CAS
CAS(Compare And Set),多个线程通过CAS尝试修改同一个变量,只有一个线程在同一时刻进行修改,而其他的操作失败,
失败的线程不会挂起,告诉失败的线程可以再次尝试:
CAS操作涉及到三个操作数:
需要读写的内存位置(V)、进行比较的预期的原值(A)、待写入的新值(B)
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,
它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)
CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;
否则,不要更改该位置,只告诉我这个位置现在的值即可
CAS引起ABA的问题
假设存在以下线程操作序列:
1、线程1从内存位置V中取得A
2、线程2从内存位置V获取A
3、线程2进行一些操作,将A修改为其他的结果,将B修改为A
4、线程2将A再次写入到位置V
5、线程1进行CAS操作,发现位置V中任然是A,直接修改为B,操作成功
6、尽管线程1操作成功,但并不该表该过程没有问题,对于线程1而言,线程2 的修改以导致数据丢失
举例说明:一个链表ABA的例子:
1、现有一个单向链表实现的堆栈,栈顶为A,线程1获取到A.next为B,线程1希望通过CAS操作将栈顶替换为B
2、在线程1执行CAS操作之前,线程2来执行,将A、B出站,在依次入栈D、C、A,而对象B处于游离状态
3、此时线程1执行CAS操作,检测栈顶为A,CAS成功执行,栈顶为B,实际是B.next = null,
此时堆栈只有一个B,C和D组成的链表不在堆栈中,CD 被丢弃了
(这种情况产生原因:另一个线程对原栈进行了增加元素入栈操作,而对本该在栈内的元素实施了出栈操作,
导致两个元素没有牵连,所以产生ABA问题)
ABA问题的解决方案
ABA问题的解决需要使用版本号,在变量前加上版本号,每次变量的变更操作版本号+1,那么A-B-A就变成1A-2B-3A
AtomicStampedReference解决:
package ThreadSafe; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; //原子类解决非原子操作问题 /* getStamp()获取时间戳 getReference()获得预期值 compareAndSet(预期值,更新值,预期时间戳,更新时间戳),实现CAS时间戳和预期值的对比 */ public class ThreadAtomic { // static int n; //static AtomicInteger n; static AtomicStampedReference<Integer> n; public static void main(String[] args) throws InterruptedException { int j=0; while (j<10){ //n=new AtomicInteger(0);//初始化值为0 n=new AtomicStampedReference<Integer>(0,0); Thread thread1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { // n++;用原子方法操作替代 // n.getAndIncrement();//即n++ //while语句内成功,经过非后,为false,不会再次循环;否则若两个的预期值都不相同,则会一直循环 int stamp; Integer reference; do{ stamp = n.getStamp(); reference=n.getReference(); }while (!n.compareAndSet(reference,reference+1,stamp,stamp+1)); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { int stamp; Integer reference; do{ stamp = n.getStamp(); reference=n.getReference(); }while (!n.compareAndSet(reference,reference+1,stamp,stamp+1)); } } }); thread1.start();thread2.start(); thread1.join();thread2.join(); j++; System.out.println("n的最终结果:"+n.getReference()); } } }
使用CAS会引发的问题
CAS虽然比Java中提供的锁的开销小,但是存在问题
1、ABA问题
ABA问题通过版本号解决
2、循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
synchronized关键字
线程安全的解决方案还有Synchronized,提供了线程同步的方式
Synchronized的使用方式
关键字可以修饰方法或者代码块,确保多个线程在同一时刻,只能有一个线程处理方法或者是同步块,保证线程对访问变量的可见性,有序性,原子性
- 修饰普通方法
- 修饰静态方法
- 修饰代码块
- 修饰普通方法
//修饰普通方法
public synchronized void add() {
//do something
}
synchronized加在普通方法上,锁住的是当前的对象实例
- 修饰静态方法
//修饰静态方法
public static synchronized void update() {
//do something
}
synchronized加在静态方法上锁住的是当前的class实例,class数据存储在方法区中,锁的静态方法相当于是该类的全局锁
- 修饰代码块
//修饰代码块
public void del(Object obj) {
synchronized (obj) {
//do something
}
}
synchronized加在obj的实例上,锁的是当前obj的代码块
Synchronized特点
Synchronized修饰的方法或者代码块相当于并发中的临界区,在同一时刻JVM只允许一个线程进入执行。synchronized通过锁机制来达到同一时刻只允许一个线程进入执行的效果,在并发编程中,Synchronized锁机制可以做到线程并发的原子性,有序性,可见性
通过synchronized关键字来处理统计1秒钟count++的次数
private static boolean flag = true;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (isTrue()) {
count++;
}
System.out.println("count :" + count);
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//1秒钟后将标志位修改为false
// flag = false;
CountDemo.setFlag(false);
System.out.println("1秒钟结束");
}
});
thread.start();
thread1.start();
}
private static synchronized boolean isTrue(){
return CountDemo.flag;
}
private static synchronized void setFlag( boolean falg) {
CountDemo.flag = falg;
}
用法1:修饰代码块(同一个对象时)
不同对象时:
注:应该是两把锁,所以两个线程可以同时执行
线程thread1执行的是syncThread1对象中的synchronized代码(run),而线程thread2执行的是syncThread2对象中的synchronized代码(run);
我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行(所以可能出现0 1 2 2 3 4这种情况)。
总结:
一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。(两个不同的对象可以同时访问两个非synchronized代码块,不能同时访问一个synchronized代码块)
2.修饰方法
写法一:
{
// todo
}
{
synchronized(this) {
// todo
}
}
1. synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
在子类方法中加上synchronized关键字(默认子类继承不同步,手动加上后才会同步)
class Parent { public synchronized void method() { } } class Child extends Parent { public synchronized void method() { } }
//在子类方法中调用父类的同步方法(子类也会同步) class Parent { public synchronized void method() { } } class Child extends Parent { public void method() { super.method(); } }
注:在定义接口方法时不能使用synchronized关键字。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
谈谈volatile和synchronized两者的区别
volatile:可见性。禁止重排序。
synchronized:原子性,可见性,可重排序,会阻塞。
从使用上来看
1) volatile关键字是变量修饰符,只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数、局部变量、常量等;
2) synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块或类;
3) volatile修饰的变量可以为null,synchronized关键字同步语句块的monitor对象不能为null;
对原子性的保证
1) volatile无法保证原子性;(因此volatile无法替代synchronized)
2) 由于synchronized是一种排他的机制,因此被synchronized关键字修饰的同步代码是无法被中途打断的,因此其能够保证代码的原子性;
对可见性的保证
两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同
1) Synchronized借助于JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中;(因为每次只允许一个线程进行操作)
2) 相比于synchronized关键字,volatile使用机器指令“lock;”的方式迫使其他线程工作内存中的数据失效,不 得不从主内存中进行再次加载;
对有序性的保证
1) volatile关键字禁止JVM编译器以及处理器对其进行重排序,所有它能够保证有序性;
2) 虽然synchronized关键字所修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况,但是由于synchronized关键字同步的作用,所以对程序来说没有任何的影响;
其他
1) volatile不需要加锁,比synchronized更轻量级,而且不会使得线程陷入阻塞,synchronized关键字会使得线程进入阻塞状态;
2) volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化)
注意:volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对于n=m,n=n+1,n++等操作时,volatile关键字将会失效,不能起到像synchronized一样的线程同步(原子性)的效果。因为n++不是个原子性的操作,而是个复合操作,我们可以简单将这个操作理解为由这三步组成:1.读取2.加一3.赋值。也就是说,这三个子操作可能会割开执行:假如volatile修饰的变量n原本为10,现有线程A和线程B两个线程同时进行n++操作,线程A先对变量n操作,在取出变量后,由于某因素线程A被阻塞,这时线程B又对变量n进行操作,取出变量,加一,然后将变量立即写回主内存中,由于变量n是被volatile修饰的,所以线程A的副本中的变量n的值就会变无效,但是要注意的是,线程A在进行n++操作,在取出变量时,会将变量n赋给另一个临时变量来存储(设为tmp),tmp属于工作内存的局部变量表的,所以在变量n变无效的时候,tmp仍然是有效的。这时候线程A就会接着被阻塞之前的操作继续进行,加一,写入主内存。由于线程B在线程A取出数据后,已经对变量加一过一次,所以此时线程A再将操作完的数据写回主内存就会出现问题。
synchronized的原理
Synchronized是如何做到线程安全的,我们研究一下其修饰方法和代码块的字节码文件
Synchronized修饰的代码块和方法,通过javap反编译字节码文件可知,同步代码块中使用到了monitorenter和monitorexit执行,同步方法上在flags增加了ACC_SYNCHRONIZED修饰符
两种方式,无论哪一种,本质上是获取对象的监视器(Monitor),对象监视器的获取是排他的,同一时刻只能有一个线程来获取到Synchronized所保护对象的监视器
synchronized允许使用任何的一个对象作为同步的内容,因此任意一个对象都应该拥有自己的监视器(monitor),当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。
Synchronized的优化
偏向锁、轻量级锁、重量级锁、自旋锁
锁的状态4中状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
这几个状态随着线程竞争情况逐渐升级、锁可以升级但是不可以降级,意味着偏向锁升级轻量级锁不能降回到偏向锁,轻量级锁到重量级锁是一样,目的是提高获得锁和释放锁的效率。
偏向锁
偏向锁的操作不需要操作系统的结束,每个对象的对象头中的Mark Word的表示来区分当前的锁
CAS即Compare And Set 比较并修改,是乐观锁的实现,通过CAS加volatile能实现无锁编程
JVM使用CAS操作将线程ID的记录到Mark Word当中,修改标识位,当前线程就可以获取到锁
当线程来获取锁,执行Synchronized修饰的方法或者代码块,第一次线程获取操作将线程Id记录到对象头中,当再次来后去是,JVM通过对象头的Mark Work判断(当前的线程ID,当前的线程持有的对象的锁,继续来获取当前的对象),这个就是偏向锁,在线程没有竞争的时候,一直都是一个线程来执行
偏向锁与轻量级锁:如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除连CAS都不做。偏向锁,顾名思义,它会偏向于第一个访问它的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用锁的情况,则持有偏向锁的线程将永远是不需要在进行同步。如果运行过程中,遇到其它线程抢占资源,则持有偏向锁的线程会被挂起,jvm会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁
轻量级锁也不需要操作系统的结束,JVM对偏向锁做一下升级,变成一个轻量级锁
JVM把锁对象Account恢复成无锁状态,在当前的线程(来竞争当前对象的线程)的堆栈中个分配一个空间,叫做Lock Record空间,把锁对象的Mark Work的堆中各复制一份,叫做Displaced Mark Word,当某个线程抢到锁,将当前线程的Lock Record的地址使用CAS操作放回到Mark Word当中,并且将锁的标志修改为00,意味着当前的线程获取到了轻量级锁
其他为获得锁的线程,不会阻塞(当前还是会持有COU的执行权),JVM会让线程进行自旋几次,等待获取锁的线程释放锁,
需要将需要把这个Displaced markd word 使用CAS复制回去,接下来其他的线程就可以来后去锁,
线程进行交流获取当前的锁,执行代码。这里存在着轻度的竞争,轻量级锁仅仅使用CAS操作和Lock record就避免了重量级锁的开销,轻量级锁存在少量的线程的竞争
由于偏向锁会转换成轻量级锁,那么许多人可能就会疑惑为什么不直接使用轻量级锁呢?
引入偏向级锁是为了减少在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换线程时,使用CAS操作把获取到这个锁的线程线程的ID记录在对象的Mark Word之中,从而减少性能消耗,不过遇到多线程竞争的情况时就必须撤销偏向锁。另外一个原因就是,在HotSpot虚拟机中,大多时候是不存在锁竞争的,常常是一个线程多次获取同一个锁,因此直接使用轻量级锁会增加很多不必要的消耗,所以可以才引入了偏向锁。
偏向锁的升级过程
假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking,这就是jdk1.6的默认值),那么,当锁对象第一次被线程获取时,虚拟机将会把对象头中的标志位设位01,即偏向模式,同时使用CAS操作把获取到这个锁的线程线程的ID记录在对象的Mark Word之中,由于偏向锁不会主动释放锁,所以持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不在进行任何同步操作。但当有另外的线程去尝试获取这个锁时,就需要查看锁对象头中记录的那个线程是否还存活,如果没有存活,那么锁对象就会被置为无锁状态,且这时候其他线程是可以竞争该锁,如果获取成功该锁,该锁就又被设为偏向锁;如果对象头中记录的那个线程仍存活,那就立即查找该线程的栈帧信息,判断是否还需要此锁,如果不需要,那么该锁对象就会被置为无锁状态,且偏向其他新的线程,如果还需要此锁,那么就先暂停当前线程,撤销掉偏向锁,升级为轻量级锁(00)的状态。
偏向锁可以提高带有同步但无竞争的程序性能。但是如果程序中大多数锁总是被多个不同的线程访问,那么偏向锁模式就是多余的。
总结一下:
-
偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
-
轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
-
重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况
各种锁的使用场景
偏向锁:通常只有一个线程访问临界区。
轻量级锁:可以有多个线程交替进入临界区,在竞争不激烈的时候,稍微自旋就能获得锁。
重量级锁:线程间出现了激烈的竞争就需要使用重量级锁,此时未获取到锁的线程会进入阻塞队列,需要操作系统介入。
jvm设置偏向锁和轻量级锁,就是为了避免阻塞,避免操作系统的介入。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
线程间通信
通信方法
wait():调用wait方法的线程,当前持有锁的该线程等待,直至该对象的另一个持锁线程调用notify/notifyAll操作
wait(long timeOut)、wait(long timeOut,int nanos)
notify():通知持有该对象锁的所有线程中的的随意一个线程被唤醒
notifyAll():通知持有该对象锁的所有线程被同时唤醒
线程通信的方法:wait otify otifyAll操作都是属于Object类提供的方法,即所有的对象都具有该方法
使用示例
wait和notify使用的基本案例:
加了同步代码块之后:‘
Condition对象的方法使用案例:
Object和Condition对象注意点:
- 调用notify和wait的必须是作用同一个对象
- 对于wait、notify、notifyAll的调用,必须在该对象的同步方法或者同步代码块中,锁作用的对象和wait等方法必须作用于同一个对象
- wait方法在调用进入阻塞之前会释放锁,而sleep或join是不会释放锁的
- 线程状态装换是,当wait被唤醒或超时是,并不是直接进入到运行或者就绪状态,而是先进入到Block状态,抢锁成功后,才能进入到可运行状态
- condition的await方法必须和Lock配合使用
当锁的对象和调用wait/notify的对象不是同一个对象时,会抛出IllegalMonitorStateException
抛出异常:
再看一个问题:
基于以上分析,一旦wait先调用则线程因为锁无法继续执行而阻塞下来,事实是这样嘛?
通过运行测试,程序是可以执行的,为什么?
因为wait方法在调用进入阻塞之前会释放锁,则调用notify操作的线程就可以抢到Object对象的锁,进而调用notify方法
Lock和ReentrantLock
JDK1.5之前的Synchronized锁是独占锁,性能不高,Lock锁借助JNI完成的高级锁实现
Lock接口
LOCK接口实现的锁比Synchronized锁更加钢钒,提供了更加灵活的操作,支持多个Condition对象
Lock接口在java.util.concurrent.locks包路径下
看一下Lock提供的方法
lock
void lock()
获取锁,如果锁不可用时,当前的线程会进入休眠
lockInterruptibly
void lockInterruptibly() throws InterruptedException
如果当前线程未被中断,则可以获取锁
如果当前为发生中断,且锁不可用(锁被其他线程占有),当前线程会进入到休眠
tryLock
boolean tryLock()
尝试性获取锁,仅在锁空闲是才能获取锁,如果锁可用,则立即获取锁,返回true,如果锁不可用,则返回false
通常对于不是必要必须获取锁的操作可能有用
tryLock(long time, TimeUnit unit)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
在一定时间内尝试性的获取锁,如果锁在给定的等待时间内空闲,并且当前的线程未被中断,则可以后去锁,如果锁可用,则立即获取锁,返回true,如果锁不可用,则返回false
在以下情况该线程会处于休眠状态:
- 锁有当前的线程获取
- 其他牧歌线程中断当前的线程,并且支持岁锁获取的中断
- 已超过指定的等待时间
unlock
void unlock()
释放锁,对lock()/lockInterruptibly()/trylock()/tryLock(X)等操作,加锁和释放锁都必须通过方法的显性调用来实现
newCondition
Condition newCondition()
返回绑定到此Lock实例上的新的Condition实例,Condition实例是可以进行线程间通信的
AQS(AbstractQueuedSynchronizer
)
什么是AQS
队列同步器
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组 件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获 取线程的排队工作;
AQS(AbstractQueuedSynchronizer
),AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。如果你有看过类似 CountDownLatch
类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer
的内部类 Sync
。可见 CountDownLatch
是基于AQS框架来实现的一个同步器.类似的同步器在JUC下还有不少。(eg. Semaphore
)
AQS用法
如上所述,AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore
用它来表现剩余的许可数,ReentrantLock
用它来表现拥有它的线程已经请求了多少次锁;FutureTask
用它来表现任务的状态(尚未开始、运行、完成和取消)
在介绍锁之前,介绍一下AQS(AbstractQueuedSynchronizer)是J.U.U中最复杂的一个类
通过继承体系可以看出,AQS类是countDownLatchReentrantLock...的实现前提,公平性锁和非公平性锁、从condition...的实现的基础
AQS核心字段
AQS里面有三个核心字段
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
其中state描述的是有多少线程获取锁,
AQS的state的值:
state = 0 表示锁是空闲状态
state > 0 表示锁被占用,state的值n表示被线程占用的次数
state < 0 表示溢出
head和tail加上CAS操作构成了一个FIFO的队列
waitStatus:节点的等待状态,一个节点可以存在的状态
CANCELLED = 1;
当前的线程被取消,节点的操作因为超时后者对应的线程被interrupt,节点不应该存在次状态,一旦节点为该状态,就会从队列中提出
SIGNAL = -1;
表示当前节点的后续节点包含的线程需要执行,也就是unpark节点的继承节点互殴将要称为blocked状态的线程,一旦获取锁的线程释放锁之后,就需要唤醒当前节点的后续节点
CONDITION = -2;
当前节点在等待Condition,在Condition队列中,表明节点对应的线程不满足codition条件而被阻塞,
PROPAGATE = -3;
场景下后续的acquireShared能够得以执行
0 当前的节点在sync队列,等待着获取锁,正常的状态,新产生的非Condition节点都是次状态
volatile Node prev:此节点的前一个节点,节点的waitStatus依赖前一个节点的状态
volatile Node next:次节点的后续节点,后一个节点是否被唤醒(unpark())依赖于当前节点是否被释放
volatile Thread thread:当前的阶段绑定的线程
Node nextWaiter:下一个等待条件(Condition)的节点
AQS同步器的原理
AQS的原理是实现了一个同步器,同步器支持两个主要操作
获取锁:
首先判断当前的状态是否允许获取锁,如果是则直接获取锁,否则就则塞操作或者获取失败,如果是独占锁就可能会阻塞,如果是共享锁就可能失败,如果是阻塞线程,那么线程就会进入阻塞队列,当状态为允许获取锁修改状态,并且从阻塞队列里删除掉
释放锁:
这个过程就是修改状态位,如果有线程因为状态位则塞的话就会唤醒队列中的一个 或者更多线程
支持以上操作必须满足以下条件:
- 原子性的操作同步器的状态位
- 阻塞和唤醒线程
- 一个有序的队列
状态位的原子操作
使用的是一个32位的整数state来描述状态为,使用CAS操作来修改状态
阻塞和唤醒线程
借助JNI在LockSupport类中实现来操作线程的阻塞和唤醒
LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object, long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object, long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)
park()是在当前的线程中调用,会导致线程阻塞,unpark()也是在当前线程中调用,用来唤醒线程
有序列队
在AQS采用的是CHL的FIFO队列解决线程有序等待问题
入队:
就是当前队列中的尾节点指向新节点,新节点的prev指向队列中的尾节点,然后将同步器的tail节点指向新节点。
通过源码看出,将节点添加到队列的尾节点,使用了CAS方法compareAndSetTail
入队的过程:
出队列:遵循队列FIFO的规范
需要借助于CAS保证并发的安全性,同一时刻一个线程获取到同步状态
ReentrantLock
基本实现:
syschronized和lock区别:
1. syschronized是java内置关键字,lock是个java类
2.自动释放锁与手动调用区别
3.是否能判断获取锁的公平性(公平性锁的实现:Lock类转换为公平锁FairLock,原理是:每一个Lock调用的线程都回去进入到队列,当解锁后,只有队列中的第一个线程被允许获取锁)
4.lock适合大量代码同步问题, syschronized少量
ConcurrentHashMap的文章:ConcurrentHashMap
死锁:
死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。
如何避免死锁
互斥条件不能破坏。
在有些情况下死锁是可以避免的。三种用于避免死锁的技术:
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测