synchronized的使用与原理

在上篇并发编程系列文章中,笔者从线程安全性问题的三个方面入手,简单剖析了可见性和有序性问题的本质,今天继续将从剩下的原子性问题开始,一起去了解synchronized关键字的使用。

原子性问题

什么是原子性?

我们知道数据库的事务特性,同一事务下,包含多个对数据库的事务操作(insert,update,delete),这些操作,要么全部成功,要么全部失败,不会存在部分成功的情况,这就是数据库事务的原子性。线程中的原子性也是如此,多个指令在被CPU执行的过程中,要么全部成功,要么全部失败。

那原子性会带来什么问题呢?

我们知道,CPU在执行指令的时候是具有不确定性的,可能在一个线程中,一组指令执行到一半,CPU通过时间片切换,将资源让给另一个线程去执行,此时第一个线程的指令就被中断,可能执行出来的结果会不符合预期。比如Java中常见自增操作i++,这个简单指令,却不是一个原子指令,它是由三个指令组成的,取值+1赋值,下面我们可以看看这个指令被转成字节码后样子。

private static int count;

public static void main(String[] args){
  count++;
  System.out.println(count);
}

笔者将上面这几句简单的指令转成字节码文件

 0: getstatic     #2                  // Field count:I
 3: iconst_1
 4: iadd
 5: putstatic     #2                  // Field count:I
 8: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
11: getstatic     #2                  // Field count:I
14: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
17: return

可以看到count++被分成以下几个指令

  • getstatic:将变量count从内存加载到CPU中
  • iconst_1:将变量压入栈中
  • iadd:在CPU寄存器中执行+1操作
  • putstatic: 将计算结果保存到内存中

在CPU执行其中任意一个指令的时候都有可能存在中断的情况,这个时候就会出现原子性问题。我们可以看看下面例子

public class AtomicQuestionDemo {
    public static void main(String[] args) {
        List<Future> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            FutureTask task = new FutureTask(() -> {
                Thread.sleep(1);
                return incr();
            });
            list.add(task);
            new Thread(task).start();
        }

        list.stream().forEach(f -> {
            try {
                f.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });
        System.out.println(count);
    }

    private static int count = 0;

    private static int incr() {
        return count++;
    }
}

开启1000个线程去累加全局变量count,最后得到的结果会小于等于1000,这就是非原子性指令在多线程的情况下可能会出现的问题,大家可以先看一下多线程下该指令可能执行的顺序:

  • 线程1执行getstatic指令,将变量加载到线程1内存中,然后被中断
  • 线程2执行getstatic指令,将变量加载到线程2的内存中,然后执行add操作
  • 线程1重新继续执行add指令,对工作内存中的变量执行add操作

线程1被中断时,此时已经将变量加载到自己的工作内存中了,线程1被中断后,线程2再加载到变量到自己的工作内存中,此时线程2线程1加载的变量值都是相同,所以各自执行完add操作后的值是一样的,最终得到的结果就是count变量会比预期的小。

在多线程的情况下,如何保证非原子的指令在执行过程中不因为上下文切换而被中断呢?Java中synchronized便提供了这样一个功能。我们可以在incr()方法上加上synchronized关键字,此时重新执行上面的例子,得到结果就是1000.

  private synchronized static int incr() {
      return count++;
  }

synchronized使用方法

synchronized是一个同步锁,它将原本并行执行的代码串行化,从而保证线程安全性,但是,这种串行化执行明显会损失性能。

synchronized的使用方法比较简单,主要使用方式有两种,一种是修饰方法,另一种是修饰代码块

public class SynchronizedDemo {

    private int count = 0;

    /**
     * 第一种方式,修饰方法
     */

    private synchronized void incr1() {
        count++;
    }

    /**
     * 第二种方式,修饰代码块
     */

    private void incr2() {
        synchronized (this) {
            count++;
        }
    }
}
  • 第一种方式表示对incr1()方法加锁,当有多个线程同时访问该对象中此方法时,只有一个线程能够执行成功;

  • 第二种方式表示对代码块进行加锁,它与第一种方式类似,但是加锁的粒度更小,只有执行到被修饰的代码块时才会竞争锁资源,从而保证只有一个线程能够执行成功。

synchronized作用域

synchronized的作用范围也有两种,一种是对象级别,一种是类级别。

1)、对象锁

