数据同步与对synchonized的深入理解
前言
在上文中,我写了ReentrantLock有关的代码分析,它是基于Lock基础类的。在Java中一般有两种实现锁的方式,一种是基于Lock的,一种是基于JVM的synchonized锁的,在本文中将要介绍后面一种方式。jDK官网中对它进行了这样的解释:它可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来继续来进行。
它具备下面几个特点
- 线程在解锁前必须把共享变量的最新值刷新到主内存中。
- 线程在加锁时将清空工作内存中共享变量的值,从而在使用共享变量时需要从主内存中重新获取最新的值
- 线程解锁前对共享变量的修改在下次加锁时对其他线程可见
- 严格遵守Java happens-before规则,一个monitor exit指令之前必定有一个monitor enter。
数据不一致问题
举一个经典的例子,创建两个线程,index=0,执行index++,并打印输出,当大于300时退出程序。
理想的状态是index从0逐一变到300,
但实际的状况会出现下面几种情况:
- 两个线程中的index数据一致,比如都是10
- 数据输出出现了跳跃,比如,上一个数据是30,但接下来的一个却是32,丢失了31
- 数据超过了300,出现了301
对上面情况进行解析: - 线程1执行index+1, 然后被暂停,执行线程2操作,由于线程1并没有对index进行赋值操作,index仍然为原值,并没有增加为10,线程2执行index+1并赋值,变成了10,之后线程执行原来停留的index+1,也为10,出现了数据的重复。
- 当线程1和线程2都执行到了index=30的位置,其中线程2将index修改为31后执行输出之前切换到了线程1,执行index+1,index变成了32,并输出,而中间的31却没有被输出
- 当index=299的时候,线程1和线程2都看到了条件满足,线程2暂时停顿,线程1中index+1变成了300,之后线程2继续执行,因为它此时已经在条件判断代码块里面,不受条件的控制,执行index+1,就变成了301
monitor
monitor机制需要几个元素来配合,分别是:
- 临界区
- monitor对象及锁
- 条件变量以及定义在monitor对象上的wait和signal操作。
使用monitor机制主要是为了互斥进入临界区,每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得。sychronized关键字包含monitor enter和monitor exit两个JVM指令。
monitor存在计数器,如果为0,则说明该monitor的lock还没有被获得。某个线程获得后,该计数器会加1,当计数器为0,那就意味着该线程不再拥有该monitor的所有权。
synchonized出现的锁
在synchonized中会出现3类锁,偏向锁、轻量级锁和重量级锁。
- 偏向锁是指在一段同步代码一直被一个线程访问,那么该线程就会自动获取锁,降低获取锁的代价。
- 轻量级锁是指当锁为偏向锁的时候,被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方法尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁时,另一个线程在自旋,当尝试一定次数之后,还是没有获取到锁,就会进入阻塞,该锁就会变成重量级锁,重量级锁会让其他申请的线程进入阻塞,性能降低。
synchonized锁的实现
主要有三种形式:
- 修饰实例对象,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码前获得给定对象的锁
对象锁(synchonized method{})和类锁(static synchonized method{})的区别
对象锁也叫实例锁,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁
对象锁能防止在同一时刻多个线程访问同一个对象的synchonized块
类锁是一个全局锁,无论多少个对象共享一个锁,当一个线程访问时,其他线程等待。
将synchronized关键字加static方法和不加static方法有时可能效果是一样的,但两者有着本质的不同
synchronized关键字加static静态方法是将Class类对象作为锁,而synchronized关键字加到非static静态方法是将方法所在类的对象作为锁
public class Service {
synchronized public static void printA() {
try {
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printA");
Thread.sleep(3000);
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void printB() {
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printB");
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printB");
}
synchronized public void printC() {
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printC");
System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printC");
}
}
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.printA();
}
}
public class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
super();
this.service = service;
}
@Override
public void run(){
service.printB();
}
}
public class ThreadC extends Thread {
private Service service;
public ThreadC(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.printC();
}
}
public class Run {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
ThreadC c = new ThreadC(service);
c.setName("C");
c.start();
}
}
运行结果:
线程名称为:A在1561702771777进入printA
线程名称为:C在1561702771778进入printC
线程名称为:C在1561702771778离开printC
线程名称为:A在1561702774777离开printA
线程名称为:B在1561702774777进入printB
线程名称为:B在1561702774777离开printB
从中可以看出加上static和不加static是有区别的,主要是产生了不同的锁,一个是将类Service的对象作为锁,另一个是将Service类对应的Class类的对象作为锁,A、B线程和C线程是异步的关系,而A线程和B线程是同步的关系。
synchronized锁重入
该关键字拥有重入锁的功能,即在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时也可以得到该对象锁。
public class Mysyn {
synchronized public void syn1() {
System.out.println("syn1");
syn2();
}
synchronized public void syn2() {
System.out.println("syn2");
syn3();
}
synchronized public void syn3() {
System.out.println("syn3");
}
}
public class MyThread extends Thread {
@Override
public void run() {
Mysyn syn = new Mysyn();
syn.syn1();
}
}
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
结果输出:
syn1
syn2
syn3
上述代码中的锁是对象锁,当这个对象锁还没有被释放时,还可以重新进入获取该对象锁,如果不是可重入锁的话,是不能调用sync2()方法的。
不同锁形式调用
当在一个类中定义了不同形式的synchronized()方法时,混合调用可能会产生不同的效果。
- 锁重入支持继承的环境,当父类中定义了一个synchronized()方法时,子类继承并重新定义了该方法,并在方法内部调用了父类的方法,在实际执行中会先调用子类的方法,然后调用父类的方法。
- 重写方法如果不使用synchronized关键字,会变成非同步方法,使用后会变成同步方法
- 如果只是在方法的部分区域定义synchronized代码块,在区域内部的代码会同步,在区域外部的代码会异步
- 当先后调用synchronized(this)方法和synchronized public void func1()方法时,都会将当前类的对象作为锁,都是一把锁,运行的结果是同步的效果。
- 使用同步代码块锁非this对象,即synchronized(非this)代码块中的程序与同步方法是异步的,因为有两把锁,不与其他锁this同步方法争抢this锁,可以大大提高运行效率。
- 多个锁就是异步执行
- 同步syn static方法可以对类的所有对象实例起作用
- 同步syn(class)代码块可以对类的所有对象实例起作用
- 在大多数情况下,同步synchronized代码块不使用String作为锁对象,这是String常量池所带来的问题