ReentrantLock与synchronized的区别

一、概述:

在多线程的访问下,为了解决数据的不一致性或脏数据的出现,引入了锁的概念,对共享的临界资源进行加锁,只有获得这把锁的线程才能访问这段资源,与此同时,别的线程不能进行访问,只有当前线程执行完毕释放锁后竞争到锁的线程才能够访问。下面我们通过一个例子分别介绍两种锁:

例:下面有一个计数器类,一个静态成员变量count初始值为0,包含2个实例方法,其中add()对count做1万次++操作,dec()对count做1万次--操作。测试类中用两个线程同时对count进行操作。

// 计数器类
class Counter{
	// 用于计数的公共变量
	public static int count = 0;
	// 递增
	public void add() {
		for (int i = 0; i < 10000; i++) {
				Counter1.count += 1;
		}
	}
	// 递减
	public void dec() {
		for (int i = 0; i < 10000; i++) {
				Counter1.count -= 1;
		}
	}
}

执行结果:

我们创建两个线程同时对count操作,t1线程执行++操作,t2线程执行--操作,启动t1、t2,并让主线程等待t1、t2执行完成后输出count的值。如果按照理论来说,最后的count应该=0

但通过执行结果来看,不等于0并且值不固定。实际上自增和自减的这一个操作对应程序中的3条指定:1、读取count值;2、进行count+1;3、保存count;整个自增自减操作是没有原子性的。在2个线程同时执行add()和decline(),同时对count进行操作,就会存在add()进行count+1后,同时刻dec()对count进行count-1中的count是在add()+1前的count值或者反过来dec()--,add()++时的count是dec()--之前的count值。

二、synchronized

synchronized属于java中的关键字,它可作用于方法、代码块上。

1、作用于静态方法上,默认使用的是当前类的Class对象作为锁

public static void add() {};
public static void dec() {};

若Counter类中的add()和dec()都是静态方法,通过synchronized加锁,如下:

public synchronized static void add() {
        for (int i = 0; i < 10000; i++) {
                Counter1.count += 1;
        }
    }
    public static void dec() {
        for (int i = 0; i < 10000; i++) {
            synchronized (this.getClass()) {
                Counter1.count -= 1;
            }
        }
    }

测试类主要代码部分展示:

执行结果:

add()加锁形式与dec()的加锁形式效果等同

 注意:synchronized作用在静态方法上,相当于当前类的class对象作锁

在线程的run()执行体中调用add()和dec()对象的Class对象相等才能达到加锁的目的

2、作用于实例方法上,默认使用的是当前类的实例对象作为锁

实例方法中加锁

public synchronized void add() {
……此处代码内容同上省略
};
public void dec() {
	for (int i = 0; i < 10000; i++) {
		synchronized (this){
			Counter1.count -= 1;
	    }
	}
}

测试类

add()中将锁直接加在方法声明上和dec()加在内部代码块上效果一致

 注意:synchronized作用在实例方法上,相当于当前类对象作锁

在线程的run()执行体中调用add()和dec()对象是同一个才能达到加锁的目的

3、作用于代码块上,默认通常使用自定义的Object对象作锁

class Counter3 {
    // 用于计数的公共变量
    public static int count = 0;
    private final Object lock=new Object();//对象锁
    public void add() {
        for (int i = 0; i < 10000; i++) {
            synchronized (lock){
                Counter3.count += 1;
            }
        }
    }
    public void dec() {
        for (int i = 0; i < 10000; i++) {
            synchronized (lock){
                Counter3.count -= 1;
            }
        }
    }
}

测试类

 执行结果:

同样注意:synchronized作用在代码块上,在类中自定义的Object对象作锁,该Object对象为当前类的对象所属,在线程的run()执行体中调用add()和dec()对象是同一个才能达到加锁的目的

以上三种情况是synchronized锁实现线程安全的使用,另外在补充一个点:

在java的标准库下的java.util.atomic包下提供了一部分具有原子性操作的类,针对上面的例子,我们可以使用java.util.atomic包下的AtomicInteger类

