一、引言
多线程的开发过程中,也许会遇到这么一个场景:多个线程同时操作一个变量时,线程之间会有时间差,而在时间差内,该共享数据的值也许已经发生了改变,那么我们要怎么才能保证在多线程的环境下,每个线程读取到的数据值都是最新的呢?线程同步机制了解一下~
二、线程同步的“锁”
前面了解了多线程场景下,需要保证每个线程读取数据的值都要是最新的,那么我们就需要一个“锁”,来保证当前线程操作此数据时,别的线程无法使用,相当于当前线程“锁”住了此时数据的读写权限,下面我们来学习下如何实现这一场景。
首先我们先了解几大神器,以及其大致的作用:
- 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();
}
}
}
操作结果:

来源:https://www.cnblogs.com/riches/p/11990386.html