HotSpot虚拟机中一个Java对象在内存中由对象头、实例数据和对齐字节填充部分组成。其中对象头又分为三部分,MarkWord、指向类的指针和数组长度(只有数组对象有)。如下:
MarkWord用于存储对象自身的运行时数据,如哈希码和GC分代年龄等,是实现轻量级锁和偏向锁的关键;
MarkWord在32位和64位虚拟中分别占用32bit和64bit;
由于是与对象自身数据无关的额外存储成本,MrakWord被设计为非固定的动态数据结构,会根据对象的状态复用自己的存储空间,以便在极小的空间存储更多的信息。对象的状态主要有未锁定、轻量级锁定、重量级锁定、GC标记、可偏向等状态。
用于存储指向方法区对象类型数据的指针,Java虚拟机通过该指针确定该对象是哪个类的实例。
数组对象会有一个记录数组长度的部分。
https://zhuanlan.zhihu.com/p/356010805
Java语言中各种操作共享的数据有五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
不可变的对象一定是线程安全的。使用final修饰的(严格来说是JVM里没有发生this引用逃逸的)。
// 在多线程中不需要考虑value的安全性
private final int value = 1;
不管运行时环境如何,调用者都不需要任何额外的同步措施。这个定义十分严格,在JavaAPI中标注线程安全的类大多数都不是绝对的线程安全。例如Vector
是一个线程安全的容器,它的add
、get()
、size()
等方法都被synchronized
修饰。但是也不代表它是绝对线程安全,例如:
public class VectorDemo {
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(() -> {
for (int i = 9; i >= 0; i--) {
vector.remove(i);
}
});
Thread printThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(vector.get(i));
}
});
removeThread.start();
printThread.start();
}
}
上述代码运行会报ArrayIndexOutOfBoundsException
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
at java.base/java.util.Vector.get(Vector.java:780)
at org.numb.concurrency.chapter03.VectorDemo.lambda$main$1(VectorDemo.java:20)
at java.base/java.lang.Thread.run(Thread.java:834)
因为一个线程去移除vector元素,一个又去获取元素,导致获取元素时此元素已被删除。
对象单次的操作是线程安全的,这也是通常意义上所讲的线程安全。如Vector
、HashTable
、synchronizedCollection()
方法包装的集合。
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发的环境中安全使用。如ArrayList
、HashMap
等。
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。线程对立通常是有害的,典型的例子是Thread类suspend()
和resume()
方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在死锁风险。
实现线程安全的方法主要有:1、互斥同步;2、非阻塞同步、3、无同步方案
同步是指对个线程并发访问共享数据时,保证共享数据在同一时刻只被一条(或者一些)线程使用。互斥是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方式。是一种悲观的同步措施。
synchronized关键字经过Javac编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码。这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,就以这个对象的引用作为reference;如果没有明确指定,那将synchronized修饰的方法类型,来决定是取代吗所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
执行monitorenter指令时,首先尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值加1,而执行monitorexit时就将计数器减一。一旦计数器为0锁即被释放,获取锁失败当前线程就被阻塞等待,直到请求锁定的对象持有它的线程释放为止。
ReentrantLock
是Lock
接口的最常见实现。基于Lock
接口,用户能够以非块结构来实现互斥同步。ReentrantLock
比synchronized
增加的功能有:等待可中断、可实现公平锁和可以绑定多个条件。
等待可中断
当持有锁的线程长时间不释放锁时,等待的线程可选择放弃
可实现公平锁
ReentrantLock
默认是非公平锁,可以通过带布尔值的构造函数实现公平锁
锁可以绑定多个条件
一个ReentrantLock
对象可以同时绑定多个Condition
对象
性能
JDK 5及以前版本ReentrantLock
效率高于synchronized
,JDK 6针对synchronized做了大量优化,现在性能基本持平。
synchronized优势
ReentrantLock优势
ReentrantLock功能比synchronized更强大,可以实现等待可中断、可实现公平锁和可以绑定多个条件。
阻塞同步在对共享数据进行加锁后,会导致用户态与内核态的切换、维护锁计数器和检查线程是否需要唤醒等开销。而非阻塞同步是基于冲突检测的乐观并发策略,即先进行操作,然后检测如果有没有其他线程竞争共享数据,则成功,如果有竞争则不断尝试,直至没有竞争的共享数据,即无锁编程。
这种策略前提是操作和冲突检测必须具备原子性,而原子性的保证如果再依赖加锁和互斥同步实现就失去了意义,所以必须依赖硬件实现,即可以通过一条CPU指令就能完成,这类指令有
测试并设置(Test-and-Set)
获取并增加(Fetch-and-Increment)
交换(Swap)
比较并交换(Compare-and-Swap,即CAS)
加载链接/条件储存(Load-Linked/Store-Conditional,即LL/SC)
CAS指令需要三个操作数,分别为内存位置V、旧的预期值A和新的预期值B。CAS执行时当且仅当V符合预期A时,处理器才会用B来更新V的值,否则不更新。不管是否更新V值,都会返回V的旧值。上述操作是一个原子操作,执行期间不会被其他线程打断。
CAS存在一个逻辑漏洞,又称为ABA问题,即如果一个变量V初次读取的时候是A,并且在准备赋值的时候检查它仍为A,但是无法保证这个期间它的值曾经被修改过
J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类AtomStampedReference
,它可以通过控制变量值的版本来保证CAS的正确性。不过目前这个类非常鸡肋,大部分情况下ABA不会影响程序并发的正确性。如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更为高效。
要保证线程安全不一定非要采用同步,只需保证共享数据的正确性即可,有些代码本身就是线程安全的如:
synchronized
关键字、Lock
的实现类(ReentrantLock
、ReentrantReadWriteLock
)java.util.concurrent.atomic
包下面的AtomicBoolean
、AtomicInteger
、AtomicReference
等下面实现一个简单生产者消费者模型,一个线程对num++,另一个线程判断是奇数则消费
不加锁时
class Demo{
private static Integer num = 0;
public static void noLockScene() {
Thread t1 = new Thread(() -> {
while (true) {
if (num % 2 == 1) {
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 1, num = " + num);
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
});
t1.start();
t2.start();
}
}
输出如下:由于未加锁,竞争导致逻辑混乱
thread 1, num = 2
thread 1, num = 5
thread 1, num = 6
thread 1, num = 8
synchronized
悲观锁
class Demo{
private static byte[] lock = new byte[0];
public static void synchronizedScene() {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (lock) {
if (num % 2 == 1) {
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 1, num = " + num);
}
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
synchronized (lock) {
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}
});
t1.start();
t2.start();
}
}
输出:这里未保证不重复消费和按顺序消费,消费都是奇数,正确
hread 1, num = 25
thread 1, num = 47
thread 1, num = 333
ReentrantLock
悲观锁
class Demo{
public static void reentrantLockScene() {
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
while (true) {
reentrantLock.lock();
if (num % 2 == 1) {
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 1, num = " + num);
}
// ReentrantLock必须手动释放锁
reentrantLock.unlock();
}
});
Thread t2 = new Thread(() -> {
while (true) {
reentrantLock.lock();
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
reentrantLock.unlock();
}
});
t1.start();
t2.start();
}
}
输出:
thread 1, num = 79
thread 1, num = 839
thread 1, num = 1249
AtomicInteger
乐观锁
class Demo{
public static void optimisticLockScene() {
AtomicInteger number = new AtomicInteger();
Thread t1 = new Thread(() -> {
while (true) {
int origin = number.get();
if (origin % 2 == 1) {
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!number.compareAndSet(origin, origin)) {
System.out.println("the num has changed");
} else {
System.out.println("thread 1, num = " + origin);
}
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
number.incrementAndGet();
}
});
t1.start();
t2.start();
}
}
输出:
thread 1, num = 1235
the num has changed
thread 1, num = 1241
the num has changed
thread 1, num = 1245
这两种锁都是最底层的锁实现,一般跟操作系统有关
pthread_mutex_lock
pthread_spin_lock
synchronized
关键字在优化后,底层也会加自旋锁,且会适应性自旋,详见后面synchronized
介绍。
利用CAS,实现自旋锁
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
// 利用CAS加锁失败则自旋
while (!cas.compareAndSet(null, thread)) {
// 自旋,可以等待或者什么都不做
}
}
public void unlock() {
Thread thread = Thread.currentThread();
cas.compareAndSet(thread, null);
}
}
synchronized
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
// 非公平锁,默认非公平锁
ReentrantLock lock = new ReentrantLock(false);
公平锁与非公平锁示例
public class FairLock {
public static void fairLock() {
// 公平锁
ReentrantLock lock = new ReentrantLock(true);
Runnable runnable = () -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + " lock");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
System.out.println(Thread.currentThread().getName() + " unlock");
};
// 启动5个线程,按顺序依次获取锁
for (int i = 0; i < 5; i++) {
Thread t = new Thread(runnable);
t.setName("thread-" + i);
t.start();
}
}
public static void noFairLock() {
// 非公平锁
ReentrantLock lock = new ReentrantLock(false);
Runnable runnable = () -> {
lock.lock();
System.out.println(Thread.currentThread().getName() + " lock");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
System.out.println(Thread.currentThread().getName() + " unlock");
};
// 启动5个线程,随机获取锁
for (int i = 0; i < 5; i++) {
Thread t = new Thread(runnable);
t.setName("thread-" + i);
t.start();
}
}
}
可重入锁:Java中的大部分锁都是可重入锁,synchronized
、ReentrantLock
不可重入锁:可以自己实现,具体为加锁一次后再次加锁就死锁
读写锁通常是一起用来提高并发读写性能,如ReentrantReadWriteLock
public class ReentrantReadWriteLockDemo {
private static int num;
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Thread t1 = new Thread(() -> {
while (true) {
lock.readLock().lock();
if (num % 2 == 1) {
try {
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 1, num = " + num);
}
lock.readLock().unlock();
}
});
Thread t2 = new Thread(() -> {
while (true) {
lock.writeLock().lock();
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
lock.writeLock().unlock();
}
});
t1.start();
t2.start();
}
}
public class InterruptLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
lock.lockInterruptibly();
// 持有锁后保持10s
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("thread 1 is interrupted");
}
} catch (InterruptedException e) {
// 线程被打断
System.out.println("thread 1 is interrupted");
} finally {
// 必须释放锁,否则thread2 仍然无法获取锁
lock.unlock();
System.out.println("release lock in thread 1");
}
});
Thread t2 = new Thread(() -> {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 2 get lock");
lock.unlock();
});
t1.start();
TimeUnit.SECONDS.sleep(1);
t2.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
}
}
输出:
thread 1 is interrupted
release lock in thread 1
thread 2 get lock
synchronized锁的是对象的监视器,可以详见3.2。即java对象头的MarkWord中存有指向监视器monitor的指针
由上可以看出,cxq队列中的线程有可能会比EntryList中的线程先抢到锁,所以是非公平锁。
synchronized锁的状态有四种:无锁状态、偏向锁、轻量级锁、重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
synchronized加锁之后,可以调用wait()实现等待,调用notify()唤醒一个等待线程(WaitSet中的线程)或notifyAll()唤醒所有等待线程去抢占锁。
ReentrantLock内部有两个同步器:FairSync
和NonFairSync
,依赖它们实现加锁。
AQS定义了一套多线程访问共享资源的同步框架,在它内部维护了一个volatile int state
(代表共享资源)和一个FIFO队列(多线程竞争共享资源时进入此队列)
public abstract class AbstractQueuedSynchronizer{
// FIFO队列头
private transient volatile Node head;
// FIFO队列尾
private transient volatile Node tail;
// 共享资源
private volatile int state;
}
state的访问方式有三种:
ReentrantLock一个线程加锁后,会调用AQS的tryAcquire(),加锁成功后其他线程再调用tryAcquire()时就会失败
同一个线程可以重复调用AQS的tryAcquire(),每次调用state都会+1,每次unlock()都会-1,所以ReentrantLock加锁几次就要解锁几次,保证最终state=0
ReentrantLock内部有两个同步器FairSync
和NonFairSync
,可以分别实现公平锁和非公平锁
Condition可以使ReentrantLock加锁的线程wait,主要是await()/signal()
synchronized | ReentrantLock | |
---|---|---|
是否悲观锁 | 是 | 是 |
是否公平锁 | 非公平 | 默认非公平可实现公平 |
是否可重入锁 | 是 | 是 |
是否可中断锁 | 否 | lockInterruptibly可以实现可中断 |
等待唤醒方式 | Object的wait()、notify()/notifyAll | Condition的await()、signal()和signalAll() |
可中断锁
公平锁
锁可以绑定多个条件,实现指定线程唤醒
一个ReentrantLock
对象可以同时绑定多个Condition
对象,每一个Condition
可以关联一个指定线程,实现指定线程唤醒。
ReentrantLock与Condition一起使用可以实现指定线程唤醒,而notify()是随机唤醒一个等待线程。
volatile关键字主要有两个作用
volatile只保证了可见性(每次从内存获取)与有序性(禁止指令重排序),不能保证原子性,所以无法取代synchronized。synchronized可以保证原子性。
CountDownLatch作用类似于线程计数器,比如让某一个线程执行多少次后,另一个线程继续执行。
// 初始化一个线程计数器,初始化后数量便无法改变,清零后也无法重新使用
public CountDownLatch(int count)
// 阻塞直到计数器清零
public void await() throws InterruptedException
// 计数器-1
public void countDown()
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(5);
Thread t1 = new Thread(() -> {
try {
System.out.println("线程阻塞...");
latch.await();
System.out.println("计数结束...");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
while (true) {
latch.countDown();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
CyclicBarrier的作用是让一组线程等待至某个状态之后再全部同时执行 ,适用于保证一批线程同时结束
// 初始化需要一批次执行的线程数量
public CyclicBarrier(int parties)
// 线程等待调用此方法
public int await() throws InterruptedException, BrokenBarrierException
// 用完后重置以重用
public void reset()
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
// 并发开启5个线程处理业务, 等到业务处理完后,再处理结果
for (int i = 0; i < 5; i++) {
final int num = i;
Thread thread = new Thread(() -> {
System.out.println("线程" + num + "并发处理业务");
try {
// 等待其余并发线程处理完成
cyclicBarrier.await();
System.out.println("线程" + num + "处理本线程的结果");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
输出:
线程0并发处理业务
线程1并发处理业务
线程2并发处理业务
线程3并发处理业务
线程4并发处理业务
线程4处理本线程的结果
线程0处理本线程的结果
线程3处理本线程的结果
线程2处理本线程的结果
线程1处理本线程的结果
CyclicBarrier可以重用,CountDownLatch 不可以。
CountDownLatch 在AQS队列中park的是主线程,而CyclicBarrier在AQS中park的是所有的子线程。
CountDownLatch 是放到AQS队列中,而CyclicBarrier是将子线程放到Condition队列中。
CountDownLatch 唤醒的是主线程,而CyclicBarrier 是通过singleAll函数,将所有的子线程移动到AQS队列中,然后再开始执行。
作用:Semaphore 可以控制同时访问的线程个数
// 初始化一个信号量
public Semaphore(int permits)
// 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可
public void acquire() throws InterruptedException
// 获取 permits 个许可
public void acquire(int permits)
// 释放许可。注意,在释放许可之前,必须先获获得许可。
public void release()
// 释放 permits 个许可
public void release(int permits)
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
// semaphore保证最多并发运行5个线程,模拟一个线程池
for (int i = 0; i < 10; i++) {
final int num = i;
Thread thread = new Thread(() -> {
try {
semaphore.acquire();
System.out.println("线程池仍有空闲,线程" + num + "提交成功");
TimeUnit.SECONDS.sleep(2);
System.out.println("线程" + num + "结束");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
输出:
线程池仍有空闲,线程0提交成功
线程池仍有空闲,线程2提交成功
线程池仍有空闲,线程1提交成功
线程池仍有空闲,线程3提交成功
线程池仍有空闲,线程4提交成功
线程0结束
线程3结束
线程1结束
线程池仍有空闲,线程6提交成功
线程2结束
线程池仍有空闲,线程5提交成功
线程4结束
线程池仍有空闲,线程8提交成功
线程池仍有空闲,线程9提交成功
线程池仍有空闲,线程7提交成功
线程8结束
线程6结束
线程5结束
线程9结束
线程7结束