多线程编程基础--Java线程同步机制

1. 前言

从广义来说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相关的API。本文主要介绍Java平台中用于协调线程间共享数据访问的相关关键字和API,当然也是常用的线程同步方法。

2. 锁概述

学习线程都知道,在多个线程并发访问共享变量、共享资源时,会造成线程安全问题,那怎么解决呢?

我们很容易想到一种保障线程安全的方法–将多个线程对于并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,该线程访问结束后其他线程才能对其进行访问。锁(Lock)就是利用这种思路以保障线程安全的线程同步机制。

一些名词概念:

  • 临界区: 锁的持有线程在其获得锁之后和释放锁之前这段时间内所执行的代码称作临界区(Critical Section)。
  • 可重入性(Reentrancy): 一个线程在其持有一个锁的时候能否再次或多次申请该锁。
  • 锁的争用与调度: 资源的争用、调度的概念对锁也是同样适用的。
  • 锁的粒度: 一个锁实例所保护的共享数据的数量大小就被称为该锁的粒度(Granularity)。数量大,称锁的粒度粗,否则,称粒度细。

这些复杂概念就不深研究了,只简单了解,想深入研究的小伙伴自行查阅相关书籍。

3. 内部锁:synchronized 关键字

Java平台中的任何一个对象都有唯一一个与之关联的锁,这种锁称为监视器(Monitor)或者内部锁(Intrinsic Lock)。内部锁能够保障原子性、可见性和有序性。内部锁时通过synchronized 关键字实现的。synchronized提供了一种独占的加锁方式,是比较常用的线程同步的关键字,一般在“线程安全的单例”中普遍使用。该关键字能够保证代码块的同步性和方法层面的同步。

代码块同步

synchronized 关键字修饰的代码块就被称为同步块

//使用synchronized关键字实现线程安全的单例模式
    private static Singleton instance;
    public static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class)
            {
                if(instance == null){
                    instance = new Singleton();
                }              
            }
        }
        return instance;
}
privateSingleton(){ }

方法同步

synchronized 关键字修饰的方法就被称为同步方法
同步方法的这个方法体就是一个临界区。

public static synchronized Singleton getInstance2(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
private Singleton(){ }

4. 显式锁:Lock接口

以读写锁为例,Lock的使用方式很简单,只需要在需要加锁的地方先获取锁,操作完成之后释放锁,需要注意的是

要在finally中释放锁,这样做的目的是保证在获取到所之后,最终都是能够被释放;
不要讲获取锁的过程写在try代码块中,防止在获取锁发生异常时导致的锁无故释放。

Lock lock  = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
  lock.ublock();
}

Lock api
Lock是一个接口,定义了锁的获取和释放等基本操作。

  • void lock()
    线程调用该方法获取锁,获取锁后返回;

  • void lockInterruptibly() throws InterruptedException
    可中断地获取锁,和lock()方法的区别在于该方法可以响应中断;

  • boolean tryLock()
    尝试非阻塞获取锁,线程调用该方法后立刻返回,成功获取到锁返回true,否则返回false;

  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    超时获取锁,该方法在以下3中情况会返回:
    在超时时间内获得锁;
    在超时时间被中断;
    超时时间结束仍未获得,返回false。

  • void unlock()
    释放锁;

  • Condition newCondition()
    获取等待通知Condition组件,该组件和当前锁绑定,只有线程获取到了锁才能调用await()方法,调用后,当前线程释放锁。

Lock与synchronized之间的区别和联系

在总结两者的区别和联系之前先引入两个概念:隐式锁和显式锁。
上面都有涉及,下面仔细讲解一下。

隐式锁: 隐式获取锁,synchronized是它的代表,使用者不需要关心其内部锁的获取和释放,所有的锁的相关操作都由具体的关键字完成;
显式锁: 显示地获取锁,Lock是它的代表,需要使用者在使用的时候显示地获取和释放锁。

显式锁和隐式锁都实现了对临界区访问的控制,但是显式锁提供了更灵活、更强大的接口:

  • 隐式锁将锁的获取和释放固化了,只能先获取再释放;显式锁显然无此约束,可以按照自己的需要来做锁的释放;
  • 显式锁提供了可中断获取锁以及超时获取锁等多种隐式锁不具备的同步特性;
  • 提供维度更小的等待与唤醒(Condition)。

就Lock接口提供的synchronized关键字不具备的特性做一个分析描述:

特性 描述
尝试非阻塞获取锁 当前线程尝试获取锁,如果锁未被其他线程获取,当前线程成功获取并持有锁
可中断获取锁 获取锁的线程能够响应中断,当获取到锁的线程被中断时,抛出中断异常,锁被释放
超时获取锁 在指定的时间内获取锁,如果在指定的时间内未获取到,获取锁失败,返回

5. 轻量级同步机制:volatile 关键字

volatile “不稳定”的意思。volatile关键字用于修饰共享可变变量,既没有使用 final 关键字修饰的实例变量或静态变量,相应的变量就被称作 volatile变量。
private volatile int logLevel;

volatile 关键字表示被修饰的变量的值容易发生变化(即被其他线程更改),因而不稳定。volatile变量的不稳定性意味着对这种变量的读和写都必须从高速缓存或者主内存中读取,以读取变量的相对新值。

volatile 关键字常被称为轻量级锁,其作用与锁的作用有相同的地方:保证可见性和有序性。所不同的是,在原子性方面它仅能保障写volatile 变量操作的原子性,但没有锁 的排他性;其次,volatile 关键字的使用不会引起上下文切换(这是volatile 被称为轻量级的原因)。

6. 小结&参考资料

小结

对于多线程编程来说,此文介绍的仅是九牛一毛,在解决负载均衡问题,充分利用CPU资源方面,多线程编程起着至关重要的作用。
我才刚刚入门,加油,菜鸡!!

参考资料

发布了42 篇原创文章 · 获赞 48 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/chachapaofan/article/details/98751035