Java synchronized 介绍

我们知道Java API提供了丰富的多线程机制,但是要想多线程机制能够正常运转,需要采取一些措施来防止多个线程访问相同的资源。为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程加上锁以后,其他线程便不能再使用那个资源,除非被解锁。

而在java中,对这种特殊的资源—— 对象中的内存—— Java 提供了内建的机制来防止它们的冲突。用Java中的Synchronized关键字来标记一个方法或者一个代码块就可以实现资源的同步。

synchronized主要使用在多线程环境中,关于线程的相关介绍请参考【java线程详解】:http://blog.csdn.net/suifeng3051/article/details/49251959

一、Synchronized 关键字

在java中实现资源同步非常简单,只需要用synchronized关键字来标记即可。需要记住的是,在java中,同步加锁的是一个对象或者一个类,而不是代码。在多线程环境中,对象的所有synchronized方法一次只能被一个线程访问,其它所有访问同步块的线程会被一直阻塞直到同步块中的线程退出。

synchronized方法控制对类对象方法的访问,每个类对象都对应一把锁,每个 synchronized 方法都必须获得该方法所属对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该对象锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个对象实例,其所有声明为 synchronized 的实例函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。注意,其它非synchronized的函数仍可被其它线程同时访问

在 Java 中,不光是类的对象,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功,因此, Java 为我们提供了更好的解决办法,那就是 synchronized 代码块。

synchronized关键字可标记四种代码块:

1. 实例方法
2. 静态方法
3. 实例方法中的代码块
4. 静态方法中的代码块

二、同步实例方法

 public synchronized void add(int value){
  this.count += value;
 }

通过上面示例可以看出,同步实例方法很简单,在类的实例方法中添加synchronized关键字即可。需要注意,同步实例方法在java中同步加锁的其实是这个方法所属的对象,对于这个对象的同步方法,一次只能有一个线程访问。如果想让两个线程同时访问这个类的方法,那么为每个线程构造一个实例即可。因此,对于同步的实例方法,每个对象都会有自己的同步方法。在某个特定对象中,它的同步方法一次之能被一个线程访问,如果有多个对象,那么每个对象都可以允许一个线程访问自己的同步方法。下面我们通过两个例子来说明一下同步实例方法。

2.1 实例一:一个对象中的同步实例方法一次只允许一个线程访问

1.构造一个计数器类Counter ,这个计数器可能会被多个线程访问,需要对其中的add()方法进行同步

public class Counter {
  long count = 0;
  //同步实例方法
  public synchronized void add() {
         count++;
         try {
              Thread. sleep(100);
        } catch (InterruptedException e) {
               e.printStackTrace();
        }
        System. out.println(Thread. currentThread().getName() + "--" + count);
  }
}

2.构造计数器线程类CounterThread 来访问计数器对象的add()方法

public class CounterThread extends Thread {
  protected Counter counter = null;
  public CounterThread( Counter counter) {
         this. counter = counter;
  }
  public void run() {
         //用多个线程调用同步实例方法
         for ( int i = 0; i < 5; i++) {
               counter.add();
        }
  }
}

3.构造一个CounterExample,用两个线程来同时访问一个对象实例

public class CounterExample {
  public static void main(String[] args) {       
        //构造一个含同步方法的对象实例   
        Counter counter = new Counter();
        Thread threadA = new CounterThread( counter);
        Thread threadB = new CounterThread( counter);
         threadA.start();
         threadB.start();
  }
}

我们看一下运行结果:

Thread-0--1
Thread-1--2
Thread-0--3
Thread-1--4
Thread-1--5
Thread-0--6
Thread-1--7
Thread-1--8
Thread-0--9
Thread-0--10

通过运行结果可以看到,每次打印的结果都增加了1,说明同步的add()方法一次只有一个线程访问。

2.2 实例二:为每个线程构造一个实例对象,每个线程调用自己对象的同步实例方法

计数器类和计数器线程类不变,我们只改造CounterExample方法:

public class CounterExample {
  public static void main(String[] args) {
         //构造两个实例,让每个线程访问一个实例
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread threadA = new CounterThread( counter1);
        Thread threadB = new CounterThread( counter2);
         threadA.start();
         threadB.start();
  }
}

我们构造了两个对象,然后再构造两个线程,让每个线程访问一个对象的同步方法。此时,两个线程会同时访问各自对象的同步方法,我们来看一下运行结果:

Thread-0--1
Thread-1--1
Thread-0--2
Thread-1--2
Thread-0--3
Thread-1--3
Thread-0--4
Thread-1--4
Thread-0--5
Thread-1--5``

我们看到,每个线程打印出来的结果都是顺序加1,两个线程互不干扰。

三、同步静态方法

同步静态方法和同步实例方法的唯一区别就是同步静态方法把synchronized关键字加到了静态方法上,看下面例子:

public static synchronized void add(int value){
  count += value;
}

我们知道静态方法即类方法,它属于一个类而不是某个对象。因此同步静态方法同步的是类的方法,不是实例方法。所以即使是多个线程访问不同的对象的同步静态方法,它们之间每次也只能有一个线程访问。下面我们通过一个例子证明:

实例三:测试同步静态方法

我们把上面Counter类中的add()方法改为静态的,然后再加synchronized关键字

public class Counter {
  static long count = 0;
  //同步的是静态方法
  public static synchronized void add() {
         count++;
         try {
              Thread. sleep(100);
        } catch (InterruptedException e) {
               e.printStackTrace();
        }
        System. out.println(Thread. currentThread().getName() + "--" + count);
  }
}

然后同样用多个线程访问多个实例:

public class CounterExample {
  public static void main(String[] args) {
         //构造两个实例,让每个线程访问一个实例
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread threadA = new CounterThread( counter1);
        Thread threadB = new CounterThread( counter2);
         threadA.start();
         threadB.start();
  }
}

我们看一下运行结果:

Thread-0--1
Thread-0--2
Thread-0--3
Thread-1--4
Thread-1--5
Thread-1--6
Thread-0--7
Thread-0--8
Thread-1--9
Thread-1--10``

我们可以看到,两个线程打印的结果都是连续加1的,add()方法是被同步访问的。

如果我们去掉上面add()方法中的synchronized关键字,大家猜一下结果又会是如何呢?我们看一下实验结果:

Thread-0--2
Thread-1--2
Thread-1--4
Thread-0--4
Thread-0--6
Thread-1--6
Thread-1--8
Thread-0--9
Thread-0--10
Thread-1--10

可以看到,两个线程每次打印结果都增加了2(线程的调度是不确定的,实际中的打印值有可能不同),因为,add()方法没有加同步关键字,所以add()方法同时被两个线程访问,所以每个线程打印出来结果都增加了2。

四、同步实例方法中的代码块

我们不必每次同步整个方法,有些时候我们希望仅仅是同步方法中的某个代码块。同步代码块主要有三种方式 :synchronized(this)synchronized (obj)synchronized(Object.class)(这种方法在同步静态代码块中会讲到),这三种方式分别是指同步本对象、同步其它对象、同步某个类。下面我们先分别举例说明前两种情况

3.1 实例四:同步代码块之synchronized(this)

我们把实例一Counter类 add()方法声明中的synchronized关键字加到add()代码块中,其它类不变:

public class Counter {
long count = 0;
public void add() {
    //同步代码块
    synchronized(this){
        count++;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--" + count);
    }

}
}

上面这个例子就是通同步代码块的方式同步代码,运行结果应该和实例一的一致。

请注意,同步代码块中有一个构造参数,这个参数值是一个对象,被称作监控对象,意思是同步的是此监控对象中的同步方法。上面这个例子中,(this)指的本对象。

下面我们再举个参数是一个其它对象的例子。

3.2 实例五:同步代码块之synchronized(obj)

我们先构造一个普通的Counter类

public class Counter {
  long count = 0;
  public void add() {
         count++;
         try {
              Thread. sleep(100);
        } catch (InterruptedException e) {
               e.printStackTrace();
        }
        System. out.println(Thread. currentThread().getName() + "--" + count);
  }
}

接下来再构造一个含同步代码块的CountSynBlock类,在这个类的add方法中,synchronized 构造参数中是Counter类的一个实例变量

public class CounterSynBlock {
  //声明一个实例变量
  private Counter counter;
  CounterSynBlock(Counter counter){
         this. counter= counter;
  }
  public void add() {
         //同步的是Counter对象实例
         synchronized ( counter) {
               counter.add();
        }
  }
}

我们再运行一个CounterExample

public class CounterExample {
  public static void main(String[] args) {
         //构造两个实例,让每个线程访问一个实例
        Counter counter= new Counter();
        CounterSynBlock counterSynBlock1 = new CounterSynBlock( counter);
        CounterSynBlock counterSynBlock2 = new CounterSynBlock(counter);
        Thread threadA = new CounterThread( counterSynBlock1);
        Thread threadB = new CounterThread( counterSynBlock2);
         threadA.start();
         threadB.start();
  }

大家看,在这个CounterExample中,我们分别实例化了2个CounterSynBlock实例,然后用两个线程分别访问这两个实例的同步代码块,但这两个实例中的同步的不是CounterSynBlock 对象,而是counter对象,所以,counter中的add()方法应该是被同步访问的。

看一下运行结果:

Thread-0--1
Thread-0--2
Thread-0--3
Thread-1--4
Thread-0--5
Thread-0--6
Thread-1--7
Thread-1--8
Thread-1--9
Thread-1--10

如果我们把CounterSynBlock类add()方法中的synchronized ( counter)换成synchronized(this),则同步的就成了CounterSynBlock的add()方法了,那么两个线程则会分别访问自己的同步方法,我们举例验证一下:

1.首先把CounterSynBlock类中的同步代码块synchronized ( counter)改回synchronized(this)

public class CounterSynBlock {
  //声明一个实例变量
  private Counter counter;
  CounterSynBlock(Counter counter){
         this. counter= counter;
  }
  public void add() {
         //同步的是本对象
         synchronized ( this) {
               counter.add();
        }
  }
}

2.我们再运行一下CounterExample

public class CounterExample {
  public static void main(String[] args) {
         //构造两个实例,让每个线程访问一个实例
        Counter counter= new Counter();
        CounterSynBlock counterSynBlock1 = new CounterSynBlock( counter);
        CounterSynBlock counterSynBlock2 = new CounterSynBlock(counter);
        Thread threadA = new CounterThread( counterSynBlock1);
        Thread threadB = new CounterThread( counterSynBlock2);
         threadA.start();
         threadB.start();
  }

3.看一下结果:

Thread-1--2
Thread-0--2
Thread-1--4
Thread-0--5
Thread-1--6
Thread-0--7
Thread-1--8
Thread-0--9
Thread-1--10
Thread-0--10

从结果可以看到,当把同步的对象由counter改为this后,打印的结果并不是连续的加1,说明两个线程产生了并发访问counter的同步方法情况。

五、同步静态方法代码块

上面讲到,除了在代码块中使用synchronized(this)synchronized(obj)之外,还可以使用synchronized(Object.class)同步一个类,既然这个类被同步了,那么多线程对所有的类变量访问也该是同步的,下面我们就举例说明。

实例六:同步静态代码块之synchronized(Object.class)

首先,构造一个Counter类,在这个类中,同步的是类本身

public class Counter {
//注意,此处的变量是静态的,属于类变量
static long count = 0;
public  void add() {
    //同步的是这个类本身
    synchronized(Counter.class){
        count++;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
            System.out.println(Thread.currentThread().getName() + "--" + count);
    }
}
}

其次,构造一个线程对象CounterThread ,访问Counter的同步代码块,代码块中同步的是Counter类本身

   public class CounterThread extends Thread {
protected Counter counter = null;
public CounterThread(Counter counter) {
    this.counter = counter;
}
public void run() {
    //用多个线程调用同步实例方法
    for (int i = 0; i < 5; i++) {
        counter.add();
    }
}
}

最后,运行下面这个测试类

public class CounterExample {
public static void main(String[] args) {
    //构造两个实例,让每个线程访问一个实例
    Counter counter1=new Counter();
    Counter counter2=new Counter();
    CounterThread threadA = new CounterThread(counter1);
    CounterThread threadB = new CounterThread(counter2);
    threadA.start();
    threadB.start();
}
}

我们看一下运行结果:

Thread-0--1
Thread-0--2
Thread-0--3
Thread-1--4
Thread-0--5
Thread-1--6
Thread-0--7
Thread-1--8
Thread-1--9
Thread-1--10

虽然每个线程访问的是各自的对象,由于Counter类中的count变量是static的,所以count是类的变量,因为我们同步的是Counter类本身,而不是它的实例,因此多线程对类变量的访问是同步的。

六、总结

最后做一下总结

  1. 在多线程环境中,可以使用synchronized关键字对资源进行同步
  2. synchronized关键字可以同步方法和代码块
  3. 同步的是对象或者类,而不是代码
  4. 一个对象中的同步方法一次只能被一个线程访问,如果有多个同步方法,一个线程一次也只能访问其中的一个同步方法,但是非同步方法不受任何影响
  5. 同步是通过加锁的形式来控制的,让一个线程访问一个同步方法时会获得这个对象的锁,只有退出同步方法时才会释放这个锁,其它线程才可访问
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值