Java线程同步机制

线程同步机制就是保障线程安全、协调线程直接数据访问的机制。
Java提供的线程同步机制包括:

  • volatile关键字
  • final关键字
  • static关键字
  • 相关API(如Object.wait()等)
  • ……
锁 锁保障线程安全的思路为:将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。
我们平时听到用到的锁有很多种:公平锁/非公平锁、可重入锁/不可重入锁、共享锁/排他锁、乐观锁/悲观锁、分段锁、偏向锁/轻量级锁/重量级锁、自旋锁。其实这些都是在不同维度或者锁优化角度对锁的一种叫法。
JVM把锁分为隐式锁(内部锁)和显示锁两种。所谓的显示和隐式,就是指使用者使用时要不要手动写代码去获取锁和释放锁的操作。
  • 隐式锁/内部锁:通过synchronized关键字实现。
  • 显示锁:通过Lock接口的实现类实现。主要有ReentrantLock、StampedLock、ReadWriteLock、ReentrantReadWriteLock等。
在使用synchronized关键字时,我们不用写其他的代码程序就能够获取锁和释放锁,那是因为当synchronized代码块执行完成后,系统会自动让程序释放占用的锁,这是由系统维护的,如果非逻辑问题的话,是不会出现死锁的。
在使用Lock时,我们需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁。
synchronized(隐式锁)
synchronized可以修饰代码块或方法。
个人理解,锁住同一个变量的代码块或方法共享同一把锁,而同一时刻,共享了同一把锁的代码块或方法只能被一个线程访问。只要明白了这一点,所谓方法锁(其实也是对象锁)、对象锁、类锁就很容易理解了。
  • 同步代码块
    //1.锁住非静态变量。锁住同一个变量的代码块共享同一把锁。非静态变量不被其他线程共享,所以多个实例调用同一方法也不会受这个锁的影响。 private Object lock = new Object(); public void method() { synchronized (lock) { //code... } }//2.锁住this对象。所有锁住this的代码块共享同一把锁。同样多个实例调用这些方法也不会受锁的影响。 synchronized (this) { //code... }//3.锁住静态变量。由于静态变量在JVM方法区只有一份,而方法区被所有线程共享,因此锁住静态变量相当于类锁。 private static Object lock = new Object(); public void method() { synchronized (lock) { //code... } }//4.锁住xxx.class。即类锁,类锁是所有线程共享的。 synchronized (XXX.class) { //code... }

  • 同步方法
    //1.锁住非静态方法。相当于锁对象是this。 public synchronized void method() { //code... }//2.锁住静态方法。相当于类锁。 public static synchronized void method() { //code... }

Lock(显式锁)
synchronized关键字的锁是JVM层实现的。JDK5之后在java.util.concurrentjava.util.concurrent.locks包里有了显式锁,即Lock和ReadWriteLock两个接口。常见实现类有:
  • ReentrantLock(Lock的实现类)
  • ReentrantReadWriteLock(ReadWriteLock的实现类)
Java线程同步机制
文章图片
Lock接口有以下方法:
// 获取锁 void lock()// 如果当前线程未被中断,则获取锁,可以响应中断 void lockInterruptibly()// 返回绑定到此 Lock 实例的新 Condition 实例 Condition newCondition()// 仅在调用时锁为空闲状态才获取该锁,可以响应中断 boolean tryLock()// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁 boolean tryLock(long time, TimeUnit unit)// 释放锁 void unlock()

ReadWriteLock接口有两个方法:
// 返回用于读取操作的锁 Lock readLock()// 返回用于写入操作的锁 Lock writeLock()

ReentrantLock lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作。
其中,这四种获取锁的方法有什么区别?参考《Java锁Lock接口详解》一文。lock()和unlock()的使用方法如下:
Lock lock = new ReentrantLock(); lock.lock(); // 获取锁 try{ //处理任务 }catch(Exception ex){}finally{ lock.unlock(); //释放锁 }

ReentrantReadWriteLock ReadWriteLock维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持,而写入锁是独占的。
private Object data = https://www.it610.com/article/null; //共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。 ReadWriteLock lock = new ReentrantReadWriteLock(); // 读数据 public void get() { lock.readLock().lock(); // 加读锁 try { //处理任务 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); // 释放读锁 } }// 写数据 public void put(Object data) { lock.writeLock().lock(); // 加写锁 try { //处理任务 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); // 释放写锁 } }

锁的类型
锁/类型 公平/非公平锁 可重入/不可重入锁 共享/独享锁 乐观/悲观锁
synchronized 非公平锁 可重入锁 独享锁 悲观锁
ReentrantLock 都支持 可重入锁 独享锁 悲观锁
ReentrantReadWriteLock 都支持 可重入锁 读锁-共享,写锁-独享 悲观锁
公平锁和非公平锁、可重入锁和不可重入锁、共享锁和独享锁、乐观锁和悲观锁有什么区别?可参考《Java锁Lock的种类》一文。
volatile volatile关键字是线程同步的轻量级实现,性能比synchronized要好。volatile只能修饰变量,可以解决变量在多个线程的可见性。
使用方法如下:
private static volatile int count;

其他线程同步机制 除了锁机制和volatile关键字,Java中还有一些实现线程同步的方法。
  • final关键字
    其原理是通过禁止CPU的指令集重排序来提供线程的可见性,从而保证线程同步。
    当一个对象发布到其他线程的时候,该对象的所有final字段都初始化完成,即其他线程读取到的都是相应字段的初始值而非默认值。
  • static关键字
    即使在未使用其他线程同步机制的情况下,static关键字保证一个线程可以读到一个类静态变量的初始值,但这种可见性仅限于初次读取该变量。
参考 多线程和高并发(二)volatile关键字
Java锁Lock的种类
java里的锁总结(这篇讲得过于深入)
Java并发之显式锁和隐式锁的区别
对象锁和类锁的区别
【Java线程同步机制】java 锁 Lock接口详解

    推荐阅读