synchronized关键字全析

前言

多线程可以并发执行,多条线程互不干扰的执行各自的任务,这大大提高了我们的编程效率,也方便了我们的生活.可是我们都知道并发执行存在着同步的问题,一不留神就有可能发生多条线程重复处理同一问题的情况.就好比过年想要回家然后网上购票发现只剩下了一张票,于是赶紧购买,可是当购买完毕之后发现自己并没有获得这张票而是同一时间还有一个人他买走了这张票,这是多么悲催啊.在JAVA中,synchronized关键字帮助我们很好的解决了这个问题!

线程同步问题引出

每当逢年过节,就有成千上万的人从四面八方回到自己的故乡,国家运输部门便要提前做好各种准备保证线上线下购票都能够顺利的进行.假设现在还有10张通往北京的票,有三个黄牛党想要买完这十张票(这里只是举例,我们应该严格打压黄牛票贩维护社会秩序,嘿嘿).这三个黄牛党可以当成三个线程,他们要同时获得资源(票).
写下如下代码..

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
class BuyTickte implements Runnable{
private int ticket = 10;

public void run() {
while(ticket>0){
ticket--;
System.out.println(Thread.currentThread().getName()+"购买车票 "+"还剩余车票数: "+ ticket);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class SellTicket {

public static void main(String[] args) {
BuyTickte buyTickte = new BuyTickte();
Thread thread1 = new Thread(buyTickte,"黄牛A");
Thread thread2 = new Thread(buyTickte,"黄牛B");
Thread thread3 = new Thread(buyTickte,"黄牛C");

thread1.start();
thread2.start();
thread3.start();
}
}

运行后结果如下:

红圈内黄牛A,C购票后都显示还剩余了两张票,那也就是说他们两个人都买走了第三张票,这里出现了一票多卖的情况,如果这是在现实生活中可是会耽误很大的事,造成这样的原因就是因为线程的不同步访问对资源进行了抢夺,当还有三张票的时候两个线程同时走到了run()方法,拿走了第三张票但是他们之间不知道对方也正在购买这张票于是造成了一票多卖的情况.怎么解决这种不同步问题呢?
我们要想办法让一个线程在购票的时候其他线程不会进入,等到一个线程购买完毕另外一个在进入买票,即让线程之间单独访问资源.这时我们就要对代码进行同步处理(所谓同步就是让线程能够一个一个进入到方法中执行而不是一起进入).
这时我们便要用到Synchronized关键字!在多线程中他扮演了很重要的一个角色,他就像一把锁,可以给我们的代码上锁,以此来控制访问资源的线程数
先看看如何修改上面代码以使得三个票贩能够分得这10张票
● 方法一: 给代码块上锁
使用同步代码块必须设置一个要锁定的对象,一般可以锁定当前对象: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
33
34
35
36
class BuyTickte implements Runnable {
private int ticket = 10;

//同步方法
public synchronized void sell(){ //给方法单独上锁
if(this.ticket>0){
this.ticket--;
System.out.println(Thread.currentThread().getName()+"购买车票 "+"还剩余车票数: "+ this.ticket);
}
}

public void run() {
while(this.ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.sell();
}
}
}

public class SellTicket {

public static void main(String[] args) {
BuyTickte buyTickte = new BuyTickte();
Thread thread1 = new Thread(buyTickte, "黄牛A");
Thread thread2 = new Thread(buyTickte, "黄牛B");
Thread thread3 = new Thread(buyTickte, "黄牛C");

thread1.start();
thread2.start();
thread3.start();
}
}

运行结果和上面相同
同步虽然可以保证数据的完整性(线程的安全操作),但是其执行的速度会很慢

synchronized锁多对象&全局锁

我们上面的卖票例子中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
public class SynchronizedMoreTest {

public static void main(String[] args) {

Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
thread2.start();
}
}

class Sync {
public synchronized void test(){
System.out.println("执行对象: "+Thread.currentThread().getName()+" test任务开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行对象: "+Thread.currentThread().getName()+" test任务执行完毕");
}
}

class Thread1 extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}


class Thread2 extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}

代码如上我们已经给需要完成的任务加上了锁,运行:

可以看到synchronized并没有起到我们预期的作用,这两个线程还是同时的运行了这个代码块,这是为什么?
实际上,synchronized(this)和非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段,锁的都是方法所在的对象.(synchronized锁住的是对象而不是代码段!)在上面代码段中,他锁住的只是Sync类的对象,当两个线程访问这个方法的时候并不会受这把锁的干扰,他们会不分顺序的一同进行方法的访问,那么应该如何做到两个线程按照顺序一个完成自己的任务后另一个在进行自己的任务呢?有两种方法可以解决这个问题:
方法①.我们只要锁住方法所在类的对象,让多线程访问这个对象的方法的时候只能依次进行.即锁住他们共同的一个对象

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
public class SynchronizedMoreTest {

public static void main(String[] args) {
Sync sync = new Sync();

Thread1 thread1 = new Thread1(sync);
Thread2 thread2 = new Thread2(sync);
thread1.setName("Thread-1");
thread2.setName("Thread-2");
thread1.start();
thread2.start();
}
}

class Sync {
public void test(){
synchronized (this){
System.out.println("执行对象: "+Thread.currentThread().getName()+" test任务开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行对象: "+Thread.currentThread().getName()+" test任务执行完毕");
}
}
}

class Thread1 extends Thread{
private Sync sync;

public Thread1(Sync sync){
this.sync=sync;
}

@Override
public void run() {
this.sync.test();
}
}


class Thread2 extends Thread{
private Sync sync;
public Thread2(Sync sync){
this.sync = sync;
}

@Override
public void run() {
this.sync.test();
}
}

修改后的代码中我们在每个线程创建的时候都传入一个相同的对象(即加锁过的任务对象),这样在每一个线程使用这个对象的test方法的时候其他线程都会因为没有锁权限而在外面等待.达到了我们预期的目的
方法②.使用全局锁将这段代码锁住,同一时间只允许一个对象对他进行访问
这个方法的作用在于使用全局锁将方法所在对象对应的类上锁,使得每一个线程在访问这个类的方法时都依次进行.

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
public class SynchronizedMoreTest {

public static void main(String[] args) {
Sync sync = new Sync();

Thread1 thread1 = new Thread1(sync);
Thread2 thread2 = new Thread2(sync);
thread1.setName("Thread-1");
thread2.setName("Thread-2");
thread1.start();
thread2.start();
}
}

class Sync {
public void test(){
synchronized (Sync.class){
System.out.println("执行对象: "+Thread.currentThread().getName()+" test任务开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行对象: "+Thread.currentThread().getName()+" test任务执行完毕");
}
}
}

class Thread1 extends Thread{
private Sync sync;

public Thread1(Sync sync){
this.sync=sync;
}

@Override
public void run() {
this.sync.test();
}
}


class Thread2 extends Thread{
private Sync sync;
public Thread2(Sync sync){
this.sync = sync;
}

@Override
public void run() {
this.sync.test();
}
}

因此,如果要想锁的是代码段,锁住多个对象的同一方法,就使用这种全局锁,他锁的是类而不是this
static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了代码段

synchronized实现原理浅谈

对象锁机制(monitor)

在Java的锁机制中JVM为每一个对象都设定了属于自己的一把锁(也就是监视器),也就是说我们在编程中创建的每一个对象都有属于他们自己的一把隐式锁,只不过我们看不到罢了.
而每一个对象在内存中(堆)的结构都分三部分:对象头,实例变量,填充数据.在对象头中就保存了该对象对应的锁信息.
当我们使用synchronized关键字修饰了某一个对象,他就会获取自己对应的锁.在对象头中就会保存锁信息用来记录此时这个对象已被上锁,其他想要获得该锁的对象需要等待
接下来说一下一个对象获得锁的底层实现原理
上一段简单的代码

1
2
3
4
5
6
7
8
public class Test{
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object) {
System.out.println("hello world");
}
}
}

