公平锁和非公平锁

定义及优缺点

  • 公平锁:多个线程按照申请的顺序来获取锁,类似排队打饭,先来后到。在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列。如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
  • 非公平锁:多个线程获取锁的顺序并不是按照申请所的顺序,有可能申请的线程比先生请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁
锁 - 图1

非公平锁优点:

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

非公平锁缺点:非公平锁忽视了排队的公平性,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的”锁饥饿”

代码

  1. // 公平锁与非公平锁
  2. public class JUC28 {
  3. private final ReentrantLock lock;
  4. public JUC28(boolean isFair) {
  5. super();
  6. lock = new ReentrantLock(isFair);
  7. }
  8. public void serviceMethod() {
  9. try {
  10. lock.lock();
  11. System.out.println("ThreadName=" + Thread.currentThread().getName() + "获取锁定");
  12. } finally {
  13. lock.unlock();
  14. }
  15. }
  16. public static void main(String[] args) {
  17. final JUC28 service = new JUC28(true); // 通过入参true或者false来控制公平锁或非公平锁
  18. Runnable runnable = () -> {
  19. System.out.println("★线程" + Thread.currentThread().getName() + "运行了");
  20. service.serviceMethod();
  21. };
  22. Thread[] threadArray = new Thread[10];
  23. for (int i = 0; i < 10; i++) {
  24. threadArray[i] = new Thread(runnable);
  25. }
  26. for (int i = 0; i < 10; i++) {
  27. threadArray[i].start();
  28. }
  29. }
  30. }

对于ReentrantLock而言,通过构造函数指定该所是否是公平锁,默认是非公平锁。非公平锁的有点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁


可重入锁(递归锁)

定义

同一线程外层函数获得锁之后,内存递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。ReentrantLock/Synchronized就是一个典型的可重入锁。可重入锁的最大作用就是避免死锁。

代码

//  可重入锁代码
class Phone implements Runnable {

    public synchronized void sendSMS() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
        sendEmail();
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
    }

    //----------------------------------------------------------------------------------------

    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    public void get() {
        //可以写多次,但加几次、解几次
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t invoked get()");
            set();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t invoked set()");
        } finally {
            lock.unlock();
        }
    }
}
public class ReenterLockDemo {
    public static void main(String[] args) {

        Phone phone = new Phone();

        new Thread("t1") {
            @Override
            public void run() {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();

        new Thread("t2") {
            @Override
            public void run() {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println();

        Thread t3 = new Thread(phone, "t3");
        t3.start();


        Thread t4 = new Thread(phone, "t4");
        t4.start();
    }
}

Synchronized可重入原理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

ReentrantLock可重入原理

加锁次数要和释放次数相等
锁 - 图2锁 - 图3

死锁代码

public class DeadLockDemo {
    public static void main(String[] args) {
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();


        new Thread(() -> {
            synchronized (objectLockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + "自己持有A,希望获得B");
                //暂停几秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectLockB) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "A-------已经获得B");
                }
            }
        }, "A").start();
        new Thread(() -> {
            synchronized (objectLockB) {
                System.out.println(Thread.currentThread().getName() + "\t" + "自己持有B,希望获得A");
                //暂停几秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectLockA) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "B-------已经获得A");
                }
            }
        }, "B").start();
    }
}

自旋锁

定义

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

代码

// 自旋锁
public class SpinLockDemo {
    //原子引用线程
    AtomicReference atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in.");
        while (!atomicReference.compareAndSet(null, thread)) {}
    }

    public void myUnlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t invoke myUnlock().");
    }

    public static void main(String[] args) {
        SpinLockDemo demo = new SpinLockDemo();

        new Thread("t1") {
            @Override
            public void run() {
                demo.myLock();
                try {
                    sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                demo.myUnlock();
            }
        }.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread("t2") {
            @Override
            public void run() {
                demo.myLock();
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                demo.myUnlock();
            }
        }.start();
    }
}

独占锁(写)共享锁(读)/互斥锁

读写锁

读写锁ReentrantReadWriteLock并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的。大多实际场景是“读/读”线程间并不存在互斥关系,只有”读/写”线程或”写/写”线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。只有在读多写少情境之下,读写锁才具有较高的性能体现。

