Synchronized关键字
synchronized是一种悲观锁
乐观锁和悲观锁
- 乐观锁
- 设计思想 : 通一个时间点,经常只有一个线程对一个共享变量进行操作(可以理解为没有人和你抢吃的,你可以光明正大的吃)
- 原理:线程直接操作共享变量
- 实现 :基于CAS来实现
- 悲观锁
- 设计思想:同一个时间点,经常会有多个线程对一个共享变量进行操作(可以理解为有人和你抢吃的,需要偷偷藏起来吃,自己吃够了,把吃的才会拿出来)
- 原理:线程操作变量的时候需要先加锁,再对变量进行操作
- 实现:加锁操作
CAS
-
怎木说呢 我们可以把他理解为一种思想 Compare and Swap(比较并交换)
线程去修改一个共享变量V的时候,线程先从主内存中获取V的值记为O,然后在修改主内存中V值的同时进行对O和V比较,如果O和V的值不同,那木赋值失败,相反则赋值成功
内存中元数据 V = 1
旧的预期值O 更新的新值N = 2
1. 先从主存中读取V的的值等于1 将预期的旧值O赋予1
2. 将N的值2给直接向V赋值 但是同时比较 O与V是否相同
3. O V 相同 则赋值成功 ;O V 不同 则赋值失败
-
CAS引发ABA问题
就是在第一步和第二步的中间, 如果有线程B将V的值修改成3,而线程C将V的值又改为1。
解决方法 :使用版本号,在主内存的V数据上加上版本号,每次被修改后版本号+1,而在第一步的时候CAS操作也会读取版本号,在进行第二步操作的时候,也会对版本号进行比较。
Synchronized加锁方式
synchronized是对对象头加锁,同一个对象加锁的线程同步互斥。
-
对像头 Header
- Mark Word 储存对象的 HashCode 分代年龄 锁标志位信息
- Klass Point 对象指向他类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的数据
-
Monitor
翻译为监视器或者管程
每一个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,Mark Word中的锁标志就会指向这个monitor对象。
其实synchronized的可重入机制就是基于这个来实现的,当每有一个synchronized关键字对这个对象的加锁时候,也就是进入 +1,当释放锁的时候 就 -1。
monitor的结构如下
我们需要注意的一点就是,synchronized必须进入的是同一个对象的monitor,也就是对同一个对象进行加 锁,不加synchronized的对象是不会关联到监视器的,是不遵循上述的关系的。
-
Monitor中的monitorenter 和 monitorexit
package Package3; public class D1 { public static void main(String[] args) { int i = 0; synchronized (D1.class) { i++; } } }
当我们对上述代码进行反汇编的时候得到
C:\JavaEE_study\Demo11\src\Package3>javap -c D1.class Compiled from "D1.java" public class Package3.D1 { public Package3.D1(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_0 1: istore_1 2: ldc #2 // class Package3/D1 4: dup 5: astore_2 6: monitorenter 7: iinc 1, 1 10: aload_2 11: monitorexit 12: goto 20 15: astore_3 16: aload_2 17: monitorexit 18: aload_3 19: athrow 20: return Exception table: from to target type 7 12 15 any 15 18 15 any }
其中mointorenter出现一次,代表得到锁并且进入代码块
monitorexit出现两次,都代表释放锁,不过一种是正常的执行完代码块,正常释放锁,一种是代码块中出现异常,然后释放锁。
类似于
try { 执行完 monitorexit } catch { 执行过程中出现异常 monitorexit }
JVM对synchronized的优化
原理 :基于对象头的锁状态来实现,从低到高(锁只可以升级,不能降级)
-
无锁状态
-
偏向锁 :为了在资源没有其他锁竞争的情况下尽可能减少锁带来的性能开销。当A线程获取到锁的时候,如果该锁没有被其他线程竞争。那木该锁对象的对象头中的ThreadId字段就会被设置为A线程的ID,当线程下次获取锁的时候,就会判断是否和ThreadId的字段是否一致,若是一致,就不会产生重复获取锁的过程,减少开销。如果有线程竞争这个锁,那木就会升级为轻量级锁
-
轻量级锁:基于CAS来实现。比较并交换
package Package2; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class Counter { // 线程安全 private AtomicInteger atomicI = new AtomicInteger(0); private int i = 0; // CAS + 自旋锁 private void safeCount() { for (;;) { int i = atomicI.get(); // 这里必须是 ++i或者i+1 // 不可以是 i++ boolean suc = atomicI.compareAndSet(i, ++i); if (suc) { break; } } } // private void unSafeCount() { i++; } public static void main(String[] args) throws InterruptedException { final Counter cas = new Counter(); List<Thread> ts = new ArrayList<>(600); Long start = System.currentTimeMillis(); for (int j = 0; j < 100; j++) { Thread a = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { cas.unSafeCount(); cas.safeCount(); } } }); ts.add(a); } for (Thread t : ts) { t.start(); } for (Thread t : ts) { t.join(); } System.out.println( "i: " + cas.i); System.out.println(cas.atomicI.get()); Long time = System.currentTimeMillis() - start; System.out.println("time : " + time ); } }
结果
i: 9997 Integer: 10000 time : 30
-
重量级锁:基于系统的mutex锁,同一个时间点,经常有多个线程来竞争锁
死锁
同步的本质在于一个线程等大另一个线程执行完毕之后执行完成之后才会继续执行,但如果相关的几个线程彼此中间都在等待,那木就会造成死锁。
package Package4;
class Cup {
public String name;
public Cup() {
this.name = "杯子";
}
}
class Water {
public String name;
public Water() {
this.name = "水";
}
}
public class DeadLock {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Cup.class) {
System.out.println(Thread.currentThread().getName() + "有水没杯子");
synchronized (Water.class) {
System.out.println(Thread.currentThread().getName() + "有水有杯子");
}
}
}
}, "jack");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Water.class) {
System.out.println(Thread.currentThread().getName() + "有水没杯子");
synchronized (Cup.class) {
System.out.println(Thread.currentThread().getName() + "有水有杯子");
}
}
}
}, "Lisa");
t.start();
t2.start();
}
}
一个有可能出现死锁的代码
当我们发现死锁的时候,可以通过jconsole来查看线程的死锁状态。
死锁的四个必要条件
- 互斥条件,一个资源只可以被一个线程使用
- 请求与保持条件,一个线程因为请求资源而阻塞的时候,对已经获得的资源不再释放
- 不剥夺条件:线程已获得资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接循环等待资源的条件
解决死锁的方式:
- 资源一次性分配:破坏请求与保持条件
- 可剥夺资源:在线程满足条件的时,释放已占有的资源
- 资源有序分配:为每一个资源赋予一个编号,每个线程按照编号请求资源,释放则相反。