线程安全本质上是多个线程操作同一数据,要保证数据的准确。而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对象如果值相等,可能其对应的对象引用也完全一样,即两个对象完全一样
-