synchronized关键字的实质及用法
本文旨在揭示用synchronized关键字实现同步的本质,由于纯粹的理论比较枯燥,所以在阐述了基本观点以后,本文的后办部分提供了大量实例并对实例进行了具体的对比分析,通过这些现象及解析,我们可以对文章中的理论知识有更加直观和深刻的理解。希望能给对java多线有疑惑的朋友们一些帮助。这些只是我个人的看法,如果有问题的话请大家及时指出,在此谢过。
synchronized关键字用于实现多线程并发操作时对某一资源的互斥访问。通俗地讲,就是说当多个线程同时访问被synchronized修饰的方法或者语句块的时候,只能有其中的一个线程获得对被修饰内容的操作权。
synchronized关键字的实质
在研究的过程中必须谨记的是:synchronized的本质是使用锁的机制来实现线程互斥的。如果有多个资源被同一个锁锁定,那么只要其中一个资源在被一个线程访问,那么这些资源均不可在此期间被其他线程访问。也就是说只有用相同的锁来锁定的资源在同一时刻才只能被一个线程访问,如果用两个不同的锁来锁定两块资源,那么这两块资源可以被两个不同的线程同时访问。只有理解了这句话,对synchronized关键字的使用才能一通百通。多个资源被同一个锁锁定和这些资源在同一时刻只能被一个线程访问是互为充要条件的。
synchronized关键字的基本用法
其实,synchronized关键字的基本用法只有一种,就是synchronized(object){……被锁定的代码……}。其中的object就是用已锁定代码的锁,它可以是任何类的实例。如果object是this,那么就意味着用当前对象来加锁。
synchronized关键字同步整个实例方法体
如果一个类的某个实例方法的整个方法体都需要同步,我们就可以使用下边的方式来实现:
public void simpleMethod(){
synchronized (this) {
//省略被加锁的代码
}
}
这里的“this”指代的就是当前对象,它也可以换做任何类的实例。但是为了方便起见,我们通常用this关键字。还有一种更加简便的方式就是,我们可以省略this,而只将synchronized放在方法的返回值前做为修饰就可以了,这可以理解为java为我们实现整个方法的同步提供了一种快捷方式。这种方式如下所示:
public synchronized void simpleMethod(){
//省略被加锁的代码
}
以上两种形式是完全等价的。对于一个类来说,多个实例方法都被synchronized修饰就相当于这些所有被修饰的方法的整个方法体都被同一个默认的锁——当前实例this——锁定了,因此他们都是同步的。
synchronized关键字同步代码块
我们都知道,被synchronized修饰的资源如果正在被一个线程访问,那么其余要访问这个资源的线程必须等待,所以为了让在等待中的线程等待的时间更短,我们就应该尽可能地减少被synchronized修饰的资源。有的时候并不是整个方法都需要同步,而仅仅需要同步这个方法中的一部分,那么我们就可以使用synchronized修饰部分代码片段而非整个方法,如下所示:
//用当前对象同步方法中的部分语句
public void method10() {
for (int i = 0; i < 100; i++) {
print("*");
}
synchronized (this) {
for (int i = 0; i < 100; i++) {
print("method10");
}
}
}
这样只有当线程推进到到被synchronized包围的代码时,线程才会开始等待,这样就减少了等待时间,提高了程序的执行效率。
synchronized关键字同步类方法
对类方法(用static修饰的方法),但是synchronized后的参数不能为this了,只能为static类型的任何类的实例:
public class SimpleClass {
private static Object object=new Object();
public static void simpleMethod(){
synchronized (object) {
//省略被加锁的代码
}
}
}
这与下边的方式是等价的:
public class SimpleClass {
public synchronized static void simpleMethod(){
//省略被加锁的代码
}
}
只是此时默认的锁对象已经不是this了,而是SimpleClass类本身。因此如果一个类中有多个类方法被synchronized修饰,就相当于他们都被SimpleClass类本身加锁了从某种程度上一样变成了一个整体,所以,它们都是同步的。
几个实验
为了对上述理论知识有更加深刻的理解,特提供以下实例。实例中涉及到的所有类如下所示:
OperateNum | 提供了各种同步的非同步的实例或者类方法的简单实现。 |
Operator | 根据不同的参数执行OperateNum中的不同方法。 |
Test | 可执行类,需要按照不同的实验目的提供不同的实现。 |
前两个类的代码如下所示
OperateNum.java源码如下:
public class OperateNum{
//当前实例的名称
private String name;
//充当锁对象的Object实例
private Object lock1 =new Object();
//另一个充当锁对象的Object实例
private Object lock2 =new Object();
public OperateNum(String name) {
this.name = name;
}
//用synchronized同步的实例方法
public synchronized void method1() {
for (int i = 0; i < 100; i++) {
print("method1");
}
}
//用synchronized同步的实例方法
public synchronized void method2() {
for (int i = 0; i < 100; i++) {
print("method2");
}
}
//未同步的实例方法
public void method3() {
for (int i = 0; i < 100; i++) {
print("method3");
}
}
//未同步的实例方法
public void method4() {
for (int i = 0; i < 100; i++) {
print("method4");
}
}
//用synchronized同步的静态方法
public synchronized static void method5(){
for (int i = 0; i < 100; i++) {
System.out.println("当前的线程是:"+Thread.currentThread()+",当前执行的方法是:method5");
}
}
//用synchronized同步的静态方法
public synchronized static void method6(){
for (int i = 0; i < 100; i++) {
System.out.println("当前的线程是:"+Thread.currentThread()+",当前执行的方法是:method6");
}
}
//未同步的静态方法
public static void method7(){
for (int i = 0; i < 100; i++) {
System.out.println("当前的线程是:"+Thread.currentThread()+",当前执行的方法是:method7");
}
}
//未同步的静态方法
public static void method8(){
for (int i = 0; i < 100; i++) {
System.out.println("当前的线程是:"+Thread.currentThread()+",当前执行的方法是:method8");
}
}
//用当前对象同步整个方法体
public void method9() {
synchronized (this) {
for (int i = 0; i < 100; i++) {
print("method9");
}
}
}
//用当前对象同步方法中的部分语句
public void method10() {
for (int i = 0; i < 100; i++) {
print("*");
}
synchronized (this) {
for (int i = 0; i < 100; i++) {
print("method10");
}
}
}
//用lock1锁定整个方法体
public void method11() {
synchronized (lock1) {
for (int i = 0; i < 100; i++) {
print("method11");
}
}
}
//用lock1锁定整个方法体
public void method12() {
synchronized (lock1) {
for (int i = 0; i < 100; i++) {
print("method12");
}
}
}
//用lock1锁定整个方法体
public void method13() {
synchronized (lock2) {
for (int i = 0; i < 100; i++) {
print("method13");
}
}
}
public void print(String string){
System.out.println("当前的线程是:"+Thread.currentThread()+",当前实例的名称是:"+this.name+",当前执行的方法是:"+string);
}
}
Operator.java的源码如下所示:
public class Operator extends Thread{
private int operationType;
private OperateNum operateNum;
public Operator(int operationType,OperateNum operateNum) {
this.operationType = operationType;
this.operateNum=operateNum;
}
public void run() {
switch (operationType) {
case 1:
operateNum.method1();
break;
case 2:
operateNum.method2();
break;
case 3:
operateNum.method3();
break;
case 4:
operateNum.method4();
break;
case 5:
OperateNum.method5();
break;
case 6:
OperateNum.method6();
break;
case 7:
OperateNum.method7();
break;
case 8:
OperateNum.method8();
break;
case 9:
operateNum.method9();
break;
case 10:
operateNum.method10();
break;
case 11:
operateNum.method11();
break;
case 12:
operateNum.method12();
break;
case 13:
operateNum.method13();
break;
default:
break;
}
}
}
下边我们根据不同的实验目的编写Test测试类,并对其运行结果进行分析:
实验一 两个线程同时访问一个没有同步的方法
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum=new OperateNum("NO1");
new Operator(3, operateNum).start();
new Operator(3, operateNum).start();
}
}
此时的运行结果为:
从上述结果可以看出这两个线程交替执行被访问的方法。
实验二 两个线程访问一个被同步的方法
这时Test.java的代码如下
public class Test {
public static void main(String[] args){
OperateNum operateNum=new OperateNum("NO1");
new Operator(1, operateNum).start();
new Operator(1, operateNum).start();
}
}
运行的结果如下所示:
运行的结果显示,在Thread-1执行结束后Thread-2才开始执行,也就是说被同步的方法在同一时刻只允许一个线程访问。
实验三 两个线程访问两个未同步的方法
这时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum=new OperateNum("NO1");
new Operator(3, operateNum).start();
new Operator(4, operateNum).start();
}
}
运行结果如下所示:
从上述结果可以看出,两个方法是分别被两个线程交替执行的。
实验四 两个线程访问两个同步的方法
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum=new OperateNum("NO1");
new Operator(1, operateNum).start();
new Operator(2, operateNum).start();
}
}
此时的运行结果如下所示:
此时两个线程没有交叉执行,实验一和实验二对比可以告诉我们,在同一个类中所有用synchronized修饰的实例方法是一个整体,当其中一个被一个线程访问时,其余的被修饰的方法都不可被任何线程访问。
实验五 用this同步整个方法体
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum=new OperateNum("NO1");
new Operator(1, operateNum).start();
new Operator(9, operateNum).start();
}
}
此时的运行结果如下所示:
此实验结果中并未出现两个方法交替执行的情况,与实验四对比可知OperateNum类中method1和method9两个方法是等价的,即用synchronized修饰方法和用this对象锁定正方法体是等价的。
实验六 两个线程分别访问同一个类的两个不同实例的同一个同步的方法
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum1=new OperateNum("NO1");
OperateNum operateNum2=new OperateNum("NO2");
new Operator(3, operateNum1).start();
new Operator(3, operateNum2).start();
}
}
此时的运行结果如下所示
上图中的结果明显出现了交叉执行的现象,参照实验五的结论并将本实验与实验二对比可以发现:用不同的实例对同一个方法加锁,其结果等同与没有加锁。也就是说只有用同一个锁来对不同的资源进行锁定时才可以实现同步。
实验七 两个线程分别访问两个同步的类方法
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum1=new OperateNum("NO1");
OperateNum operateNum2=new OperateNum("NO2");
new Operator(5, operateNum1).start();
new Operator(6, operateNum2).start();
}
}
此时的运行结果如下所示:
从图中可以看出两个线程没有出现交叉执行的现象,该实验与实验六对比可以发现,用synchronized修饰类方法时,省略的锁不是this指代的实例,而是OperateNum.class。它在当前虚拟机中是惟一的,因此这两个方法实现了同步。
实验八 两个线程分别访问一个同步的类方法和一个同步的实例方法
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum1=new OperateNum("NO1");
OperateNum operateNum2=new OperateNum("NO2");
new Operator(1, operateNum1).start();
new Operator(5, operateNum2).start();
}
}
此时的运行结果如下所示:
结果中出现了明显的交叉执行方法,由此可以看出用synchronized修饰的类方法和实例方法并不是用的同一个锁,因此两个方法没有实现同步。
实验九 两个线程同时访问一个同步部分代码的方法
此时Test.java的源代码如下所示:
public class Test {
public static void main(String[] args){
OperateNum operateNum=new OperateNum("NO1");
new Operator(10, operateNum).start();
new Operator(10, operateNum).start();
}
}
此时的运行结果如下所示:
上述结果很有意思,我们首先刨除输出*的语句,在剩下的语句中可以看出两个线程并没有出现交叉现象,这是因为输出method的方法是同步的。但是输出*的语句却出现了交叉现象,这是因为输出*的语句并没有实现同步。
基于OperateNum和Operator两个类还可以组合出很多个实利,有兴趣的读者可以自行探索研究。