并发控制 - Java的锁

并发控制 - Java的锁

之前谈论了关于两种并发控制思路,乐观锁与悲观锁。在实际开发过程中,在多线程开发环境,还需要保证多线程的原子性、可见性、有序性,这时候通常就会引入锁机制,由于本人的主要开发语言是Java,所以这回就来主要谈论一下Java内对于锁的实现和使用。

Java种有两种锁实现,分别是Java内置的Synchronized关键字和由Java1.5引入的java.util.concurrent.locks

Synchronized

Synchronized是Java内置的关键字,一般用于关键部分的同步处理控制,以将并发的任务逻辑转换成串行的处理逻辑。同理,Synchronized在保证了数据的一致性的同时,也大大的降低了任务的处理效率。因此,在高性能、高并发、高流量的WEB服务器上,对于Synchronized关键字,还是能不用尽量不用。

Synchronize使用:

/*
 * 同步方法块
 * 给obj对象加锁
 */
synchronized(obj){
    // do something
    globalVariable = ...
}
/*
 * 同步方法
 * 相当于给当前对象加锁
 */
synchronized method(Object param){
    // do something
    globalVariable = ...
}
/*
 * 同步静态方法
 * 给类加锁
 */
static synchronized method(Object param){
    // do something
    globalVariable = ...
}
复制代码
Tip使用 synchronized关键字施加的同步块会在同步块内抛出异常后自动释放锁,而Lock不会,需要调用 lock.unlock();才会释放锁

Synchronized原理

查看Synchronized部分编译后的字节码文件可以发现有两个指令,mointorentermointorexit指令,可以了解到Jvm种的同步机制都是基于Mointor对象实现的,但要注意,同步方法并不是由 monitorentermonitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

java.util.concurrent.locks

Concurrent并发包是在java1.5时引入的,旨在更优雅的解决并发所带来的问题,本文要谈论其中重点的两个锁,ReentrantLock重入锁,和ReadWriteLock读写锁。

在具体了解重入锁和读写锁之前,我们得先了解他们的公共实现的Lock接口:

public interface Lock {
    // 显示加锁
    void lock();

    // 获得锁,但优先响应中断
    void lockInterruptibly() throws InterruptedException;

    // 尝试获取锁,若锁被占用,则返回false
    boolean tryLock();

    // 尝试在x时间内获取锁,若时间内无法获取锁,则放弃
    boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

    // 显示释放锁
    void unlock();

    Condition newCondition();
}
复制代码

简单实践:

private static ReentrantLock lock = new ReentrantLock();
static int count; // 全局变量

public static void main(String[] args){
    Runnable runnable = () -> {
        lock.lock();
        try{
            count ++;
        } finally{
            // 判断锁持有者是否为当前线程
            if(locak.isHeldByCurrentThread){
                lock.unlock();
            }
        }
    };
    
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    
    thread1.start();
    thread2.start();
}
复制代码

可以注意到,相对于synchronized关键字加锁来说,lock的加锁和释放锁都是显示的,需要注意的事当获取锁线程出现异常后并不会自动释放锁,需要在finally块中显示释放锁,不然该锁可能永远无法释放变成死锁。

Interrupted(中断) 中断机制是Java并发工具包里提供以应付无限等待的情况的机制,在synchronized关键字加锁情况中,当锁被占有时,线程会进入锁池,等待锁持有线程释放锁,这时候调用thread.interrupt(),线程是不会立即响应的。这时若锁持有线程发生了死锁,则会在锁池积压大量的线程无法释放。

实践:

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws Exception{
	Runnable runnable = () -> {
		try{
			lock.lockInterruptibly();
			Thread.sleep(30000); // 模拟死锁
		}catch(InterruptedException e){
			System.out.printf("thread interrupted %s \n", Thread.currentThread().getName());
		}finally{
			 if (lock.isHeldByCurrentThread()){
				lock.unlock();
			 }
		}
	};
	
	Thread thread1 = new Thread(runnable);
	Thread thread2 = new Thread(runnable);
	
	thread1.start(); // thread1将会获得锁
	thread2.start(); // thread2将会进入等待
	Thread.sleep(500);
	thread2.interrupt(); // thread2响应中断
}
复制代码

Timeout(超时) 超时机制也是解决锁无限阻塞线程一种方式,该功能的使用比较简单,可以通过Lock的tryLock()方法和tryLock(long timeout, TimeUtil timeUtil)方法实现,看方法签名就可以了解传入时长,设置时间单位,tryLock()方法在规定时间过后会放弃锁竞争。