只有在多个线程在使用同一个对象实例的同步方法时,才会产生互斥,这就是对象锁,上面例子synchronized的使用方式的就是属于对象锁。下面给大家看一个例子。

public class SynchronizedDemo {

    private static int count = 0;

    /**
     * 修饰方法
     */

    private synchronized void incr1() {
        count++;
    }

    public static void main(String[] args) {
        List<Future> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            FutureTask task = new FutureTask(() -> {
                Thread.sleep(1);
                new SynchronizedDemo().incr1();
                return 1;
            });
            list.add(task);
            new Thread(task).start();
        }
        
        list.stream().forEach(f -> {
            try {
                f.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });
        System.out.println(count);
    }
}

运行结果

995

从运行结果可以看到,即使incr1()方法已经加上了synchronized关键字,运行结果还是无法保证正确的,而例子中synchronized是一个对象级别的同步锁,意味着同一时刻只能有一个线程抢占到共享资源 (SynchronizedDemo实例对象),而示例中每个线程的都会自己单独创建一个实例,因此无法达到排他的特性,所以程序最终的运行结果无法得到保证。知道了运行结果不正确的原因,我们对例子进行修改。

public static void main(String[] args) {
        SynchronizedDemo sd = new SynchronizedDemo();
        List<Future> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            FutureTask task = new FutureTask(() -> {
                Thread.sleep(1);
                sd.incr1();
                return 1;
            });
            list.add(task);
            new Thread(task).start();
        }

        list.stream().forEach(f -> {
            try {
                f.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });
        System.out.println(count);
    }

在修改后的例子中,每个线程都会使用同一个对象实例的incr1()方法,因每个线程需要争抢的共享变量都是同一个SynchronizedDemo对象实例,所以此时可以保证排他性,从而保证了结果的正确性。

2) 类锁

类锁是全局锁,与对象锁不同的是,类锁支持跨对象实例,可以在多个线程调用不同对象实例的同步方法时保证排他性。具体方式有如下

  • 修饰静态方法;
    private static synchronized void incr1() {count++;}
  • 修饰代码块时,锁对象是类
private void incr1() {
    synchronized (SynchronizedDemo.class{
        count++;
    }
}

将对象锁第一个例子中的incr1()方法改成上面这两种方式的任意一种,都能够实现保证运行结果的正确性。为什么类锁的粒度会如此之大呢? Class是在JVM启动过程中加载的,每个.class文件被装载后会产生一个Class对象,Class对象在JVM进程中是全局唯一的。而通过static修饰的成员对象及方法的生命周期都是属于类级别,因此,可以做到跨对象的效果。

synchronized在使用时需要修饰一个共享资源对象来实现互斥性,那线程之间是如何识别共享资源已经被抢占的呢?实际上,在对象头中,有一个锁标记位,用来记录锁状态。

对象头

Java中对象存储结构可以分为三个部分:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)。 对象头又由三个部分组成:

  • Mark Word:记录对象和锁相关信息
  • Klass Pointer:指针,指明对象具体属于哪个类
  • Length:数组长度,只有构建对象数组时才会有该属性

Mark Word

Mark Word存储对象自身的运行时数据,比如HashCode,GC分代年龄,锁标记状态,线程持有的,偏向线程ID等,具体结构如下(以32位系统为例)synchronized的使用与原理

从图中可以看到,锁的状态有五种:无锁(01),偏向锁(01),轻量级锁(00),重量级锁(10),GC标记(11),其中还额外用1bit来区分无锁(0)和偏向锁(1)。 这些锁有什么含义呢?在JDK1.6之前,synchronized只提供了重量级锁,而重量级锁的本质就是阻塞,也就是没有获得锁的线程会被挂起进行阻塞,接着获得锁的线程会唤醒阻塞的线程让其继续抢占锁,直到抢占成功,这种方式需要从用户态切换到内核态执行,这种切换带来的性能开销是非常大的,因此,在JDK1.6之后,官方对synchronized进行了优化,引入了偏向锁轻量级锁,让线程在不阻塞的情况下也能达到线程安全的目的。synchronized在加锁的过程中,会按照 无锁 -> 偏向锁(偏向锁有启动的情况下) -> 轻量级锁 -> 重量级锁这样的顺序逐步对锁进行升级。

偏向锁

