Java并发编程:synchronized关键字解析

本文探讨了多线程编程中的线程安全问题,并详细介绍了Java中的synchronized关键字如何帮助解决这类问题。通过示例代码展示了不同场景下synchronized的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     虽然多线程编程极大地提高了效率,但是也会带来一定的隐患。比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入了相同的数据。今天我们就来一起讨论下线程安全问题,以及Java中提供了什么机制来解决线程安全问题。


一、什么时候会出现线程安全问题


      在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:

  由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。

举个简单的例子:

  现在有两个线程分别从网络上读取数据,然后插入一张数据库表中,要求不能插入重复的数据。

  那么必然在插入数据的过程中存在两个操作:

  1)检查数据库中是否存在该条数据;

  2)如果存在,则不插入;如果不存在,则插入到数据库中。

  假如两个线程分别用thread-1和thread-2表示,某一时刻,thread-1和thread-2都读取到了数据X,那么可能会发生这种情况:

  thread-1去检查数据库中是否存在数据X,然后thread-2也接着去检查数据库中是否存在数据X。

  结果两个线程检查的结果都是数据库中不存在数据X,那么两个线程都分别将数据X插入数据库表当中。

  这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。

  这里面,这个资源被称为:临界资源(也有称为共享资源)。

  也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。

  不过,当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。


二、如何解决线程安全问题


那么一般来说,是如何解决线程安全问题的呢?

  基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

  通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

  在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock(下节讲述)。


三、JVM角度看下锁


在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:

  •  保存在堆中的实例变量
  •  保存在方法区中的类变量

这两类数据是被所有线程共享的。
(程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)

      在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。

  • 对于对象来说,相关联的监视器保护对象的实例变量。
  • 对于类来说,监视器保护类的类变量。

       (如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。)
为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。

但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)

类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。

一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。

java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。

在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。


四、synchronized及其所锁对象



synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。


  很多人都知道,在Java多线程编程中,有一个重要的关键字,synchronized。但是很多人看到这个东西会感到困惑:“都说同步机制是通过对象锁来实现的,但是这么一个关键字,我也看不出来Java程序锁住了哪个对象阿?“,看demo1:

public class ThreadTest extends Thread {    
    private int threadNo;    
    public ThreadTest(int threadNo) {    
        this.threadNo = threadNo;    
    }    
    public static void main(String[] args) throws Exception {    
        for (int i = 1; i < 10; i++) {    
           new ThreadTest(i).start();    
            Thread.sleep(1);    
        }    
     }    
     
    @Override   
     public synchronized void run() {    
        for (int i = 1; i < 10000; i++) {    
            System.out.println("No." + threadNo + ":" + i);    
        }    
     }    
 }   
      这个程序其实就是让10个线程在控制台上数数,从1数到9999。理想情况下,我们希望看到一个线程数完,然后才是另一个线程开始数数。但是这个程序的执行过程告诉我们,这些线程还是乱糟糟的在那里抢着报数,丝毫没有任何规矩可言。
      但是细心的读者注意到:run方法还是加了一个synchronized关键字的,按道理说,这些线程应该可以一个接一个的执行这个run方法才对阿。

  对于一个成员方法加synchronized关键字,这实际上是以这个成员方法所在的对象本身作为对象锁。在本例中,就是 以ThreadTest类的一个具体对象,也就是该线程自身作为对象锁的。一共十个线程,每个线程持有自己 线程对象的那个对象锁。这必然不能产生同步的效果。换句话说,如果要对这些线程进行同步,那么这些线程所持有的对象锁应当是共享且唯一的!


再看demo2:

public class ThreadRelated extends Thread{

    private int threadNo;
    private String lock;

    public ThreadRelated(int threadNo,String lock) {
        this.threadNo = threadNo;
        this.lock=lock;
    }

	public static void main(String[] args) throws Exception {

		String lock = new String("lock");
                for (int i = 1; i < 5; i++) {
                    new ThreadRelated(i,lock).start();
                Thread.sleep(1);
            }
	}

	public  void run(){
		synchronized(lock){
	        for (int i = 1; i < 100; i++) {
	            System.out.println("No." + threadNo + ":" + i);
	        }
		}
	}
}
  

       我们注意到,该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest2的构造函数,将这个对象赋值 给每一个ThreadTest2线程对象中的私有变量lock。根据Java方法的传值特点,我们知道,这些线程的lock变量实际上指向的是堆内存中的 同一个区域,即存放main函数中的lock变量的区域。
       程序将原来run方法前的synchronized关键字去掉,换用了run方法中的一个synchronized块来实现。这个同步块的对象锁,就是 main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!

于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。

demo3:

public class ThreadRelated extends Thread{

       private int threadNo;

       public ThreadRelated(int threadNo) {
           this.threadNo = threadNo;
       }

	public static void main(String[] args) throws Exception {


        for (int i = 1; i < 5; i++) {
               new ThreadRelated(i).start();
               Thread.sleep(1);
           }
	 }

	public static synchronized void a(int no){
		for(int i=0;i<100;i++){
			 System.out.println("No." + no + ":" + i);
		}
	}

	public  void run(){
		a(threadNo);
	}
}
      细心的读者发现了:这段代码没有使用main方法中创建的String对象作为这10个线程的线程锁。而是通过在run方法中调用本线程中一个静态的同步 方法a而实现了线程的同步。我想看到这里,你们应该很困惑:这里synchronized静态方法是用什么来做对象锁的呢?

       我们知道,对于同步静态方法,对象锁就是该静态放发所在的类的Class实例,由于在JVM中,所有被加载的类都有唯一的类对象,具体到本例,就是唯一的 ThreadRelated.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!


这样我们就知道了:

 

1、对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;

2、如果采用method级别的同步,则对象锁即为method所在的对象,如果是静态方法,对象锁即指method所在的Class对象(唯一);

3、对于代码块,对象锁即指synchronized(abc)中的abc;

4、因为第一种情况,对象锁即为每一个线程对象,因此有多个,所以同步失效,第二种共用同一个对象锁lock,因此同步生效,第三个因为是

static因此对象锁为ThreadTest3的class对象,因此同步生效。


五、总结


  •  无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  •  每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。


转:

http://blog.youkuaiyun.com/luoweifu/article/details/46613015

http://www.cnblogs.com/dolphin0520/p/3923737.html

http://blog.youkuaiyun.com/yangzhijun_cau/article/details/6432216


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值