Java并发编程:synchronized

场景:两个线程同时往一个数据库表插入不重复的数据。

当使用两个线程同时往一个数据表中插入数据时,通常分如下2步来进行

①首先校验该条记录在数据库表中是否存在

②如果不存在,则插入数据

 

  虽然多线程编程极大地提高了效率,但是也会带来一定的隐患。比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入相同的数据。

 

1.什么时候会出现线程安全问题

多个线程同时访问一个资源(eg:同一个数据库)时,会导致程序运行结果并不是想看到的结果。

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

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

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

 

 

2.如果解决线程安全问题

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

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

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

在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock

 

 

 

3.synchronized同步方法或者同步块

      在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,顾名思义:能到达互斥访问目的的锁。

      举个简单的例子:如果对临界资源加上互斥锁,当一个线程访问该临界资源时,其他线程便只能等待。

在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

      在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchroniezed方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

      下面通过几个简单的例子来说明synchronized关键字的使用:

1.synchronized方法

下面这段代码中两个线程分别调用insertData对象插入数据:

package com.bjsxt.demo;

import java.util.ArrayList;
import java.util.List;

public class SychronizedDemo {
	
	public static void main(String[] args) {
		//把insertData修饰成final,则变量insertData不能再赋予其他的引用了
		final InsertData insertData = new InsertData();
		
		//创建并启动线程1
		new Thread(new Runnable(){
			@Override
			public void run() {
				insertData.insert(Thread.currentThread());
			}
		}).start();
		
		//创建并启动线程2
		new Thread(new Runnable(){
			@Override
			public void run() {
				insertData.insert(Thread.currentThread());
			}
		}).start();
		
	}

}

class InsertData{
	//模拟数据表
	private List<Integer> arrayList = new ArrayList<Integer>();
	
	public void insert(Thread thread){
		for(int i = 0; i < 5 ; i++){
			System.out.println(thread.getName()+"在插入数据"+i);
			//往模拟表中插入数据
			arrayList.add(i);
		}

	}
}

 此时程序的输出结果为:

说明两个线程在同时执行insert方法代码块。

而如果在insert方法前面加上关键字synchronized的话,运行结果为:

此时程序的输出结果为:

从上输出结果说明,Thread-1插入数据是等Thread-0插入完数据之后进行的。说明Thread-0和Thread-1是顺序执行insert方法的。

这就是synchronized方法。

不过有几点需要注意:

1)当一个线程正在访问一个对象的synchronized方法的时候,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

2)当一个线程正在访问一个对象的synchronized方法的时候,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用临界资源,那么其他线程是可以访问这个方法的。

3)如果一个线程A需要访问对象object1的synchronized方法的fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型,也不是产生线程安全,因为他们访问的是不同的对象,所以不存在互斥问题。

 

2.synchronized代码块

synchronized(synObject) {
         
    }

 

synchronized代码块类似于以下这种形式:

当在某个线程中执行这段代码块,该线程会获取对象synObject的锁,从而使得其他线程无法同时访问该代码块。
  synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。
  比如上面的insert方法可以改成以下两种形式:

形式1:

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
     
    public void insert(Thread thread){
        synchronized (this) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}

 

 

形式2:

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();
     
    public void insert(Thread thread){
        synchronized (object) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}

从上面可以看出,synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码需要同步。如果此时对整个方法用synchronized进行同步,会影响程序的执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。

 

 

另外,每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。

 

并且如果一个线程执行一个对象的synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问synchronized方法占用的是对象锁,所以不存在互斥现象。

 

看下面这段代码就明白了:

package com.bjsxt.demo;

import java.util.ArrayList;
import java.util.List;

public class SychronizedDemo {
	
	public static void main(String[] args) {
		//把insertData修饰成final,则变量insertData不能再赋予其他的引用了
		final InsertData insertData = new InsertData();
		
		//创建并启动线程1
		new Thread(new Runnable(){
			@Override
			public void run() {
				insertData.insert(Thread.currentThread());
			}
		}).start();
		
		//创建并启动线程2
		new Thread(new Runnable(){
			@Override
			public void run() {
				insertData.insert(Thread.currentThread());
			}
		}).start();
		
		//创建并启动线程2
		new Thread(new Runnable(){
			@Override
			public void run() {
				InsertData.staticInsert(Thread.currentThread());
			}
		}).start();
		
	}

}

class InsertData{
	//模拟数据表
	private List<Integer> arrayList = new ArrayList<Integer>();
	
	//线程使用对象锁
	public synchronized void insert(Thread thread){
			for(int i = 0; i < 5 ; i++){
				System.out.println(thread.getName()+"在插入数据"+i);
				//往模拟表中插入数据
				arrayList.add(i);
			}
	}
	
	//线程使用类锁
	public static synchronized void staticInsert(Thread thread){
		for(int i = 0; i < 5 ; i++){
			System.out.println(thread.getName()+"----"+i);
		}
	}
}

 执行结果:

Thread-0和Thread-1线程里执行insert方法,不会导致staticInsert方法发生堵塞现象。

 

 

下面我们来看一下synchronized关键字到底做了什么事情,我们反编译它的字节码看看,下面这段代码反编译后的字节码为:

public class InsertData {
    private Object object = new Object();
     
    public void insert(Thread thread){
        synchronized (object) {
         
        }
    }
     
    public synchronized void insert1(Thread thread){
         
    }
     
    public void insert2(Thread thread){
         
    }
}

从反编译获得的字节码可以看出,synchronized的代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数器减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记加密,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

 

有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

 

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

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值