线程安全本质上是多个线程操作同一数据,要保证数据的准确。而Synchronized提供了线程互斥访问,同一时刻只能有一个线程来执行特定方法实现对数据的操作。

使用方式:

  • 同步普通方法,锁的是当前对象。

  • 同步静态方法,锁的是当前 Class 对象。

  • 同步块,锁的是 {} 中的对象。

可重入

synchronized 是可重入锁。

从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。

我们回来看synchronized,synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的

在 java 内部,同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入

实现原理

每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

线程的异常

线程是独立执行的代码片断,线程的问题应该由线程自己来解决,而不要委托到外部,也就是说,线程方法的异常(无论是checked还是unchecked exception),都应该在线程代码边界之内(run方法内)进行try catch并处理掉

异常会释放锁

 

synchronized 既保证了原子性也保证了可见性。

面试题:模拟银行账户,对业务写方法加锁,对业务读方法不加锁,这样行不行?(容易产生脏读。)

因为这种情况不能保证写操作完成后再进行读操作,没写完就读容易读到不准确的中间态数据,即产生脏读。

所以为了不产生这种情况,同时对读方法也进行加锁,保证读方法和写方法同时只有一个方法在执行。(给读方法也加synchronized)

 

锁升级过程

jdk1.5之前,是重量锁

后来改进成锁升级。

sync(Object)

markword 记录这个线程id 偏向锁

如果有线程正用,升级为 自旋锁 CAS

自旋10次之后升级为重量级锁,向OS申请。

 

什么时候用自旋锁(占用CPU)?什么时候用重量级锁(系统锁)?

执行时间短(加锁代码),线程数少,用自旋锁。

执行时间长,线程数多,用自旋锁。

用户态/内核态

 

总结一下:

  • 锁的是对象,不是代码;

  • 锁this XX.class

  • 锁定方法和非锁定方法可以同时执行

  • 锁升级:偏向锁-自旋锁-重量级锁

  • synchronized(Object) 不能用String常量,Integer,Long 基础数据类型

    对于Sting来说,其在堆内存中同一个值一般只有一份,也就是说两个String对象如果值相等,可能其对应的对象引用也完全一样,即两个对象完全一样

  •