Java 多线程梳理(三、线程同步机制)

链接: Java 多线程梳理(三、线程同步机制).


  • 线程同步机制
  • 线程同步机制简介
  • 锁概述
  • 锁相关的概念
  • 内部锁 syncchronized
  • 轻量级同步锁 volatile

3、线程同步机制

3.1 线程同步机制简介

线程同步机制是一套用于协调线程之间的数据访问的机制.该机制可以保障线程安全.

Java 平台提供的线程同步机制包括: , volatile 关键字, final 关键字,static 关键字, 以及相关的 API, 如 Object.wait()/Object.notify()

3.2 锁概述

线程安全问题的产生前提是多个线程并发访问共享数据. 将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问.锁就是复用这种思路来保障线程安全的锁(Lock)可以理解为对共享数据进行保护的一个许可证.

对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证. 一个线程只有在持有许可证的情况下才能对这些共享数据进行访问; 并且一个许可证一次只能被一个线程持有; 许可证线程在结束对共享数据的访问后必须释放其持有的许可证.

一线程在访问共享数据前必须先获得锁; 获得锁的线程称为锁的持有线程; 一个锁一次只能被一个线程持有. 锁的持有线程在获得
锁之后 和释放锁之前这段时间所执行的代码称为临界区(CriticalSection).

锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有.这种锁称为排它锁或互斥锁(Mutex).

Java 多线程梳理(三、线程同步机制)

JVM 把锁分为内部锁和显示锁两种. 内部锁通过 synchronized 关键字实现; 显示锁通过 java.concurrent.locks.Lock 接口的实现类实现

3.3 锁的作用

锁可以实现对共享数据的安全访问. 保障线程的 原子性,可见性有序性.

可见性的保障:通过写线程冲刷处理器的缓存读线程刷新处理器缓存这两个 动作实现的.
java 平台中,锁的获得隐含着刷新处理器缓存的动作, 锁的释放隐含着冲刷处理器缓存的动作.

有序性的保障:写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的

注意: 使用锁保障线程的安全性,必须满足以下条件:

  1. 这些线程在访问共享数据时必须使用同一个锁
  2. 即使是读取共享数据的线程也需要使用同步锁

3.4 锁相关的概念

3.4.1 可重入性

可重入性(Reentrancy)描述这样一个问题: 一个线程持有该锁的时候能再次(多次)申请该锁

如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的, 否则就称该锁为不可重入的

package cn.qnut.P3;

public class LockReentrancy {
    void methodA() {
        // 申请 a 锁
        methodB();
        // 释放 a 锁
    }

    void methodB() {
        // 申请 a 锁
        // 释放 a 锁
    }
}

3.4.2 锁的争用与调度

  1. Java 平台中内部锁属于非公平锁
  2. 显示锁Lock,既支持公平锁又支持非公平锁

3.4.3 锁的粒度

锁可以保护的共享数据的数量大小称为锁的粒度.

  1. 锁保护共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细.
  2. 锁的粒度过粗会导致线程在申请锁时会进行不必要的等待.
  3. 锁的粒度过细会增加锁调度的开销.

3.5 内部锁:synchronized 关键字

Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock). 这种锁也称为监视器(Monitor), 这种内部锁是一种排他锁,可以保障原
子性,可见性与有序性. 内部锁是通过 synchronized 关键字修饰代码块,修饰该方法 实现的

修饰代码块的语法:

synchronized( 对象锁 ) {
	// 同步代码块,可以在同步代码块中访问共享数据
}

// 修饰实例方法就称为同步实例方法
public synchronized void method1() {
}
// 修饰静态方法称称为同步静态方法
public synchronized static void method1() {
}

3.6 轻量级同步机制:volative 关键字


3.6.1 volatile 的作用

  1. volatile 关键的作用使变量在多个线程之间可见.
package cn.qnut.P3;

/**
 * volatile 的作用可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取
 */
public class Test02 {
    public static void main(String[] args) {
        // 创建 PrintString 对象
        PrintString printString = new PrintString();
//        printString.printStringMethod();

        // 开启子线程,让子线程执行 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() + "结束++++++++++++++");
        }
    }
}

3.6.2 volatile 与 synchronized 比较

  1. volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比 synchronized 要好; volatile 只能修饰变量,而 synchronized 可以修
    饰方法,代码块. 随着 JDK 新版本的发布,synchronized 的执行效率也有较大的提升,在开发中使用 sychronized 的比率还是很大的.
  2. 多线程访问 volatile 变量不会发生阻塞,而 synchronized 可能会阻塞
  3. volatile 能保证数据的可见性,但是不能保证原子性; 而synchronized 可以保证原子性,也可以保证可见性
  4. 关键字 volatile 解决的是变量在多个线程之间的可见性;

3.6.3 volatile 非原子特性

volatile 关键字增加了实例变量在多个 线程之间的可见性,但是不具备原子性.

package cn.qnut.P3;

public class Test03 {
    public static void main(String[] args) {
        // 在 main 线程中创建 10 个子线程
        for (int i = 0; i < 100; i++) {
            new MyThread().start();
        }
    }

    static class MyThread extends Thread {
        // volatile 关键仅仅是表示所有线程从主内存读取 count 变量的值
        // volatile public static int count;
        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);
        }
        
         */

        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();
        }
    }
}

上一篇:史上最牛逼的synchronized教程来了,建议收藏


下一篇:java语言手写高synchronized同步器锁和红黑树 以及对volatile关键字的底层分析(未完待续。。。)