深入理解synchronized关键字

一、什么是synchronized

简单解释:synchronized提供一种排他机制,也就是同一时间只能有一个线程执行某些操作。
权威解释:synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么该对象的所有读写都将通过同步的方式来进行。
具体表现为:

  • synchronized关键字提供一种锁机制,能够确保变量的互斥访问,从而防止数据不一致问题的出现。
  • synchronized关键字包括monitorenter和monitorexit两个JVM指令,他能保证在任何时候任何线程执行到monitorenter成功之前都必须从主内存获取数据,而不是从缓存中,在monitorexit运行成功之后,共享变量被更新的值必须刷入主内存
  • 一个monitorexit指令之前必须有一个monitorenter。

二、synchronized关键字的用法

synchronized关键字可以修饰方法和代码块,不能修饰class
同步方法:

 [public|private|protected] synchronized [static] type method()

同步代码块:

private Object object = new Object();
public void sync(){
	synchronized(object){
	...
	}
}

三、JVM指令分析

synchronized关键字提供一种互斥机制,本质是某线程获取了与MUTEX关联的monitor锁。使用JDK命令javap对有同步代码块的类进行反汇编,输出大量的JVM指令,在这些指令中有monitor enter和monitor exit成对出现。
这里写图片描述
1、monitorenter
每个对象都与一个monitor相关联,在一个线程尝试获得与对象关联的monitor的所有权时发生如下几件事:

  • 如果monitor的计数器为0,意味着该monitor的lock还没有被获得,某个线程获得之后立即对计数器加一。
  • 如果一个已经拥有该monitor的线程重入,monitor计数器继续累加。
  • 如果monitor已经被某线程获得,其他线程尝试获得时会进去阻塞状态,直到计数器变为0,才会再次尝试获得该monitor的所有权。

2、monitorexit
释放monitor所有权就是将monitor计数器减一,直到计数器为0,意味着该线程不再拥有该monitor所有权,与此同时被该monitor阻塞的线程可再次尝试获得对该monitor的所有权。

四、使用synchroniezd关键字需要注意的问题

1、与monitor关联的对象不能为空
2、synchronized作用域不应太大
由于synchronized关键字线程排他性,所有线程必须串行经过synchronized包裹的代码块,如果synchronized作用域太大,则效率越低。
3、不同monitor企图锁相同方法
示例:

public class Task implements Runnable {
    private final Object OBJECT = new Object();
    @Override
    public void run() {
        //...
        synchronized (OBJECT){
            //...
        }
        //...
    }
}
public class Test {
    public static void main(String[] args) {
        for (int i=0;i<5;i++){
            new Thread(Task::new).start();
        }
    }
}

上面的代码构造了5个线程,同时也构造了5个Runnable实例。Runnable作为逻辑单元传递给Thread,每一个Runnable实例彼此独立,也就是线程争抢的monitor关联引用彼此独立,因此起不到互斥的作用。
4、多个锁交叉导致死锁
线程1获得了A的锁,等待B的锁:线程2获得了B的锁,等待A的锁。导致交叉死锁。

五、程序死锁的原因

1、交叉锁
线程1获得了A的锁,等待B的锁:线程2获得了B的锁,等待A的锁。导致交叉死锁。
2、内存不足
两个线程T1和T2,执行某个任务时,T1已经获得10M内存,T2已经获得20M内存,每个线程执行单元都需要30M内存,但剩余可用内存不足,那么可能两个线程都在等待彼此释放资源内存,造成死锁。
3、一问一答式的数据交换
服务端开启某个端口等待客户端访问,客户端发送请求立即等待接收,但是由于某种原因服务端没有接收到请求,此时服务端和客户端都在等待对方发送数据。造成死锁。
4、数据库锁
比如某个线程执行了for update语句退出了事务,其他线程访问数据库时将陷入死锁。
5、文件锁
某个线程获得文件锁时意外退出,其他试图读取该文件的线程将陷入死锁,直到文件句柄资源被释放。
6、死循环引起的死锁
程序由于代码原因陷入死循环,查看线程堆栈信息不会发现任何死锁迹象,但程序不工作,CPU占有率居高不下,这种死锁一般称作系统假死。

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/82457903