1.1 官方翻译
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。
1.2 一句话概括synchronized作用
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
1.3 如何实现
如果一段代码被synchronized关键词修饰,那么这段代码就会以原子的方式执行,多个线程在执行该段代码时不会相互干扰,因为多个线程不会同时执行该段代码,从而不会发生并发问题。如何保证其它线程不执行该段代码的呢?当一个线程执行改段代码,那么该线程将会获得一把锁,它将独占该段代码,其它等待执行该段代码的线程都将等待、堵塞,直到代码执行完毕或者在一定的条件下释放该锁,其它获取到该锁的线程才能执行该段代码。
3.1 代码实战:两个线程同时a++ ,最后结果会比预计的少
public class DisappearRequest1 implements Runnable {
static DisappearRequest1 instance = new DisappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
//t1 t2线程执行完毕后再打印
t1.join();
t2.join();
//结果小于等于20000
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
为什么会造成这种后果呢?
count++ ;它看上去只是一个操作,实际上包含了三个动作:
对象锁:包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
类锁:指synchronized修饰静态的方法或指定锁为Class对象。
2.1 使用this对象锁修饰代码块
this对象锁只有一个,如果有多个需要同步的代码块,他们执行的要求不是一个执行其它都不允许执行,那么就不能使用this对象锁了,需要自定义多个对象来获取锁
public class Synchronized0bjectCodeBlock1 implements Runnable {
static Synchronized0bjectCodeBlock1 instance = new Synchronized0bjectCodeBlock1();
@Override
public void run() {
/**
* 获取this,当前实例对象锁,this只有一份
*/
synchronized (this) {
System.out.println("我是对象锁的代码块形式。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束。");
}
}
public static void main(String[] args) {
Thread tl = new Thread(instance);
Thread t2 = new Thread(instance);
tl.start();
t2.start();
while (tl.isAlive() || t2.isAlive()) {
}
System.out.println("thread:"+Thread.currentThread().getName()+"is finished");
}
}
2.2 自定义对象锁修饰代码块
public class Synchronized0bjectCodeBlock2 implements Runnable {
static Synchronized0bjectCodeBlock2 instance = new Synchronized0bjectCodeBlock2();
Object lock1 = new Object();
Object lock2 = new Object();
@Override
public void run() {
/**
* 获取this,当前实例对象锁,this只有一份
*/
synchronized (lock1) {
System.out.println("我是对象锁的代码块形式。我叫 lock1 " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock1部分运行结束。");
}
synchronized (lock2) {
System.out.println("我是对象锁的代码块形式。我叫 lock2 " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock2部分运行结束。");
}
}
public static void main(String[] args) {
Thread tl = new Thread(instance);
Thread t2 = new Thread(instance);
tl.start();
t2.start();
while (tl.isAlive() || t2.isAlive()) {
}
System.out.println("thread:"+Thread.currentThread().getName()+" is finished");
}
}
2.3 普通方法锁
普通方法锁使用的是this对象锁
public class Synchronized0bjectMethodBlock implements Runnable {
static Synchronized0bjectMethodBlock instance = new Synchronized0bjectMethodBlock();
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println("我的对象锁的方法修饰符形式,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread tl = new Thread(instance);
Thread t2 = new Thread(instance);
tl.start();
t2.start();
while (tl.isAlive() || t2.isAlive()) {
}
System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
}
}
3.1 概念
3.2 类锁的形式一:静态方法锁
public class SynchronizedClassStatic implements Runnable {
static SynchronizedClassStatic instance1 = new SynchronizedClassStatic();
static SynchronizedClassStatic instance2 = new SynchronizedClassStatic();
@Override
public void run() {
method();
}
//去掉synchronized 关键字,两线程将并行运行该方法。
public static synchronized void method() {
System.out.println("我是类锁的第一种形式形式:static形式。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread tl = new Thread(instance1);
Thread t2 = new Thread(instance2);
tl.start();
t2.start();
while (tl.isAlive() || t2.isAlive()) {
}
System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
}
}
3.2 类锁的形式二:*.class代码块
public class SynchronizedClassClass implements Runnable {
static SynchronizedClassClass instance1 = new SynchronizedClassClass();
static SynchronizedClassClass instance2 = new SynchronizedClassClass();
@Override
public void run() {
method();
}
public void method() {
synchronized (SynchronizedClassClass.class) {
System.out.println("我是类锁的第二种形式形式:synchronized(*.class)。我叫"
+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
Thread tl = new Thread(instance1);
Thread t2 = new Thread(instance2);
tl.start();
t2.start();
while (tl.isAlive() || t2.isAlive()) {
}
System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
}
}
1、两个线程同时访问一个对象的同步方法。
由于同步方法锁使用的是this对象锁,同一个对象的this锁只有一把,两个线程同一时间只能有一个线程持有该锁,所以该方法将会串行运行。
2、两个线程访问的是两个对象的同步方法。
由于两个对象的this锁互不影响,synchronized将不会起作用,所以该方法将会并行运行。
3、两个线程访问的是synchronized的静态方法。
synchronized修饰的静态方法获取的是当前类模板对象的锁,该锁只有一把,无论访问多少个该类对象的方法,都将串行执行。
4、同时访问同步方法与同步方法
非同步方法不受影响。
5、访问同一个对象的不同的普通同步方法。
由于this对象锁只有一个,不同线程访问多个普通同步方法将串行运行。
6、同时访问静态synchronized和非静态synchronized方法
静态synchronized方法的锁为class对象的锁,非静态synchronized方法锁为this的锁,它们不是同一个锁,所以它们将并行运行。
7、方法抛异常后,会释放锁。(与Lock类不同,Lock类必须手动释放锁)
下面代码演示了方法一抛出异常后JVM会帮助线程当前线程释放锁,方法二线程会立即获取锁正常执行代码。
public class SynchronizedException implements Runnable {
static SynchronizedException instance = new SynchronizedException();
@Override
public void run() {
if ("Thread-0".equals(Thread.currentThread().getName())) {
method1();
} else {
method2();
}
}
public synchronized void method1() {
System.out.println("我抛出异常的同步方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
}
public synchronized void method2() {
System.out.println("我正常运行的同步方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread tl = new Thread(instance);
Thread t2 = new Thread(instance);
tl.start();
t2.start();
while (tl.isAlive() || t2.isAlive()) {
}
System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
}
}
运行结果
我抛出异常的同步方法。我叫Thread-0
我正常运行的同步方法。我叫Thread-1
Exception in thread "Thread-0" java.lang.RuntimeException
at com.example.demo.SynchronizedException.method1(SynchronizedException.java:22)
at com.example.demo.SynchronizedException.run(SynchronizedException.java:9)
at java.lang.Thread.run(Thread.java:748)
Thread-1运行结束
thread:main is finished
总结
1.1 概念
同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。
1.2 举例
北京买车需要上牌照,但是需要摇号,如果我摇到一个号,但是家里有三辆车,我能不能给家里三辆车都上牌照?但是,不行,每次摇到一个号对应一次上牌照的权力,这就是不可重入。如果是可重入的,情况就是这样,我摇到一个号就可以一直获取牌照,直到我自己不愿意获取到更多了,主动结束。
这里的“我”代表线程,“摇到号”代表获取到了锁,“获取牌照的过程”就是使用锁,摇到一个号只能获取一次牌照就是不可重入的,摇到一个号可以无限次数获取牌照就是可重入的。
所以一个线程拿一旦拿到一把锁,它可以对锁进行多次使用,那么就是可重入的,所以可重入锁也叫递归锁;如果一个线程拿到一把锁后,如果想要再次使用就必须先释放锁,然后和其它线程进行竞争,这就是不可重入。
1.3 好处
避免死锁:比如说有两个方法A、B都被synchronized修饰了,线程1执行到了方法A,获取了这把锁,要想执行方法B也需要获取该锁。假设synchronized不具备可重入性质,线程1执行方法A虽然获取了该锁,但是想要访问方法B不能直接使用该锁,此时既想获取锁又不想释放锁,这就造成永远等待即死锁。
提升封装性:避免了多次解锁加锁过程,简化了开发难度。
粒度:synchronized的默认加锁范围是线程。
1.4 可重入粒度测试
1.4.1 证明同一个方法是可重入的
以下方法能够正常执行
public class SynchronizedRecursion {
int a = 0;
public static void main(String[] args) {
SynchronizedRecursion synchronizedRecursion1 = new SynchronizedRecursion();
synchronizedRecursion1.method1();
}
private synchronized void method1() {
System.out.println("这是 method1,a = " + a);
if (a == 0) {
a++;
method1();
}
}
}
1.4.2 证明可重入不要求是同一个方法
以下代码能够正常执行
public class SynchronizedOtherMethod {
private synchronized void method1() {
System.out.println("我是method1");
method2();
}
private synchronized void method2() {
System.out.println("我是method2");
}
public static void main(String[] args) {
SynchronizedOtherMethod synchronizedOtherMethod = new SynchronizedOtherMethod();
synchronizedOtherMethod.method1();
}
}
1.4.3 证明可重入不要求是同一个类中的
以下方法能够正常执行
public class SynchronizedSuperClass {
protected synchronized void method() {
System.out.println("我是父类方法");
}
}
class SynchronizedChildClass extends SynchronizedSuperClass{
@Override
protected synchronized void method() {
System.out.println("我是子类方法");
super.method();
}
public static void main(String[] args) {
SynchronizedChildClass synchronizedChildClass = new SynchronizedChildClass();
synchronizedChildClass.method();
}
}
由此可见,在java中synchronized关键字的粒度范围是线程范围,也就是说在一个线程如果已经获取到某把锁后,如果接着需要这把锁去访问其它方法或者其它类的,那么可重入性质就会被激发,就可以不用显示的去释放锁、获取锁
一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程釋放这个锁。如果别人永远不释放锁,那么我只能永远地等下去。
相比之下,Lock类可以拥有中断的能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得我等待的时间太长了不想再等了,也可以退出。
1.1 现象
每个类的实例对应一把锁,每个被synchronized修饰的方法或者代码块的调用都需要一把锁才能执行,方法或者代码块一旦执行就会独占该锁知道代码执行结束或者抛出异常,才将锁释放,然后那些被阻塞的线程才能进行竞争获取该锁进入可执行状态。所有的java对象都持有一个互斥锁,该锁由JVM自动获取和释放,我们只需要指定这个对象就可以。
1.2 获取和释放锁的时机:内置锁
每个java对象都可以用作一个实现同步的锁,这个锁被称作内置锁,或者监视器锁(Monitor Lock)。线程在进入同步代码块或方法前会自动获取该锁,并且退出时(正常对出、抛出异常退出)会自动的释放锁。获取该锁的唯一途径就是进入synchronized保护的代码块或者同步方法中。
1.3 等价代码
public class SynchronizedEquals {
private Lock lock = new ReentrantLock();
private synchronized void method1() {
System.out.println("我是synchronized方法");
}
private void method2() {
lock.lock();
System.out.println("我是lock方法");
lock.unlock();
}
public static void main(String[] args) {
SynchronizedEquals synchronizedEquals = new SynchronizedEquals();
synchronizedEquals.method1();
synchronizedEquals.method2();
}
}
2.1 概况
java每个对象都有一个对象头,每个头都可以存储很多东西,其中有个部分就是用来存储synchronized关键字锁的。
当线程访问一个同步代码块或者方法时必须得到这把锁,当退出或者抛出异常时会释放这把锁,进入锁释放锁是基于monitor对象来实现同步的,monitor对象主要有两个指令monitorenter(插入到同步代码开始位置)、monitorexit(插入到同步代码结束的时候),JVM会保证每个enter后有exit与之对应,但是可能会有多个exit和同一个enter对应,因为退出的时机不仅仅是方法退出也可能是方法抛出异常。每个对象都有一个monitor与之关联,一旦持有之后,就会处于锁定状态,当线程执行到monitorenter这个指令时,就会尝试获取该对象monitor所有权(尝试获取该对象锁)。
2.2 如何反编译
1、反编译源代码
public class Decompilation {
private Object object = new Object();
public void insert(Thread thread) {
synchronized (object) {
}
}
}
2、利用javac命令生成class文件。
3、利用java提供的工具反编译class文件:javap -verbose .\Decompilation.class。
Classfile /H:/my_projects/F2FPay_Demo_Java/demo/src/main/java/com/example/demo/Decompilation.class
Last modified 2019-2-22; size 492 bytes
MD5 checksum a0ee0e14677988f525bcb82797fb9277
Compiled from "Decompilation.java"
public class com.example.demo.Decompilation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#20 // java/lang/Object."":()V
#2 = Class #21 // java/lang/Object
#3 = Fieldref #4.#22 // com/example/demo/Decompilation.object:Ljava/lang/Object;
#4 = Class #23 // com/example/demo/Decompilation
#5 = Utf8 object
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 insert
#12 = Utf8 (Ljava/lang/Thread;)V
#13 = Utf8 StackMapTable
#14 = Class #23 // com/example/demo/Decompilation
#15 = Class #24 // java/lang/Thread
#16 = Class #21 // java/lang/Object
#17 = Class #25 // java/lang/Throwable
#18 = Utf8 SourceFile
#19 = Utf8 Decompilation.java
#20 = NameAndType #7:#8 // "":()V
#21 = Utf8 java/lang/Object
#22 = NameAndType #5:#6 // object:Ljava/lang/Object;
#23 = Utf8 com/example/demo/Decompilation
#24 = Utf8 java/lang/Thread
#25 = Utf8 java/lang/Throwable
{
public com.example.demo.Decompilation();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
LineNumberTable:
line 3: 0
line 4: 4
public void insert(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_2
6: monitorenter //monitorenter对象
7: aload_2
8: monitorexit //第一个monitorexit
9: goto 17
12: astore_3
13: aload_2
14: monitorexit //第二个monitorexit
15: aload_3
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
LineNumberTable:
line 7: 0
line 9: 7
line 10: 17
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class com/example/demo/Decompilation, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "Decompilation.java"
2.3 monitorenter和monitorexit指令
monitorenter和monitorexit指令会在执行的时候让锁计数减一或者加一,每个对象都与你一monitor对象关联,一个monitor的lock锁只能被一个线程在同一时间获得,一个线程在尝试获得与这个对象关联的monitor所有权时只会发生以下三种情况之一:
主要利用加锁次数计数器,具体是这样:
下图描述了两个线程是如何使用共享变量的:为了加快程序运行,线程会将共享变量复制一份到本地内存,这样一来就可能因为不同线程之间共享变量的读写带来风险。因此线程之间就需要进行通知来告诉彼此变量的变化。(JMM是Java内存模型缩写)
下图描述了线程之间是如何保证共享变量的事实一致性的:线程A使用变量前会通知线程B,线程A使用完变量后会将变量值写入到主存中,然后通知线程B,线程B去主存中获取的变量值就是最新的。
synchronized是如何做到可见性的实现的:某个被synchronized修饰的方法或者代码块在执行完毕后,该方法或者代码块中对共享变量的所作的任何修改都要在释放锁之前从线程内存写入到主内存中,因此就保证了线程内存的变量与主内存中变量的一致性。同样,在进入同步代码块或者方法中所得到的共享变量值也是直接从主内存中获取的。
加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),锁住找个对象,找个对象就是这把锁,除非释放这个对象才是解开这把锁。相对而言,读写锁读操作不加锁,写操作加锁。
相比直线Lock锁可以去尝试获取锁,返回获取结果,成功去做某些操作,不成功去做另外一些操作。
public class SynchronizedDeadLock {
public static void main(String[] args) {
MyThread t1 = new MyThread(true);
MyThread t2 = new MyThread(false);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class MyThread extends Thread {
private boolean flag;
static Object object1 = new Object();
static Object object2 = new Object();
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
if (flag) {
synchronized (object1) {
System.out.println("object1锁住了" + Thread.currentThread().getName());
Thread.sleep(100);
// object1.wait(100);
System.out.println(Thread.currentThread().getName() + "等待获取object2的锁");
synchronized (object2) {
System.out.println("object2锁住了" + Thread.currentThread().getName());
}
System.out.println(Thread.currentThread().getName() + "释放了object1和object2");
}
}
if (!flag) {
synchronized (object2) {
System.out.println("object2锁住了" + Thread.currentThread().getName());
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "等待获取object1的锁");
synchronized (object1) {
System.out.println("object1锁住了" + Thread.currentThread().getName());
}
System.out.println(Thread.currentThread().getName() + "释放了object1和object2");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果:
object1锁住了线程1
object2锁住了线程2
线程1等待获取object2的锁
线程2等待获取object1的锁
思路:有现成工具就用,没有就优先用synchronized,用这个关键字可以减少代码编写,至少比Lock少出错,需要Lock特性就用Lock。
上面已有介绍
---------------------
作者:风中漫步者
原文:https://blog.csdn.net/qwqw3333333/article/details/87358290