偏向锁指的是如果一个线程获得了锁(偏向锁),那么当该线程后续继续访问相同的同步方法或者相同锁时,只需要判断对象头的线程ID和该线程是否相等,如果相等,则不再需要去抢占锁,直接访问即可。下面我们可以通过一个例子看看对象头中偏向锁标记。

public class BaseLockDemo {

    public static void main(String[] args) throws InterruptedException {
        BaseLockDemo baseLockDemo = new BaseLockDemo();
        System.out.println("=====before lock=======");
        System.out.println(ClassLayout.parseInstance(baseLockDemo).toPrintable());
        System.out.println("=====after lock");
        synchronized (baseLockDemo){
            System.out.println(ClassLayout.parseInstance(baseLockDemo).toPrintable());
        }
    }
}

运行结果

=====before lock=======
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
org.example.synchronize.BaseLockDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=====after lock========
org.example.synchronize.BaseLockDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c0 89 ff 09 (11000000 10001001 11111111 00001001) (167741888)
4 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,在加锁之前,对象头第一个字节00000001最后三位001,表示此时是无锁状态,加锁之后,对象头第一个字节会变成11000000,最后三位变成000,表示轻量级锁。按照上面的说法,从无锁状态开始,之后应该会是偏向锁,为什么此时标记位显示的却是轻量级锁呢? 这是因为JVM启动时,偏向锁是延迟开启的,之所以要延迟开启,是因为JVM在启动时会有很多线程运行,可能会存在线程竞争的情况,此时开启偏向锁的意义不大。偏向锁的延迟启动时间由启动参数-XX:BiasedLockingStartupDelay控制,默认是4秒。此时将参数设置为0(-XX:BiasedLockingStartupDelay=0),也就是不延迟开启偏向锁,再次运行代码看看结果。

=====before lock=======
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
org.example.synchronize.BaseLockDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=====after lock========
org.example.synchronize.BaseLockDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 90 80 36 (00000101 10010000 10000000 00110110) (914395141)
      4     4        (object header)                           81 7f 00 00 (10000001 01111111 00000000 00000000) (32641)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

此时可以看到加锁后,对象头第一个字节最后三位都会变成101,表示偏向锁,符合预期。 细心的朋友应该也观察到,加锁之前的对象头第一个字节最后三位也是101,为什么会出现这种情况呢?笔者查阅资料找到一种分析,加锁之前并没有存储线程ID,加锁之后才会有一个线程ID(914395141),因此在获得偏向锁之前,101应该是表示当前是一种可偏向的预先状态,而不是当前已经处于状态。

轻量级锁

轻量级锁也称自旋锁,就是没有抢占到锁的线程,进行一定次数的CAS(Compare And Swap)操作去争抢锁,如果在重试的过程中抢占到锁,则该线程不需要被阻塞。 因为轻量级锁是通过自旋的方式实现,如果不断自旋重试,CPU会一直处于运行状态,而且只做没有意义的循环,这对CPU资源是极大的浪费;如果持有锁的线程占有锁的时间比较短,其他线程经过少量的重试操作就能获取到锁,那么这种自旋等待相比阻塞与唤醒线程而言,所带来的性能提升也是比较明显的。因此,在JDK1.6中,默认的自旋次数是10次,我们可以通过-XX:PreBlockSpin来设置自旋的次数。 JDK1.6中还对自旋锁进行了优化,引进了自适应自旋锁,如果一个线程通过自旋的方式获取到锁,那么下一次,针对同一锁对象,JVM会将线程的自旋时间相对延长;如果通过自旋的方式很难获取到锁,那么JVM会相对减少自旋的次数。

重量级锁

如果轻量级锁在经过一定次数的自旋之后,还获取不到锁,此时过多的循环操作会过多浪费CPU资源,synchronized便会将锁升级为重量级锁,挂起线程,使其进入阻塞状态,释放CPU资源,此时对象头的的锁标记会变成10。 下面例子是在分别在线程获取锁之前,获取到锁后,main线程争抢到锁三个地方打印对象头信息。

