死锁出现的第一种情况
可重入
同一个线程针对同一个锁连续继续加锁多次的行为。如果发生了死锁情况,那就是发生了不可重入,反之不会发生死锁,就是可重入的。
当进行多次加锁会发生什么情况
在这个方法中实现了在外面对方法的加锁(当一旦运行这个方法时,运行这个方法的线程立刻加锁),又在内部实现了对this对象也就是对自身的加锁对这个线程的加锁。在外层加锁时因为此时这个线程没有人加锁,所以加锁会成功,在内部实行加锁时(以前所学认为锁只能加一次),因为锁已经加了,想要在加锁必须等待方法调用时加的锁解开才能进行加锁。现在内部想要加锁就要等待调用方法时加的锁解开,想要解开锁就要让这个方法运行完,但是现在这个方法内部又要进行加锁,方法不能运行完。这一多次加锁加不上的线现象就认为是死锁。为了解决这一问题就将synchronized设计为可以进行多次加锁的可重入锁。这可以使上述的情况不会出现死锁情况。
实现
可重入锁的内部,会记录当前的锁被哪个线程占用的,也会记录加锁次数,比如当线程a对自己进行第一次加锁时,内部会记录为线程a加锁次数1,然后线程a在进行加锁时就只是将加锁的次数由1改变为2来进行统计加锁的次数,此时没有真正的进行加锁,只是伪加锁,后续在进行解锁的时候由于只是进行的伪加锁所以只将次数减一就好,直至减到0时,才真正进行锁的解开。这一实现降低了使用成本,提高了开发效率,但提高了程序维护线程时的开销(维护哪个线程被锁,锁了几次)降低了运行效率。
死锁出现的其他情况
N个线程,M把锁
哲学家就餐问题
桌子上有五根筷子,五个哲学家,当他们想要吃饭的时候,必须要同时拿两根筷子才可以。我们规定当想要吃饭的时候,先拿左边的筷子,再拿右边的筷子。同时如果想吃饭的时候如果自己不能同时拿到两根筷子就会一直等其他哲学家吃完之后再拿起来吃饭,在这样的情况下如果他们同时拿起左边的筷子就会进入一个死锁的情况他们会一直等待下去。要解决这样类似的问题,只能先将筷子进行排序然后在那个在最小序号筷子旁边的人先同时拿两个筷子,然后按照筷子的顺序,在他们旁边的依次拿一根筷子,等待最小序号吃完之后将筷子方下就可以解决这样类似的死锁问题。
我们知道了什么情况下会产生死锁,总结这些情况发现死锁产生的必要条件
互斥使用:一个锁被一个线程占用之后,其他线程不能占用。
不可抢占:一个锁被一个线程占用之后,其他线程不能把这个锁给抢走。
请求和保持:当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是该线程持有的。
环路等待:等待关系成为一个循环。A等B,B等C,C又等A。
上述这些必要条件中第一条和第三条是锁的基本特性,我们想要避开锁在日常开发中,只能从第四个条件进行入手,在设计时约定好针对多把锁加锁时,有固定的顺序,使其不能成环。当然在日常开发中一般也不经常出现嵌套着使用锁,如果出现这种情况一定要规定好锁的属性。
java的内存模型(JMM Java Memory Model)
JMM是将内存寄存器等硬件在java中用术语重新封装实现了一遍。
工作内存(work memory):CPU寄存器,不是平常说的内存。
主内存(main memory):平常所提及的内存
这两者组成了JMM,因为java是一个跨平台的语言,所以要将计算机中运算的组件封装起来,如果某个计算机中没有上述的硬件也可以运行。
wait notify 方法
作用:用来处理调度的随机性问题,让线程之间有一个固定的顺序。这两种方法都是Object对象的方法,一调用wait方法的线程,线程立刻就会陷入阻塞。阻塞到有其他线程通过使用notify方法来唤醒线程。
单独使用wait或者notfy方法会发生这样的报错,因为在使用wait时其内部在进行三件事,先释放锁,等待其他线程的通知,收到通知后,重新获取锁,在往下执行。所以使用时就要搭配着synchronized来进行加锁。
从输出结果可以看出其处于线程thread处于阻塞中。