使用synchronized需要注意的问题
1.与monitor关联的对象不能为空
private fianl Object mutex = null;
public void syncMethod(){
synchronized(mutex){
//
}
}
2.synchronized作用域太大
由于synchronized关键字在排他性,也就是说所有的线程必须串行地经过synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率低,甚至还会丧失并发的优势。
3.不同的monitor企图锁相同的方法
public static class Task implements Runnable{
private final Object MUTEX = new Object();
@Override
public void run(){
//....
synchronized(MUTEX){
//..
}
//..
}
public static void main(String[] args){
for(int i = 0;i < 5; i++){
new Thread(Task :: new).start();
}
}
}
上面的代码构造了五个线程,同时也构造了五个Runnable实例,Runnable作为线程逻辑执行单元传递给Thread,然后你将会发现,synchronized根本互斥不了与之对应的作用域,线程之间进行monitor lock 的争抢只能发生在与monitor关联的同一个引用上,上面的代码每一个线程争抢的monitor关联引用都是彼此独立的,因此不可能起到互斥的作用。
4.多个锁的交叉导致死锁
多个锁的交叉容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作,如下面的代码所示,
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read(){
synchronized(MUTEX_READ){
synchronized(MUTEX_WRITE){
//......
}
}
}
public void write(){
synchronized(MUTEX_WRITE){
synchronized(MUTEX_READ){
//......
}
}
}
程序死锁
1.交叉锁可导致程序出现死锁
线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待获取R1的锁(典型的哲学家吃面),这种情况最容易导致程序死锁。
2.内存不足
当并发请求系统可用内存时,如果此时系统内存不足,则可能会出现死锁的情况,举个例子,两个线程T1和T2,执行某个任务,其中T1已经获取了10MB内存,T2获取了20MB内存,如果每个线程的执行单元都需要30MB的内存,但是剩余可用的内存刚好为20M,那么两个线程有可能都在等待彼此能够释放内存资源。
3.一问一答式的数据交换
服务端开启某个端口,等待客户端访问,客户端发送请求立即等待接收,由于某种原因服务端错过了客户端的请求,仍然在等待一问一答式的数据交换,此时服务端和客户端都在等待着双方发送数据
4.数据库锁
无论是数据库级别的锁,还是行级别的锁,比如某个线程执行for update 语句退出了事物,其它线程访问该数据库时都将陷入死锁。
5.文件锁
某个线程获得了文件锁意外退出,其他读取该文件的线程也将会进入死锁直到系统释放文件句柄资源。
6.死循环引起的死锁
程序由于代码原因或者对某些异常处理不得当,进入死循环,虽然查看线程堆信息不会发现任何死锁的迹象,但是程序不工作,CPU占有率又居高不下,这种死锁一般称为系统假死,是一种最为致命也是最难排查的死锁现象,由于重现困难,进程对系统资源的使用量又达到了极限,想要做出dump有时候也是非常困难的。
程序死锁举例
程序由于交叉锁引起的死锁情况,代码如下:
public class DeadLock {
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read(){
synchronized (MUTEX_READ){
System.out.println(Thread.currentThread().getName() + " get MUTEX_READ");
synchronized (MUTEX_WRITE){
System.out.println(Thread.currentThread().getName() + " get MUTEX_WRITE");
}
System.out.println(Thread.currentThread().getName() + " release MUTEX_WRITE");
}
System.out.println(Thread.currentThread().getName() + " release MUTEX_READ");
}
public void write(){
synchronized (MUTEX_WRITE){
System.out.println(Thread.currentThread().getName() + " get MUTEX_WRITE");
synchronized (MUTEX_READ){
System.out.println(Thread.currentThread().getName() + " get MUTEX_READ");
}
System.out.println(Thread.currentThread().getName() + " release MUTEX_READ");
}
System.out.println(Thread.currentThread().getName() + " release MUTEX_WRITE");
}
public static void main(String[] args) {
final DeadLock deadLock = new DeadLock();
new Thread(() ->{
while (true){
deadLock.read();
}
},"READ-THREAD").start();
new Thread(() ->{
while (true){
deadLock.write();
}
},"WRITE-THREAD").start();
}
死锁诊断
1. 交叉锁引起的死锁
运行DeadLod代码,程序将陷入死锁,打开jstack工具或者jconsole工具,jstack -l PID会直接发现死锁的信息,在cmd下输入jsp,可以查看到DeadLock的进程id,使用jstack pid可以看到进程的状态,如图:
一般交叉锁引起的死锁线程都会进入BLOCKED状态,CPU资源占用不高,很容易借助辅助工具来发现。
(2)死循环引起的死锁(假死)
运行如下程序(谨慎运行,可能会导致你的电脑死机,运行前将文件做好备份):
public class HashMapDeadLock {
private final HashMap<String,String> map = new HashMap<>();
public void add(String key,String value){
this.map.put(key,value);
}
public static void main(String[] args) {
final HashMapDeadLock hmd1 = new HashMapDeadLock();
for(int x = 0;x < 2;x++){
new Thread(() -> {
for(int i = 1;i < Integer.MAX_VALUE;i++){
hmd1.add(String.valueOf(i),String.valueOf(i));
}
}).start();
}
}
}
运行HashMapDeadLock 程序,也可以使用jstack,jconsole,jvisualvm工具或者jProfiler(收费)工具进行诊断,但是不会给出很明显的提示,因为工作的线程并未BLOCKED,而是始终处于RUNNABLE状态,CPU使用率高居不下,甚至都不能正常运行你的命令。
严格意义上来说循环会导致程序假死,算不上真正的死锁,但是某个线程对CPU消耗过多,导致其他线程等待CPU,内存等资源也会陷入死锁等待。
本文深入探讨Java并发编程中常见的陷阱,包括空对象锁、synchronized作用域过大、不同monitor锁同一方法、多锁交叉死锁等问题,通过实例解析如何避免这些陷阱。
324

被折叠的 条评论
为什么被折叠?