特点

  • 可重入
  • 读写分离

    代码

    ```java class MyResource { Map map = new HashMap<>(); //=====ReentrantReadWriteLock 一体两面,读写互斥,读读共享 ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key, String value) {

      rwLock.writeLock().lock();
      try {
          System.out.println(Thread.currentThread().getName() + "\t" + "---正在写入");
          map.put(key, value);
          //暂停毫秒
          try {
              TimeUnit.MILLISECONDS.sleep(500);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "\t" + "---完成写入");
      } finally {
          rwLock.writeLock().unlock();
      }
    

    } public void read(String key) {

      rwLock.readLock().lock();
      try {
          System.out.println(Thread.currentThread().getName() + "\t" + "---正在读取");
          String result = map.get(key);
          // 后续开启注释修改为2000,演示一体两面,读写互斥,读读共享,读没有完成时候写锁无法获得
          //try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
          System.out.println(Thread.currentThread().getName() + "\t" + "---完成读取result:" + result);
      } finally {
          rwLock.readLock().unlock();
      }
    

    } } public class ReentrantReadWriteLockDemo { public static void main(String[] args) {

      MyResource myResource = new MyResource();
    
    for (int i = 1; i <= 10; i++) {
        int finalI = i;
        new Thread(() -> {
            myResource.write(finalI + "", finalI + "");
        }, String.valueOf(i)).start();
    }

    for (int i = 1; i <= 10; i++) {
        int finalI = i;
        new Thread(() -> {
            myResource.read(finalI + "");
        }, String.valueOf(i)).start();
    }
    // 暂停几秒钟线程
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 读全部over才可以继续写
    for (int i = 1; i <= 3; i++) {
        int finalI = i;
        new Thread(() -> {
            myResource.write(finalI + "", finalI + "");
        }, "newWriteThread===" + String.valueOf(i)).start();
    }
}

}

<a name="Mp8l3"></a>
## 独占锁和共享锁
<a name="FVOYQ"></a>
### 定义

- 独占锁:指该所一次只能被一个线程锁持有。对ReentrantLock和Synchronized而言都是独占锁
- 共享锁:该锁可以被多个线程锁持有。ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。读锁的共享锁可以保证并发读取时是非常高效的。读写、写读、写写的过程时互斥的。
<a name="gOesl"></a>
### 代码
```java
// 读写锁
class MyCache {
    private volatile Map map = new HashMap<>();
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();


    public void put(String key, Object value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写" + key);
            //暂停一会儿线程
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写完了" + key);
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    public Object get(String key) {
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读" + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读完了" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
        return result;
    }
}
/**
 * 多个线程同时读一个资源类没问题,所以为了满足并发量,读取共享资源该可以同时进行
 * 但是,如果有一个线程想去写共享资源,就不该再有其它线程可以对该资源进行读或写
 * 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不允许被分割、被打断
 */
public class JUC05 {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.put(num + "", num + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                myCache.get(num + "");
            }, String.valueOf(i)).start();
        }
    }
}

锁降级

ReentrantReadWriteLock 写锁-> 读锁(锁降级)
锁降级:遵循获取写锁 -> 再获取读锁 -> 再释放写锁的次序,写锁能够降级成为读锁。如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
锁 - 图4
锁降级是为了在数据一致性不被破坏的前提下,尽可能的缩短写锁的持有阶段。线程获取读锁是不能直接升级为写入锁的。在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。

/**
 * 锁降级:遵循获取写锁  ->  再获取读锁 ->  再释放写锁的次序,写锁能够降级成为读锁。
 * 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
 */
public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        writeLock.lock();
        System.out.println("-------正在写入");

        readLock.lock();
        System.out.println("-------正在读取");

        writeLock.unlock();
    }
}

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。因此分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:读锁全完,写锁有望;写锁独占,读写全堵;如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁

class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // Recheck state because another thread might have
                // acquired write lock and changed state before we did.
                if (!cacheValid) {
                    data = ...
                    cacheValid = true;
                }
                // Downgrade by acquiring read lock before releasing write lock
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }
        }

        try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}
  • 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。
  • 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程。

在这个问题里,如果不想使用锁降级,可以做如下操作:

  1. 可以继续持有写锁,完成后续的操作。
  2. 也可以先把写锁释放,再获取读锁。

但问题是

  1. 如果继续持有写锁,如果 use 函数耗时较长,那么就不必要的阻塞了可能的读流程
  2. 如果先把写锁释放,再获取读锁。在有些逻辑里,这个 cache 值可能被修改也可能被移除,这个看能不能接受。另外,降级锁比释放写再获取读性能要好,因为当前只有一个写锁,可以直接不竞争的降级。而释放写锁,获取读锁的过程就面对着其他读锁请求的竞争,引入额外不必要的开销。

可以让流程不被中断的降低到低级别锁,并且相对同样满足业务要求的其他手段性能更为良好。

StampedLock(邮戳锁)

StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。

锁饥饿问题

  ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了, 假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了,因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。

