一、引言
多线程的开发过程中,也许会遇到这么一个场景:多个线程同时操作一个变量时,线程之间会有时间差,而在时间差内,该共享数据的值也许已经发生了改变,那么我们要怎么才能保证在多线程的环境下,每个线程读取到的数据值都是最新的呢?线程同步机制了解一下~
二、线程同步的“锁”
前面了解了多线程场景下,需要保证每个线程读取数据的值都要是最新的,那么我们就需要一个“锁”,来保证当前线程操作此数据时,别的线程无法使用,相当于当前线程“锁”住了此时数据的读写权限,下面我们来学习下如何实现这一场景。
首先我们先了解几大神器,以及其大致的作用:
- synchronized :关键字,可以修饰方法,也可以修饰代码块
- volatile :特殊域变量
- ReentrantLock :重入锁
- Atomic :原子变量
三、线程同步详解
synchronized同步方法
定义:有synchronized关键字修饰的方法。
- 每个java对象都有一个内置锁,使用synchronized关键字修饰方法时, 内置锁就会“锁住”整个方法。
- 每个线程在调用该方法前,都需要获得内置锁,否则就处于阻塞状态。
- PS:若用synchronized关键字修饰静态方法,此时如果调用该静态方法,将会锁住整个类
/** * synchronized同步方法 */ public class MySynchronizedMethod { private static int count = 0; public static void main(final String[] arguments) throws InterruptedException { //创建TestThread对象 TestThread threadRunable = new TestThread(); //增加10个Thread线程对象 Thread thread1 = new Thread(threadRunable); Thread thread2 = new Thread(threadRunable); Thread thread3 = new Thread(threadRunable); Thread thread4 = new Thread(threadRunable); Thread thread5 = new Thread(threadRunable); Thread thread6 = new Thread(threadRunable); Thread thread7 = new Thread(threadRunable); Thread thread8 = new Thread(threadRunable); Thread thread9 = new Thread(threadRunable); Thread thread10 = new Thread(threadRunable); //10个线程同时启动 thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); thread6.start(); thread7.start(); thread8.start(); thread9.start(); thread10.start(); } /** * 同步方法 */ public static synchronized void countNum() { for (int i = 0; i < 100000; i++) { count++; System.out.println(count); } } /** * 线程实体(实现Runnable接口) */ static class TestThread implements Runnable { public void run() { //调用同步方法 countNum(); } } }
synchronized同步代码块
与同步方法类似,但是同步是一种高开销的操作,通常没必要同步整个方法,所以同步块相对同步方法来说更优一些。
代码与上面类似,只贴出不一样的地方:
/** * 同步方法 */ public static void countNum() { for (int i = 0; i < 100000; i++) { //PS:由于这里是个静态方法,要同步语句块时,就需要到整个类的内置锁(synchronized的入参代表要获取内置锁的实体) synchronized (MySynchronizedMethod.class) { count++; } System.out.println(count); } }
特殊域变量(volatile)
- volatile关键字为域变量的访问提供了一种免锁机制
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
//使用volatile关键字来修饰变量
private static volatile int count = 0;
PS:volatile只有在原子操作(如:n=m+1)的时候才有效,若操作的值与自身有关时(如:n++;n=n+1;)就不起作用了,不熟悉的话不建议用这个关键字实现同步。
ReenreantLock重入锁
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
//声明一个重入锁 private static Lock lock = new ReentrantLock(); /** * 同步方法 */ public static void countNum() { for (int i = 0; i < 100000; i++) { lock.lock();//开始加锁 count++; System.out.println(count); lock.unlock();//解锁 } }
Atomic原子变量
从上面的例子可以看出,类似count++;的方法,实际上并不是一个原子操作,而是经过了读取、修改、写入三个步骤。
实现原理:CAS原理(比较并交换),每个线程操作前会用旧的预期值与内存值进行比较,相同的时候把内存值修改为新值,当一个线程在执行此操作时,其他线程都失败,并可以再次尝试,或者什么都不做,此时线程并不会被挂起。
存在问题:ABA问题 详解参考这里~
/** * 原子变量Atomic实现Synchronized锁的同步功能 */ public class MyAtomic { // 多个线程共享的变量(使用AtomicInteger来替代Synchronized锁) private static AtomicInteger count = new AtomicInteger(0); //获取共享变量的值 public static Integer getCount() { return count.get(); } //自增方法 public static void increase() { count.incrementAndGet(); } public static void main(final String[] arguments) throws InterruptedException { //创建50个线程的线程池 ExecutorService executor = Executors.newFixedThreadPool(50); //放入50个线程对象并执行 for (int i = 0; i < 50; i++) { executor.submit(new TestThread()); } } /** * 同步方法 */ public static void countNum() { for (int i = 0; i < 10000; i++) { increase(); System.out.println(getCount()); } } /** * 线程实体(实现Runnable接口) */ static class TestThread implements Runnable { public void run() { //调用同步方法 countNum(); } } }
操作结果: