我们知道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类本身,而不是它的实例,因此多线程对类变量的访问是同步的。
六、总结
最后做一下总结
- 在多线程环境中,可以使用synchronized关键字对资源进行同步
- synchronized关键字可以同步方法和代码块
- 同步的是对象或者类,而不是代码
- 一个对象中的同步方法一次只能被一个线程访问,如果有多个同步方法,一个线程一次也只能访问其中的一个同步方法,但是非同步方法不受任何影响
- 同步是通过加锁的形式来控制的,让一个线程访问一个同步方法时会获得这个对象的锁,只有退出同步方法时才会释放这个锁,其它线程才可访问