public class BlockLockDemo {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        System.out.println("===before lock====");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("=====thread get lock======");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        Thread.sleep(1000);
        synchronized (lock) {
            System.out.println("======main get lock======");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}

运行结果

===before lock====
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=====thread get lock======
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           e0 c9 5d 0a (11100000 11001001 01011101 00001010) (173918688)
      4     4        (object header)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

======main get lock======
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a b1 82 64 (01001010 10110001 10000010 01100100) (1686286666)
      4     4        (object header)                           c5 7f 00 00 (11000101 01111111 00000000 00000000) (32709)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,在线程未获取到锁之前,对象头第一个字节最后三位是001,表示无锁;获取到锁之后,最后两位变成了00,表示轻量级锁;main线程争抢锁之后,变成了10,表示重量级锁,可以看到,synchronized底层会有一个锁升级机制,JVM根据线程的竞争情况逐步升级锁,在保证安全性的情况下,尽量提升了同步锁的性能,这个过程不需要开发者干预,且是不可逆的。

死锁

虽然synchronized可以解决线程安全问题,但是,如果使用不当,会造成死锁,导致请求一直被阻塞无法返回。 什么是死锁呢?两个或两个以上的线程由于争夺共享资源而造成的相互等待的对象,在这个过程中,两个线程都无法正常向下执行,这种线程也称为死锁线程。下面看看死锁的例子。

public class DeadLockDemo {

    @Data
    @AllArgsConstructor
    static class Person {
        private String name;
        private Long money;
    }


    public static void transferMoney(Person from, Person to, long money) {
        if (from.money <= 0 || to.money <= 0) {
            System.out.println("Any one do not have enough money");
            System.out.println(from.name + " has " + from.money);
            System.out.println(to.name + " has " + to.money);
            return;
        }
        synchronized (from) {
            from.money -= money;
            synchronized (to) {
                to.money += money;
                System.out.println("Transferring money... from: " + from.name + " to:" + to.name + " money:" + money);
            }
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("p1"100000L);
        Person p2 = new Person("p2"100000L);
        new Thread(() -> {
            for (; ; ) {
                transferMoney(p1, p2, 10);
            }
        },"TRANSFER_1").start();
        new Thread(() -> {
            for (; ; ) {
                transferMoney(p2, p1, 5);
            }
        },"TRANSFER_2").start();
    }
}

在例子中模拟转帐场景,p1p2同时向对方转钱,在转账方法中,会先对出账方进行加锁,然后减去对应金额,然后对入账方加锁,加上对应的金额,两个线程分别相同锁对象加锁,但加锁的顺序不一样,这必然会造成死锁问题。我们可以通过jstack查看线程的堆栈信息。

"TRANSFER_2" #12 prio=5 os_prio=31 tid=0x00007fe25e818800 nid=0x6003 waiting for monitor entry [0x000000030af69000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at org.example.synchronize.DeadLockDemo.transferMoney(DeadLockDemo.java:31)
        - waiting to lock <0x000000076ac2f938> (a org.example.synchronize.DeadLockDemo$Person)
        - locked <0x000000076ac2fb18> (a org.example.synchronize.DeadLockDemo$Person)
        at org.example.synchronize.DeadLockDemo.lambda$main$1(DeadLockDemo.java:47)
        at org.example.synchronize.DeadLockDemo$$Lambda$2/2129789493.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"TRANSFER_1" #11 prio=5 os_prio=31 tid=0x00007fe27f8bd800 nid=0x5d5f waiting for monitor entry [0x000000030ae66000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at org.example.synchronize.DeadLockDemo.transferMoney(DeadLockDemo.java:31)
        - waiting to lock <0x000000076ac2fb18> (a org.example.synchronize.DeadLockDemo$Person)
        - locked <0x000000076ac2f938> (a org.example.synchronize.DeadLockDemo$Person)
        at org.example.synchronize.DeadLockDemo.lambda$main$0(DeadLockDemo.java:42)
        at org.example.synchronize.DeadLockDemo$$Lambda$1/764977973.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

可以看到,两个转账线程同时处于BLOCKED状态,无法继续向下执行。

产生死锁的必要条件

导致死锁有四个必要条件

  • 互斥条件:共享资源A和B只能被一个线程占用;
  • 请求和保持条件:线程1已经抢占到共享资源A,在抢占资源B时,不释放资源A;
  • 不可剥夺条件:线程1抢占到的资源A,在未主动释放之前,其他线程不能强行抢占线程1的资源;
  • 循环等待条件:线程1等待线程2占有的资源,线程2等待线程1占有的资源。

预防死锁问题方法

导致死锁问题,以上四个条件缺一不可,但相对应的,我们只需要破坏其中任意一个条件,就能避免死锁的产生,因为互斥条件是资源使用的固有特性,所以我们无法改变,但其他三个条件是可以破坏的,

