琐碎
java在每个对象上都关联了一个监视器和一个等待集合(wait sets,是一个线程集合),即有监视器这个东西
操作监视器:
synchronized作用于方法:称为同步方法,同步方法被调用时,自动执行加锁操作,只有加锁成功,方法体才会得到执行
如果被synchronized修饰的方法是实例方法,那么该实例的监视器会被锁定
如果是static静态方法,那么线程会锁住相应的Class对象的监视器,方法体执行完或者遇到异常退出后,自动执行解锁操作
面试:
1 一个类中两个synchronized static方法之间是否构成同步
构成同步
2 synchronized 作用于静态方法时是对 Class 对象加锁,作用于实例方法时是对实例加锁,他们之间不构成同步
注意:实际使用的时候可以用局部锁,即如果有4个线程,1和2线程获取到对象1,而3号线程获取到对象2,则应该在获取到具体的对象以后,1和2线程给对象1加锁,3线程给对象2加锁,而不是最外围4个线程获取对象前加锁,这样效率是低下的。
案例代码:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
@Slf4j
public class Test {
private static ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
public static void main(String[] args) {
map.put("obj1", new Object());
map.put("obj2", new Object());
Thread[] threads = new Thread[2];
for (int i = 0; i < 2; i++) {
int finalI = i;
threads[i] = new Thread(() -> {
oneObject(finalI);
// twoObject(finalI);
});
threads[i].start();
}
Stream.of(threads).forEach(x -> {
try {
x.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("end");
}
private static void twoObject(int i) {
Object test;
if (i == 0)
test = map.get("obj1");
else
test = map.get("obj2");
synchronized (test) {
log.info(Thread.currentThread().getName() + "获取锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName() + "释放锁 " + test);
}
}
private static void oneObject(int i) {
Object test;
if (i == 0)
test = map.get("obj1");
else
test = map.get("obj1");
synchronized (test) {
log.info(Thread.currentThread().getName() + "获取锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName() + "释放锁 " + test);
}
}
}
执行oneObject()结果:
执行twoObject()结果:
可以从上面清晰的看到2者的区别
我们以前在实际使用的时候可以采用如下加锁方式
优化前代码:
synchronized(obj){ //或者直接方法锁
GroupKey groupKey = new GroupKey();
BatchHolder holder = getOrCreateBatchHolder(groupKey);
return appendToHolder(groupKey);
}
但是这种优化前代码鸡肋且效率低下,我们用上面学到的理论来优化这段代码:
优化后代码:
GroupKey groupKey = new GroupKey();
BatchHolder holder = getOrCreateBatchHolder(groupKey);
synchronized (holder) {
return appendToHolder(groupKey);
}
优化后使得只有获得相同holder的线程的操作才加锁,获得不同holder的并发执行,提高了执行效率
等待集合:
对集合进行操纵:Object.wait,Object.notify,Object.notifyAll
sleep(),join()可以感知到线程的wait和notify
调用wait方法后,线程释放锁,然后重新进入等待集合,被其他线程唤醒(notify)后,从等待集合移出来,
但是无法马上往下执行,该线程还需要重新获取锁才行
wait有可能被假唤醒
每个线程在一系列(可能导致它从等待集合中移除出去)的事件中,必须决定一个顺序,必须表现为他是按照那个顺序发生的
如果线程 t 被中断,此时中断状态为 true,则 wait 方法将抛出 InterruptedException 异常,并将中断状态重新设置为 false。
Notify:
唤醒的时候,线程t随机唤醒某个等待集合线程m,唤醒后,m线程加锁不会成功,直到线程t完全释放锁,因为调用notify不会释放锁
wait会阻塞,notify不会阻塞
Interrupt:
中断
线程自己也可以在自己执行代码中调用Thread.interrupt()
设置中断状态为true的时候,如下几个方法会感知:wait(),join(),sleep(),这些方法方法声明上都有throws InterruptedException,
这个就是用来响应中断状态修改的
线程阻塞在上面几个方法中,当线程感知到中断状态设置为true后(次线程的interrupt()方法被调用),会将中断状态重新设置为false,
然后执行相应的操作(通常是跳到catch异常处)
LockSupport的park()方法也能自动感知到线程被中断,但是他不会重置中断状态为false,只有上面的wait,join,sleep会在感知到
中断后先重置中断状态为false,再继续执行
注意 keyPoint
如果有一个对象 m,而且线程 t1此时在 m 的等待集合中
线程t2设置线程t1的中断状态->t1线程恢复->t1获取对象m的监视器锁->获取锁之后,抛出interrutpedException
线程t1被中断,wait方法返回,并不会立即抛出interruptedException异常,而是在重新获取监视器锁之后才会抛出异常
interrupt:仅仅是设置线程的中断标志位为true
thread.isInterrupted()可以知道线程的中断状态,不做其他操作
thread.interrupted()可以返回当前线程的中断状态, 同时将中断状态设置为false
notify和中断的影响
@Slf4j
public class WaitNotify {
volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
WaitNotify waitNotify = new WaitNotify();
Thread thread1 = new Thread(() -> {
synchronized (object) {
log.info("线程1 获取到监视器锁");
try {
object.wait(); //释放锁,进入等待队列
log.info("线程1 正常恢复啦。中断状态是"+Thread.currentThread().isInterrupted());
} catch (InterruptedException e) {
log.info("线程1 wait方法抛出了InterruptedException异常");
}
}
}, "线程1");
thread1.start();
Thread thread2 = new Thread(() -> {
synchronized (object) {
log.info("线程2 获取到监视器锁");
try {
object.wait(); //释放锁,进入等待队列
log.info("线程2 正常恢复啦。");
} catch (InterruptedException e) {
log.info("线程2 wait方法抛出了InterruptedException异常");
}
}
}, "线程2");
thread2.start();
// 这里让 thread1 和 thread2 先起来,然后再起后面的 thread3
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
Thread t3=new Thread(() -> {
synchronized (object) {
log.info("线程3 拿到了监视器锁。");
log.info("线程3 设置线程1中断");
thread1.interrupt(); // 1
waitNotify.a = 1; // 这行是为了禁止上下的两行中断和notify代码重排序
log.info("线程3 调用notify");
object.notify(); //2 //这里notify是随机唤醒,如果是唤醒线程2,则线程1因为中断最后获取到锁
log.info("线程3 调用完notify后,休息一会");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
log.info("线程3 休息够了,结束同步代码块");
}
}, "线程3");
t3.start();
thread1.join();
thread2.join();
t3.join();
System.out.println("主线程退出");
}
}
一般情况下,上面代码会出现如下结论:
然而,同样存在如下情况:
有可能发生 线程1 是正常恢复的,虽然发生了中断,它的中断状态也确实是 true,但是它没有抛出 InterruptedException,而是正常返回。此时,thread2 将得不到唤醒,一直 wait。
如果一个线程在等待期间,同时发生了通知和中断,它将可能发生如下两种情况:
- 从 wait 方法中正常返回,同时不改变中断状态(也就是说,调用 Thread.isInterrupted 方法将会返回 true)
- 由于抛出了 InterruptedException 异常而从 wait 方法中返回,中断状态设置为 false
wait有可能是假唤醒,即有顺序关系,线程可能因为被notify唤醒,也可能因为中断唤醒,如果它没有因为中断唤醒,则不会抛出interruptedException。并且不会重置中断标志位为false,即你打印 isInterrupted 仍然是其他线程给他设置的true
可重入锁和不可重入锁的区别
可重入锁:如果我们现在有个方法A,方法A中调用了方法B,而且这两个方法都要加锁,A加的是1号锁,B同时也想加这个1号锁,如果是可重入的,如果是可重入的,A方法一进来加了锁了那A方法执行了,A方法要调用里面的B方法,B方法相当于也要跟A加同一把锁,B一看A已经加了这把锁了,那B方法就直接拿来用,相当于B方法可以直接执行,执行完以后A释放锁即可
如果设计为不可重入锁那就糟糕了:如果A要加1号锁,A里面调用B,B也想加1号锁。A把1号锁持有了,B相当于要等待A释放1号锁它才能抢到1号锁,这就是不可重入锁的设计。这样很明显是有问题的, 这就是一个死锁,B永远等不到A去来释放,因为A还想等着B执行完了才释放
所以一句话:所有的锁都应该设计为可重入锁,避免死锁问题