b2186e1bb44421518773af3c41d7f92ea6a4b87a.png@1320w_1270h.webp

synchronize关键字(常见问题)
多个线程去访问同一个资源的时候,需要上锁,目的是保证状态的一致,就像数据库的事务一样.
类比场景:多个同学去厕所蹲坑.

锁的是什么?
是一个对象(包括class对象),拿到锁之后才能去执行某段代码.而不是锁的代码.

注意不要用String常量和Integer等基础数据类型作为锁的对象
不能用String的原因:所有的字符串常量都是同一个对象,假如引用的依赖包中用了String常量作为锁的对象,自己的程序也刚好用了相同String常量作为锁的对象,那么肯定会引起奇怪的问题.
也尽量不用String对象.
不能用Integer等基础数据类型的原因:Integer内部做了一些特殊处理,Integer的对象的值一旦改变,就会变成一个新对象

  1. public class T {
  2. private int count = 10;
  3. private Object o = new Object();
  4. public void m() {
  5. synchronized(o) { //任何线程要执行线面的代码,必须先拿到对象o的锁
  6. count--;
  7. System.out.println(Thread.currentThread().getName() + " count = " + count);
  8. }
  9. }
  10. }

synchronize底层怎么实现?什么是锁升级?synchronize一定比原子类慢?

JVM规范中没有任何要求,只要能保证功能完整就行.
HotSpot是这样的:对象头(64位)中,拿出2位来记录这个对象是不是被锁定了,mark word.

synchronize修饰的对象,编译之后,class文件中会加上monitorenter和monitorexit来实现的

JDK早期,synchronize是重量级的,每次都去找操作系统申请锁,效率很低;
后来改进了,引入了锁升级(可以看没错,我就是厕所所长一文)
2.1 偏向锁:当第一个线程去执行带synchronize方法时,先在对象头的markword上记录这个线程的线程号,不加锁;如果下次又是该线程执行那个方法,就直接访问不需获取锁.(偏向第一个线程)
2.2. 自旋锁:如果有线程争用,则升级为自旋锁,比如线程t1正在访问带锁资源r,这时t2也要访问r,那么t2先不加锁,while(true)空执行一会,看t1是不是会马上释放锁.默认自旋10次,如果10后还得不到锁,则升级为重量级锁
2.3. 重量级锁:去操作系统申请资源加锁

Hotspot目前的实现,锁只能升级,不能降级.
所以现在的synchronize并不一定比那些原子类慢,因为有锁升级
自旋锁占用CPU,但是不去跟操作系统申请资源加锁,只是在用户态,不经过内核态.
当加锁方法执行时间很长,或者线程数很多时,用操作系统锁比较好;
当执行时间很短,且线程不太多时,用自旋锁合适.
synchronize的特点
可以在方法上加synchronize关键字,锁定当前对象(非静态方法),或者当前类的class对象(静态方法)
synchronize(this)锁定当前对象;
对于非静态方法,synchronize(this)如果锁住了方法中的所有代码,那就和直接在方法上加synchronize是一样的;对于静态方法,方法上的synchronize相当于synchronize(T.class)
(每一个.class文件,load到内存以后,会生成一个对应的Class对象)
synchronize既保证可见性,又保证原子性
可重入.假如两个方法m1,m2都对同一个对象加了锁;如果m1中调用了m2,是可以再次获得锁的.(如果不允许重入,那就死锁了).可重入的概念是在同一个线程的基础上的.
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
class对象是单例的吗?
同一个ClassLoader内是单例的;多个ClassLoader间,不是单例;但是不同加载器之间不能互相访问.所以,可以认为是单例.

父类中有一个synchronize方法,在子类中调用,那么锁的是谁?
是子类对象,打印一下this即可证明:

  1. public class T {
  2. synchronized void m() {
  3. System.out.println("super m start");
  4. System.out.println(this);
  5. try {
  6. TimeUnit.SECONDS.sleep(1);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println("super m end");
  11. }
  12. public static void main(String[] args) {
  13. new TT().m();
  14. System.out.println("---");
  15. new TT().m2();
  16. System.out.println("---");
  17. new TT().m3();
  18. }
  19. }
  20. class TT extends T {
  21. @Override
  22. synchronized void m() {
  23. System.out.println("child m start");
  24. System.out.println(this);
  25. super.m();
  26. System.out.println("child m end");
  27. }
  28. synchronized void m2() {
  29. System.out.println("child m2 start");
  30. System.out.println(this);
  31. super.m();
  32. System.out.println("child m2 end");
  33. }
  34. void m3() {
  35. System.out.println("child m2 start");
  36. System.out.println(this);
  37. super.m();
  38. System.out.println("child m2 end");
  39. }
  40. }

带锁的方法和不带锁的方法可以同时执行吗?

可以.

程序中如果抛出了异常,锁会被释放吗?

抛出异常会释放锁,所以一定要处理好异常,防止出现异常后被其他线程访问资源,从而导致各种状态不一致的问题.

锁优化

锁细化:锁住(synchronize包括)的代码,在能保证业务逻辑OK下,越少越好.
锁粗化:假如一段业务逻辑,中间有很多个细化的小锁,这些小锁的方法别的业务又不会调用,那就把这些小锁合并成一个大锁.

锁的属性(field)发生变化,会影响锁的使用效果吗?

锁的属性变化不会影响锁的功能;
但是如果锁的变量(或者说”引用”)指向了别的对象,那就不是同一把锁了,会出问题.可以通过给变量加上final关键字避免这个问题.