解决了并发问题,“缓存可见性问题”,“原子性问题”,“指令顺序性问题”。
【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
多个对象使用synchronized

  1. public class SynchronizedCountDown {
  2. public static void main(String[] args) {
  3. System.out.println("使用关键字synchronized");
  4. SyncThread syncThread = new SyncThread();
  5. Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
  6. Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
  7. thread1.start();
  8. thread2.start();
  9. }
  10. }
  11. class SyncThread implements Runnable {
  12. private static int count;
  13. public SyncThread() {
  14. count = 0;
  15. }
  16. @Override
  17. public void run() {
  18. synchronized (this){
  19. for (int i = 0; i < 5; i++) {
  20. try {
  21. System.out.println("线程名:"+Thread.currentThread().getName() + ":" + (count++));
  22. Thread.sleep(100);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. }
  29. }

这时,某次结果是:

  1. 使用关键字synchronized
  2. 线程名:SyncThread2:0
  3. 线程名:SyncThread1:1
  4. 线程名:SyncThread2:3
  5. 线程名:SyncThread1:2
  6. 线程名:SyncThread2:4
  7. 线程名:SyncThread1:4
  8. 线程名:SyncThread2:5
  9. 线程名:SyncThread1:6
  10. 线程名:SyncThread2:8
  11. 线程名:SyncThread1:7

该问题就是不同对象调用同一个方法,但是其中的锁每次只能锁住一个对象,当两个对象就分别开启了synchronized锁,锁住了不同的对象,这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。

写法

1、隐式加锁
synchronized修饰对象方法,效果等于synchronized(this),this也就是Test的当前实例对象

  1. class Test{
  2. public synchronized void test() {
  3. }
  4. }
  5. // 等价于
  6. class Test{
  7. public void test() {
  8. synchronized(this) {
  9. }
  10. }
  11. }

2、隐式加锁
synchronized修饰静态类方法,效果等于synchronized(Test.class)
这里需要注意Test.class是在JVM虚拟机真实存在的一个对象,JVM代码在经过编译、加载之后就会在堆里面创建一个 Test.class的对象,所以网上说锁住了Test.class对象就相当于锁住了Test的实例对象,这种说法是错误的,因为他们两个没有关系,不是一回事。

  1. class Test{
  2. public synchronized static void test() {
  3. }
  4. }
  5. // 等价于
  6. class Test{
  7. public static void test() {
  8. synchronized(Test.class) {
  9. }
  10. }
  11. }

3、显示加锁
在synchronized修饰代码块时会需要显示的指定一个加锁的对象,synchronized(任何对象)。

  1. public class Test {
  2. public void codeLock(){
  3. //显示加锁,锁的是当前Test实例对象
  4. synchronized(this){
  5. //Todo
  6. }
  7. }
  8. Object objA=new Object();
  9. public void objLock(){
  10. //显示加锁,锁的是objA对象
  11. synchronized(objA){
  12. //Todo
  13. }
  14. }
  15. }

上面几种方式加锁与对象的关系如下图
synchronized同步锁 - 图1
synchronized锁的升级过程。
场景1(无锁状态):程序不会有锁的竞争。
那么这种情况我们不需要加锁,所以这种情况下对象锁状态为无锁。
场景2(偏向锁):经常只有某一个线程来加锁。
也许获取锁的经常为同一个线程,这种情况下为了避免加锁造成的性能开销,所以并不会加实际意义上的锁,偏向锁的执行流程如下:
1、线程首先检查该对象头的线程ID是否为当前线程
2、 A:如果对象头的线程ID和当前线程ID一样,则直接执行代码;
B:如果不是当前线程ID则使用CAS方式替换对象头中的线程ID,如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁。
3、如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码。
4、执行代码完成之后释放锁,把对象头的线程ID修改为空。
synchronized同步锁 - 图2
场景3(轻量级锁):有线程来参与锁的竞争,但是锁竞争的时间很短。
当开始有锁的冲突了,那么偏向锁就会升级到轻量级锁;线程获取锁出现冲突时,线程必须做出决定是继续在这里一直询问,还是回家等别人打电话通知,而轻量级锁采用的方式就是采用继续在这里询问的方式,=。
当发现有锁冲突,线程首先会使用自旋(循环)的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU,当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了。

场景4(重量级锁):有大量的线程参与锁的竞争,冲突性很高。
我们知道当获取锁冲突多,时间越长的时候,我们的线程肯定不能一直一致在这里循环的询问,这种方式太消耗CPU资源,而且也是没意义的。所以这个时候最好的方式就是先让线程进入队列等待,然后等前面获取锁的线程释放了锁之后再开启下一轮的锁竞争,而这种形式就是我们的重量级锁,只有当锁升级为重量级锁的时候才真正意义上的在操作系统层面进行了加锁操作。