synchronzied 关键字
文章目录
1、概念
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。
因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。
在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)
2、修饰修饰实例方法
(1)修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁 。
public class Test implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<10;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Test test=new Test();
Thread t1=new Thread(test);
Thread t2=new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果:
10
从结果可以看出:
synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。如果多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。
(2)再看下面的情况:
public static void main(String[] args) throws InterruptedException {
Test test1 =new Test();
Test test2 = new Test();
Thread t1=new Thread(test1);
Thread t2=new Thread(test2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。
3、修饰静态方法
继续以上的代码,把increase()方法改为静态方法。
public static void main(String[] args) throws InterruptedException {
Test test1 =new Test();
Test test2 = new Test();
Thread t1=new Thread(test1);
Thread t2=new Thread(test2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
结果:
10
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。
需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象**,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**
注意:
对象锁和类锁是两个不同的锁,类锁属于类对象,每个类就只有一个,如static修饰的synchronized静态方法的访问,就需要类锁,而非static修饰的synchronized方法的访问,需要的是对象锁。
4、同步代码块
除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作。
public class Test2 implements Runnable{
static Test2 test=new Test2();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(test){
for(int j=0;j<10;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(test);
Thread t2=new Thread(test);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
可以看出:
(1)将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有test实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作
(2)除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<10;j++){
i++;
}
}
//class对象锁
synchronized(Test2.class){
for(int j=0;j<10;j++){
i++;
}
}
5、可重入锁
(1)概念
自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
例子:
public class Service {
synchronized public void service1() {
System.out.println("service1");
service2();
}
synchronized public void service2() {
System.out.println("service2");
service3();
}
synchronized public void service3() {
System.out.println("service3");
}
}
public class MyThread extends Thread {
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
6、底层的实现原理
理解原理之前先来了解一下java对象模型:
1、对象模型
(1)每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。
使用new创建一个对象的时候,JVM会在对内存中创建一个instanceOopDesc
对象,这个对象中包含了两部分信息,对象头以及实例数据。
(2)对象头中包括两部分:
Mark Word 一些运行时数据,其中就包括和多线程相关的锁的信息。
Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
Klass Point 元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化。
2、Moniter
(1)为了解决线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor。
(2)每一个Object对象中内置了一个Monitor对象。(对象头的MarkWord中的LockWord指向monitor的起始地址)
Monitor相当于一个许可证,线程拿到许可证即可以进行操作,没有拿到则需要阻塞等待。
ObjectMonitor中有几个关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
线程T等待对象锁:_EntryList中加入T
线程T获取对象锁:_EntryList移除T,_owner置为T,计数器_count+1
持有对象锁的线程调用wait():_owner置为T,计数器_count+1,_WaitSet中加入T
3、原理
看个例子
public class SynchronizedTest {
public synchronized void test1(){
}
public void test2(){
synchronized (this){
}
}
}
查看编译后的字节码:
(1)同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
(2)同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
7、Synchronized 和 ReenTrantLock 的对比
(1)两者都是可重入锁
(2)synchronized依赖于JVM而ReenTrantLock依赖于API
(3)ReenTrantLock比synchronized增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
(4)在JDK1.6之后,synchronized的性能是比ReenTrantLock差不多