多线程二之高并发详解之同步synchronized关键字

高并发详解之同步synchronized关键字

image-20200201014314239

synchronized简介

作用

官方解释

同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的

一句话说出synchronized的作用

能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果

地位

synchronized是Java的关键字,被Java语言原生支持

是最基本的互斥同步手段

是并发编程中的元老级角色,是并发编程的必学内容

不使用并发的后果

代码实战

两个线程同时执行a++,最后结果会比预计的少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
*描述:消失的请求数
*
*/
public class DisappearRequest1 implements Runnable {
static DisappearRequest1 instance=new DisappearRequest1();
static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}

@Override
public void run() {
for (int j=0;j<100000;j++){
count++;
}
}
}

输出结果

1
181399

原因

count++,看上去只有一个操作,实际上包含三个动作

  1. 读取count
  2. 将count加1
  3. 将count的值写入到内存中

注:

在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将早于子线程结束。这时,如果主线程想等子线程执行完成才结束,比如子线程处理一个数据,主线程想要获得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

  join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

  join方法中如果传入参数,则表示这样的意思:如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。(其实join()中调用的是join(0))

  join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。

两种用法

对象锁

包括方法锁(默认锁对象为this,即当前实例对象)和同步代码块锁(自己指定锁对象)

方法锁、同步代码块锁

类锁

包括synchronized修饰静态的方法和指定锁对象为Class对象

静态锁、Class对象

对象锁

形式1