使用javap进行反编译查看代码部分对应的字节码

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
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2
3: dup
4: astore_1
5: monitorenter //!! 注意
6: getstatic #3
java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4
11: invokevirtual #5
(Ljava/lang/String;)V
14: aload_1
15: monitorexit // !!注意
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // !!注意
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any

我们摘重点省略不重要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2
3: dup
4: astore_1
5: monitorenter //!! 注意: 进入同步方法
......
15: monitorexit // !!注意: 退出同步方法
......
21: monitorexit // !!注意: 退出同步方法
......

从字节码中可以知道执行同步代码块是由monitorenter和monitotexit指令实现的.monitorenter指向同步代码块的开始位置,monitorexit指向同步代码块的结束位置.使用synchronized进行同步就是对对象自己的监视器进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待.而获取的过程是互斥的,即同一时刻只有一个对象可以获取到monitor.
再看下synchronized修饰的同步方法的字节码又是怎么一回事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public synchronized void foo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 注意!!!!!
Code:
stack=2, locals=1, args_size=1
0: getstatic #5
java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String hello world
5: invokevirtual #7
(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8

当用 synchronized 修饰方法后,访问标记 ACC_SYNCHRONIZED提示JVM进行 monitorenter 操作。而在退出该方法时,不管是正常返回,还是向调用者抛异常,Java 虚拟机均需要进行 monitorexit 操作。monitorenter 和 monitorexit 操作所对应的锁对象是隐式的。
对于monitorenter和monitorexit我们可以这样来理解:
每个锁对象拥有一个计数器和一个指向该锁所使用线程的指针.
当执行monitorenter时 –>
1.如果目标锁的计数器为0,那么表示该锁没有被其他线程所占用,则Java 虚拟机会将该锁对象的持有线程设置为当前线程,计数器+1;
2.如果目标锁的计数器不为0,当前线程又是该锁的持有线程,则计数器+1(即一个线程可以多次获得属于自己的锁),否则只能够等待锁的释放
当执行monitorexit时 –>
每执行一次monitorexit指令,计数器都会 -1,直到为0为止,同步代码块执行完毕,代表该锁已经被释放
之所以这样设计是因为锁是具有可重入性的,即一个对象可以多次对同一个锁进行重复加锁操作

补充:Lock锁了解

Lock锁体系是JDK1.5之后加入进来的,当时在JDK1.5中,synchronized是性能低效的.因为这是一个重量级操作,他对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,类似这样的频繁操作会对操作系统带来很大的压力.而相比之下使用Lock对象,性能能更高点
lock对象使用范例:

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
public class LockTest implements Runnable {

private int ticket = 10;
private Lock lock = new ReentrantLock();

public void run() {
while(this.ticket > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
lock.lock();
if(this.ticket>0){
ticket --;
System.out.println(Thread.currentThread().getName()+"购票 "+" 剩余票数: "+this.ticket);
}
}finally {
lock.unlock();
}
}
}

public static void main(String[] args) {
LockTest lockTest = new LockTest();
for(int i=0; i<3; i++){
Thread thread = new Thread(lockTest);
thread.setName("Thread-"+i);
thread.start();
}
}
}

Lock锁使用的效果和synchronized一样,但是有一点不同,Lock锁的获取和释放都需要我们手动控制,如果忘记释放锁,那么这个锁不会自己解除,当前线程也就会一直占用造成其他线程的阻塞!