  • 破坏“请求和保持条件”:我们可以一次性申请所有的资源,这样便不存在等待问题;
  • 破坏“不可抢占条件”:在已经占有了一部分资源的情况下,如果申请不到其他资源,可以主动释放掉已经占有的资源;
  • 破坏“循环等待条件”:在申请资源的时候,可以先申请资源序号小的,再申请资源序号大的,这种显性化申请资源自然就不存在循环等待了。

下面将基于上面的例子对代码进行改造,针对三种破坏死锁的方法进行演示。

1、破坏请求和保持条件

在原来的基础上,笔者新增了一个ResourceLock类,在transferMoney()方法中通过该类的trrlock()方法统一申请资源,然后统一通过unlock()方法释放所有资源,这样便打破了请求和保持条件。

static class ResourceLock {
       private List<Object> list = new ArrayList<>();

       public synchronized boolean trylock(Object lock_1, Object lock_2) {
           if (list.contains(lock_1) || list.contains(lock_2)) {
               return false;
           }
           list.add(lock_1);
           list.add(lock_2);
           return true;
       }

       public synchronized void unlock() {
           list.clear();
       }
   }

   private static ResourceLock lock = new ResourceLock();

   public static void transferMoney(Person from, Person to, long money) {
       if (from.money <= 0 || to.money <= 0) {
           System.out.println("Any one do not have enough money");
           System.out.println(from.name + " has " + from.money);
           System.out.println(to.name + " has " + to.money);
           return;
       }
       while (!lock.trylock(from, to)) {

       }
       try {
           from.money -= money;
           to.money += money;
       } finally {
           lock.unlock();
       }
   }
2、破坏不可抢占条件

破坏不可抢占条件,需要当前线程能够主动释放占有的资源,synchronized因为抢占不到资源时会进入阻塞状态,所以无法实现破坏不可抢占条件这一点。但是JUC中的Lock锁可以轻松解决这个问题。

  private static Lock lock = new ReentrantLock();

   public static void transferMoney(Person from, Person to, long money) {
       if (from.money <= 0 || to.money <= 0) {
           System.out.println("Any one do not have enough money");
           System.out.println(from.name + " has " + from.money);
           System.out.println(to.name + " has " + to.money);
           return;
       }
       if (lock.tryLock()) {
           try {
               from.money -= money;
               if (lock.tryLock()) {
                   try {
                       to.money += money;
                       System.out.println("Transferring money... from: " + from.name + " to:" + to.name + " money:" + money);
                   } finally {
                       lock.unlock();
                   }
               } else {
                   System.out.println("抢占失败");
               }
           } finally {
               lock.unlock();
           }
       } else {
           System.out.println("抢占失败");
       }
   }
3、破坏循环等待条件

破坏循环等待,就是按照资源的某种顺序进行编号,然后根据序号获取锁,笔者这里直接用Person的名称作为排序。

public static void transferMoney(Person from, Person to, long money) {
       if (from.money <= 0 || to.money <= 0) {
           System.out.println("Any one do not have enough money");
           System.out.println(from.name + " has " + from.money);
           System.out.println(to.name + " has " + to.money);
           return;
       }
       String name1 = from.name;
       String name2 = to.name;
       Object lock1 = null;
       Object lock2 = null;
       if (name1.compareTo(name2) >= 0) {
           lock1 = from;
           lock2 = to;
       } else {
           lock1 = to;
           lock2 = from;
       }
       synchronized (lock1) {
           synchronized (lock2) {
               from.money -= money;
               to.money += money;
               System.out.println("Transferring money... from: " + from.name + " to:" + to.name + " money:" + money);
           }
       }
   }

总结

本文主要从原子问题开始,讲解synchronized关键字的使用,了解了synchronized在对象头中的锁标记,为了在保证安全性的前提下,提升锁的性能,引入了偏向锁和轻量级锁,最后对死锁问题做了简单阐述与解决死锁问题的演示。

Java中除了synchronized可以解决线程安全问题外,在JUC(java.util.concurrent)包下还有各种工具可以解决线程同步与通信问题,比如:ReentrantLock,CountDownLatch,ThreadLocal,Semaphore等等,后续会逐一给大家讲解。

对于synchronized的相关知识就讲到这里。读完记得 一个,如发现文章有错误知识点,可以点击 阅读原文 给笔者留言修正。


原文始发于微信公众号(DevUnion):synchronized的使用与原理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/35448.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!