代码块形式:手动指定锁对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 描述:对象锁示例1,代码块形式
*/
public class SynchronizedObjectCodeBlock2 implements Runnable {
static SynchronizedObjectCodeBlock2 instance=new SynchronizedObjectCodeBlock2();
@Override
public void run() {
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 t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
我是对象锁的代码块形式。我叫Thread-0
Thread-0运行结束。
我是对象锁的代码块形式。我叫Thread-1
Thread-1运行结束。
finished

0开始执行,0运行结束,只有在0运行结束的时候,1才开始运行并运行结束

如果没有synchronized的保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 描述:对象锁示例1,代码块形式
*/
public class SynchronizedObjectCodeBlock2 implements Runnable {
static SynchronizedObjectCodeBlock2 instance=new SynchronizedObjectCodeBlock2();
@Override
public void run() {
//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 t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
我是对象锁的代码块形式。我叫Thread-0
我是对象锁的代码块形式。我叫Thread-1
Thread-0运行结束。
Thread-1运行结束。
finished

则0和1这两个线程一定是同时执行的,他们是并行执行的,锁不同对结果有不同的表现

形式2

方法锁形式:synchronized修饰普通方法,锁对象默认为this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 描述:对象锁实例2,方法锁形式
*/
public class SynchronizedObjectMethod3 implements Runnable{
static SynchronizedObjectMethod3 instance=new SynchronizedObjectMethod3();

@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 t1 = new Thread(instance,"Thread1");
Thread t2 = new Thread(instance,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
对象锁的方法修饰符形式,我叫Thread1
Thread1运行结束
对象锁的方法修饰符形式,我叫Thread2
Thread2运行结束
finished

image-20200212011104820

All指的是把整个JVM停止下来,包括其他的线程都会停止下来,而Thread表示只把当前线程停止下来

Thread1的状态是Runnable

image-20200212011710032

Thread2的状态是Blocked,表现为MONITOR

image-20200212012103548

类锁

概念(重要)

Java类可能有很多个对象,但只有一个Class对象

本质:所谓类锁,不过是Class对象的锁而已

用法和效果:类锁只能在同一时刻被一个对象所拥有

形式1

synchronized加在static方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 描述:类锁的第一种形式,static形式
*/
public class SynchronizedClassStatic4 implements Runnable{

static SynchronizedClassStatic4 instance1=new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2=new SynchronizedClassStatic4();
@Override
public void run() {
method();
}


public static 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 t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
类锁的第一种形式,我叫Thread1
Thread1运行结束
类锁的第一种形式,我叫Thread2
Thread2运行结束
finished

如果不在方法上声明static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 描述:类锁的第一种形式,static形式
*/
public class SynchronizedClassStatic4 implements Runnable{

static SynchronizedClassStatic4 instance1=new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2=new SynchronizedClassStatic4();
@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 t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
类锁的第一种形式,我叫Thread1
类锁的第一种形式,我叫Thread2
Thread1运行结束
Thread2运行结束
finished

当遇到需要在全局的情况下同步方法,而不仅仅是对象层面,就应该使用static方法来进行全局保护

形式2

synchronized(*.Class)代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SynchronizedClassClass5 implements Runnable {
static SynchronizedClassClass5 instance1=new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2=new SynchronizedClassClass5();
@Override
public void run() {
method();
}

public void method(){
synchronized (SynchronizedClassClass5.class) {
System.out.println("类锁的第二种形式synchronized (SynchronizedClassClass5.class),我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronized (SynchronizedClassClass5.class)"+Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
类锁的第二种形式synchronized (SynchronizedClassClass5.class),我叫Thread1
synchronized (SynchronizedClassClass5.class)Thread1运行结束
类锁的第二种形式synchronized (SynchronizedClassClass5.class),我叫Thread2
synchronized (SynchronizedClassClass5.class)Thread2运行结束
finished

若将类锁改为对象锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SynchronizedClassClass5 implements Runnable {
static SynchronizedClassClass5 instance1=new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2=new SynchronizedClassClass5();
@Override
public void run() {
method();
}

public void method(){
synchronized (this) {
System.out.println("类锁的第二种形式synchronized (SynchronizedClassClass5.class),我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("synchronized (SynchronizedClassClass5.class)"+Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
类锁的第二种形式synchronized (SynchronizedClassClass5.class),我叫Thread1
类锁的第二种形式synchronized (SynchronizedClassClass5.class),我叫Thread2
synchronized (SynchronizedClassClass5.class)Thread1运行结束
synchronized (SynchronizedClassClass5.class)Thread2运行结束
finished

可以看出类锁与对象锁在多个对象时的不同

多线程访问同步方法的7种情况

面试常考

  1. 两个线程同时访问一个对象的同步方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SynchronizedObjectMethod3 implements Runnable{
static SynchronizedObjectMethod3 instance=new SynchronizedObjectMethod3();

@Override
public void run() {
method();
}

//默认以this对象作为锁
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 t1 = new Thread(instance,"Thread1");
Thread t2 = new Thread(instance,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
对象锁的方法修饰符形式,我叫Thread1
Thread1运行结束
对象锁的方法修饰符形式,我叫Thread2
Thread2运行结束
finished
  1. 两个线程访问的是两个对象的同步方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SynchronizedClassStatic4 implements Runnable{

static SynchronizedClassStatic4 instance1=new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2=new SynchronizedClassStatic4();
@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 t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
类锁的第一种形式,我叫Thread1
类锁的第一种形式,我叫Thread2
Thread1运行结束
Thread2运行结束
finished
  1. 两个线程访问的是synchronized的静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SynchronizedClassStatic4 implements Runnable{

static SynchronizedClassStatic4 instance1=new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2=new SynchronizedClassStatic4();
@Override
public void run() {
method();
}


public static 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 t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
类锁的第一种形式,我叫Thread1
Thread1运行结束
类锁的第一种形式,我叫Thread2
Thread2运行结束
finished
  1. 同时访问同步方法与非同步方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SynchronizedYesAndNo6 implements Runnable{
static SynchronizedYesAndNo6 instance1=new SynchronizedYesAndNo6();
static SynchronizedYesAndNo6 instance2=new SynchronizedYesAndNo6();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread1")) {
method1();
}else {
method2();
}
}

public void method1(){
System.out.println("不加锁的方法,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("不加锁的方法"+Thread.currentThread().getName() + "运行结束");
}

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 t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance2,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
不加锁的方法,我叫Thread1
加锁的方法,我叫Thread2
不加锁的方法Thread1运行结束
加锁的方法Thread2运行结束
finished

synchronized关键字只作用于指定的方法中,对于没有加synchronized关键字的方法没有任何影响

  1. 访问同一个对象的不同的普通同步方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SynchronizedDefferentMethod7 implements Runnable{
static SynchronizedDefferentMethod7 instance1=new SynchronizedDefferentMethod7();

@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread1")) {
method1();
}else {
method2();
}
}
public synchronized void method1(){
System.out.println("加锁的方法,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("加锁的方法"+Thread.currentThread().getName() + "运行结束");
}

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 t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance1,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
加锁的方法,我叫Thread1
加锁的方法Thread1运行结束
加锁的方法,我叫Thread2
加锁的方法Thread2运行结束
finished

两个方法得到的this锁是一样的,所以没有办法同时执行,因此会出现串行的情况

  1. 同时访问静态synchronized和非静态synchronized方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SynchronizedStaticAndNormal8 implements Runnable {
static SynchronizedStaticAndNormal8 instance1=new SynchronizedStaticAndNormal8();

@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread1")) {
method1();
}else {
method2();
}
}
public static synchronized void method1(){
System.out.println("静态加锁的方法1,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("静态加锁的方法1"+Thread.currentThread().getName() + "运行结束");
}

public synchronized void method2(){
System.out.println("非静态加锁的方法2,我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("非静态加锁的方法2"+Thread.currentThread().getName()+"运行结束");
}

public static void main(String[] args) {
Thread t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance1,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}
}

输出结果

1
2
3
4
5
静态加锁的方法1,我叫Thread1
非静态加锁的方法2,我叫Thread2
非静态加锁的方法2Thread2运行结束
静态加锁的方法1Thread1运行结束
finished

一个锁时class类,一个锁时this对象,两个是不同的锁,所以他们之间是没有冲突的,虽然都都被同步关键字synchronized修饰,但依然可以同时运行

  1. 方法抛出异常,会释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 描述:方法抛出异常后,会释放锁。展示不抛出异常前和抛出异常后的对比
* 一旦抛出异常,第二个线程会立即进入同步方法,意味着锁已经释放
*/
public class SynchornizedExeption9 implements Runnable{
static SynchornizedExeption9 instance1=new SynchornizedExeption9();

@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread1")) {
method1();
}else {
method2();
}
}
public synchronized void method1(){
System.out.println("静态加锁的方法1,我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
throw new RuntimeException();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}

public synchronized void method2(){
System.out.println("非静态加锁的方法2,我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("非静态加锁的方法2"+Thread.currentThread().getName()+"运行结束");
}

public static void main(String[] args) {
Thread t1 = new Thread(instance1,"Thread1");
Thread t2 = new Thread(instance1,"Thread2");
t1.start();
t2.start();
while (t1.isAlive()||t2.isAlive()){

}
System.out.println("finished");
}

}

输出结果

1
2
3
4
5
6
7
8
静态加锁的方法1,我叫Thread1
java.lang.RuntimeException
at synchronizedtest.SynchornizedExeption9.method1(SynchornizedExeption9.java:22)
at synchronizedtest.SynchornizedExeption9.run(SynchornizedExeption9.java:13)
at java.lang.Thread.run(Thread.java:748)
非静态加锁的方法2,我叫Thread2
非静态加锁的方法2Thread2运行结束
finished

线程1抛出异常,由JVM帮助释放锁,之后线程2得到锁进行运行

synchronized抛出异常会自动的释放锁

lock抛出异常,但没有显式释放锁,lock是不会释放锁的

总结

先进入一个被synchronized修饰的方法,而在这个方法里面调用没有被synchronized修饰的方法,此时不是线程安全的

synchronized的性质

可重入

什么是可重入

指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁

线程得到一把锁,内层函数想再次获得锁,如果再想获得锁就必须放弃现在的锁与其他线程进行竞争,这就是不可重入锁

可重入锁也叫递归锁

ReentrantLock和synchronized是可重入锁

好处:避免死锁,提升封装性

可重入的粒度(指的就是范围scope):synchronized关键字默认加锁的范围是线程,线程而非调用(Linux中的pthread就是以调用为粒度的,与synchronized不同)

粒度

情况1:证明同一个方法是可重入的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 描述:可重入粒度测试:递归调用本方法
*/
public class SynchronizedRecursion10 {
int a=0;
public static void main(String[] args) {
SynchronizedRecursion10 synchronizedRecursion10=new SynchronizedRecursion10();
synchronizedRecursion10.method1();
}

private synchronized void method1() {
System.out.println("这是method,a="+a);
if (a==0) {
a++;
method1();
}
}
}

输出结果

1
2
这是method,a=0
这是method,a=1

情况2:证明可重入不要求是同一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SynchronizedOtherMethod11 {
public synchronized void method1(){
System.out.println("我是method1");
method2();
}

private synchronized void method2() {
System.out.println("我是method2");

}

public static void main(String[] args) {
SynchronizedOtherMethod11 s=new SynchronizedOtherMethod11();
s.method1();
}
}

输出结果

1
2
我是method1
我是method2

情况3:证明可重入不要求是同一个类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 描述:可重入粒度测试,调用父类的方法
*/
public class SynchronizedSuperClass12 {
public synchronized void doSomething(){
System.out.println("我是父类的方法");
}
}
class TestClass extends SynchronizedSuperClass12{
@Override
public synchronized void doSomething(){
super.doSomething();
System.out.println("我是子类方法");
}

public static void main(String[] args) {
TestClass s=new TestClass();
s.doSomething();
}
}

输出结果

1
2
我是父类的方法
我是子类方法

可以证明这个可重入的粒度是线程范围的,在同一个线程中如果已经拿到了一把锁,又想接着使用这把锁去访问其他的方法或其他类的方法,只要需要的锁依然是现在这一把锁,那么可重入的性质就会激发出来,就不需要重新获取锁

而不是调用范围的

不可中断

一旦这个锁已经被别人获得了,如果还想获得,就只能选择等待或者阻塞,直到别的线程释放这个锁。如果别的线程永远不释放锁,那么就只能永远等下去

相比之下,Lock类具有中断的能力

第一点,如果觉得等的时间太长,有权中断现在已经获取到锁的线程的执行

第二点,如果觉得等的时间太长不想再等,也可以退出

可见性

原理

加锁和释放锁的原理

现象

有synchronized修饰的代码块或代码块,要想执行这段代码就必须得到锁,如果此时的锁被其他调用者占用,就必须等它被释放,所有的java对象都含有一个互斥锁,这个锁由JVM自动获取和释放,我们只需要指定对象即可,至于锁的释放和获取不需要操心

获取和释放的时机

内置锁(监视器锁),每一个java对象都可以用做一个实现同步的锁,这个锁被称为内置锁,线程在进入同步代码块之前会自动获得这个锁,并且在退出同步代码块时会自动的释放锁,无论是正常退出还是抛出异常退出。

获得内置锁的唯一途径就是进入这个锁的同步代码块或者同步方法中,这样就理解了synchronized锁获取和释放的时机

等价代码

加锁和释放锁的等价代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SynchronizedToLock13 {
Lock lock=new ReentrantLock();
public synchronized void method1(){
System.out.println("我是Synchronized形式的锁");
}

public void method2(){
lock.lock();
try {
System.out.println("我是lock形式的锁");
}finally {
lock.unlock();
}
}

public static void main(String[] args) {
SynchronizedToLock13 synchronizedToLock13=new SynchronizedToLock13();
synchronizedToLock13.method1();
synchronizedToLock13.method2();
}
}

输出结果

1
2
我是Synchronized形式的锁
我是lock形式的锁

加锁和释放锁的原理:深入JVM看字节码

概况

在JVM规范中,对于synchronized的实现原理,它的加锁和释放锁是基于monitor对象来实现同步方法和同步代码块的,monitor中最主要有两个指令,monitorenter会插入到同步代码块开始的位置,monitorexit会插入到同步代码块结束和退出的时候。JVM会保证每一个monitorenter之后必须有monitorexit和它对应,但是也有可能有多个monitorexit和一个monitorenter对应,因为退出的时机不仅仅是方法结束,也可能是方法异常,每一个对象都有一个monitor和它关联,并且一旦一个monitor被持有之后这个对象就会处于锁定状态

当线程执行到monitorenter这个指令的时候,将会尝试获取这个对象所对应的monitor的所有权,也就是说尝试获取这个对象的对象锁

反编译

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 反编译字节码
*/
public class Decompilation14 {
private Object object=new Object();

public void insert(){
synchronized (object){

}
}
}

进行反编译

javap -verbose Decompilation14.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
Classfile /F:/Development/DateStructrue/src/synchronizedtest/Decompilation14.class
Last modified 2020-2-1; size 447 bytes
MD5 checksum 59078cbd93695aea1aa1136c45e7bf39
Compiled from "Decompilation14.java"
public class synchronizedtest.Decompilation14
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/lang/Object
#3 = Fieldref #4.#20 // synchronizedtest/Decompilation14.object:Ljava/lang/Object;
#4 = Class #21 // synchronizedtest/Decompilation14
#5 = Utf8 object
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 insert
#12 = Utf8 StackMapTable
#13 = Class #21 // synchronizedtest/Decompilation14
#14 = Class #19 // java/lang/Object
#15 = Class #22 // java/lang/Throwable
#16 = Utf8 SourceFile
#17 = Utf8 Decompilation14.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Utf8 java/lang/Object
#20 = NameAndType #5:#6 // object:Ljava/lang/Object;
#21 = Utf8 synchronizedtest/Decompilation14
#22 = Utf8 java/lang/Throwable
{
public synchronizedtest.Decompilation14();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
LineNumberTable:
line 3: 0
line 4: 4

public void insert();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
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 synchronizedtest/Decompilation14, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "Decompilation14.java"

提取其中的一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void insert();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return

monitorenter和monitorexit指令

monitorenter和monitorexit指令会在执行的时候让对象的锁计数加一或者减一,实际上每一个对象都与一个monitor相关联,一个monitor的lock锁只能同一时间被一个对象所获得

一个线程在尝试获得与这个对象的monitor的所有权的时候,会出现如下3中情况

  1. 如果monitor计数器为0,表示目前还没有获得锁,此时线程就会立即获得,并把计数器加一,这就意味着当前线程为这个monitor的所有者
  2. 如果对象获得锁的所有权,但又重入了,这样就会导致计数器累加
  3. 如果monitor已经被其他线程锁持有了,其他线程就会获取不到进入阻塞状态,直到monitor计数器变为0才能获取锁

monitorexit

释放对于monitor的所有权,前提是已经拥有了锁,释放就是将monitor计数器减一,变为0则表示不在拥有锁的所有权,通常表示解锁

可重入原理

可重入原理:加锁次数计数器

首先每一个对象都自动的含有一把锁,JVM负责跟踪对象被加锁的次数

线程第一次给对象加锁的时候,计数变为1,。每当这个相同的线程在此对象上再次获得锁时(可重入的时候),计数或递增

每当任务离开时,计数递减,当计数为0的时候,锁被完全释放

可见性原理

Java内存模型

image-20200201001545881

拥有副本的作用是加速程序的运行,线程使用内存的速度要比主内存中的速度要快,但会带来风险

*线程A向线程B发送数据 *

image-20200201002243938

线程A将自己修改的内容储存到主内存中,线程B再从主内存中读取数据,这个过程是通过JMM来控制的

synchronized保证了数据的可见性

synchronized的缺陷

效率低

锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程

不够灵活

读写锁更加灵活,读的时候不会加锁,写的时候才会加锁

加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的

无法预判是否成功获取到锁

常见面试问题

1使用注意点

锁对象不能为空,作用域不宜过大,避免死锁

锁的信息是保存在对象头中的,如果对象没有就没有对象头,那么这些锁是不能工作的

作用域过大的话会影响程序执行的速度

2如何选择Lock或synchronized关键字

如果可以的话既不要使用Lock也不要使用synchronized关键字,而是使用java.util.concurrent工具包中的各种各样的类,如果代码中可以使用synchronized关键字,则优先使用synchronized关键字,因为这样可以减少用户所写的代码,减少出错,如果需要使用lock等这些独有的特性的时候,才使用lock或condition

3多线程访问同步方法的各种具体情况

一共有7种情况

思考题

1.多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?

涉及内部锁的调度机制,持有锁的线程在运行结束或者抛出异之后就会释放锁,则正在等待锁的线程和刚进入synchronized但处于Runnable代码块的线程,在申请锁的时候恰好锁正在释放,还没有来得及进入Block状态,这两个状态来竞争锁,而JVM选取锁是随机的

2.synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?

①优化使用范围,要尽可能的小②使用其他的锁,比如读写锁

3.想要更灵活地控制锁的获取和释放,该怎么办?

可以自己设计锁

4.什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?

总结

一句话介绍synchronized

JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质

文章目录
  1. 1. 高并发详解之同步synchronized关键字
    1. 1.1. synchronized简介
      1. 1.1.1. 作用
      2. 1.1.2. 地位
      3. 1.1.3. 不使用并发的后果
    2. 1.2. 两种用法
      1. 1.2.1. 对象锁
        1. 1.2.1.1. 形式1
        2. 1.2.1.2. 形式2
      2. 1.2.2. 类锁
        1. 1.2.2.1. 概念(重要)
        2. 1.2.2.2. 形式1
        3. 1.2.2.3. 形式2
    3. 1.3. 多线程访问同步方法的7种情况
      1. 1.3.1. 总结
    4. 1.4. synchronized的性质
      1. 1.4.1. 可重入
        1. 1.4.1.1. 粒度
      2. 1.4.2. 不可中断
      3. 1.4.3. 可见性
    5. 1.5. 原理
      1. 1.5.1. 加锁和释放锁的原理
        1. 1.5.1.1. 现象
        2. 1.5.1.2. 获取和释放的时机
        3. 1.5.1.3. 等价代码
      2. 1.5.2. 加锁和释放锁的原理:深入JVM看字节码
        1. 1.5.2.1. 概况
        2. 1.5.2.2. 反编译
        3. 1.5.2.3. monitorenter和monitorexit指令
      3. 1.5.3. 可重入原理
      4. 1.5.4. 可见性原理
    6. 1.6. synchronized的缺陷
    7. 1.7. 常见面试问题
      1. 1.7.1. 1使用注意点
      2. 1.7.2. 2如何选择Lock或synchronized关键字
      3. 1.7.3. 3多线程访问同步方法的各种具体情况
    8. 1.8. 思考题
    9. 1.9. 总结
|