多线程编程中,最关键、最关心的问题应该就是同步问题,这是一个难点,也是核心。
从jdk最早的版本的synchronized、volatile,到jdk 1.5中提供的java.util.concurrent.locks包中的Lock接口(实现有ReadLock,WriteLock,ReentrantLock),多线程的实现也是一步步走向成熟化。
同步,它是通过什么机制来控制的呢?第一反应就是锁,这个在学习操作系统与数据库的时候,应该都已经接触到了。在Java的多线程程序中,当多个程序竞争同一个资源时,为了防止资源的腐蚀,给第一个访问资源的线程分配一个对象锁,而后来者需要等待这个对象锁的释放。
是的,Java线程的同步,最关心的是共享资源的使用。
先来了解一些有哪些线程的共享资源,
从JVM中了解有哪些线程共享的数据是需要进行协调:
1,保存在堆中的实例变量;2,保存在方法区的类变量。
而在Java虚拟机加载类的时候,每个对象或类都会与一个监视器相关联,用来保护对象的实例变量或类变量;当然,如果对象没有实例变量,或类没有变量,监视器就什么也不监视了。
为了实现上面的说的监视器的互斥性,虚拟机为每一个对象或类都关联了一个锁(也叫隐形锁),这里说明一下,类锁也是通过对象锁来实现的,因为在类加载的时候,JVM会为每一个类创建一个java.lang.Class的一个实例;所以当锁对对象的时候,也就锁住这个类的类对象。
另外,一个线程是可以对一个对象进行多次上锁,也就对应着多次释放;它是通过JVM为每个对象锁提供的lock计算器,上一次锁,就加1,对应的减1,当计算器的值为0时,就释放。这个对象锁是JVM内部的监视器使用的,也是由JVM自动生成的,所有程序猿就不用自己动手来加了。
介绍完java的同步原理后,我们进入正题,先来说说synchronized的使用,而其它的同步,将在后面的章节中介绍。
先来运行一个例子试试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package thread_test;
/**
* 测试扩展Thread类实现的多线程程序
*
*/
public class TestThread extends Thread{
private int threadnum;
public TestThread( int threadnum) {
this .threadnum = threadnum;
}
@Override
public synchronized void run() {
for ( int i = 0 ;i< 1000 ;i++){
System.out.println( "NO." + threadnum + ":" + i );
}
}
public static void main(String[] args) throws Exception {
for ( int i= 0 ; i< 10 ; i++){
new TestThread(i).start();
Thread.sleep( 1 );
}
}
}
|
运行结果:
1
2
3
4
5
6
7
8
9
10
11
|
NO.0:887
NO.0:888
NO.0:889
NO.0:890
NO.0:891
NO.0:892
NO.0:893
NO.0:894
NO.7:122
NO.7:123
NO.7:124
|
上面只是一个片段,说明一个问题而已。
细心的童鞋会发现,NO.0:894后面是NO.7:122,也就是说没有按照从0开始到999。
都说synchronized可以实现同步方法或同步块,这里怎么就不行呢?
先从同步的机制来分析一下,同步是通过锁来实现的,那么上面的例子中,锁定了什么对象,或锁定了什么类呢?里面有两个变量,一个是i,一个是threadnum;i是方法内部的,threadnum是私有的。
再来了解一下synchronized的运行机制:
在java程序中,当使用synchronized块或synchronized方法时,标志这个区域进行监视;而JVM在处理程序时,当有程序进入监视区域时,就会自动锁上对象或类。
那么上面的例子中,synchronized关键字用上后,锁定的是什么呢?
当synchronized方法时,锁定调用方法的实例对象本身做为对象锁。本例中,10个线程都有自己创建的TestThread的类对象,所以获取的对象锁,也是自己的对象锁,与其它线程没有任何关系。
要实现方法锁定,必须锁定有共享的对象。
对上面的实例修改一下,再看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package thread_test;
/**
* 测试扩展Thread类实现的多线程程序
*
*/
public class TestThread extends Thread{
private int threadnum;
private String flag; //标记
public TestThread( int threadnum,String flag) {
this .threadnum = threadnum;
this .flag = flag;
}
@Override
public void run() {
synchronized (flag){
for ( int i = 0 ;i< 1000 ;i++){
System.out.println( "NO." + threadnum + ":" + i );
}
}
}
public static void main(String[] args) throws Exception {
String flag = new String( "flag" );
for ( int i= 0 ; i< 10 ; i++){
new TestThread(i,flag).start();
Thread.sleep( 1 );
}
}
}
|
也就加了一个共享的标志flag。然后在通过synchronized块,对flag标志进行同步;这就满足了锁定共享对象的条件。
是的,运行结果,已经按顺序来了。
通过synchronized块,指定获取对象锁来达到同步的目的。那有没有其它的方法,可以通过synchronized方法来实现呢?
根据同步的原理:如果能获取一个共享对象锁或类锁,及可实现同步。那么我们是不是可以通过共享一个类锁来实现呢?
是的,我们可以使用静态同步方法,根据静态方法的特性,它只允许类对象本身才可以调用,不能通过实例化一个类对象来调用。那么如果获得了这个静态方法的锁,也就是获得这个类锁,而这个类锁都是TestThread类锁,及达到了获取共享类锁的目的。
实现代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package thread_test;
/**
* 测试扩展Thread类实现的多线程程序
*
* @author ciding
* @createTime Dec 7, 2011 9:37:25 AM
*
*/
public class TestThread extends Thread{
private int threadnum;
public TestThread( int threadnum) {
this .threadnum = threadnum;
}
public static synchronized void staticTest( int threadnum) {
for ( int i = 0 ;i< 1000 ;i++){
System.out.println( "NO." + threadnum + ":" + i );
}
}
public static void main(String[] args) throws Exception {
for ( int i= 0 ; i< 10 ; i++){
new TestThread(i).start();
Thread.sleep( 1 );
}
}
@Override
public void run(){
staticTest(threadnum);
}
}
|
运行结果略,与第二个例子中一样。
以上的内容主要是说明两个问题:同步块与同步方法。
1,同步块:获取的对象锁是synchronized(flag)中的flag对象锁。
2,同步方法:获取的是方法所属的类对象,及类对象锁。
静态同步方法,由于多个线程都会共享,所以一定会同步。
而非静态同步方法,只有在单例模式下才会同步。