锁状态

Java中锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

锁自旋
我们知道在当某个线程在进入同步方法/代码块时若发现该同步方法/代码块被其他现在所占,则它就要等待,进入阻塞状态,这个过程性能是低下的。

在遇到锁的争用或许等待事,线程可以不那么着急进入阻塞状态,而是等一等,看看锁是不是马上就释放了,这就是锁自旋。锁自旋在一定程度上可以对线程进行优化处理。

偏向锁
偏向锁主要为了解决在没有竞争情况下锁的性能问题。在大多数情况下锁锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当某个线程获得锁的情况,该线程是可以多次锁住该对象,但是每次执行这样的操作都会因为CAS(CPU的Compare-And-Swap指令)操作而造成一些开销消耗性能,为了减少这种开销,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

当有其他线程在尝试着竞争偏向锁时,持有偏向锁的线程就会释放锁。

锁膨胀
多个或多次调用粒度太小的锁,进行加锁解锁的消耗,反而还不如一次大粒度的锁调用来得高效。

轻量级锁
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。轻量级锁在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的指向和状态。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

加锁方式

在使用synchronized时,我们是这样使用锁的:

public class ThreadTest {
    public void test(){
        synchronized(this){
            //do something
        }
    }
}

synchronized可以确保在同一时间内只有一个线程在执行dosomething。下面是使用lock替代synchronized:

public class ThreadTest {
    Lock lock = new Lock();
    public void test(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

lock()方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。

锁的公平性

公平性的对立面是饥饿。那么什么是“饥饿”呢?如果一个线程因为其他线程在一直抢占着CPU而得不到CPU运行时间,那么我们就称该线程被“饥饿致死”。而解决饥饿的方案则被称之为“公平性”——所有线程均可以公平地获得CPU运行机会。

导致线程饥饿主要有如下几个原因:
高优先级线程吞噬所有的低优先级线程的CPU时间。我们可以为每个线程单独设置其优先级,从1到10。优先级越高的线程获得CPU的时间越多。对大多数应用来说,我们最好是不要改变其优先级值。

线程被永久堵塞在一个等待进入同步块的状态。Java的同步代码区是导致线程饥饿的重要因素。Java的同步代码块并不会保证进入它的线程的先后顺序。这就意味着理论上存在一个或者多个线程在试图进入同步代码区时永远被堵塞着,因为其他线程总是不断优于他获得访问权,导致它一直得到不到CPU运行机会被“饥饿致死”。

线程在等待一个本身也处于永久等待完成的对象。如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。

为了解决线程“饥饿”的问题,我们可以使用锁实现公平性。

锁的可重入性

我们知道当线程请求一个由其它线程持有锁的对象时,该线程会阻塞,但是当线程请求由自己持有锁的对象时,是否可以成功呢?答案是可以成功的,成功的保障就是线程锁的“可重入性”。
“可重入”意味着自己可以再次获得自己的内部锁,而不需要阻塞。如下:

public class Father {
    public synchronized void method(){
        //do something
    }
}
public class Child extends Father{
    public synchronized void method(){
        //do something 
        super.method();
    }
}

如果所是不可重入的,上面的代码就会死锁,因为调用child的method(),首先会获取父类Father的内置锁然后获取Child的内置锁,当调用父类的方法时,需要再次后去父类的内置锁,如果不可重入,可能会陷入死锁。

java多线程的可重入性的实现是通过每个锁关联一个请求计算和一个占有它的线程,当计数为0时,认为该锁是没有被占有的,那么任何线程都可以获得该锁的占有权。当某一个线程请求成功后,JVM会记录该锁的持有线程 并且将计数设置为1,如果这时其他线程请求该锁时则必须等待。当该线程再次请求请求获得锁时,计数会+1;当占有线程退出同步代码块时,计数就会-1,直到为0时,释放该锁。这时其他线程才有机会获得该锁的占有权。

  • ReentrantLock:一个可重入的互斥锁,为lock接口的主要实现。
  • ReadWriteLock:ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。
  • ReentrantReadWriteLock:可重入读写锁
  • Semaphore:一个计数信号量。
  • Condition:锁的关联条件,目的是允许线程获取锁并且查看等待的某一个条件是否满足。
  • CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。