class Counter4 {
    //AtomicInteger具有原子性
    public static AtomicInteger count=new AtomicInteger(0);
    // 递增
    public void add() {
        for (int i = 0; i < 10000; i++) {
            count.getAndIncrement();

        }
    }
    // 递减
    public void dec() {
        for (int i = 0; i < 10000; i++) {
            count.getAndDecrement();
        }
    }
}

测试类的代码同上面任何一个都可以,执行结果:

我们可以将公共的静态成员变量AtomicInteger定义为AtomicInteger类型,它能够保证你的操作具有原子性,避免出现结果不一致的线程安全问题。

三、ReentrantLock

ReentrantLock是java中关于锁Lock接口中的一个实现类,它在java.util.concurrent.locks包下

它通常用于方法体中,它提供了一对方法来"加锁"和"释放锁":ReentrantLock对象的lock()和

ReentrantLock对象unlock().在这对方法中间的代码操作就能够保证只一个线程访问,对于上面的例子,为了保证结果的正确性ReentrantLock是如何使用的呢?

class Counter5 {
    //定义ReentrantLock锁
    private final ReentrantLock lock=new ReentrantLock();
    //全局变量
    public static int count=0;
    // 递增
    public void add() {
        for (int i = 0; i < 10000; i++) {
            lock.lock();
            count++;
            lock.unlock();
        }
    }
    // 递减
    public void dec() {
        for (int i = 0; i < 10000; i++) {
            lock.lock();
            count--;
            lock.unlock();
        }
    }
}

执行结果:

ReentrantLock实现线程安全的方式:对不安全的操作用lock()和unlock()包围起来,同样ReentrantLock若定义在类中,由类对象所有,那么对数据操作是用同一个对象调用,保证不同线程对访问类中的方法是同一把锁。

我们分别用synchronized和ReentrantLock实现了上面例子的线程安全。

它们都加锁完成了线程安全,那么synchronized和ReentrantLock之间有没有区别呢?

synchronized是一种同步锁、可重入锁。

四、synchronized和ReentrantLock的区别

ReentrantLock

Synchronized

锁实现机制

AQS

监视器Monitor

获取锁

可以通过tryLock()尝试获取锁,更灵活

线程抢占模型

释放锁

必须显示通过unlock()释放锁

自动释放

锁类型

支持公平锁和非公平锁

非公平锁

可重入性

可重入

可重入

1、底层实现锁机制所用的技术不同。

2、ReentrantLock中有tryLock()可尝试获取锁,线程能设置等待时间和单位,超时若还没有获取到锁我们可以进行别的操作代码继续向下走;Synchronized是线程抢占模式,没有抢到锁的线程只能一直等待,进入阻塞状态,这过程中不能够进行别的操作,一直处于等待,可能会产生死锁。

3、ReentrantLock的释放锁需要手动调用unlock();Synchronized会在它所包裹的代码块或方法结束自动释放。

4、ReentrantLock的锁类型(公平/非公平)可通过构造方法设定,默认非公平(NonfairSync);Synchronized是一种非公平锁,谁抢到锁谁执行。

5、ReentrantLock和Synchronized都是可重入锁。

可重入锁是指在同一方法中能够多次获取到该锁,下面通过例子来看(以Synchronized为例):

class Task{
    public void Q1(){
        synchronized (this){
            System.out.println(Thread.currentThread().getName()+"已准备就绪");
            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"开始执行任务");
            }
        }
    }
}

 执行结果:

 Task类中对Q1()使用了两次Synchronized关键字作用在代码块上,通过执行结果:

第二个Synchronized包围的代码块中的代码执行了,同一线程在方法获得了当前锁后可以再次获得该锁。在获取锁同时,会将锁的头部信息进行修改,包括当前锁被哪个线程持有,目前是第几次获取该锁。每获取一次,会+1,释放一次会-1,直到为0才表示该锁真正释放。

 小结:

  • 完成加锁的同时需确保多个线程对共享资 源的访问时使用的是同一把锁
  • ReentrantLock相比Synchronized有尝试获取锁的操作,线程更加安全
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值