多线程
1.Java中的多线程实现方式:
继承Thread
实现Runnable、Callable
线程池(推荐)
2.多线程常用操作方法:
sleep():线程休眠,运行态 -> 阻塞态,不会释放锁,立即交出CPU
yield():线程让步,运行态 -> 就绪态,不会释放对象锁,交出CPU时间不确定,由系统调度
只会让拥有相同优先级的线程有获取CPU的机会。
join():当前线程等待另一线程执行完毕后再恢复执行,运行态 -> 阻塞态,会释放对象锁
多线程等待与唤醒机制
wait()/notify():synchronized
要使用wait/notify,必须在同步方法或同步代码块中使用,会释放对象锁
wait() 运行态 -> 阻塞态
notify() 阻塞态 -> 就绪态
Object及其子类的每个对象都有两个队列:
同步队列:获取该对象锁失败的线程进入同步队列
等待队列:调用wait()的线程进入等待队列(等待被notify)
守护线程:
Java线程只有两类,用户线程,守护线程
创建的线程默认都是用户线程,包括主线程
守护线程:后台线程,只有当当前JVM进程中最后一个用户线程终止,守护线程会随着JVM一同停止。
GC线程就是典型的守护线程
setDeamn将用户线程置为守护线程
同步
保护的对象是谁 锁是谁
JVM 内存模型(JMM):并发程序
描述共享变量(类成员变量、静态变量)如何存储
工作内存:变量在线程中的操作(读写)必须在工作内存中进行,
工作内存中保存了所有变量的副本
主内存:所有变量必须在主内存中存储
线程特性:
原子性:一组操作要么同时发生,要么一个都不发生
基本数据类型的读写操作都属于原子性操作
可见性:某一线程对于变量的修改对于其他线程而言是立即可见的
synchronized(lock)、volatile、final
有序性:
在单线程场景下,代码执行顺序就是代码书写顺序。
多线程场景下,所有代码都是乱序的。
线程安全指的是以上三个特性同时满足
使用synchronized解决同步问题
同步代码块
synchronized(对象)
-任意类的对象
-类.class
同步方法
-修饰类成员方法 锁的是当前对象this
-修饰类方法(static) 锁的是当前类的反射对象
class Account {
String password;
short sal;
Object salLock = new Object();
Object passLock = new Object();
short quQian() {
synchornizd(salLock)
}
void cunQian() {
synchornizd(salLock)
}
String seePassword() {
synchornizd(passLock)
}
void gaiPassword() {
synchornizd(passLock)
}
}
锁对象
salLock -> sal
passLock -> password
synchronized底层实现:对象Monitor机制
任意Object及其子类对象内部在JVM中都附加Monitor.获取一个对象的锁,实际上就是获取该对象Monitor(计数器).
当一个线程尝试获取对象Monitor时,
I.若此时Monitor值为0,该对象未被任何线程获取,当前线程获取Monitor,将持有线程置为当前线程,Monitor值+1;
II.若此时Monitor值不为0,此时该Monitor已被线程持有
a.若当前线程恰好是持有线程,Monitor值再次+1,当前线程继续进入同步块(锁的可重入)
b.若持有线程不是当前线程,当前线程进入同步队列等待Monitor值减为0
加锁:monitorenter + 1
解锁:monitorexit - 1
任意时刻只有当Monitor值为0表示无锁状态。
JDK1.6后synchronized优化
CAS:Compare And Swap 无锁保证线程安全
CAS(O,V,N)
其中:
V:主内存存放的实际变量值
O:当前线程认为的变量值
N:希望将变量替换的值
例如:
主内存 ticket = 0;
线程1 ticket = 0; cas(1,1,0) O==V,认为此时没有线程修改主内存的值,0 -> 主内存
线程2 ticket = 0; cas(1,0,0) O != V,
认为此时已经有别的线程修改了主内存的值,修改失败,返回主内存的最新值
线程3 ticket = 1;
当O == V时,认为此时没有线程修改变量值,成功将N值替换回主内存
当O != V时,此时已有线程修改变量值,替换失败,返回主内存的最新值再次重试。
ABA问题:
添加版本号
num = 0;
线程1 cas(0,0,1) 1-> 主内存 num.1
线程2 cas(1,1,0) 0 -> 主内存 num.2
线程3 cas(0,0,5) 5 -> 主内存 num.0
此时线程2恰好将值修改为主内存中的值,线程3会认为在他前面并没有线程修改过主内存中的值,但实际上线程1和线程2都修改过,为了避免此问题,使用添加版本号来解决。
关于锁的问题:
偏向锁 -> 轻量级锁 -> 重量级锁(JDK1.6之前 synchronized就是重量级锁)
重量级锁(悲观锁):
获取Monitor的失败的线程进入同步队列,状态置为阻塞态
同一时刻有不同线程在进入同步块
偏向锁(乐观锁):
认为只有一个线程在来回进入同步块,
直接将加锁与解锁的过程都省略,每次进入同步块之前
只是判断一下同步块线程是否是当前线程
轻量级锁:
不同时刻有不同的线程进入同步块
每次线程在进入同步块时都需要加锁与解锁
随着竞争的不断升级,锁也会不断升级,锁不会降级。
自适应自旋:重量级锁的优化
获取锁失败的线程不会立即阻塞,而是在CPU空跑一段无用代码,若在此时间段成功获取锁,
则下次再获取锁失败时,空跑时间适当延长;否则下次空跑时间缩短。
锁粗化
static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
sb.append("hello");
sb.append("world");
sb.append("bit");
}
将多次连续的加减锁过程粗化为一次大的加锁与解锁过程,减少无用的加减锁过程,提高效率.
锁消除
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("hello");
sb.append("world");
sb.append("bit");
}
当变量为线程私有变量时,将原先方法上的synchronized消除掉。
死锁
class Boy{}
class Girl{}
public class DeadLockTest {
public static void main(String[] args) {
Boy boy = new Boy();
Girl girl = new Girl();
Thread boyThread = new Thread(() -> {
synchronized (boy) {
System.out.println("my name is zs");
synchronized (girl) {
System.out.println("我是男孩!");
}
}
});
Thread girlThread = new Thread(() -> {
synchronized (girl) {
System.out.println("my name is beauty");
synchronized (boy) {
System.out.println("我是女孩!");
}
}
});
boyThread.start();
girlThread.start();
}
}
产生死锁的四个条件:
1.互斥:资源x在任意一个时刻只能被一个线程持有
2.占有且等待:线程1占有资源x的同时等待资源y,并不释放x
3.不可抢占:资源x一旦被线程1占有后,其他线程不能抢占x
4.循环等待:线程1持有x,等待y;线程2持有y,等待x;
死锁的产生原因:以上四个条件同时满足
synchronized如何解锁死锁?
JDK1.5 Lock体系
a.使用格式
try {
// 同步代码块
// 显式加锁
lock.lock();
}catch (Exception e) {
}finally {
// 显式解锁
lock.unlock();
}
b.常用方法
lock() : 加锁,语义与synchronized完全一致
unlock() : 解锁
void lockInterruptibly() throws InterruptedException:响应中断加锁
boolean tryLock():非阻塞式获取锁.获取锁成功返回true,进入同步块;
获取锁失败返回false,线程继续执行其他代码
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException: 支持超时
synchronized与ReentrantLock的关系与区别:
1.都属于独占锁(任意一个时刻,只有一个线程能获取到资源)的实现,
都支持可重入锁。
2.synchronized是关键字,JVM层面实现;
ReentrantLock是Java语言层面实现的"管程"
3.ReentrantLock具备一些synchronized不具备的功能:
响应中断、非阻塞式获取锁、支持超时获取锁、支持公平锁
公平锁:等待时间最长的线程最先获取到锁