目录
1.什么是线程同步?
2.线程同步简介?
2.1 线程同步的来源
2.1 线程同步
3.有关线程同步机制??
3.1 临界区
?3.2 互斥对象
?3.3 信息量
3.4 事件对象?
Java中提供线程同步的机制?
4. 第一种同步机制 synchronized 关键字
?4.1 锁
?4.1.1 JVM 将锁分为两种:
?4.2 锁的作用
4.2.1 原子性的保障:
4.2.2 可见性的保障:
4.2.3 有序性的保障:
4.3 锁相关的概念
4.3.1 可重入性(Reentrancy)
4.3.2 锁的争用与调度
4.3.3 锁的粒度
4.4 内部锁synchronized
4.4.1 使用 synchronized 关键字修饰代码块
?4.4.1 使用 synchronized 关键字修饰方法
?4.4.2 同步代码块如何选择
4.5 死锁
?4.6 线程同步过程中如果出现异常会自动释放锁
5. 第二种同步机制 :volatile 关键字
5.1 轻量级同步机制:volatile关键字
5.2 volatile 非原子特性
6. synchronized 和 volatile 关键字比较
?
1.什么是线程同步
线程同步:
- 即当有一个线程在对内存进行操作时,
- 其他线程都不可以对这个内存地址进行操作,
- 直到该线程完成操作,
- 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
2.线程同步简介
2.1 线程同步的来源
- 在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。
- 但是多个线程同时运行的时候可能调用线程函数,
- 在多个线程同时对同一个内存地址进行写入,
- 由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。
2.1 线程同步
- 同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
- “同”字从字面上容易理解为一起动作,其实不是,
- “同”字应是指协同、协助、互相配合。
- 如进程、线程同步,
- 可理解为进程或线程A和B一块配合,
- A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作
- 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。
- 按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
- 例如Window API函数SendMessage。
- 该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。
- 在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
3.有关线程同步机制?
3.1 临界区
- 临界区(Critical Section)、互斥对象(Mutex):主要用于互斥控制;
- 都具有拥有权的控制方法,只有拥有该对象的线程才能执行任务,所以拥有,执行完任务后一定要释放该对象。
- 信号量(Semaphore)、事件对象(Event):事件对象是以通知的方式进行控制,主要用于同步控制!
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
- 在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
- 它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。
总结下关键段:
- 1)关键段共有初始化、销毁、进入和离开关键区域四个函数。
- 2)关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。
- 3)推荐关键段与旋转锁配合使用。
?3.2 互斥对象
2、互斥对象:互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。
因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。
当前拥有互斥对象的线程处理完任务后必须将互斥对象交出,以便其他线程访问该资源。
总结下互斥量(Mutex):
- 1)互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
- 2)互斥量能够用于多个进程之间线程互斥问题,并且能解决某进程意外终止所造成的“遗弃”问题。?
?3.3 信息量
3、信号量:信号量也是内核对象。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。
一般是将当前可用资源计数设置为最 大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号。
但是当前可用计数减小 到0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。
线程在处理完共享资源后,应在离 开的同时通过ReleaseSemaphore ()函数将当前可用资源计数加1 。
在任何时候当前可用资源计数决不可能大于最大资源计数。
3.4 事件对象?
4、事件对象: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
总结下事件Event
1)事件是内核对象,事件分为手动置位事件和自动置位事件。
? ? ?事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位? ? ? ?事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2)事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。
? ? ?还可以由PulseEvent()来发出一个事件脉冲。
3)事件可以解决线程间同步问题,因此也能解决互斥问题。
分====================================割================================线
Java中提供线程同步的机制
- 锁
- volatile 关键字
- final 关键字
- static 关键字
- 以及相关的API、如Object.wait() / Object.notify()
4. 第一种同步机制 synchronized 关键字
?4.1 锁
- 锁(Lock)可以理解为对一个共享数据保护的许可证,
- 对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须持有这个许可证。
- 锁的实现是利用互斥对象机制的思想完成的。即一次共享数据只能被一个线程访问。
- 一个线程只有在持有许可证的情况下才能访问这些共享数据。
- 并且许可证一次只能被一个线程持有。
- 持有许可证的线程在结束对共享数据的访问之后必须释放其持有的许可证。
- 一个线程在访问共享数据前必须先获得锁。
- 获得锁的线程称为锁的持有线程。
- 一个锁一次只能被一个线程持有。
- 锁的持有线程在获得锁之后和释放锁之前这个时间段所执行的代码称为临界区(Critical Section)。
- 锁具有排它性(Exclusive),即一个锁一次只能被一个线程持有,这种锁称为排它锁或者互斥锁(Mutex);
?4.1.1 JVM 将锁分为两种:
- 内部锁:通过synchronized关键字实现
- 显示锁:通过 java.concurrent.locks.Lock 接口的实现类实现。
?
?4.2 锁的作用
锁可以实现对共享数据的安全访问,保障线程的原子性,可见性与有序性
4.2.1 原子性的保障:
- 锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保障了临界区的代码一次只能被一个线程执行。
- 使得临界区的代码所执行的操作自然而然的具有不可分割的特性,既具备原子性。
4.2.2 可见性的保障:
- 可见性的保障是通过写线程 冲刷处理器缓存 和读线程 刷新处理器缓存 这两个动作实现的
- 在Java 平台中,锁的获得隐含着 刷新处理器缓存的动作。
- 锁的释放,隐含着 冲刷处理器缓存的动作。
4.2.3 有序性的保障:
- ?写线程(锁释放)在临界区所执行的,在读线程(获得锁)所执行的临界区看来像是完全按照源码顺序执行的。
- 注意:
- 使得锁保障线程的安全性,必须满足以下条件:
- 这些线程在访问共享数据的时候,必须使用同一个锁。
- 即使是读共享数据的线程也需要使用同步锁。
🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂🥂👀👀👀👀👁?👁?👁?👁?👁?👁?👁?
4.3 锁相关的概念
4.3.1 可重入性(Reentrancy)
- 描述的是这样一个问题:一个线程在持有该锁的时候能再次(多次)申请该锁
void methodA(){
申请 a 锁
methodB();
释放 a 锁
}
void methodB(){
申请 a 锁
....
释放 a 锁
}
- 如果一个线程在持有该锁的时候,还能继续申请该锁,称该锁可重入,否则称该锁不可重入。
4.3.2 锁的争用与调度
- Java 平台内部锁属于非公平锁。
- 显示锁(Lock)即支持公平锁又支持非公平锁。
4.3.3 锁的粒度
- 一个锁可以保护的共享数据的数据量的大小称为锁的粒度。
- 锁保护的共享数据越大,称为该锁的粒度越粗。否则反之。
- 锁的粒度过粗会导致线程在申请锁时会进行不必要的等待,锁的粒度过细会增加锁调用的开销。
4.4 内部锁synchronized
- Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock)
- 这种锁也称为监视器(Monitor)
- 这种内部锁是一种排它锁,可以保障原子性,可见性和有序性。
- 内部锁是通过 synchronized 关键字实现的。
- synchronized可以修饰方法代码块。
4.4.1 使用 synchronized 关键字修饰代码块
package stu.my.cdn.intrinsiclock;
/**
* 同步代码块
*/
public class Synchronized {
public static void main(String[] args) {
// 创建两个线程,分别调用 print()
// 先创建一个对象,通过 对象名.方法名()调用方法
Synchronized aSynchronized = new Synchronized();
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print(); //使用的锁对象this就是aSynchronized
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print(); //使用的锁对象this就是aSynchronized
}
}).start();
}
// 定义方法,打印100行字符串
public void print(){
/** 1.获得锁 ,使用锁的前提(对于同一个对象前提)
* 2.共享共享数据,
* 3.释放锁
* 4. 下一个排队线程获得锁,指定临界代码块
* 临界区(获得锁和释放锁中间阶段)
* */
// 使用同步锁
synchronized (this){ // this代表当前使用锁的对象
for(int i = 1; i <= 1000; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
}
package stu.my.cdn.intrinsiclock;
/**
* 不是同一个对象,则不能线程排队执行
*/
public class Synchronized2 {
public static void main(String[] args) {
// 创建两个线程,分别调用 print()
// 先创建一个对象,通过 对象名.方法名()调用方法
Synchronized2 aSynchronized = new Synchronized2();
Synchronized2 synchronized2 = new Synchronized2();
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print(); //使用的锁对象this就是 aSynchronized
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
synchronized2.print(); //使用的锁对象this就是synchronized2 对象
}
}).start();
}
// 定义方法,打印100行字符串
public void print(){
// 使用同步锁
synchronized (this){ // this代表当前使用锁的对象
for(int i = 1; i <= 1000; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(100); // CTRL + SHIFT + T
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
package stu.my.cdn.intrinsiclock;
/**
* 使用常量对象作为锁对象,
* 使用的是同一个常量对象,则能进行线程排队
*/
public class Synchronized3 {
public static void main(String[] args) {
// 创建两个线程,分别调用 print()
// 先创建一个对象,通过 对象名.方法名()调用方法
Synchronized3 aSynchronized = new Synchronized3();
Synchronized3 synchronized2 = new Synchronized3();
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print();
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
synchronized2.print();
}
}).start();
}
public static final Object obj = new Object(); // 定义一个常量
// 定义方法,打印100行字符串
public void print(){
// 使用同步锁
synchronized (obj){ // obj代表常量对象
for(int i = 1; i <= 1000; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
try {
Thread.sleep(100); // CTRL + SHIFT + T
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
package stu.my.cdn.intrinsiclock;
/**
* 不管是普通方法还是静态方法,是要是使用了同一个对象,就可以线程排队
*/
public class Synchronized4 {
public static void main(String[] args) {
// 创建两个线程,分别调用 print()
// 先创建一个对象,通过 对象名.方法名()调用方法
Synchronized4 aSynchronized = new Synchronized4();
Synchronized4 synchronized2 = new Synchronized4();
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print(); // 使用的是OBJ常量
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
synchronized2.print(); //使用的是OBJ常量
}
}).start();
// 线程3 调用静态方法
new Thread(new Runnable() {
@Override
public void run() {
staticPrint(); //使用的是OBJ对象
}
}).start();
}
//定义常量
public static final Object OBJ = new Object();
// 普通方法
public void print() {
// 使用同步锁
synchronized (OBJ){ // obj代表常量对象
for(int i = 1; i <= 100; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
// 静态方法,打印100行字符串
public static void staticPrint(){
// 使用同步锁
synchronized (OBJ){ // obj代表常量对象
for(int i = 1; i <= 100; i++){
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
}
?4.4.1 使用 synchronized 关键字修饰方法
package stu.my.cdn.intrinsiclock;
/**
* 使用 synchronized 修饰普通方法,默认使用 this 作为锁对象
*/
public class Synchronized5 {
public static void main(String[] args) {
// 创建两个线程,分别调用 print()
// 先创建一个对象,通过 对象名.方法名()调用方法
Synchronized5 aSynchronized = new Synchronized5();
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print();
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print2();
}
}).start();
}
// 定义方法,打印100行字符串
public void print() {
// 使用同步锁,这里使用的 this 作为锁对象
synchronized (this){
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
/**
* 使用synchronized修饰实例方法,同步实例方法
* 默认this作为锁对象
*/
public synchronized void print2() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
package stu.my.cdn.intrinsiclock;
/**
* synchronized 同步静态方法
* 把整个方法作为同步代码块
* 默认的锁对象是当前类的运行实例对象
* ----> 有人称为类锁
*/
public class Synchronized6 {
public static void main(String[] args) {
// 创建两个线程,分别调用 print()
// 先创建一个对象,通过 对象名.方法名()调用方法
Synchronized6 aSynchronized = new Synchronized6();
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print(); //使用的锁对象是Synchronized06.class
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
Synchronized6.print2(); //使用的锁对象是Synchronized06.class
}
}).start();
}
// 定义方法,打印100行字符串
public void print() {
// 使用当前类的运行时类对象作为锁对象,可以简单的理解为把 Synchronized06 类的字节码文件作为锁对象
synchronized (Synchronized6.class){
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
/**
* 使用synchronized修饰静态方法,同步静态代码块
* 默认 运行时类(Synchronized06.class)作为锁的对象
*/
public synchronized static void print2() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
?4.4.2 同步代码块如何选择
package stu.my.cdn.intrinsiclock;
/**
* 同步方法和同步代码块如何选择:
* 同步代码块:执行效率高
* 同步方法粒度粗:执行效率低
*/
public class SynchMedAndSynchCode {
public static void main(String[] args) {
SynchMedAndSynchCode syncMedAndSynchCode = new SynchMedAndSynchCode();
new Thread(new Runnable() {
@Override
public void run() {
syncMedAndSynchCode.doLongTimeTask();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
syncMedAndSynchCode.doLongTimeTask();
}
}).start();
}
/**
* 同步代码块,并发效率高
* 两个同时准备
*/
public void doLongTimeTask() {
try {
System.out.println("Task Begin");
Thread.sleep(3000); // 模拟任务需要准备三秒
synchronized (this) {
System.out.println("开始同步");
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
System.out.println("Task End");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 同步代码块 并发 效率低
* 一个一个准备
*/
public synchronized void doLongTimeTask2() {
try {
System.out.println("Task Begin");
Thread.sleep(3000); // 模拟任务需要准备三秒
System.out.println("开始同步");
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
System.out.println("Task End");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.5 死锁
? ?死锁: ?? 在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死? ? ?锁。 ?????????如果避免死锁: ??? ?????????当需要使用多个锁的时候,所有线程获得锁的顺序保持一致即可。
package stu.my.cdn.intrinsiclock;
public class DeathLock {
public static void main(String[] args) {
SubThread t1 = new SubThread();
t1.setName("a");
t1.start();
SubThread t2 = new SubThread();
t2.setName("b");
t2.start();
}
static class SubThread extends Thread{
private static Object lock1 = new Object();
private static Object lock2 = new Object();
@Override
public void run() {
if("a".equals(Thread.currentThread().getName())){
synchronized (lock1){
System.out.println("a线程获得了lock1锁,还需要获得lock2锁");
synchronized (lock2){
System.out.println("a线程获得了lock1后又获得了lock2,可以做想做的事了");
}
}
}
if("b".equals(Thread.currentThread().getName())){
synchronized (lock2){
System.out.println("b线程获得了lock2锁,还需要获得lock1锁");
synchronized (lock1){
System.out.println("b线程获得了lock2之后又获得了lock1,可以做做做的事情了");
}
}
}
}
}
}
?4.6 线程同步过程中如果出现异常会自动释放锁
package stu.my.cdn.intrinsiclock;
public class Synchronized9 {
public static void main(String[] args) {
Synchronized9 aSynchronized = new Synchronized9();
new Thread(new Runnable() {
@Override
public void run() {
aSynchronized.print(); //使用的锁对象是Synchronized06.class
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Synchronized9.print2(); //使用的锁对象是Synchronized06.class
}
}).start();
}
public void print() {
synchronized (Synchronized9.class){
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
if(i==50){
//把字符串转换为int类型时,如果字符串不符合数字格式就会产生异常
Integer.parseInt("abc");
}
}
}
}
public synchronized static void print2() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
??线程0执行异常,锁对象给了线程1
💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻
5. 第二种同步机制 :volatile 关键字
5.1 轻量级同步机制:volatile关键字
- volatile 的作用是使变量在多个线程之间可见
- volatile 关键字强制线程读取变量每次从公共内存中读取,而不是工作内存。
package stu.my.cdn.volatilekw;
public class Test02 {
public static void main(String[] args) {
//创建PrintString对象
PrintString printString = new PrintString();
//开启子线程,让子线程执行printString对象的printStringMethod()方法
new Thread(new Runnable() {
@Override
public void run() {
printString.printStringMethod();
}
}).start();
//main线程睡眠1000毫秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("在main线程中修改打印标志");
printString.setContinuePrint(false);
//程序运行,查看在main线程中修改了打印标志之后,子线程打印是否可以结束打印
//程序运行后,可能还会出现死循环的情况
//分析原因:main线程修改了printString对象的打印标志,子线程读不到
//解决办法:使用volatile关键字修饰printString对象的打印标志。
// volatile的左右可以强制线程会从公共内存中读取变量的值,而不是从工作内存中拿。
}
//定义类打印字符串
static class PrintString{
private volatile boolean continuePrint = true;
public PrintString setContinuePrint(boolean continuePrint){
this.continuePrint = continuePrint;
return this;
}
public void printStringMethod(){
System.out.println(Thread.currentThread().getName() +"开始...");
while(continuePrint){
}
System.out.println(Thread.currentThread().getName() + "结束.....");
}
}
}
5.2 volatile 非原子特性
volatile增加了多个实例变量在线程之间的可见性,但是不能保证原子性
package stu.my.cdn.volatilekw;
/**
* volatile不具备原子性
*/
public class Test03 {
public static void main(String[] args) {
//在main线程中创建10个子线程
for (int i = 0;i < 10; i++) {
new MyThread().start();
}
}
static class MyThread extends Thread{
//volatile关键字仅仅是表示所有线程从主内存中读取count变量的值
volatile public static int count;
//这段代码不是安全的,想要线程安全,需要使用synchronized进行同步,如果使用synchronized,也就不需要volatile
/*public static void addCount(){
for(int i = 0; i < 1000; i++){
//count++不是原子操作
count++;
}
System.out.println(Thread.currentThread().getName() + " count=" + count);
}*/
//想要保证原子操作的使用synchronized关键字
public synchronized static void addCount(){
for(int i = 0; i < 1000; i++){
//count++不是原子操作
count++;
}
System.out.println(Thread.currentThread().getName() + " count=" + count);
}
@Override
public void run() {
addCount();
}
}
}
分------------------------------------------------------------割---------------------------------------------------------线
💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻💻
6. synchronized 和 volatile 关键字比较
1.性能方面:
- volatile 关键字是线程同步的轻量级实现,所以效率高于 synchronized
- 随着 JDK 的版本更新,synchronized 效率也是不断提升的。
2.修饰的范围:
- volatile 只修饰变量
- synchronized 可以修饰代码块,方法。
3.数据的安全方面:
- 多线程访问 volatile 变量不会发生阻塞,而synchronized 可能会阻塞。
- volatile 能保证数据的可见性,但保证不了数据的原子性。
- synchronized 既可以保证数据的原子性,也可以保证数据的可见性。
4.应用方案:
- 关键字 volatile 解决的是变量在多个线程之间的可见性问题。
- synchronized 解决的是多个线程访问共享数据的同步性。
|