在Java5当中,专门提供了锁对象,利用锁可以方便的实现对资源的封锁,用来控制对竞争资源并发访问控制,这些内容主要集中在java.util.concurrent.locks包下面。
基本概念:
- 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
- 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
- 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
- CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 ——
内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在
CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B
放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
Synchronized
Synchronized是java当中的关键字,它提供了一种独占枷锁的方式,Synchronized的获取和释放由JVM来实现,不需要显示的释放锁,非常方便。Synchronized也有一定的局限性
(1)、Synchronized获取不到锁,就会一直阻塞。
(2)、如果获取锁的线程进入休眠或者阻塞状态时,除非当前线程异常,否则其它线程获取锁时就会一直等待。
ReentrantLock
- lock():如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
- tryLock()):如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
- tryLock(long timeout,TimeUnitunit)):如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
- lockInterruptibly):如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
ReentrantLock 一些特性:
(1)、等待可中断,避免出现死锁的情况。
(2)、ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好
实现原理:
ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
总结:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术
(1)synchronized:在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。
(2)、eentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
public class Main {
public static void main(String args[]){
//创建并发访问的账户
MyCount myCount=new MyCount("370911199123345123",10000);
//创建一个锁对象
Lock lock=new ReentrantLock();
//创建一个线程池
ExecutorService executorService= Executors.newCachedThreadPool();
User user1=new User(lock,"张三",myCount,-2000);
User user2=new User(lock,"李四",myCount,-500);
User user3=new User(lock,"王五",myCount,3000);
User user4=new User(lock,"赵六",myCount,-6000);
executorService.execute(user1);
executorService.execute(user2);
executorService.execute(user3);
executorService.execute(user4);
//关闭线程池
executorService.shutdown();
}
}
/**
* 信用卡用户
*/
class MyCount {
private String oid;//账户
private int cash;//余额
MyCount(String oid,int cash){
this.oid=oid;
this.cash=cash;
}
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public int getCash() {
return cash;
}
public void setCash(int cash) {
this.cash = cash;
}
@Override
public String toString() {
return "MyCount{" +
"oid='" + oid + '\'' +
", cash=" + cash +
'}';
}
}
/**
* 信用卡账户,可随意透支
*/
class User implements Runnable {
private String name; //用户名
private MyCount myCount;//账户
private int cash; //操作的金额
private Lock myLock; //执行操作所需要的对象
User(Lock myLock,String name,MyCount myCount,int cash){
this.myLock=myLock;
this.name=name;
this.myCount=myCount;
this.cash=cash;
}
@Override
public void run() {
//获取锁
this.myLock.lock();
//执行现金业务
System.out.println(name+"正在操作"+this.myCount+"账户,金额为"+this.cash+",当前金额为"+this.myCount.getCash());
this.myCount.setCash(this.myCount.getCash()+this.cash);
System.out.println(name+"操作"+this.myCount+"账户成功,金额为"+this.cash+",当前金额为"+this.myCount.getCash());
//释放锁
this.myLock.unlock();
}
}
结果如下:
张三正在操作MyCount{oid='370911199123345123', cash=10000}账户,金额为-2000,当前金额为10000
张三操作MyCount{oid='370911199123345123', cash=8000}账户成功,金额为-2000,当前金额为8000
李四正在操作MyCount{oid='370911199123345123', cash=8000}账户,金额为-500,当前金额为8000
李四操作MyCount{oid='370911199123345123', cash=7500}账户成功,金额为-500,当前金额为7500
王五正在操作MyCount{oid='370911199123345123', cash=7500}账户,金额为3000,当前金额为7500
王五操作MyCount{oid='370911199123345123', cash=10500}账户成功,金额为3000,当前金额为10500
赵六正在操作MyCount{oid='370911199123345123', cash=10500}账户,金额为-6000,当前金额为10500
赵六操作MyCount{oid='370911199123345123', cash=4500}账户成功,金额为-6000,当前金额为4500
从上面的输出看出,利用lock对象来锁定对象实现同步非常的方便。一定要注意,当锁定对象以后,还需要释放锁,以供其它的线程使用。