锁饥饿问题解决

  • 公平锁:new ReentrantReadWriteLock(true);使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的
  • StampedLock:ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化。所以在获取乐观读锁后,还需要对结果进行校验。

    StampedLock特点

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功

  • 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
  • StampedLock是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁
  • StampedLock 的悲观读锁和写锁都不支持条件变量(Condition)。
  • 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法。如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()

    StampedLock三种访问模式

  • Reading(读模式):功能和ReentrantReadWriteLock的读锁类似

  • Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
  • Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式 ```java // 乐观读模式code演示,读的过程中也允许获取写锁介入 public class StampedLockDemo { static int number = 37; static StampedLock stampedLock = new StampedLock();
public void write() {
    long stamp = stampedLock.writeLock();
    System.out.println(Thread.currentThread().getName() + "\t" + "=====写线程准备修改");
    try {
        number = number + 13;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        stampedLock.unlockWrite(stamp);
    }
    System.out.println(Thread.currentThread().getName() + "\t" + "=====写线程结束修改");
}

//悲观读
public void read() {
    long stamp = stampedLock.readLock();
    System.out.println(Thread.currentThread().getName() + "\t come in readlock block,4 seconds continue...");
    //暂停几秒钟线程
    for (int i = 0; i < 4; i++) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t 正在读取中......");
    }
    try {
        int result = number;
        System.out.println(Thread.currentThread().getName() + "\t" + " 获得成员变量值result:" + result);
        System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        stampedLock.unlockRead(stamp);
    }
}

//乐观读
public void tryOptimisticRead() {
    long stamp = stampedLock.tryOptimisticRead();
    int result = number;
    //间隔4秒钟,我们很乐观的认为没有其他线程修改过number值,实际靠判断。
    System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)" + "\t" + stampedLock.validate(stamp));
    for (int i = 1; i <= 4; i++) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t 正在读取中......" + i +
                "秒后stampedLock.validate值(true无修改,false有修改)" + "\t"
                + stampedLock.validate(stamp));
    }
    if (!stampedLock.validate(stamp)) {
        System.out.println("有人动过--------存在写操作!");
        stamp = stampedLock.readLock();
        try {
            System.out.println("从乐观读 升级为 悲观读");
            result = number;
            System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }
    System.out.println(Thread.currentThread().getName() + "\t finally value: " + result);
}

public static void main(String[] args) {
    StampedLockDemo resource = new StampedLockDemo();


    new Thread(() -> {
        // resource.read();
        resource.tryOptimisticRead();
    }, "readThread").start();

    // 2秒钟时乐观读失败,6秒钟乐观读取成功resource.tryOptimisticRead();,修改切换演示
    try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

    new Thread(() -> {
        resource.write();
    }, "writeThread").start();
}

}



<a name="alDU7"></a>
# Synchronized(同步锁,悲观锁)

- **同步锁:**为了保证每个线程都能正常执行的原子操作,Java引入了线程同步机制。Java程序运行可以使用任何资源作为同步监听对象,但是一般的我们把当前并发访问的共同资源作为同步监听对象
- **同步方法:**使用synchronized修饰的方法叫做同步方法。
<a name="v6reo"></a>
## 单例模式
```java
// 单例模式:懒汉式
public class JUC16 {
    private JUC16() {
    }
    private static volatile JUC16 instance = null;

    public static JUC16 getInstance(){
        if (instance == null) {
            synchronized (JUC16.class){
                if (instance == null) {
                    instance = new JUC16();
                }
            }
        }
        return instance;
    }
}
// 单例模式:饿汉式
public class JUC17 {
    private static final JUC17 instance = new JUC17();
    private JUC17(){}
    public static JUC17 getInstance(){
        return instance;
    }
}

线程8锁

class Phone {
    public synchronized void sendSMS() throws Exception {
        System.out.println("------sendSMS");
    }
    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }
    public void getHello() {
        System.out.println("------getHello");
    }
}
public class JUC21 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone.sendEmail();
                //phone.getHello();
                //phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}
  • 标准访问,先打印短信还是邮件(短信)
  • 停4秒在短信方法内,先打印短信还是邮件(短信)

一个对象里面如果有多个synchronized方法,某一个时刻内, 只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待。换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

  • 新增普通的hello方法,是先打短信还是hello(加个普通方法后和同步锁无关)
  • 现在有两部手机,先打印短信还是邮件(换成两个对象后,不是同一把锁了,情况立刻变化)
  • 两个静态同步方法,1部手机,先打印短信还是邮件
  • 两个静态同步方法,2部手机,先打印短信还是邮件
  • 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
  • 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件

Synchonized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

字节码角度分析Synchronized实现

  • synchronized同步代码块:实现使用的是monitorentermonitorexit指令
  • synchronized普通同步方法:调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor
  • synchronized静态同步方法:ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法

