一、概述:
在多线程的访问下,为了解决数据的不一致性或脏数据的出现,引入了锁的概念,对共享的临界资源进行加锁,只有获得这把锁的线程才能访问这段资源,与此同时,别的线程不能进行访问,只有当前线程执行完毕释放锁后竞争到锁的线程才能够访问。下面我们通过一个例子分别介绍两种锁:
例:下面有一个计数器类,一个静态成员变量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有尝试获取锁的操作,线程更加安全