文章目录
什么是锁
首先我们来解释一下什么是锁呢?JAVA中的锁其实严格来说更像是一个门,我们在进行某个代码操作的时候如果先对这个代码上锁那么就会使得在一个时间内只有一个线程进行这类代码,最终的结果就是当我们对一个资源做修改的时候只有一个线程在进行。
为什么需要锁
为什么需要锁呢?我们之前举过一个列子,代码如下
java
public class Main {
public static int count=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(count);
}
}
按照这个代码的运行我们预测的结果应该是1万的。那么最终他的运行结果到底是多少呢?
很明显这个代码最终的结果并不像我们想的那般。但是如果加上锁呢?
如何加锁
synchorized 如何加锁呢?我们要用到加锁的关键字,也就是synchorized当然了还有lock但是呢我们这篇文章主要围绕的synchorized 那么这个关键字如何使用的呢,首先他有两种使用方法第一种就是对方法进行修饰,第二种就是对代码块加锁 加锁也可以说成是加了一个同步监视器,
synchorized 的使用
这个关键字的使用其实是要求修饰某个对象,然后以这个对象为一种参照,我们可以这样理解那就是synchorized表示对某个代码块关门上锁,而synchorized小括号里修饰的那个对象就是这扇门的钥匙,我们想要进行这个代码修改这部分资源就必须获得这个钥匙才可以,那么如何获得呢?就是用synchorized(对象) 这样即可。例如以下代码
synchronized 修饰方法
java
class myrun{
public static int count=0;
public synchronized void run(){
count++;
}
}
这里面synchronized 就是对一个方法进行修饰了,这时候有人可能会疑惑认为这不对啊,因为上面说了synchorized需要一个任意类型的对象但是为什么对这个方法进行修饰的时候我们就没有像上面说的再加个括号里面随便写一个某个类的对象呢?其实很简单就可以想到的,因为我们这是对类里的某个方法进行修饰的因此我们的synchorized所修饰的需要的那个对象其实就是调用这个方法的那个对象。
那么除了这种办法外还有没有别的办法呢?
synchronized 修饰代码块
synchronized修饰代码块就跟我们上面说的格式一样了就是需要一个任意类型的对象当作钥匙进行修饰我们以上面那个bug为例如果加锁上面这个代码会变成什么样子呢?
java
public class Main {
public static int count=0;
public static Object ob=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (ob){
for(int i=0;i<5000;i++){
count++;
}
}
});
Thread t2=new Thread(()->{
synchronized (ob){
for(int i=0;i<5000;i++){
count++;
}
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(count);
}
}
上面的代码我们可以看出来,当我们将其加锁后我们可以发现原本应该并行的代码变成了串行执行的了,也正因为如此我们的程序才可以运行成功。那么为什么会如此呢?
我们说的加锁加锁,指的到底是什么呢?我是这么理解的,所谓加锁相当于是找了一个对象作为一个证明类似于门禁卡
比如说有个线程A和B他们都想获取某个资源但是这个资源在一个时间内只能一个人获取如下图
这时候A和B就达成了一个协议
于是呢两个人从刚开始的争取这个协议就变成了争取这个门禁卡然后获得这个门禁卡的人才可以对这个资源进行修改。
而没获得门禁卡的人只能在外面等候,等待对方用完后才可以轮到自己就像上厕所一样没有抢到坑位的人必须等里面的人上好厕所后才可以进去使用是一个道理。
死锁问题
那么上述情况说完后,我们来讲一下锁可能会出现的问题,其中最为常见的一个问题就是死锁问题我们的java的synchronized有三大特性如下
互斥:synchronized获取的锁只能在同一时刻被一个线程所使用不能同时被多个线程使用
刷洗内存:这一点的话说法比较多有人说可以有人说不行
可重入:这一点非常重要他避免了java的一种死锁方式如下代码
上面说的可重入避免了一种死锁是什么意思呢?请看如下代码
java
public class thread {
public static Object ob=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (ob){
synchronized (ob){
System.out.println("打印成功");
}
}
});
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
我们来看一下下面的这个代码,我们发现了一个问题那就是,我们的synchronized对一个对象连续的加锁了两次,那么按照正常的锁的互斥执行思路来看的话我们可以这样嘛?很明显是不可以的,因为当这把锁已经被加过锁之后想要再次加锁就必须要解锁,但是想要解锁又必须要让里面的代码块执行完毕这时候就出现了矛盾的问题,因此我们说这种情况是死锁,但是java会出现这种问题嘛很明显不会的,因为java满足了可重入这个特性因此是没事的。但是c++的话就不行了。
那种场景会造成死锁
那么Java可重入的话就不会造成死锁了吗?当然不是的。那还有那种情况会造成死锁呢?那么首先我们要先了解一下死锁的本质究竟是什么
死锁的本质
死锁的本质概括起来就是,由于某个线程拥有了一把锁并且由于代码问题导致这个线程无法释放自己手里的锁从而引起其余需要这把锁的线程永久性阻塞的问题就是死锁。
那么既然如此,除了上述问题外还有哪些情况会引起死锁呢?
由于内部存在无限循环导致的死锁
java
public class thread {
public static Object ob=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (ob){
while(true){
System.out.println("打印成功");
}
}
});
Thread t2=new Thread(()->{
synchronized (ob){
while(true){
System.out.println("打印成功");
}
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
上述代码中t1线程获取了锁之后但是由于其内部有无限循环导致了他的锁无法释放从而导致t2线程无法获取到锁,由此导致造成了死锁问题。
死锁的第二种情况
两把锁两个线程一人一把。代码如下
java
public class thread {
public static Object ob=new Object();
public static Object ob1=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (ob1){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (ob){
while(true){
System.out.println("打印成功");
}
}
}
});
Thread t2=new Thread(()->{
synchronized (ob){
synchronized (ob1) {
while(true){
System.out.println("打印成功");
}
}
}
});
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
在这个代码中我们可以看到首先
t1获得了锁ob1 t2获得了锁ob
当t1想要获取锁ob的时候 t2此时因为无法获取ob1导致无法释放
t2想要和获取ob1的时候t1又因为无法获取ob导致无法释放。
这就特别像一个例子那就是你想要打开家门,但是家门钥匙忘在车上了你想要打开车却又发现自己的车钥匙忘在了家里。那么这种解决办法和模型在下面,有一个哲学家吃饭模型
哲学家吃饭模型
首先给大家描述一下哲学家吃饭模型,大致意思就是有五个哲学家五根筷子,然后这些哲学家坐在一个圆桌子上,这时候要吃饭了每位哲学家都需要拿起身边的两双筷子如下图
这是当只有一个哲学家要吃饭的时候其实还好解决直接拿起一双筷子即可,但是当所有的哲学家都需要吃饭怎么办呢?这时候筷子就不够分了,那么这时候有什么解决办法呢?那么我们的解决办法就是让哲学家必须遵守一个规定那就是只能优先拿两边编号小的那双筷子之后才可以获取身边编号大的筷子,那么我们来看看这是怎么解决问题的呢?
由此便解决了线程死锁的问题了
造成死锁的必要条件
那么上面讲解了死锁我们也可以总结一下死锁的必要条件有哪些呢?
- 互斥使用:同一把锁只能在一个时间内被一个线程使用
- 不可抢占:没有获取到这把锁的线程不能强行干预只能等到该线程释放锁之后才能拿到
- 请求和保持:当一个线程嵌套上锁的时候那么当申请使用第二把锁的时候第一把锁不需要释放
- 循环等待:参考哲学家吃饭模型其实就是一个循环等待。
如果下辈子还能遇见你,你是我三生三世换来的珍遇。