多线程
3 线程同步
3.1 线程同步机制简介
线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全.
Java 平台提供的线程同步机制包括: 锁, volatile 关键字, final 关键字,static 关键字,以及相关的 API,如 Object.wait()/Object.notify()等。
3.2 锁概述
线程安全问题的产生前提是多个线程并发访问共享数据.
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问.锁就是复用这种思路来保障线程安全的。
锁(Lock)可以理解为对共享数据进行保护的一个许可证. 对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证. 一个线程只有在持有许可证的情况下才能对这些共享数据进行访问; 并且一个许可证一次只能被一个线程持有; 许可证线程在结束对共享数据的访问后必须释放其持有的许可证。
一个线程在访问共享数据前必须先获得锁; 获得锁的线程称为锁的持有线程; 一个锁一次只能被一个线程持有. 锁的持有线程在获得锁之后 和释放锁之前,这段时间所执行的代码称为临界区(CriticalSection).
锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有,这种锁称为排它锁或互斥锁(Mutex).
JVM把锁分为内部锁和显式锁两种:内部锁通过synchronized关键字实现;显式锁通过java.concurrent.locks.Lock接口的实现类实现。
3.2.1 锁的作用
锁可以实现对共享数据的安全访问. 保障线程的原子性,可见性与有序性.
锁通过互斥保障原子性:一个锁只能被一个线程持有, 这就保证临界区的代码一次只能被一个线程执行.使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性.
锁能够保障可见性:通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个 动作实现的. 在 java 平台中,锁的获得隐含着刷新处理
器缓存的动作, 锁的释放隐含着冲刷处理器缓存的动作.
锁能够保障有序性.写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的.
注意:使用锁保障线程的安全性,必须满足以下条件:
线程在访问共享数据时必须使用同一个锁;
即使是读取共享数据的线程,也需要使用同步锁。
3.2.2 锁相关的概念
(1)可重入性
可重入性(Reentrancy): 一个线程持有该锁的时候能再次(多次)申请该锁
void methodA(){
申请 a 锁
methodB();
释放 a 锁
}
v
void methodB(){
申请 a 锁
....
释放 a 锁
}
如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的, 否则就称该锁为不可重入的。
(2)锁的争用与调度
Java平台中内部锁(通过synchronized关键字实现)属于非公平锁, 显式Lock 锁既支持公平锁又支持非公平锁。
(3)锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度.
锁保护共享数据量大,称该锁的粒度粗, 否则就称该锁的粒度细.
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待;粒度过细会增加锁调度的开销。
3.3 内部锁:synchronized 关键字
Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock). 这种锁也称为监视器(Monitor), 这种内部锁是一种排他锁,可以保障原子性,可见性与有序性.
内部锁是通过synchronized关键字实现的,synchronized 关键字修饰代码块或修饰一个方法。
修饰代码块的语法:
synchronized(对象锁) {
同步代码块,可以在同步代码块中访问共享数据
}
修饰实例方法称为同步实例方法,修饰静态方法称为同步静态方法。
3.3.1 synchronized 同步代码块
示例代码:
package com.yupeng.intrinsiclock;
/**
* synchronized 同步代码块
* this锁对象
* @author Yupeng
* @create 2020-12-07 9:59
*/
public class Test01 {
public static void main(String[] args) {
// 创建两个线程,分别调用mm()方法
// 创建Test01的实例对象,通过对象名调用mm()方法
Test01 obj = new Test01();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm();
}
}).start();
}
/**
* 定义方法,打印5行字符串
*/
public void mm() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
}
如果线程的锁对象不同,不能实现同步:
package com.yupeng.intrinsiclock;
/**
* synchronized 同步代码块
* 如果线程的锁对象不同,不能实现同步
* @author Yupeng
* @create 2020-12-07 9:59
*/
public class Test02 {
public static void main(String[] args) {
// 创建两个线程,分别调用mm()方法
// 创建Test01的实例对象,通过对象名调用mm()方法
Test02 obj = new Test02();
Test02 obj2 = new Test02();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm(); // 锁对象是obj
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj2.mm(); // 锁对象是obj2
}
}).start();
}
/**
* 定义方法,打印5行字符串
*/
public void mm() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
}
使用常量对象作为锁对象,可以实现同步:
package com.yupeng.intrinsiclock;
/**
* synchronized 同步代码块
* 使用常量对象作为锁对象
* @author Yupeng
* @create 2020-12-07 9:59
*/
public class Test03 {
public static void main(String[] args) {
// 创建两个线程,分别调用mm()方法
// 创建Test01的实例对象,通过对象名调用mm()方法
Test03 obj = new Test03();
Test03 obj2 = new Test03();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm(); // 锁对象是常量OBJ
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj2.mm(); // 锁对象是常量OBJ
}
}).start();
}
/**
* 定义一个常量
*/
public static final Object OBJ = new Object();
/**
* 定义方法,打印5行字符串
*/
public void mm() {
synchronized (OBJ) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
}
使用常量对象作为锁对象,不同方法中的同步代码块也可以同步:
package com.yupeng.intrinsiclock;
/**
* synchronized 同步代码块
* 使用常量对象作为锁对象,不同方法中的同步代码块也可以同步
* @author Yupeng
* @create 2020-12-07 9:59
*/
public class Test04 {
public static void main(String[] args) {
// 创建两个线程,分别调用mm()方法
// 创建Test01的实例对象,通过对象名调用mm()方法
Test04 obj = new Test04();
Test04 obj2 = new Test04();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm(); // 锁对象是常量OBJ
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj2.mm(); // 锁对象是常量OBJ
}
}).start();
// 第三个线程调用静态方法
new Thread(new Runnable() {
@Override
public void run() {
Test04.sm();
}
}).start();
}
/**
* 定义一个常量
*/
public static final Object OBJ = new Object();
/**
* 定义方法,打印5行字符串
*/
public void mm() {
synchronized (OBJ) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
/**
* 定义另一个方法
*/
public static void sm() {
synchronized (OBJ) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}
}
3.3.2 同步方法
(1)同步实例方法,将整个方法作为同步代码块:
package com.yupeng.intrinsiclock;
/**
* synchronized 同步代码块
* this锁对象
* @author Yupeng
* @create 2020-12-07 9:59
*/
public class Test05 {
public static void main(String[] args) {
// 创建两个线程,分别调用mm()方法
// 创建Test05的实例对象,通过对象名调用mm()方法
Test05 obj = new Test05();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm(); // 锁对象为this
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
obj.mm2(); // 此方法为同步方法,默认锁对象为this
// new Test05().mm2(); // 锁对象是一个新的实例对象,与obj不同,所以不能同步
}
}).start();
}
/**
* 定义方法,打印5行字符串
*/
public void mm() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
/**
* 定义另一个方法,使用synchronized关键字修饰,同步整个方法,默认this为同步对象
*/
public synchronized void mm2() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()