共享问题
问题举例
多个线程共享一个变量,对变量的值进行读取和修改,会出现这个变量的最终值不符合预期的结果。这是由分时机制导致的。举例以下代码:
@Slf4j
public class Test01 {
static int j = 9;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable bolWatter = ()->{
for(int i=0; i<5000; i++){
j++;
}
};
Runnable mulAction = ()->{
for(int i=0; i<5000; i++){
j--;
}
};
Thread t2 = new Thread(bolWatter, "t2");
Thread t3 = new Thread(mulAction, "t3");
t2.start();
t3.start();
t2.join();
t3.join();
log.info("j: {}", j);
}
}
实际上在字节码中,他是做如下操作,不是一步到位,cpu时间分片随时都有可能停止,导致指令交错的问题
临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源,虽然多个线程读取共享资源没有问题,但是如果多个线程对共享资源进行读写操作时发生了指令交错,就很可能会出现意想不到的情况
- 一段代码块如果存在对共享资源的多线程读写操作,称这段代码为临界区。
- 举例,对共享资源进行读写的方法块
竞态条件
多个线程在临界区执行,由于代码的执行序列不同而导致无法预测的结果,称之为发生了竞态条件
synchronized
应用之互斥
为了解决上面提到的竞态条件的发生,有以下几点方法
- 阻塞式的方案:synchronized,Lock(加锁)
- 非阻塞式的方案:原子变量
相关语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
使用示例
针对刚刚所提及的线程安全问题的代码进行修改
@Slf4j
public class Test01 {
static int j = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Runnable bolWatter = ()->{
for(int i=0; i<50000; i++){
synchronized (Test01.class){
j++;
}
}
};
Runnable mulAction = ()->{
for(int i=0; i<50000; i++){
synchronized (Test01.class){
j--;
}
}
};
Thread t2 = new Thread(bolWatter, "t2");
Thread t3 = new Thread(mulAction, "t3");
t2.start();
t3.start();
t2.join();
t3.join();
log.info("j: {}", j);
}
}
发现运行代码之后已经没有错误了。
你可以做这样的类比:
synchronized( 对象 ) 中的对象,可以想象为一个房间( room ),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1 , t2 想象成两个人
当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙, t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count -- 代码
案例分析
如果是静态方法,那么锁就是类对象,不是this ,这里不会互斥
线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
可以给类加上final增加类的线程安全性,防止被继承,重写方法,然后开启多线程
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
java中 arrayList的线程代替是vector
Monitor
Java对象头
以32位虚拟机为例
普通对象的对象头在内存中的结构如下图所示,klass word是指向所从属的class,可以找到他的类对象。
mark word结构如下图所示
举例:int占4字节,Integer加上对象头8加上值4字节,一共12字节
Monitor(锁)
Monitor被翻译为监视器或者管程,每个java对象都可以 关联Monitor对象。
如果使用synchronized给对象上锁(重量级)之后,该对象的Mark word中就被设置指向Monitor对象的指针
原理之synchronized
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但是多线程访问时间是错开的,也就是没有竞争,那么可以用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是synchronized
首先解释下以下代码块:
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
下图所示,线程1发现锁对象的状态为已经时轻量级锁
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果线程自旋成功,即这时候持有锁的线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞。自旋可以理解为竞争资源的时候,线程不马上进行阻塞,而是原地观察,如果这段时间还没能等待锁,才进入阻塞状态
偏向锁
轻量级锁在没有竞争时(就只有自己这个线程),每次重入仍然需要执行CAS操作,这样仍然比较耗时,java 6中引用偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的mark word头,之后发现这个线程ID是自己的就表示没有经常,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
重入:在加了锁的代码块中调用其他方法,其他方法同样加了这个对象的锁,如下图所示
偏向锁的失效
1.偏向锁通过对象头状态区分,有无偏向锁是取决于倒数第三个字节
2.调用对象hashCode方法会撤销对象的偏向锁,因为偏向锁会让对象头失去hashCode
3.多个线程访问同一个对象锁也会让偏向锁失效。一个线程当他第一次获得对象锁时,对象锁会成为偏向锁,当他运行期间有其它线程来竞争,会升级为重量锁。如果运行期间没有其他线程来竞争,会维持偏向锁(线程Id有存储),如果在这期间有其它线程来到临界区,就会把偏向锁升级为轻量锁
4.调用wait/notify也会使偏向锁失效,因为这两个操作是存在于重量级锁
批量重偏向
多个线程访问同一个锁对象,但没有竞争,这是偏向了线程1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID 。
当撤销偏向锁阈值超过20次以后,jvm会觉得我是不是偏向错了呢?于是就会在给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过40次后,jvm会觉得偏向锁撤销太频繁了,根本就不应该偏向,就会将这个类的所有对象变为不可偏向,就算是新建的对象也是不可偏向的
锁消除
JIT是即时编译器会对反复执行的代码进行优化,如果一个锁对象不会被共享,那么会被优化成没有加锁,这就是锁消除
wait/notify
为了防止一个线程长时间占用锁(这个线程在等待其他资源),导致其他竞争相同锁对象的线程一直在阻塞,我们可以使用wait和notify,可以让线程等待,让其他线程notify唤醒。
wait/notify原理
waiting和blocked的区别是。waiting是获得了锁,但是又释放了锁的使用权,blocked是没有获得过锁,在等待锁。
API介绍
obj.wait()让进入object监视器的线程到waitSet等待
obj.notify()在object上正在witSet等待的线程挑一个唤醒
obj.notifyAll()让object上正在waitSet等待的线程全部唤醒
以上方法都是Obejct对象的方法,必须获得此对象的锁,才能调用这几个方法
wait和notify的正确使用姿势
- 首先来看看sleep和wait的区别
- sleep是Thread方法,而wait是Object方法
- sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
- sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁
- 他们的共同点就是状态都是TIMED_WAITING
- 如果多个线程在wait,那么notify可能会错误唤醒线程(虚假唤醒),可以改成notifyAll,然后让代码块改成循坏,如果被唤醒就判断条件是否成立,不成立就重新进入等待状态
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
模式之保护性暂停
同步模式之保护性暂停
- 即guarded suspension,用在一个线程等待另一个线程的执行结果
- 要点
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
- 如果有结果不断从一个线程到另一个线程,那么可以视同消息队列
- JDK,join的实现,future的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
以下代码和用join方法相比,消费线程不需要等待生产线程结束,只要被唤醒了就可以执行
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GuarderObject {
private Object response;
public Object getResponse(){
synchronized (this){
while(response == null){
try {
log.info("等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
log.info("获取结果");
return response;
}
public void setResponse(Object response){
synchronized (this){
log.info("赋值");
this.response = response;
log.info("唤醒");
this.notifyAll();
}
}
}
class Test{
public static void main(String[] args) {
GuarderObject guarderObject = new GuarderObject();
Runnable r1 = guarderObject::getResponse;
Runnable r2 = () -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
guarderObject.setResponse("213");
};
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2");
t1.start();
t2.start();
}
}
原理之join
扩展
Future
异步模式之生产者/消费者
@Slf4j
public class Test02 {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(1);
for(int i=0; i<3; i++){
int finalI = i;
new Thread(() -> {
messageQueue.put(new Message(finalI, "消息"+finalI));
}, "生成者"+i).start();
}
new Thread(() -> {
while(true){
try {
Thread.sleep(1000);
messageQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者").start();
}
}
@Slf4j
class MessageQueue{
private final LinkedList<Message> messageQueue = new LinkedList<>();
private int capacity;
MessageQueue(int capacity) {
this.capacity = capacity;
}
//获取消息
public Message take(){
synchronized (messageQueue){
while(messageQueue.isEmpty()){
try {
log.info("队列为空");
messageQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Message message = messageQueue.removeFirst();
log.info("take message: {}", message.getMessage());
messageQueue.notifyAll();
return message;
}
}
public void put(Message message){
synchronized (messageQueue){
while(capacity == messageQueue.size()) {
try {
log.info("队列已满");
messageQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
messageQueue.addLast(message);
log.info("put message: {}", message.getMessage());
messageQueue.notifyAll();
}
}
}
final class Message{
private int id;
private Object message;
public Message(int id, Object message) {
this.id = id;
this.message = message;
}
public int getId() {
return id;
}
public Object getMessage() {
return message;
}
}
park&Unparl
他们是LockSupport类中的方法
wait/notify和park/unpark区别
park/unpark原理
park底层原理
unpark底层原理
线程状态转换
重新理解线程状态转换
假设有线程 Thread t
情况 1 NEW -- > RUNNABLE
当调用 t.start() 方法时,由 NEW -- > RUNNABLE
情况 2 RUNNABLE < -- > WAITING
t 线程 用 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时, t 线程 从 RUNNABLE -- > WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功, t 线程 从 WAITING -- > RUNNABLE
竞争锁失败, t 线程 从 WAITING -- > BLOCKED
情况 3 RUNNABLE < -- > WAITING
当前线程 调用 t.join() 方法时, 当前线程 从 RUNNABLE -- > WAITING
注意是 当前线程 在 t 线程对象 的监视器上等待
t 线程 运行结束,或调用了 当前线程 的 interrupt() 时, 当前线程 从 WAITING -- > RUNNABLE
情况 4 RUNNABLE < -- > WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE -- > WAITING
调用 LockSupport.unpark( 目标线程 ) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -- >
RUNNABLE
情况 5 RUNNABLE < -- > TIMED_WAITING
t 线程 用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时, t 线程 从 RUNNABLE -- > TIMED_WAITING
t 线程 等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功, t 线程 从 TIMED_WAITING -- > RUNNABLE
竞争锁失败, t 线程 从 TIMED_WAITING -- > BLOCKED
情况 6 RUNNABLE < -- > TIMED_WAITING
当前线程 调用 t.join(long n) 方法时, 当前线程 从 RUNNABLE -- > TIMED_WAITING
注意是 当前线程 在 t 线程对象 的监视器上等待
当前线程 等待时间超过了 n 毫秒,或 t 线程 运行结束,或调用了 当前线程 的 interrupt() 时, 当前线程 从
TIMED_WAITING -- > RUNNABLE
情况 7 RUNNABLE < -- > TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE -- > TIMED_WAITING
当前线程 等待时间超过了 n 毫秒, 当前线程 从 TIMED_WAITING -- > RUNNABLE
情况 8 RUNNABLE < -- > TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时, 当前线
程 从 RUNNABLE -- > TIMED_WAITING
调用 LockSupport.unpark( 目标线程 ) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从
TIMED_WAITING -- > RUNNABLE
情况 9 RUNNABLE < -- > BLOCKED
t 线程 用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE -- > BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程 竞争
成功,从 BLOCKED -- > RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE < -- > TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
多把锁
如果有几个操作其实是互不相干的,但是如果只用一个对象锁的话,那么他们就是类似于串行,并发度很低,我们的解决办法就是使用多把锁
活跃性
死锁
情况:有一个线程需要同时获取多把锁,这时就容易发生死锁
活锁
两个线程共享了一个变量,这个变量决定了结束条件,如果两个线程相互让共享变量往两个不同的方向改变,那么线程就会一直运行,停止不了
饥饿
一个线程运行优先级太低,始终的不到CPU时间片,也不能结束。
顺序加锁可以避免前面说的死锁现象,两个线程获取锁的顺序是一致的
ReentrantLock
相比较于synchronized它具备以下特点:
- 可中断,不像其他说拿到了锁就没办法放开
- 可设置超时时间,可以设置等待锁的时间
- 可设置为公平锁,等待队列先进先出
- 支持多个条件变量,你不满足什么条件就去那个条件队列等待
与synchronized一样,都支持可重入
可重入
可重入指的是一个线程获取了锁之后仍然有权利在此获取这把锁,如果是不可重入锁,在第二次获得锁的时候,自己也会被锁挡住
可打断
当使用ReentrantLock的lockInterruptibly的时候,他在等待锁的时候可以被其它线程打断,就会抛出异常。防止无限制打断下去,也是一种避免死锁的方法
public class ReentrantLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {
lock.lockInterruptibly();
try {
}finally {
lock.unlock();
}
} catch (InterruptedException e) {
log.info("打断");
e.printStackTrace();
return;
}
}, "t1");
lock.lock();
try {
t1.start();
Thread.sleep(500);
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
可超时
立即失败:
@Slf4j
public class ReentrantLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
if(!lock.tryLock()){
log.info("获取不到锁");
return;
}
try {
log.info("获取到锁");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
try {
t1.start();
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
公平锁
ReentrantLock默认是不公平的,公平锁一般是没有必要的,会降低并发度。
条件变量
就是线程等待的时候(wait)进入了休息室,之前都是进入同一个休息室,唤醒的话会唤醒整个休息室的所有线程,但是ReetrantLock是可以做到多个休息室的。
@Slf4j
public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Condition condition = lock.newCondition();
Thread t1 = new Thread(()->{
lock.lock();
try {
log.info("拿到锁t1");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("醒了");
lock.unlock();
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.info("拿到锁main");
lock.lock();
try {
log.info("唤醒");
condition.signalAll();
} finally {
lock.unlock();
}
}
}
同步模式之顺序控制
交替打印abc
@Slf4j
public class Test03 {
static boolean turnToTwo = false;
public static void main(String[] args) throws InterruptedException {
Bag bag = new Bag(5);
Condition c1 = bag.newCondition();
Condition c2 = bag.newCondition();
Condition c3 = bag.newCondition();
new Thread(()->{
bag.print("a", c1, c2);
},"t1").start();
new Thread(()->{
bag.print("b", c2, c3);
},"t2").start();
new Thread(()->{
bag.print("c", c3, c1);
},"t3").start();
Thread.sleep(2000);
bag.lock();
try {
c1.signal();
}finally {
bag.unlock();
}
}
static class Bag extends ReentrantLock {
private int loopNumber;
public Bag(int loopNumber){
this.loopNumber = loopNumber;
}
public void print(String str, Condition condition, Condition next){
for (int i=0; i<loopNumber; i++){
lock();
try {
try {
condition.await();
log.info(str);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
next.signal();
unlock();
}
}
}
}
}