导致线程安全问题的根本原因在于,存在多个线程同时操作一个共享资源,要想解决这个问题,就需要保证对共享资源访问的独占性,因此人们在Java中提供了synchronized关键字,我们称之为同步锁,它可以保证在同一时刻,只允许一个线程执行某个方法或代码块。
synchronized同步锁具有互斥性,这相当于线程由并行执行变成串行执行,保证了线程的安全性,但是损失了性能。下面我们先来看一下synchronized的使用方法。
synchronized 的使用方法
synchronized 的使用方法比较简单,修饰方式有如下两种。
- 作用在方法级别,表示针对m1()方法加锁,当多个线程同时访问m1()方法时,同一时刻只有一个线程能执行。
public synchronized void m1(){//省略代码
}
作用在代码块级别,表示针对某一段线程不安全的代码加锁,只有访问到synchronized(this)这行代码时,才会去竞争锁资源。
public void m2( ){ synchronized(this){//省略代码}
}
了解了 synchronized的基本使用语法之后,我们来看如图所示的流程,它针对上面的案例增加了 synchronized 同步锁之后的执行流程。简单地说,当多个线程同时访问加synchronized关键字修饰的方法时,需要先抢占一个锁标记,只有抢到锁标记的线程才有资格调用incr()方法。这就使得在同一时刻只有一个线程执行i++操作,从而解决了原子性问题。
了解 synchronized 同步锁的作用范围
我们对一个方法增加synchronized关键字后,当多个线程访问该方法时,整个执行过程会变成串行执行,这种执行方式很明显会影响程序的性能,那么如何做好安全性及性能的平衡呢?
实际上,synchronized关键字只需要保护可能存在线程安全问题的代码,因此,我们可以通过控制同步锁的作用范围来实现这个平衡机制。在synchronized 中,提供了两种锁,一是类锁,二是对象锁。
类锁
类锁是全局锁,当多个线程调用不同对象实例的同步方法时会产生互斥,具体实现方式如下。
- 修饰静态方法:
public static synchronized void m1( ){//省略代码
}
- 修饰代码块,synchronized 中的锁对象是类,也就是Lock.class。
public class Lock{ public void m2(){synchronized(Lock.class){//省略代码}}
}
下面这段程序使用类锁来实现跨对象实例,从而实现互斥的功能。
public class SynchronizedExample{public void m1( ) {synchronized(SynchronizedExample.class) {while (true){System.out.println("当前访问的线程:"+Thread.currentThread( ).getName()); try{Thread.sleep(1000);} catch (InterruptedException e){e.printStackTrace();}}}}public static void main(String[] args){SynchronizedExample se1=new SynchronizedExample(); SynchronizedExample se2=new SynchronizedExample(); new Thread(()->se1.m1(),"t1").start(); new Thread(()->se2.m1(),"t2" ).start();}
}
- 该程序中定义了一个m1()方法,该方法中实现了一个循环打印当前线程名称的逻辑,并且这段逻辑是用类锁来保护的。
- 在 main()方法中定义了两个SynchronizedExample对象实例sel和se2,又分别定义了两个线程来调用这两个实例的m10方法。
根据类锁的作用范围可以知道,即便是多个对象实例,也能够达到互斥的目的,因此最终输出的结果是:哪个线程抢到了锁,哪个线程就持续打印自己的线程名称。
对象锁
对象锁是实例锁,当多个线程调用同一个对象实例的同步方法时会产生互斥,具体实现方式如下。
- 修饰普通方法:
public synchronized void m1( ){//省略代码
}
- 修饰代码块,synchronized中的锁对象是普通对象实例。
public class Lock{Object lock=new 0bject( ); public void m2( ){synchronized(lock){//省略代码}}
}
下面这段程序演示了对象锁的使用方法,代码如下。
public class SynchronizedForobjectExample {Object lock=new 0bject(); public void m1( ){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) {SynchronizedFor0bjectExample se1=new SynchronizedForobjectExample(); SynchronizedForObjectExample se2=new SynchronizedForobjectExample(); new Thread(()->se1.m1(),"t1").start(); new Thread(()->se2.m1(),"t2" ).start();}
}
我们先来看一下打印结果。
当前获得锁的线程:t1
当前获得锁的线程:t2
当前获得锁的线程:t2
当前获得锁的线程:t1
当前获得锁的线程:t1
当前获得锁的线程:t2
当前获得锁的线程:t1
当前获得锁的线程:t2
从以上结果中我们发现,对于几乎相同的代码,在使用对象锁的情况下,当两个线程分别访问两个不同对象实例的m10方法时,并没有达到两者互斥的目的,看起来似乎锁没有生效,实际上并不是锁没有生效,问题的根源在于synchronized(lock)中锁对象lock的作用范围过小。
Class是在JVM启动过程中加载的,每个.class文件被装载后会产生一个Class对象,Class对象在JVM进程中是全局唯一的。通过static修饰的成员对象及方法的生命周期都属于类级别,它们会随着类的定义被分配和装载到内存,随着类被卸载而回收。
实例对象的生命周期伴随着实例对象的创建而开始,同时伴随着实例对象的回收而结束。
因此,类锁和对象锁最大的区别是锁对象lock的生命周期不同,如果要达到多个线程互斥,那么多个线程必须要竞争同一个对象锁。
在上述代码中,通过Objectlock-new Object();构建的锁对象的生命周期是由Synchronized- ForObjectExample 对象的实例来决定的,不同的SynchronizedForObjectExample 实例会有不同的 lock锁对象,由于没有形成竞争,所以不会实现互斥的效果。如果想要让上述程序达到同步的目的,那么我们可以对lock锁对象增加.static关键字。
static Object lock=new 0bject();
最后,留下一个问题去思考,关于 synchronized 同步锁的思考?同步锁的核心特性是排他,要达到这个目的,多线程必须抢占同一个资源。。。。。。