关于Java中的synchronized关键字

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Java中的synchronized关键字相关的知识,希望对你有一定的参考价值。

【内容简介】

本文主要介绍Java中如何正确的使用synchronized关键字实现线程的互斥锁。

 

【能力需求】

至少已经完整的掌握了Java的语法基础,基本的面向对象知识,及创建并启动线程。

 

【正文】

关于synchronized关键字的使用,很多说法是“锁同一个对象”就可以确保锁是正常的,今天,有人提了一个问题,我觉得非常不错,所以与各位一起分享一下。

在这里,就不提关于线程和synchronized关键字的基本使用了,以非常传统的“银行取钱”的故事为案例,直接上代码:
Ps:以下代码是直接敲的,不是在IDE工具中复制出来的,可能存在敲错的地方,请见谅。

public class ThreadSample {
    public static Object lock = new Object(); 
    public static int money = 1000; 
    
    public static void main(String args) { 
        Card card1 = new Card(); 
        Card card2 = new Card(); 
        
        card1.start(); 
        card2.start(); 
    } 
} 

class Card extends Thread { 
    public void run() { 
        synchronize(ThreadSample.lock) { 
            if(ThreadSample.money >= 800) { 
                ThreadSample.money -= 800; 
                System.out.println("取款成功,余额:" + ThreadSample.money); 
            } else { 
                System.out.println("余额不足,取款失败!"); 
            } 
        }
    }
}

以上代码是模拟取钱的,使用static成员money表示帐户,所以保证了多个线程访问到的是同一个值,同时,基于static成员是唯一且具有共享特性的,以上代码中使用static成员lock用于synchronized锁定,这个做法是可行的,所以,在很多人的观点里会认为“锁的是同一个对象就可以实现锁定”。

但是,有人测试出了以下这样的代码并提问(忽略掉相同的代码片段):

class Card extends Thread { 
    public Integer i = 1; 
    
    public void run() { 
        synchronize(i) { 
            if(ThreadSample.money >= 800) { 
                ThreadSample.money -= 800; 
                System.out.println("取款成功,余额:" + ThreadSample.money); 
            } else { 
                System.out.println("余额不足,取款失败!"); 
            } 
        } 
    } 
}

写到这里,我不得不发自内心的再说一句,他作为一名第1次学习到线程的人,提出这样的问题,肯定是非常优秀的!

以上代码,在Card中声明了public Integer i = 1;并使用这个Integer对象作为synchronized锁的对象,最后,锁定也是成功的。

如果参考此前所说的“锁的是同一个对象就可以实现锁定”,那么,从代码里可以看到,运行过程中创建了2个Card对象,那么这2个Card对象有各自的Integer i成员,锁的就不是同一个对象,却也成功的锁定了,所以,此前的说法是不贴切的!
也许会有人说Integer或类似的包装类很特殊,也许是对应的int这些基本数据类型在作祟,那么,以下代码仍然是可行的:

class Card extends Thread { 
    public String str = "www.tedu.cn"; 
    
    public void run() { 
        synchronize(str) { 
            if(ThreadSample.money >= 800) { 
                ThreadSample.money -= 800; 
                System.out.println("取款成功,余额:" + ThreadSample.money); 
            } else { 
                System.out.println("余额不足,取款失败!"); 
            } 
        } 
    } 
}

好了,用String也是可以锁的,那就跟基本数据类型没有关系了,下一步的解释会是什么呢?先别着急,再来看一段代码:

public class ThreadSample { 
    public static int money = 1000; 
    
    public static void main(String args) { 
        Integer i1 = 1; 
        Integer i2 = 1; 
        
        Card card1 = new Card(i1); 
        Card card2 = new Card(i2); 
        
        card1.start(); 
        card2.start(); 
    } 
} 

class Card extends Thread { 
    public Integer i; 
    
    public Card(Integer i) { 
        this.i = i; 
    } 
    
    public void run() { 
        synchronize(i) { 
            if(ThreadSample.money >= 800) { 
                ThreadSample.money -= 800; 
                System.out.println("取款成功,余额:" + ThreadSample.money); 
            } else { 
                System.out.println("余额不足,取款失败!"); 
            } 
        } 
    } 
}

以上代码中,在Card中添加了带参数的构造方法,为成员Integer i赋值,并使用成员Integer i来实现锁定,并且,在运行线程之前,分别声明了Integer i1和Integer i2用于创建Card对象,结果,锁定依然是可行的。

如果有兴趣的话,可以把以上代码中的Integer换成String,可以发现,仍然还是可行的!

在这里,已经很明显了,在运行线程之前,声明了不同的对象用于创建Card对象,不管是Integer类型还是String类型,都是可行的!

那么,应该怎么解释这个问题呢?也许会有人想到equals()和hashCode(),是不是equals()对比出true,并且hashCode()值相同,就可以呢?这个想法是错的,请看以下代码:

public class ThreadSample { 
    public static int money = 1000; 
    
    public static void main(String args) { 
        long timeMillis = System.currentTimeMillis(); 
        
        Date d1 = new Date(timeMillis); 
        Date d2 = new Date(timeMillis); 
        
        Card card1 = new Card(d1); 
        Card card2 = new Card(d2); 
        
        card1.start(); 
        card2.start(); 
    } 
}

我们都知道Date类是重写了equals()和hashCode()的,以上代码中的d1和d2表示的时间完全相同,所以,d1和d2的equals()对比结果为true,且hashCode()返回值是相同的,但是,以上代码却不能成功的实现锁定!

看完了以上这么纠结的问题,最后总结一下答案,通俗一点来说:如果使用==判断结果为true,就可以用于synchronize锁定!

比如,在使用String测试的时候,声明以下2个String类型的数据:

String str1 = "www.cnblogs.com"; 
String str2 = "www.cnblogs.com";

分别使用以上2个对象创建Card,并在Card中声明String成员用于锁定,结果是OK的,但是,以下代码就不行:

String str1 = "www.cnblogs.com"; 
String str2 = ""; 
str2 += "www.cnblogs.com";

尽管组成字符是相同的,但是,str1是直接使用常量赋值的,对应的数据保存在字符串常量池中,而str2最后是通过变量运算出来的,是运行期得到的结果,对应的数据将位于堆内存中,所以,在栈内存中保存的str1和str2的引用并不相同,也就是str1 == str2的结果为false,所以,这样来锁是失败的!

上述关于String的第1段代码中,是在编译期使用String常量赋值给str1和str2,由于字符串常量池中的字符串是唯一的,所以str1、str2使用==时的结果将是true,至于Integer这些包装类,内部是通过诸多static成员来实现的,基于static的特性,所以上述代码中的i1、i2使用==判断时的结果也会是true。

所以,关于synchronized锁的对象,学术化的定义可以是“对象在栈内存中的引用相同,就是同一个对象,就可以用于锁”,通俗的表达就是“使用==判断为true就可以用于锁”,因为使用==判断其实就是判断栈内存中存储的数据。

 

【小结】

用于synchronized锁的对象,如果可以存在2个,并使用==判断的结果为true,就可以用于锁!



如果本文有不足或者错误的地方,欢迎随时指出,如需转载,请注明出处,Thank you !

 

以上是关于关于Java中的synchronized关键字的主要内容,如果未能解决你的问题,请参考以下文章

谈谈java中的synchronized关键字

Java线程和多线程(十四)——Synchronized关键字解析

java中的synchronized()的具体作用

JAVA线程安全之synchronized关键字的正确用法

java之结合代码理解synchronized关键字

java并发之synchronized解密