Java提供了synchronized锁,可以保证同一时刻,只允许一个线程执行某个方法或者代码块,因此又称为同步锁。
synchronized的使用有两组情况值得讨论,修饰方法和代码块;类锁和对象锁。这两组是从不同的角度来分析。我们一起看一下。
第一组:修饰方法和代码块
synchronized的使用比较简单,可以直接修饰方法,代码如下:
- public synchronized void incr() {
- i++;
- }
此时在多线程环境下调用过程就是如下的样子:
这个过程就像我们抢火车票,春节的时候票放出来谁抢到了票就意味着火车上有了一个属于他的座位,那他就可以带着行李进去了,否则就会被乘务拦在外面。这里的锁就是我们的票,这里要指令的代码就是我们的座位。
还有一种用法是直接加在某个代码块上,此时需要将加锁的代码给括起来,例如:
- public void f2() {
- synchronized (this) {
- ...
- }
- }
这时候我们可以指定给谁加锁,而不一定仅仅是方法,因此更加灵活。
第二组:类锁和对象锁
锁有两种类型:类锁和对象锁。
首先是类锁,我们知道如果在一个方法前加static关键字,这个方法就是属于类的,而如果再加一个synchronized关键字 ,则这个方法就是属于类的了,例如:
- public synchronized static void incr() {
- i++;
- }
就是类锁。除此之外,还可以这么写:
- public class Lock {
- public void f3() {
- synchronized (Lock.class) {
- // ...
- }
- }
- }
这两种都是类锁。如果是类锁,那么在整个JVM空间中,只有一个线程能拿到锁:
- public class SynchronizedForClassExample {
-
- public void f1() {
- synchronized(SynchronizedForClassExample.class) {
- while (true) {
- System.out.println("当前访问的线程:" + Thread.currentThread().getName());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- public static void main(String[] args) {
- SynchronizedForClassExample thread1=new SynchronizedForClassExample();
- SynchronizedForClassExample thread2=new SynchronizedForClassExample();
- new Thread(()->thread1.f1(),"t1").start();
- new Thread(()->thread2.f1(),"t2").start();
- }
- }
这段代码定义了 一个f1()方法,功能是循环打印当前线程的名称,并且用锁给保护起来了。main()方法中的两个实例thread1和thread2分别创建两个线程来操作f1()。 执行时可以发现第一个持有锁的将一直持有,能够保证存在多个对象时达到互斥的目的。
另外一种方式是对象锁,也就是针对堆中创建的具体实例的锁,如果根据同一个类创建了多个实例,则对象锁就可能存在多个。与类锁一样,其写法也有两种方式。
普通方式是:
- public synchronized void f1() {
- //....
- }
也可以修饰代码块:
- public class Lock {
- Lock lock = new Lock();
-
- public void f() {
- synchronized (lock) {
- // ...
- }
- }
- }
我们稍微改一下上面的类锁的代码来测试一下其用法:
- public class SynchronizedForObjectExample {
- Object lock = new Object();
-
- public void f1() {
- synchronized (lock) {
- while (true) {
- System.out.println("当前获得锁的线程:" + Thread.currentThread().getName());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
-
- public static void main(String[] args) {
- SynchronizedForObjectExample thread1 = new SynchronizedForObjectExample();
- SynchronizedForObjectExample thread2 = new SynchronizedForObjectExample();
- new Thread(() -> thread1.f1(), "t1").start();
- new Thread(() -> thread2.f1(), "t2").start();
- }
- }
此时会非常均匀的打印:
- 当前获得锁的线程:t1
- 当前获得锁的线程:t2
- 当前获得锁的线程:t1
- 当前获得锁的线程:t2
- 当前获得锁的线程:t2
- 当前获得锁的线程:t1
这就说明堆中存在两个类型为SynchronizedForObjectExample的对象,因此存在两个对象锁。
此时可以发现,对象锁的范围小,两个线程只有操作同一个对象时才会有作用,并不能完全保证JVM中线程的互斥访问。如果要在上面的代码中实现该功能,只要在一个位置添加一个关键字即可,你知道是哪里吗?
类锁和对象锁的不一样的本质
类锁和对象锁到底什么区别呢?这个问题还要回到JVM中关于堆内存分配策略中,Class类是JVM在启动过程中加载的,每个.class文件被装载后产生一个唯一的Class对象,通过static修饰的对象和成员方法都属于类级别,它们会随着类的定义被分配、装载和回收,可以理解为Class对象会伴随JVM的整个生命周期,一般不会被回收。
而实例对象是伴随实例的创建而开始,随着实例的回收而消失,整个过程是JVM的堆空间管理的。创建几个实例取决于业务代码的设计,可能存在多个,堆的垃圾回收策略也会将失效的类给回收掉。
在上面代码中,lock被new出来之后就是一个堆空间的对象,如果多new几次就是多个Lock类型的对象,他们彼此独立的。