java中锁的由来
为什么使用锁
多线程对同一资源进行操作时会引发线程不安全,合理的使用锁可以避免线程不安全
现象。
如下代码就会引起线程不安全现象
public staticvoidmain(String[] args) {
final CountBean countBean=new CountBean();
final CountDownLatch countDownLatch=new CountDownLatch(10000);
for(int i=0;i<10000;i++){
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
countBean.i++;
try {
countDownLatch.countDown();
}catch(Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
try {
countDownLatch.await();
}catch(InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(countBean.i);
}
如上代码如果线程安全的话结果应该是10000
实际打印出9987(注这个值时不固定的只能说明多线程对 共享资源countBean的i进行操作时引发了不安全现象)
java中产生线程不安全的原因
java 内存模型规定所有变量都存储在主内存中(main memory),每条线程还有自己的工作内存(working memory)
主内存和工作内存具体的交互协议,java内存模型规定以下8中操作来完成
1.lock(锁定):作用与主内存变量,把一个变量标志为一条线程独占状态
2.unlock(解锁):作用与主内存变量,他把一个处于锁定状态的变量释放出来,释放后变量才可以被其他线程锁定
3.read(读取):作用与主内存变量,他把一个变量从主内存传输到工作线程中,以便随后的load动作使用
4.load(载入):作用于工作内存变量,它把read操作从主内存得到的变量值放入到工作内存的变量副本中
5.use(使用):作用于工作内存的变量,他把工作内存的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时,将会执行这个动作
6.assign(赋值):作用于工作内存的变量,他把一个从执行引擎接收到的值赋给工作内存变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个动作
7.store(存储):作用于工作内存的变量,他把工作内存中的一个变量的值传送到主内存中,
以便随后的write操作使用
8.write(写入):作用于主内存变量,它把store操作从工作内存中得到的变量值放入主内存变量中
如上,因为java的线程是抢占式的,线程可以在任何地方失去cpu的执行权,
如果load动作之后write动作未完成,有其他线程read变量可能就不是最新的值引发线程不安全现象
synchronized 关键字介绍
java中每个对象都有一把锁,如果方法或者块获得了对象的锁后,其他的需要获得锁的对象只能等待直到获取锁的方法释放了锁。
synchronized 修饰非static普通方法时表示锁的是当前对象
synchronized 修饰static普通方法时表示锁的是类对象
synchronized 修饰方法块时需要指定锁的对象
java会在synchronized 块前后加上monitorenter 和monitorexit两条指令
ReentrantLock 介绍及源码分析
ReentrantLock是基于代码实现的锁,我们通过源码分析下实现原理
AQS(AbstractQueuedSynchronizer)框架介绍
主要结构如上(其实翻译过来就是同步抽象队列):
ReentrantLock源码分析
ReentrantLock是用纯代码实现的锁,
ReentrantLock实现了lock接口,使用组合方式注入NonfairSync对象,NonfairSync继承与AbstractQueuedSynchronizer
ReentrantLock构造方法
//初始化一个抽象同步队列
public ReentrantLock(){
sync = new NonfairSync();
}
ReentrantLock的lock方法
public voidlock() {
//直接调用NonfairSync的lock方法
sync.lock();
}
//NonfairSync的lock方法
finalvoidlock() {
//利用cpu的cas原理(即cpu原语修改AbstractQueuedSynchronizer的线程状态变量,如果修改成功则表示获取锁)
if (compareAndSetState(0, 1))
//设置当前线层为锁的拥有者
setExclusiveOwnerThread(Thread.currentThread());
else
//如果没有获取锁则执行acquire
acquire(1);
}
//看下acquire(1)的代码是在AbstractQueuedSynchronizer实现的
public finalvoidacquire(intarg) {
//尝试获取锁
if (!tryAcquire(arg) &&
//获取锁失败则将获取锁的线程放入AbstractQueuedSynchronizer的队列里
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protectedfinalbooleantryAcquire(intacquires) {
return nonfairTryAcquire(acquires);
}
//如果状态为0则证明锁以被释放继续尝试获取锁,如果获取锁的是当前线程则状态+1,这个就是可重入锁的说明
finalbooleannonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//将获取锁的对象封装成node对象
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq onfailure
Node pred = tail;
//先快速插入队列
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果快速插入队列失败则死循环的插入队列尾部
enq(node);
return node;
}
acquireQueued()方法,
//
finalboolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p =node.predecessor();
if (p == head&& tryAcquire(arg)) {
setHead(node);
p.next =null; // help GC
failed =false;
returninterrupted;
}
if(shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这是个死循环 如果p == head && tryAcquire(arg) 不满足则会调用parkAndCheckInterrupt阻塞线程
最终调用的是unsafe.park(false,0L),阻塞线程这是个native方法这里不解释了
最后我们看下unlock函数怎样释放锁的
public void unlock() {
sync.release(1);
}
publicfinal boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus!= 0)
unparkSuccessor(h);
return true;
}
return false;
}
privatevoid unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possiblyneeding signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waitingthread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor,which is normally
* just the next node. But if cancelledor apparently null,
* traverse backwards from tail to find theactual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null&& t != node; t = t.prev)
if (t.waitStatus <=0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
主要是修改syn的状态变量队列的移除,唤醒其他线程
读写锁简介
java读写锁适用于读多写少的场景,相对一把锁而言,读写锁在读多写少的情况下大幅度提高性能
注意场景:同一线程在持有读锁的情况下不能调用写锁的lock方法,会造成死锁。
持有写锁后可以调用读锁的lock方法,当调用写锁的unlock后写锁降级为读锁
以下是个简单的demo
package com.mq.test.readwritelock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public classReadwritelockTest {
private static ReentrantReadWriteLock lock = newReentrantReadWriteLock();
private static WriteLock writelock = lock.writeLock();
private static ReadLock readlock = lock.readLock();
public static void main(String[] args) {
final CountDownLatchcountDownLatch = newCountDownLatch(100);
final Map map = new HashMap();
for (int i = 0; i < 100; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
writelock.lock();
map.put("1",Thread.currentThread().getId());
System.out.println("我获取了写锁");
countDownLatch.countDown();
}catch(Exception e) {
// TODO: handle exception
}finally{
writelock.unlock();
}
}
}).start();
}
for (int i = 0; i < 100; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
readlock.lock();
map.put("1",Thread.currentThread().getId());
System.out.println("我获取了读锁");
countDownLatch.countDown();
}catch(Exception e) {
// TODO: handle exception
}finally{
readlock.unlock();
}
}
}).start();
}
}
}
自旋锁简介
由于锁的阻塞,1.需要挂起当前线程(即保留当前线程的线程状态),2.回复执行线程的现场
这些操作都要转入内核状态中完成,给高并发系统带来很大的性能开销。同时虚拟机团队注意到在许多应用中,共享数据的锁定状态很短,为了这段时间挂起和恢复线程并不值得,如果物理机上是多核的,可以让线程忙循坏下(即不干任何事的死循环)就不用挂起和恢复线程了。这项技术称为自旋锁(jdk1.6以后默认开启),当然阻塞时间长了白白浪费了很多cpu也是不值得的,虚拟机默认自旋10(可以修改)如果还没有获得锁则转为阻塞状态。
偏向锁简介
偏向锁的偏就是偏心的偏,偏袒的偏。偏向锁是为了消除同步操作(locking,unlock,mark word,update),
当开启了偏向锁后,当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位设置为“01",即偏向模式,利用cas将线程id记录到对象的mark work中,如果cas成功,则持有偏向锁的线程每次进入同步快时就不用进行任何同步操作了
当然如果期间有其他线程尝试获取锁,则偏向模式宣告结束
死锁简介
死锁是假设有两个线程A,B,有两个对象obj1,obj2
如果A线程获取obj1的锁后,又去申请obj2的锁
而B线程获取obj2的锁后,又去申请obj1的锁
两边都在等待对方锁的释放,又都没释放出来
demo
public staticvoidmain(String[] args) {
final Object obj1= new Object();
final Object obj2= new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized(obj1){
try {
System.out.println(Thread.currentThread().getName()+"我获得了obj1的锁");
Thread.sleep(100);
synchronized (obj2) {
System.out.println(Thread.currentThread().getName()+"我获得了obj2的锁");
}
}catch(Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized(obj2){
try {
System.out.println(Thread.currentThread().getName()+"我获得了obj2的锁");
Thread.sleep(100);
synchronized (obj1) {
System.out.println(Thread.currentThread().getName()+"我获得了obj1的锁");
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}).start();
}