ReentrantLock构造函数

public ReentrantLock() {
	this.sync = new ReentrantLock.NonfairSync();
}

public ReentrantLock(boolean var1) {
	this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
复制代码

根据方法逻辑可以看出,构造函数的boolean参数传入,若为true则是使用公平锁,默认为非公平锁。

  • 公平锁
    • 根据等待时长分配锁权限
      • 公平锁会平均分配时间片,会降低系统吞吐量,但能保证线程被顺序执行
  • 非公平锁
    • 随机分配锁权限

ReetrantLock(重入锁)

重入锁是Java并发工具包锁接口的一个具体实现,之所以叫重入锁,是指该锁可以被一个线程重复多次获取。当然,获取了多少次,就要释放多少次,释放次数少了,将无法离开锁边界,其它线程也无法得到该锁。释放次数多了,则会抛出IllegalMonitorStateException

简单实践:

public class ReentrantLockExample{
    private static Lock lock = new ReentrantLock();
    static int count;

    public static void main(String[] args){
        ReentrantLockExample example = new ReentrantLockExample();

        Runnable runnable = () -> {
            example.executeThings();
        };

        Thread testThread = new Thread(runnable);
        testThread.start();
    }

    void executeThings(){
        lock.lock();
        count++;
        if(count == 10){
            lock.unlock();
        }else{
            executeThings();
            lock.unlock();
        }
    }
}
复制代码

按照该实践逻辑,该实例线程递归地执行了executeThings()方法,因此多次的获取了了锁,在达到了执行边界后,又递归的多次释放了锁。若重入锁不能重入,则会在第一次递归时和自己产生死锁,这在执行逻辑上是不允许的,这也是重入锁的意义之一。

Condition

Condition是锁的监视方法,用于替换Object对象的wait(), notify(), notifyAll()方法。

先来看一下Condition接口的方法:

public interface Condition {
    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long var1) throws InterruptedException;

    boolean await(long var1, TimeUnit var3) throws InterruptedException;

    boolean awaitUntil(Date var1) throws InterruptedException;

    void signal();

    void signalAll();
}
复制代码

可以看出来,Condition接口的方法都能和Object的线程控制方法一一对应。

简单实践:

private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();

public static void main(String[] args){
        Runnable workThread = () -> {
            lock.lock();
            try {
                System.out.println("ready to await");
                condition.await();
                System.out.println("keep running");
            } catch (InterruptedException e) {
                // 不做任何操作
            } finally {
                if (lock.isHeldByCurrentThread()){
                    lock.unlock();
                }
            }
        };

        Runnable singleThread = () -> {
            // 获取锁 后执行唤醒方法
            lock.lock();
            condition.signal();
            lock.unlock();
        };

        Thread threadTest0 = new Thread(workThread);
        Thread threadTest1 = new Thread(singleThread);

        threadTest0.start();
        Thread.sleep(3000);
        threadTest1.start();
}

/*
 * 输出:
 * ready to await
 * 三秒后
 * keep running
 */
复制代码

ReadWriteLock(读写锁)

一般来说,对于数据的读需求回要比写需求大得多,在操作数据时可能并不一定要针对数据加锁,甚至说读内容根本无需加锁,所以在Java并发工具包中,还提供了读写锁,读写分离,以提高效率。

简单实践:

 private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock();
private static Lock writeLock = lock.writeLock();

private static volatile int value;

public static void main(String[] args) throws Exception{
	Callable<Integer> readThread  = () -> {
		readLock.lock();
		try{
			// 模拟文件读取
			Thread.sleep(100);
		}catch(InterruptedException e){
			// do nothing
		}finally {
			readLock.unlock();
		}
		return value;
	};

	Runnable writeThread = () -> {
		writeLock.lock();
		try{
			value += 2;
		}finally {
			writeLock.unlock();
		}
	};
}
复制代码

总结

Java中的锁,Synchronizedjava.util.concurrent.locks到这里就算是大体谈论完了,synchronized可以渐渐的由Locks替换掉。虽说概念性的东西和简单使用可以通过这篇文字弄明白,但是实际并发开发中会碰到的问题还是太多种多样了,尽量还是结合并发的设计模式,缓存,消息队列,锁等等工具去最大程度的解决并发问题。

ps:个人博客地址是shawjie.me,不定期会发布一些自己所经历的,所学习的,所了解的,欢迎来坐坐。

猜你喜欢

转载自juejin.im/post/5d5a9dd8e51d453c2577b789