到了并发模块,就不得不介绍一下Synchronized和ReentrantLock等等方面,锁的知识。
针对于互斥锁,我们先来介绍一下引发线程安全问题的主要诱因是什么?
- 存在共享数据(也称临界资源)。
- 存在多条线程共同操作共享数据。
而解决上面提到的问题的解决方法就是:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。
为了能让我们的解决方法得以实现,就引入了互斥锁。
互斥锁有以下几个特性
- 互斥性
也就是让同一时间只让一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性,也被称作为原子性。
- 可见性
我们必须保证在锁释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁的时候获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引发不一致。
但是我们必须明确一点,Synchronized锁住的不是普通的代码,而是对象。
前面学习jvm的时候,我们知道,堆是线程共享的,也是我们经常会操作的地带,所以,给一个对象去上合适锁,是解决线程问题的关键。
而根据获取锁 的分类,又可以分为两类:
- 获取对象锁
获取对象锁的两种用法:
①同步代码块
synchronized(this),synchronized(实例),而我们所说的“锁”,也就是小括号内的实例对象。
②同步非静态方法
synchronized(method),这里的锁指的是当前对象的实例对象。
下面也是举一个例子来说明:
package thread;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
/**
* 按照不同线程的名字执行不同的方法
*/
if (threadName.startsWith("A")) {
async();
} else if (threadName.startsWith("B")) {
syncObjectBlock1();
} else if (threadName.startsWith("C")) {
syncObjectMethod1();
}
}
/**
* 异步方法:打印当前时间,和1s之后的时间
*/
private void async() {
try {
System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 方法中有 synchronized(this|object) {} 同步代码块
*/
private void syncObjectBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized 修饰非静态方法
*/
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
输出如下:
我们先来看上面的A线程去执行async()方法的结果:
由于async方法本身和内部属性都没有被synchronized包裹,所以可以正常执行。
然后再来看看B线程执行的情况
在B线程执行的方法里,我们发现一个很有意思的事情,如果我们使用this锁,那被this锁包裹着的就能按照顺序去访问执行,而没有使用this锁的,就会异步正常执行。
再来看看C线程执行的方法,它使用的是用synchronized来修饰一个方法:
于是我们发现,被synchronized包裹起来的整个方法,都是只能允许一次性让一个线程去访问的。
- 获取类锁
获取类锁的两种用法:
① 同步代码块(synchronized(类.class)),锁是小括号()中的类对象(class 对象)
② 同步静态方法(synchronized static method) , 锁是当前对象的类对象(class 对象)
也是举一个例子来说明
package thread;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("D")) {
syncClassBlock1();
} else if (threadName.startsWith("E")) {
syncClassMethod1();
}
}
private void syncClassBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized static void syncClassMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出如下:
二者的使用差别,只有一个被static额外修饰,一个则没有。
其实我们可以发现,其执行的流程和之前是一样的,我们先看看D线程就知道了:D最开始的输出语句,也是异步执行,于是,线程1和线程2,就都打印出了没被synchronized包裹着那句话,之后,逐个获得锁,按照顺序打印出对应的话(这里只不过是线程2先获得了锁而已)
然后,再来看E,其实道理也和上面差不多,这次是锁完全覆盖,所以就会整体的获得锁,按照顺序执行
至此,我们来总结一下对象锁和类锁:
- 有线程访问对象的同步代码块的时候,另外的线程可以访问该对象的非同步代码块
- 如果锁住的是同一个对象,一个线程在访问对象的同步代码块的时候,另一个访问对象的同步代码块的线程会被阻塞。
- 如果锁住的是同一个对象,一个线程在访问对象的同步方法的时候,另一个访问对象的同步方法会被阻塞。
- 如果锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然。
- 同一个类的不同对象的锁互不干扰。
- 类锁也是一把特殊的对象锁,所以同一个类的不同对象使用类锁将是同步的。
- 类锁和对象锁互不干扰。