并发-synchronized
线程并发-synchronized和Lock简单认知
前几天刚加深了线程的了解,期间在验证各种方法及多线程时遇到一些疑问,在高并发的情况下,怎么做才能保证程序还能按照我们预期的正常运行下去,这就是我们接下来探究下关于并发中的常用的一些线程安全方法、类等,仅个人见解, 忘广大学友纠正。
synchronized同步锁
-
特性
1.1 类、方法和代码块正确的使用synchronized可以保证并发情况下互斥线程的代码同步(原子性);
1.2 保证共享的资源可见性(类似volaite),每个线程都有自己的缓存区域,如果被锁的资源发生了变化,每个线程会弃从线程缓存中获取而去系统主内存中重新获取最新的(可见性)
1.3 在并发情况下,可以保障线程的有序执行 (有序)
-
原理
同步锁可以针对类、代码块和对象进行加锁,每个被修饰的对象,在线程调用获取时都会经过监视器monitor进行操作;
当线程调用对象时,如果该对象的监视器中进入的线程数为0时,则当前线程为对象锁的拥有者,数值+1,其他的线程调用时则会阻塞进行等待;
如果当前线程再次获取该对象时,监视器进入的线程数值再+1;
在线程执行完成释放资源后,监视器中进入的线程数为0时,阻塞的线程按序获取对象。
当我们在对对象或者类(代码块)加锁时:
public class Test1 { private static Boolean flag = true; public void add() { synchronized(flag) { if (flag) { flag = false; } } } public static void main(String args[]) { Test1 t1 = new Test1(); t1.add(); System.out.println(flag); } } // 查看编译字节文件 public Test1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public void add(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // Field flag:Ljava/lang/Boolean; 3: dup 4: astore_1 5: monitorenter 6: getstatic #2 // Field flag:Ljava/lang/Boolean; 9: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z 12: ifeq 22 15: iconst_0 16: invokestatic #4 // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean; 19: putstatic #2 // Field flag:Ljava/lang/Boolean; 22: aload_1 23: monitorexit 24: goto 32 27: astore_2 28: aload_1 29: monitorexit 30: aload_2 31: athrow 32: return Exception table: from to target type 6 24 27 any 27 30 27 any
通过查看字节码(使用javap -v/verbose命令查看编码文件)来看,jvm通过monitorenter和monitorexit进入和退出监视器,为了确保释放对象锁,存在多次monitorexit;
当我们在方法体上加锁时:
public class Test { private int num; public synchronized int add() { return num ++; } public static void main(String[] args) { Test t = new Test(); System.out.println(t.add()); } } // 字节码 public Test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public synchronized int add(); descriptor: ()I flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=4, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field num:I 5: dup_x1 6: iconst_1 7: iadd 8: putfield #2 // Field num:I 11: ireturn LineNumberTable: line 6: 0
出现了ACC_SYNCHRONIZED变量,与在代码块加锁不同的是,方法体加锁是根据ACC_SYNCHRONIZED标识的true/false来判断是否同步,而代码块同步锁则是通过monitorenter和monitorexit指令进行同步操作
-
应用场景
在多线程中,我们要实现资源共享时可以通过加锁来实现;同步代码块逻辑过于复杂不推荐使用
Lock
-
特性
-
Lock需要手动加锁、手动解锁 (lock(); unlock();)
-
tryLock非阻塞方式获取锁,判断是否已经被占有锁,有则返回false,true则占有锁;同样可以设置时间,在时间之内尝试获取锁;
lock.tryLock(6000, TimeUnit.MILLISECONDS) // 线程AB先后获取锁,A占有锁B会在后面的6秒尝试再次获取获取不到直接进入false逻辑。
-
lock.lockInterruptibly(), 等待获取锁的线程可以中断 thread.interrupt(); 声明抛出InterruptedException异常
-
Lock释放锁如有异常捕获需在finally中释放锁
-
-
原理
Lock常用的实现类ReentrantLock、ReadWriteLock等,其基本都是依赖AQS(AbstractQueuedSynchronizer)提供的加锁解锁方法。
总结
-
synchronized和lock的区别
- synchronized jvm在执行了monitorexit命令后会自动释放锁,lock需要手动释放锁,否则容易造成线程死锁
- Lock是一个接口,synchronized是关键字
- Lock可以判断线程是否拥有了锁、可以设置获取锁的时间
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
-
synchronized存在于多线程中,只读数据不涉及线程安全
-
synchronized关键字会让没有得到锁的线程处于阻塞状态,在获得锁后变成可运行状态,这种变化涉及到系统用户模式和内核模式的转换,性能消耗大,代价高