1.何为线程安全 造成线程不安全的原因
线程安全即能保证多线程情况和单线程情况下的执行结果时一样的
造成线程不安全的原因主要在于对共享数据的处理上,JMM中的主线程子线程的数据不一致 是出现线程安全的根本原因,
多个线程如果同时对共享数据进行读取 则可能造成线程不安全,所以需要通过线程互斥来实现线程安全,线程互斥即
同时只会有一个线程执行,其他线程需要等待
哪些是共享数据:堆区/方法区/常量区(常量不能改可不管) 在java代码里体现的就是对象/static
数据库 当然数据库自己有锁
IO资源 比如文件 IO是阻塞性的 所以自带线程互斥
1.synchronized关键字 隐式监视器锁
synchronized代码块 字节码会有monitor监控
synchronized(s){
System.out.println(i);
setI();
}
//对应javap后的 要执行这段字节码,需要能进入到monitor 持有锁
6: monitorenter//进入monitor 执行objectMonitor
//..省略无关代码
22: monitorexit//monitor退出
28: monitorexit //增加异常机制,确保异常时monitor也会退出
29: aload_2
30: athrow
也就是对于synchronized关键字 编译成字节码时都会做对应的标记处理,要进入这一段监控的代码 需要
持有监控需要的锁,
static上的锁是class
方法上的锁是this
代码块上的锁是指定的
暂且统一称为锁对象lockObj
线程thread_a进入
->如果monitor._owner == null -> 进入执行 monitor.count++,monitor._owner=thread_a当前线程拥有这个监视器;
如果monitor._owner == thread_a ->进入执行 monitor.count++ 即可重入锁
如果monitor._owner == 其他线程 ->等待执行 进入monitor._entrySet
->执行途中出现threa.wait 则进入monitor._waitSet等待序列 并monitor.count–;
->执行完成monitor._owner=null;
因为每个对象在对象头都可以维持一个monitor,所以每个对象都可以作为锁对象,
wait/notify/notify方法操作的是monitor,这三个方法是监视器方法
而每个对象后可以维持monitor,故wait/notify/notify方法在Object中,
synchronized方法 字节码flags中会有标记
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
针对方法上的jvm会在访问标志区的ACC_SYNCHRONIZED后,自动增加monitorentry和monitorexit,
且monitorexit是在方法return/抛异常后都会执行的
可重入锁
即syn(lockObj){调用其他的syn(lockObj)方法或块}的情况下,后面的同步快发现调用者持有正确的锁时,可以执行,
同时monitor.count++,执行完仍旧会–,当count==0时 owener=null
对象头和monitor
堆区的每个对象都包含对象头
头里主要有两部分,一部分指向方法区表明自己的身份
一部分记录一些Mark Word标记信息,如是否该GC的标记/分代年龄/hascode/锁信息
因为Mark Word全部记录的话占内存,故除基本信息外,其他信息会随着对象状态而做改变 比如可能是偏向锁->轻量级锁->重量级锁中的一种 或者没有
而1.6以前是只有重量级锁,synchronized通常说的就是重量级锁
在重量锁情况下,Mark Word的重量锁指针指向的是monitor
又因为monitor是基于操作系统的,操作系统实现线程之间的切换时需要从用户态转换到核心态,这种状态的切换时间成本高,所以是重量级锁
基于此,java6对重量锁进行了优化,就有了偏向锁,轻量级锁,自锁锁(即给自己加几十次空循环 避免被操作系统层面挂起),锁消除
线程中断与synchronized
线程在非阻塞状态调用t.interrupt()这个方法是不会中断线程的,当然thread.isInterrupt判断会阻塞线程,这个判断可以让中断生效
wait等需要获得monitor对象,否则会包非法monitor异常,而synchronized可以获得monitor对象,所以wait需要放到synchronized中
2……..无锁CAS与Unsafe类及其并发包Atomic
a.sychnorized的缺点很明显,会造成其他线程阻塞,cpu利用率不高,存在死锁风险
这也是悲观锁的固有缺点
b.于是就思考无锁操作
1.用volatile关键字这种轻量级锁 提高利用率,但volatile只能保证可见性,无法保证读写操作的原子性
2.乐观锁,CAS,基于底层硬件原语,使用非阻塞的方式保证读写的原子性
但这个也是存在缺点的
ABA问题,Unsafe类里已增加方法加时间戳解决
boolean b = userAtomicReference.compareAndSet(user1, user3);//true
//true 这就是ABA问题 一个线程改了 另一个线程又改回来
boolean b1 = userAtomicReference.compareAndSet(user3, user1);
自旋时间过长的问题,目前并没有好的解决放啊,Atomic里的方法有些方法通过死循环CAS确保成功,在多线程竞争激烈时,自旋时间就会较长
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));//这种自旋可能时间会很长
return prev;
}
难以保证多数据的原子性,只能将多个数据加入到AtomicReferced中才能保证,这种情况下可以考虑用回synchronized
Unsafe类中就定义各种native方法直接操作内存,获取内存里字段值/设置内存屏障/内存大小..
2.. Lock
区别
2.volatile关键字和内存可见性
3.java5的并发包 java.util.concurrent
Future/Callable 原理
a.Callable与Runnable的转换
b.获取返回值的原理
4.java8 Fork/join机制
合久必分 分久必和
将一个大任务拆分成多个小任务 充分发挥CPU多核的优势
6.死锁 多线程死锁 / 数据库死锁 /各类资源死锁
死锁的发生
死锁的预防
死锁的实际案例
7.指令重排序
cpu优化 会造成多线程情况下的线程不安全
8.JMM中的happens-before原则
这是检验线程是否安全的原则
1 happens-before 2 即1操作在2之前发生,这里的发生指的是内存中的执行顺序,而实际发生顺序是不一定的 有可能指令重排序了
程序顺序规则:即单线程情况下按程序的顺序执行
监视器锁规则:锁释放happens-before对该锁的加锁 比如两个syn方法,a(),b(),那a方法释放锁总是在b方法加锁之前
volatile规则:写操作总是在其他线程的读操作之前
传递性:A在B之前 B在C之前 则A在C之前
start规则:开启一个子线程,start方法在子线程的所有操作之前发生
join规则:join之前的操作happens-before join之后的操作
9.并发包中的各种简介
AQS
CountDownLatch
Sempaphore
CyciylBalack
线程池
阻塞队列
Futrue Callable