多个线程对共享资源进行写操作时,会发生上下文切换,造成线程安全问题。
如以下代码
发生上下文切换,如
一段代码块中如果存在对共享资源的多线程读写操作,则称这段代码区为临界区
以上代码临界区为
多个线程在临界区内执行,代码执行序列不同导致结果无法预测,称为发生了竞态条件
避免临界区的竞态条件发生:
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
synchronized:俗称对象锁,它采用互斥的方式,使得同一时刻最多只能有一个线程持有对象锁,其他对象想要获取对象锁就只能进入阻塞状态(注意必须使用相同的锁)。
在加了锁的线程中,只有获取了锁的线程能够运行(其他线程进入BLOCKED阻塞状态,即使该线程时间片用完了,因为其他线程仍处于阻塞状态,所以该线程会重新获得时间片),synchronized内代码运行结束后线程会释放锁,其他阻塞的线程会被唤醒,然后去竞争锁,获取了锁的线程去运行……
这样保证了拥有锁的线程可以安全地执行临界区的代码,而不用担心发生上下文切换。
互斥和同步都可以采用synchronized关键字来实现,但它们还是有区别的:
互斥是避免临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的顺序不同,一个线程需要等待另一个线程执行到某一点
利用synchronized解决之前问题的代码如下:
synchronized实际上是用对象锁保证了临界区内代码的原子性,临界区内代码对外是不可分割的,不会被线程切换给打断。
用图来表示
把需要保护的共享变量放入一个类中
synchronized修饰方法锁住的是this对象,synchronized? static修饰方法锁住的是class对象
不加synchronized的方法:好比不遵守规则的人,不老实排队(翻窗户进去)
java对象头,以32位虚拟机为例
普通对象:Klass? Word指针指向它的class对象
数组对象:int为4字节,Integer为12字节(8字节对象头加上4字节的值)
Mark Word结构:倒数第三位表示是否为偏向锁,后两位表示加锁状态
Monitor(锁)被翻译为监视器或管程(操作系统层面)
每个java对象都可以关联一个Monitor对象,如果用synchronzied給对象上锁(重量级),该对象头的Mark Word中就会设置一个指针指向Monitor对象
Monitor只能有一个主人,一个线程成为了Monitor的Owner后,其他想要获取锁的线程会进入 Monitor的EntryList等待,进入BLOCKED状态。Monitor的Owner线程执行完毕后,Owner设为null,在 EntryLis中等待线程被唤醒,再去竞争锁,竞争结果由jdk底层实现决定。如下图所示。
注意synchronzied锁住同一个对象才会有同一个Monitor,不加synchronzied的对象不会关联监视
器。
刚开始Monitor中Owner为null
当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的图中WaitSet 中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程
对应的字节码为
如果6到16行出现异常,会到第19行,重置lock对象的Mark Word,唤醒EntryList,让其他线程去竞争锁,再抛出异常。synchronzied中的代码即使出现了异常也能解锁。