技术文章

8-高并发必备篇(五):线程安全操作之synchronized(1)

提到并发编程大多数第一时刻想到的就是synchronized同步锁了,synchronized也是面试中问的比较多的一个问题。在之前的文章中我们提到过线程安全的三个特性:原子性、可见性和有序性,并且说到了java中定义了一个关键字synchronized对于线程的这三个特性都实现了,这么说来synchronized关键字是可以保证线程安全的,那么如何使用synchronized来实现线程安全?它又是怎么样的一个实现原理呢?
 
这篇文章我们将从下面的几个内容来聊聊synchronized这个关键字。
 线程安全操作之synchronized
synchronized介绍
synchronized 是java中的一个关键字,翻译成汉语是同步的意思,它的作用呢就是被其修饰的方法或者代码块在同一时刻只能有一个线程进行访问和执行,只有当该线程执行完毕之后其他的线程才可以进行竞争访问,并且当前访问的线程可以重复的申请竞争访问资源。可以想象为大家都在一个窗口买东西,同一时刻只能有一个人在买,其他人都在等待,不一样的是外面等待的人并不会排好队而是都在等着正在买东西的人买完之后争夺到买东西的机会,并且现在买的人买完之后还能申请继续争夺买东西的机会。
简单来说:synchronized 可以修饰代码块和方法,它可以保证同一时刻只能有一个线程访问被修饰的代码块或者方法,从而保证了线程的安全。synchronized 犹如一把锁,当一个线程访问之后就锁定访问的共享资源代码段,达到互斥的效果,从而保证了线程的安全,并且同一个线程可以获取同一把锁多次,达到可重入的效果。
synchronized 特性
synchronized 具有原子性、可见性、有序性和可重入性;
原子性
之前文章说到过,JVM中定义的8种原子性操作中的lock 和 unlock就是把非原子性操作变成原子性操作,例如a+=1为非原子性操作。JVM中使用的两个字节码指令monitorenter和monitorexit来实现lock和unlock操作,但是在java代码中没有直接操作JVM指令的,而是把这两个指令都封装到了synchronized关键字中来实现原子性操作。(讲解synchronized原理的时候会看到monitorenter和monitorexit指令封装)
可见性
synchronized保证有序性是因为unlock解锁操作之前必须把工作内存中数据同步回主内存来实现的;而主内存是所有线程都可以访问的共享内存,所以修改之后其它线程操作该数据时都可以看到被修改后的值。
有序性
synchronized实现有序性是因为当一个共享资源变量被lock锁定操作之后,同一时刻只能被一个线程使用,而单线程执行代码是没有指令重排等问题的,所以线程也是有序的。同样被lock锁定的共享资源排斥其他线程访问所以Synchronized也具有互斥性。
可重入性
synchronized的可重入性就是当一个线程调用synchronized代码持有对象锁的时候,如果调用了该对象的其他synchronized代码,那么可以重新持有该锁,即同一个线程可以获取同一把锁多次,所以synchronized具有可重入性。
 
synchronized的使用
synchronized同步锁主要分两种,一种是对象锁,另外一种是类锁;
对象锁
对象锁顾名思义锁的作用对象是实例对象,当synchronized修饰普通的方法或者代码块的时候,都可以指定锁的对象。因一个类可以有很多对象,所以对象锁是可以有多个的。
修饰普通方法:被称为同步方法,其锁作用范围是这个普通方法的所有代码,作用的对象是调用这个普通方法的对象。
修饰代码块:被称为同步代码块,其锁作用范围是这个代码块的所有代码,作用的对象是调用这个代码块的对象。
类锁
每个类都只有一个对应的Class对象(反射对象),类锁其作用的对象就是类的Class对象了,或者当锁的对象为一个静态对象的时候也是类锁。当synchronized修饰静态方法或者代码块的时候都可以使用类锁。
修饰静态方法/代码块:其锁的作用范围为定义的静态方法或代码块的代码。作用的对象就是在调用该静态方法或代码块的所有对象。
下面我们还是以之前卖票的MyRunnable  为例看看对象锁和类锁的使用。
 
对象锁的使用代码如下:
public class MyRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
/**
 * 对象锁-同步代码块
 */
synchronized (this) {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
/**
 * 对象锁-同步普通方法
 */
public synchronized void sale() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
测试代码:
MyRunnable r = new MyRunnable();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
运行结果如图:
 线程安全操作之synchronized
分析:通过运行我们发现,当使用了同步代码块或者同步方法的对象锁方法实现,线程就是同步执行的了。
值得注意的是对象锁中同步普通的方法锁的对象是this即锁的作用对象是当前的MyRunnable对象,而对象锁的同步代码块锁的对象可以是this也可以是Object类型。
synchronized(this)和synchronized(obj)的区别:
synchronized(this)所的作用对象是当前访问的对象,而synchronized(obj)的作用对象是obj,如果多个线程共用一个obj对象那么执行的时候还是同步执行的,如果每个线程obj锁的对象不同那么还是异步执行。如下代码就是异步执行:
/**
* 锁的对象obj每个线程不一样,所以是异步执行
 */
public void run() {
Object obj = new Object();
synchronized (obj) {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
/**
* 锁的对象obj每个线程一样,所以是同步执行
 */
private Object obj = new Object();
@Override
public void run() {
synchronized (obj) {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
 
另外同步代码块的方式因为可以控制锁的代码范围即控制锁的粒度,所以有些场合下使用同步代码块的效率要更高。
类锁的使用代码如下:
public class MyRunnable2 implements Runnable {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {

/**
 * 类锁-同步代码块(锁的class对象)
 */
synchronized (MyRunnable2.class) {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
/**
 * 类锁-同步代码块(锁的静态对象)
 */
// synchronized (obj) {
// while (ticket > 0) {
// System.out.println(Thread.currentThread().getName() + ": " + ticket);
// ticket--;
// }
// }

}
/**
 * 类锁-同步静态方法
 * 多个MyRunnable2对象实例情况下,也是同步执行的
 */
public static synchronized void sale() {
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
}
因为锁的是class对象或者静态对象,所以我们测试时候可以每个线程创建不一样的MyRunable2对象来进行测试,代码如下:
MyRunnable2 r = new MyRunnable2();
MyRunnable2 r2 = new MyRunnable2();
MyRunnable2 r3 = new MyRunnable2();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r2);
Thread t3 = new Thread(r3);
t1.start();
t2.start();
t3.start();
运行结果如下:
 
 线程安全操作之synchronized
分析:通过测试我们发现类锁对于锁定的类class的所有对象都成立。