synchronized的其他属性
举个例子:
public class Thread17 {
public static void main(String[]args){
Object lock = new Object();
Thread t = new Thread(()->{
synchronized (lock){
synchronized (lock){
System.out.println("hello!");
}
}
});
t.start();
}
}
直观上好像代码中都线程会出现阻塞,但事实上它仍可以打印出“hello”,原因是这两次加锁,其实是在同一个线程进行的。当前由于是同一个线程,此时锁对象就知道了第二次加锁的线程就是持有锁的线程,第二次操作,就可以直接放行通过,不会出现阻塞,这个特性称为“可重入”。但是只有java中的锁才有这样的特性,隔壁C++就不行........
对于可重入锁来说,内部会有两个信息:1.当前这个锁是被哪个线程持有的,2.加锁次数的计数器。
对于例子来说,第二次加锁时发现是同一个线程,就只是++计数器,没别的操作了。
由于加了两次锁,使用计数器为2,第一次解锁计数器-1,但不为0,仍然不会解锁线程,第二次解锁时计数器为0,这时才是真正的解锁了。也就是说,计数器是否为0,才是判断一个线程是否解锁的关键条件。
死锁问题(经典面试题)
死锁是多线程的一类经典问题,如果加锁方式不当,可能会导致死锁问题。
死锁的三种经典场景:
1)一个线程,一把锁。
如果一个线程加锁两次,就会导致死锁问题。(java中一般不会).
2)两个线程,两把锁。
线程1获取锁1,线程2获取锁2,接下来,线程1尝试获取锁2,线程2尝试获取锁1,就会僵持。
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(()->{
synchronized (A){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (B){
System.out.println("t1得到了两把锁");
}
}
});
Thread t2 = new Thread(()->{
synchronized (B){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}synchronized (A){
System.out.println("t2得到了两把锁");
}
}
});
t1.start();
t2.start();
上面就是一个典型的死锁情况。t1在占有A的情况等待B,t2在占有B的情况下等待A,这样一直运行也是没有结果的。
3)N个线程M把锁
经典问题引入:哲学家就餐问题,和操作系统中的哲学家就餐问题一样。
特殊情况:每个哲学家在同一时刻都要吃,都拿起了一根筷子,导致死锁。
解决方法:首先明确死锁产生的条件:1)互斥使用。获取锁的过程是互斥的,一个线程拿到了这把锁,另一个也想获取,就需要阻塞等待。2)不可抢占。一个线程拿到了锁之后,只能主动解锁,不能让别的线程强走锁。3)请求保持。一个线程拿到锁A之后,在持有A的前提下,尝试获取B。4)环路等待。A拥有1等2,B拥有2等1........
要解决死锁问题,就要打破上述任意一个条件,很显然,1和2不好破坏,3不一定,所以4是最好破坏的,只要指定一定的规则,就可以有效破坏环路问题:
指定加锁顺序,针对五把锁,都进行编号,约定每个线程获取锁时,一定要先获取编号小的锁,后获取编号大的锁。
JAVA标准库中的线程安全类
ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder都是线程不安全的类,
Vector(不推荐),HashTable(不推荐,标准库即将弃用),ConcurrentHashMap,StringBuffer这几个类自带锁,在多线程情况下相对安全。
内存可见性引起的线程安全问题
如果一个线程写,一个线程读,这也可能存在线程不安全问题。
public class Thread19 {
private static int flag = 0;
public static void main(String[]args){
Thread t1 = new Thread(()->{
while(flag==0){
}
System.out.println("线程t1结束");
});
Thread t2 = new Thread(()->{
System.out.println("请输入:");
Scanner sc = new Scanner(System.in);
flag = sc.nextInt();
});
t1.start();
t2.start();
}
}
在例子中,理论上只要输入不为0的值,t1就应该结束,但实际上并不会结束。
原因是因为t1的while循环有两条核心指令:1)Loda读取内存中的flag值到cpu的寄存器里。2)拿着寄存器的值和0进行比较。
因为执行速度非常快,所以会反复执行1和2指令。
在执行过程中,1)load操作执行的结果都是一样的(因为用户输入有时间,在这个等待输入时间,已经执行了百亿次)。2)load的操作开销远远大于条件跳转(第二条指令),访问寄存器的速度远远大于访问内存。在等待输入时间中,频繁执行load和条件跳转,load的开销大,而且load的结果没有变化,此时jvm便会怀疑load是否有存在的必要。此时jvm便会进行代码优化,把load操作优化掉,优化掉后,便不会重复读内存,直接使用寄存器之前“缓存”的值,大幅度提高代码执行速度。而且多线程情况下,jvm代码优化很容易发生bug,所以即使使用Scanner修改了flag,jvm也会误判为flag仍为0。就相当于t2修改了内存,但t1没有看到这个变化,就称为“内存可见性问题”。但内存可见性问题高度依赖代码优化,如果代码不触发优化,就不一定产生内存可见性问题,比如我们向while循环中添加一些指令,使得loda执行次数没有这么多次,就有可能不会产生内存可见性问题。
针对这个问题,java提供了volatile,可以确保代码优化被强制关闭,强制重新从内存中读取数据。
用法是给变量前加上volatile。
private volatile static int flag = 0;
wait和notify
wait(等待),notify(通知)和join类似,引入wait和notify就是为了从应用层面上,干预到多个不同线程代码的执行顺序,可以让先执行的代码把代码执行完,后调动的代码放弃执行。
应用情况:当某个线程拿到锁后,发现并没有自己需要的资源,但由于线程的随机调度,该线程仍可能竞争到锁,而且概率还比较大,因为它不需要唤醒,导致需要锁的线程处于BLOCKED状态,导致线程饥饿。
wait内部做了三件事:
1)释放锁;2)进入阻塞等待;3)当其他线程调用notify时,wait解除阻塞,并重新获取到锁。
注意:wait要在synchronized里使用,因为只有拿到锁才能释放锁。
java约定notify也要放到synchronized中,
public class Thread20 {
public static void main(String[]args) throws InterruptedException {
Object ob = new Object();
Thread t = new Thread(()->{
synchronized (ob){
System.out.println("before wait");
try {
ob.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after wait");
}
});
Thread t1 = new Thread(()->{
try {
Thread.sleep(2000);
synchronized (ob){
System.out.println("before t1 notify");
ob.notify();
System.out.println("after t1 notify");
}
}catch (InterruptedException e){
e.printStackTrace();
}
});
t1.start();
t.start();
}
}
sleep(2000)是为来先让t线程拿到锁,结果就是t先拿到锁,之后wait,直到t1执行完notify。
before wait
before t1 notify
after t1 notify
after wait
注意点:1)wait和join类似,也有带有超时时间的版本。
2)wait和notify必须是同一个对象,如果两个wait是同一个对象调用的,则随机唤醒一个。
3)notifyAll,唤醒这个对象上所有等待的线程。假设有多个线程,都使用同一个对象wait,针对这个对象notifyAll,此时就会全部唤醒。但是全部唤醒后,它们还是要竞争........
经典面试题:wait和sleep的区别
wait提供了一个带有超时时间的版本,sleep也是能指定时间;都是到时间就继续执行,解除阻塞。wait和sleep都可以被提前唤醒,wait通过notify唤醒,sleep通过interrupt唤醒。但使用wait,最主要的目标,一定是在不知道要等多少时间的情况下使用的,所谓的超时时间,其实是兜底的。
使用sleep,一定是在知道要等多少时间的情况下使用的,虽然能提前唤醒,但是是通过异常唤醒的,不是一个正常情况。