这篇文章主要总结synchronized的用法,考虑到篇幅和方便自己记忆,synchronized的原理会在下篇文章详细总结。
Synchronized是Java中常用的一个关键字。
Synchronized的作用主要有三个:
(1)确保线程互斥的访问同步代码。——原子性
(2)保证共享变量的修改能够及时可见。——可见性
(3)有效解决重排序问题。——有序性
这里只总结几大特性的基础理论,会在后续文章中陆陆续续结合关键字或者实例来讲解下面的特性。
1、共享性
在多线程编程中,数据共享是不可避免的。最典型的场景是数据库中的数据,为了保证数据的一致性,我们通常需要共享同一个数据库中数据。
如果所有的数据只是在线程内有效,那就不存在线程安全性问题,但这是不可能的。
2、互斥性
资源互斥是指同时只允许一个访问者对其进行访问,具有唯一性和排它性。
同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。互斥是因,同步是果;互斥是方法,同步是目的。
Java 中提供多种机制来保证互斥性,最简单的方式是使用Synchronized。
3、原子性
原子性就是指对数据的操作是一个独立的、不可分割的整体。换句话说,就是一次操作,是一个连续不可中断的过程,数据不会执行的一半的时候被其他线程所修改。保证原子性的最简单方式是操作系统指令,就是说如果一次操作对应一条操作系统指令,这样肯定可以能保证原子性。
保证原子性,最常见的方式是加锁,如Java中的Synchronized或Lock都可以实现,代码段二就是通过Synchronized实现的。除了锁以外,还有一种方式就是CAS(Compare And Swap),即修改数据之前先比较与之前读取到的值是否一致,如果一致,则进行修改,如果不一致则重新执行,这也是乐观锁的实现原理。
4、可见性
可见性是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
从上图可以看到,java内存模型中,是依赖主内存来实现可见性,修改时新值同步到主内存,读取也是从主内存刷新变量值。无论是普通变量还是volatile变量都是如此,但是两者的区别是:volatile的特殊规则能够保证新值立即同步到主内存,以及每次使用前立即从主内存刷新。
实现可见性的关键字有:volatile,synchronized,final。
synchronized实现可见性的规则:对一个变量执行unlock之前,必须先把此变量同步回主内存中。
final的可见性:被final修饰的变量,不管是常量还是变量,其值或者引用是不可变的,在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸:其他线程可能通过这个引用访问到“初始化一半”的对象),那么其他线程就能看见final字段的值。
5、有序性
如果在本线程内观察,所有的操作都是有序的。——线程内表现为串行。
如果在一个线程中观察另一个线程,所有的操作都是无序的。——指令重排序,工作内存和主内存同步延迟。
实现有序的关键字有:volatile,synchronized。
volatile本身包含禁止指令重排序。
synchronized:一个变量在同一时刻只允许一条线程对其进行lock操作。
话不多说,先看代码:
public class DemoSync {
public synchronized void a(){
//TODO
}
public void b(){
synchronized (this){
//TODO
}
}
public synchronized static void c(){
//TODO
}
public static void d(){
synchronized (Demo.class){
//TODO
}
}
}
每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的 类实例的锁 方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为synchronized ,以控制其对类的静态成员变量的访问。
// 对象锁:形式1(方法锁)
public synchronized void method1(){
System.out.println("我是对象锁也是方法锁");
try{
System.out.println("我要睡500ms");
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
DemoSync demoSync = new DemoSync();
new Thread(()->
{
System.out.println("线程1");
demoSync.method1();
}).start();
new Thread(()->
{
System.out.println("线程2");
demoSync.method1();
}).start();
}
运行结果如下:
线程1
我是对象锁也是方法锁
我要睡500ms
线程2
我是对象锁也是方法锁
我要睡500ms
synchronized锁定的调用当前方法的对象。
线程执行本身是没有顺序的, 代码中两个线程需要去竞争demoSync 对象的锁,谁竞争到锁,谁就先执行,而另一个则后执行。
在Java中,Jvm会为每个对象内置一个监视器(monitor),监视器中有一个地方叫监视区域,任何线程要想执行这个对象的synchronized方法,都必须先进入到该对象的监视区域。监视器负责保证同一时刻只有一个线程在监视区域执行。这个会在后面文章中详细说明。
this指的是调用当前方法的对象。 因此,哪个对象的b方法被线程调用,就锁定哪个对象。
// 对象锁:形式2(代码块形式)
public void method2(){
synchronized (this){
try{
System.out.println("我是对象锁,我要睡500ms");
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
DemoSync demoSync1 = new DemoSync();
DemoSync demoSync2 = new DemoSync();
new Thread(()->
{
System.out.println("线程1中的对象1:");
demoSync1.method2();
System.out.println("线程1中的对象2:");
demoSync2.method2();
}).start();
new Thread(()->
{
System.out.println("线程2中的对象2:");
demoSync2.method2();
System.out.println("线程2中的对象1:");
demoSync1.method2();
}).start();
}
执行结果如下:
线程1中的对象1:
我是对象锁,我要睡500ms
线程2中的对象2:
我是对象锁,我要睡500ms
线程1中的对象2:
我是对象锁,我要睡500ms
线程2中的对象1:
我是对象锁,我要睡500ms
线程1中,this指定的分别是demoSync1 和demoSync2 对象。
a方法和b方法是等价的。
// 类锁:形式1 :锁静态方法
public static synchronized void method3(){
System.out.println("我是类锁1");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
// 类锁:形式1 :锁静态方法
public static synchronized void method4(){
System.out.println("我是类锁2");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(()->
{
System.out.println("线程1:");
method3();
System.out.println("线程1:");
method4();
}).start();
new Thread(()->
{
System.out.println("线程2:");
method3();
System.out.println("线程2:");
method4();
}).start();
}
执行结果如下,且每次执行结果不同:
线程1:
我是类锁1
线程2:
线程1:
我是类锁1
线程2:
我是类锁2
我是类锁2
首先,线程是无序的,所以执行结果中可以看到线程1和2交互执行。
其次,static方法不依附于任何对象而存在,是隶属于一个类的。而synchronized 锁定的是这个类,所以线程1和线程2在执行静态方法时,只能顺序的执行method3和method4,不能并发执行。
public static void method5() {
synchronized (DemoSync.class) {
System.out.println("我是类锁种类2");
}
}
public static void main(String[] args) {
DemoSync demoSync1 = new DemoSync();
DemoSync demoSync2 = new DemoSync();
new Thread(() ->
{
System.out.println("线程1:");
demoSync1.method5();
}).start();
new Thread(() ->
{
System.out.println("线程2:");
demoSync2.method5();
}).start();
}
执行结果如下:
线程1:
我是类锁种类2
线程2:
我是类锁种类2
Jvm会为每个类都维护唯一的一个Class类对象,synchronized是给DemoSync类对应的Class对象加锁的。
类的Class对象如下:
Returns the runtime class of this Object. The returned Class object is the object
that is locked by static synchronized methods of the represented class.
用static synchronized 修饰的方法锁定的也是DemoSync.class对象。所以,c和d是等价的。
若修饰1个大的方法,将会大大影响效率。
synchronized关键字会降低应用程序的性能,因此只能在并发情景中需要修改共享数据的方法上使用它。如果多个线程访问同一个synchronized方法,则只有一个线程可以访问,其他线程将等待。
若使用Synchronized关键字修饰 线程类的run(),由于run()在线程的整个生命期内一直在运行,因此将导致它对本类任何Synchronized方法的调用都永远不会成功。解决方案:使用 Synchronized关键字声明代码块。
参考及图片来源:https://blog.csdn.net/carson_ho/article/details/82992269