承接上篇博文总结一下造成线程不安全的几个原因以及解决方法:
①抢占式执行,调度过程随机:这个没有解决办法,我们无能为力
②多个线程同时修改同一个变量:适当调整代码结构,可以避免这种情况
③针对变量的操作不是原子性的(修改操作):加锁synkronized
④内存可见性(一个线程频繁读,一个操作合适时机写):需要通过volatile这个关键字来解决内存可见性问题(PS:volatile只能解决内存可见性问题)
⑤指令重排序:同样是加锁synkronized
目录
1.synkronized的四个使用实例
1)修饰普通方法
2)修饰代码块
3)修饰静态方法
4)锁类对象
2.synkronized的三个特性
1)互斥
2)刷新内存
3)可重入
3.死锁的其他场景
4.Java标准库的线程安全类
5.wait和notify
5.1wait
1)wait做的事
2)wait结束等待做的事
5.2notify
6.面试题:wait和sleep的区别
class test{
//直接修饰一个普通的方法
synchronized public void func(){
}
}
class test{
public void func2(){
synchronized (this){
}
}
}
class test{
//修饰静态方法
synchronized public static void func2(){
}
}
class test{
public void func2(){
synchronized (test.class){
}
}
}
3和4其实本质是一样的
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
其实互斥本质上是使得操作变成原子性,就比如一个上篇博文里的count++,本质上是三个指令(load,add,save),经过加锁的的操作,就会将这个指令打包成原子性
目的也是解决内存可见性问题,这个功能和volatile的功能差不多
先来介绍下不可重入问题:
简单的来说,就是同一个线程连续加锁两次,如果出现了死锁,就是不可重入,如果没有出现死锁,就是可重入
死锁:
外层先加了一层锁,同时代码里层也加了锁
外层锁:进入方法则开始加锁,能过加锁成功,当前锁没有其他的线程占用;里层锁:进入代码块,开始加锁,这次加锁不能成功,因为此时锁被外层占用着,得等到外层锁释放了之后,里层锁才能加锁成功。外层锁要想执行完整个方法才能释放,但是想要执行完整个方法,就得让里层锁加锁成功才能往下走
此时就造成了死锁这个情况,相当于自己给自己困住了
死锁的出现大大的降低了开发效率,然而在日常开发中,外层锁和里层锁同时写是很正常的事,那么该如何解决这个事呢?
死锁的出现显然是被实现JVM的大佬们发现了,干脆就把synkronized实现成为可重入锁,可重入锁就代表着,外层锁和里层锁同时出现也能够保证不出现死锁的情况
可重入锁内部会记录当前锁被哪个线程占用着,同时也会记录一个“加锁次数”,例如一个线程A,第一次加锁的时候很明显能够加锁成功,此时锁的内部就记录着当前占用的是A,同时加锁次数是1,后续在对A加锁的时候,并不是真正的加锁,而是单纯的将加锁的次数加一,等到解锁的时候将计数减一就行了,直到计数变成0,就代表着当前的线程解锁了
可重入锁的意义就是降低了程序员的负担,提高了开发效率,但是也带来了代价,程序中需要有更高的开销(维护锁属于哪个线程,并且记录计数的加减,降低了运行的效率)
1)一个线程一把锁
就如上述的例子
2)两个线程两把锁
举一个简单的例子,这种情况就相当于两个人线下交易某件物品,到了约定的地点,一手交钱一手交货,但是此时,卖家更跟买家说先给钱,而买家却说先检查物品,两个人就这样僵持住了。
3)N个线程,M把锁
举个例子:哲学家吃面条问题
五个哲学家围坐在一张圆桌旁,桌子中央一盘通心面(面假设无限),每个人面前有一只空盘,每两个人之间放一把叉子。为了吃面,每个哲学家都必须获得两把叉子,且只能从自己左边或右边取,假如五个哲学家同时拿起右手边的叉子,那么五个人都将等待相邻哲学家手中的叉子,出现“死锁”
这个问题也叫作 环路等待
那么如何避免这种问题呢,其实只要事先约定好就行了,针对多把锁加锁的时候,有固定的的顺序即可,所有的线程都遵循同一个规则顺序,就不会出现环路等待
线程不安全类:
1.ArrayList
2.LinkedList
3.HashMap
4.TreeMap
5.HashSet
6.TreeSet
7.StringBuilder
线程安全类
1.Vector
2.HashTable
3.ConcurrentHashMap
4.StringBuffer
5.String
对于前四个线程安全类,在java源码中,都能够看到synkronized的身影,而最后一个String类的源码却没有synkronized,那么为什么他会是线程安全的类呢,原因是String是不可变对象,无法在多个线程同时修改同一个String,单线程都没办法
什么是不可变对象?
从不可变对象的定义来看,其实比较简单,就是一个对象在创建后,不能对该对象进行任何更改
具体可以看这篇博文,可以更好的帮助你理解
深入理解Java中的不可变对象 - Matrix海子 - 博客园 (cnblogs.com)
由于实际开发中线程之间是抢占式执行的,因此线程之间的执行先后顺序就难以预知,但是实际开饭过程中我们希望合理的协调多个线程之间的执行先后顺序。例如篮球场上的运动员,将每个运动员比作是一个线程,运动员之间的传球到最后上篮进球,是非常合理的,所以我们也希望多个线程之间能够像运动员一样相互配合相互协作
wait():让当前线程进入等待的状态
wait和notify都是Object对象的方法,调用wait方法的线程,就会进入阻塞的状态,阻塞到其他线程通过notify来通知
public class Demo3 {
public static void main(String[] args) {
Object object = new Object();
synchronized (object){
System.out.println("等待前");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待前");
}
}
}
①先释放锁
②等待其他线程的通知
③收到通知后重新获得锁,并继续往下执行
①调用其他线程notify的方法
②wait等待超时
③其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedExceotion异常
notify 方法是唤醒等待的线程
①方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
②如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
③在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
完,也就是退出同步代码块之后才会释放对象锁
wait和notify都是针对同一个对象来操作的,假如现在有一个对象a,有五个线程都调用了a.wait,使得五个线程都进入堵塞状态,如果调用了a.notify方法,就会把五个当中的一个给唤醒,具体唤醒哪一个并不确定
1.wait需要搭配synkronized使用,为sleep不需要
2.wait是Object的方法,而sleep是Thread的静态方法