锁升级

  • synchronized锁:由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略

Java5以前,只有Synchronized,这个是操作系统级别的重量级操作,假如锁的竞争比较激烈的话,性能下降。Java5之前,用户态和内核态之间的切换
锁 - 图5
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java早期版本中, synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁偏向锁
Monitor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自身都携带一把锁内部锁或者Monitor锁。

Synchronized锁种类

锁 - 图6image.png

无锁

锁 - 图8锁 - 图9

偏向锁

  • 当一段同步代码一直被同一个线程多次访问,由于只有一个线程,那么该线程在后续访问时便会自动获得锁
  • 理论:在实际应用运行过程中发现,”锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,否则线程是不会主动释放偏向锁的。
  • 技术实现:一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。

    偏向锁的操作不涉及 用户到内核转换,不必要直接升级为最高级。我们以一个account对象的“对象头”为例,假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标识,标识当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的Mark Word判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。 结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不需要操作系统介入。上述就是偏向锁:在没有其他线程竞争的时候,一直偏向当前线程,当前线程可以一直执行。
    

偏向锁JVM指令
// 开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
// 关闭偏向锁:关闭之后程序默认会直接进入轻量级锁状态。
-XX:-UseBiasedLocking

偏向锁撤销

当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁。竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

  • 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
  • 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向

轻锁

轻量级锁是为了在线程 近乎交替执行同步块时提高性能。主要目的是在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞。当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁。假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。此时线程B操作中有两种情况

  • 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程”被”释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
  • 如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。

    轻锁和偏向锁的区别
  • 争夺轻量级锁失败时,自旋尝试抢占锁

  • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

    重锁

    有大量的线程参与锁的竞争,冲突性很高。当轻量锁自旋达到一定次数的时候会升级为重锁

  • jdk6:默认启用,默认情况下自旋的次数是 10 次 -> -XX:PreBlockSpin=10来修改,或者自旋线程数超过cpu核数一半

  • jdk6及以后:同一个锁上一次自旋的时间。拥有锁线程的状态来决定。

Synchronized锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的所撤销的消耗 适用于只有一个线程访问同步快场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果使用得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争不适用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

synchronized锁升级过程总结: 先自旋,不行再阻塞。实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式。synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

  • 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
  • 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
  • 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

锁升级步骤

锁 - 图10

Lock(悲观锁)

Lock

Lock机制提供了比Synchronized代码块和Synchronized方法更加广泛的锁定操作,同步代码块、同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象

// 使用lock加锁
class Ticket {
    private int number = 30;
    private final Lock lock = new ReentrantLock();//  可重入锁
    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出" + (number--) + "\t 还剩" + number);
            }
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class JUC18 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) ticket.sale();
        }, "AA").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) ticket.sale();
        }, "BB").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) ticket.sale();
        }, "CC").start();
    }
}

Synchronized和Lock的对比

  • Synchronized是java内置关键字,在jvm层面。Lock是个java类
  • Synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
  • synchronized会自动释放锁(a线程执行完同步代码会释放锁,b线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。Lock不能使用java7的自动资源关闭功能,因为其子类没有实现AutoClose接口。要想实现自动资源关闭功能,必须实现AutoClose接口
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去。而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了

    • ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
    • ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。
    • https://blog.csdn.net/sdmanooo/article/details/113780292

      public class JUC18 {
      public static void main(String[] args) throws InterruptedException    {
         testInterrupt();
      }
      
      /**
      * 可打断
      *
      */
      public static void testInterrupt() throws InterruptedException {
         ReentrantLock lock = new ReentrantLock();
         Thread t2 = new Thread(() -> m2(lock));
         Thread t1 = new Thread(() -> m1(lock));
         t2.start();
         Thread.sleep(2000);
         t1.start();
         try {
             System.out.println("3秒后打断t2");
             Thread.sleep(3000);
             t2.interrupt();
             System.out.println("3秒后打断t1");
             Thread.sleep(3000);
             t1.interrupt();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
      }
      
      public static void m1(ReentrantLock lock) {
         try {
             lock.lockInterruptibly();
             System.out.println("我是方法1,睡");
         } catch (InterruptedException e) {
             System.out.println("我被打断了。。。。。。");
         } finally {
             if(lock.isHeldByCurrentThread()){
                 lock.unlock();
             } else{
                 System.out.println("当前线程不持有锁");
             }
         }
      }
      
      private static void m2(ReentrantLock lock) {
         try {
             lock.lock();
             System.out.println("m2获取到了锁");
         } catch (Exception e) {
             e.printStackTrace();
         }
      }
      }
      
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)

  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题