一、前言
多线程开发离不开锁机制,现在的Java语言中,提供了2种锁,一种是语言特性提供的内置锁,还有一种是 java.util.concurrent.locks 包中的锁。
二、synchronized
首先来看看用的比较多的synchronized。synchronized是用于为某个代码块的提供锁机制,在java的对象中会隐式的拥有一个锁,这个锁被称为内置锁(intrinsic)或监视器锁(monitor locks)。线程在进入被synchronized保护的块之前自动获得这个锁,直到完成代码后(也可能是异常)自动释放锁。内置锁是互斥的,一个锁同时只能被一个线程持有,这也就会导致多线程下,锁被持有后后面的线程会阻塞。正因此实现了对代码的线程安全保证了原子性。
1、指定当前对象加锁:
private synchronized void function() {
//TODO execute something
}
2、指定当前类的Class对象加锁:
private static synchronized void function() {
//TODO execute something
}
注意此处的 static 关键字。
3、指定任意对象加锁:
private void function() {
synchronized (object) {
//TODO execute something
}
}
此时,这段同步代码块的锁加在object对象上面。该对象可以是当前对象(object == this),也可以是当前类的Class对象(object == MyClass.class)。
三、java.util.concurrent.locks.Lock
前面看了synchronized,大部分的情况下差不多就够啦,但是现在系统在并发编程中复杂性是越来越高,所以总是有许多场景synchronized处理起来会比较费劲。或者像<java并发编程>中说的那样,concurrent中的lock是对内部锁的一种补充,提供了更多的一些高级特性。java.util.concurrent.locks.Lock这个接口抽象了锁的主要操作,也因此让从Lock派生的锁具备了这些基本的特性:无条件的、可轮循的、定时的、可中断的。而且加锁与解锁的操作都是显式进行。下面是它的代码:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
1、ReentrantLock(类)
ReentrantLock就是可重入锁,连名字都这么显式。ReentrantLock提供了和synchronized类似的语义,但是ReentrantLock必须显式的调用,比如:
public class BaseClass {
private Lock lock = new ReentrantLock();
public void do() {
lock.lock();
try {
//....
} finally {
lock.unlock();
}
}
}
这种方式对于代码阅读来说还是比较清楚的,只不过有个问题,就是如果忘了加try finally或忘 了写lock.unlock()的话导致锁没释放,很有可能导致一些死锁的情况,synchronized就没有这个风险。
1)trylock
ReentrantLock是实现Lock接口,所以自然就拥有它的那些特性,其中就有trylock。trylock就是尝试获取锁,如果锁已经被其他线程占用那么立即返回false,如果没有那么应该占用它并返回true,表示拿到锁啦。另一个trylock方法里带了参数,这个方法的作用是指定一个时间,表示在这个时间内一直尝试去获得锁,如果到时间还没有拿到就放弃。因为trylock对锁并不是一直阻塞等待的,所以可以更多的规避死锁的发生。
2)lockInterruptibly
lockInterruptibly是在线程获取锁时优先响应中断,如果检测到中断抛出中断异常由上层代码去处理。这种情况下就为一种轮循的锁提供了退出机制。为了更好理解可中断的锁操作,写了一个demo来理解。
2、ReadWriteLock(接口)
顾名思义就是读写锁,这种读-写锁的应用场景可以这样理解,比如一波数据大部分时候都是提供读取的,而只有比较少量的写操作,那么如果用互斥锁的话就会导致线程间的锁竞争。如果对于读取的时候大家都可以读,一旦要写入的时候就再将某个资源锁住。这样的变化就很好的解决了这个问题,使的读操作可以提高读的性能,又不会影响写的操作。一个资源可以被多个读者访问,或者被一个写者访问,两者不能同时进行。这是读写锁的抽象接口,定义一个读锁和一个写锁。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
在JDK里有个ReentrantReadWriteLock实现,就是可重入的读-写锁。ReentrantReadWriteLock可以构造为公平的或者非公平的两种类型。如果在构造时不显式指定则会默认的创建非公平锁。在非公平锁的模式下,线程访问的顺序是不确定的,就是可以闯入;可以由写者降级为读者,但是读者不能升级为写者。如果是公平锁模式,那么选择权交给等待时间最长的线程,如果一个读线程获得锁,此时一个写线程请求写入锁,那么就不再接收读锁的获取,直到写入操作完成。
四、死锁
死锁的定义:“死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。”那么我们换一个更加规范的定义:“集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。”竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西。
1、死锁检测
在这里,我将介绍两种死锁检测工具
1)Jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
首先,我们通过jps确定当前执行任务的进程号:
root@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher
可以确定任务进程号是1362,然后执行jstack命令查看当前进程堆栈信息:
root@~$ jstack -F 1362
Attaching to process ID 1362, please wait...
Debugger attached successfully.
Server compiler detected.
2、JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。我们在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“链接”
进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”