IllegalMonitorStateException异常
这个异常 java.lang.IllegalMonitorStateException 表示在Java中发生了非法的监视器状态操作。通常,它在以下情况下抛出:
- 当一个线程试图在没有拥有锁的情况下调用 notify() 或 notifyAll() 方法时会引发此异常。
- 当一个线程试图在没有拥有锁的情况下调用 wait()、wait(long) 或 wait(long, int) 方法时也会引发此异常。
锁膨胀
1. ⽆锁状态
Java 对象刚创建时,还没有任何线程来竞争,说明该对象处于⽆锁状态(⽆线程竞争它)偏向锁标识(biased)=0、锁状态(lock)=01
2. 偏向锁状态
同步代码⼀直被⼀同个线程所访问
,线程会⾃动获取偏向锁,表示内置锁偏爱这个线程,这个线程要执⾏该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下,效率⾮常⾼。
偏向锁状态的 Mark Word 会记录内置锁⾃⼰偏爱的线程 ID,内置锁会将该线程当做⾃⼰的熟⼈。
3. 轻量级锁状态
当有两个线程开始竞争这个锁对象
,情况发⽣变化了,不再是偏向锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的 Mark Word 就指向哪个线的栈帧中的锁记录。
没有抢到锁的线程会通过⾃旋的形式尝试获取锁
,不会阻塞抢锁线程,以便提⾼性能。
⾃旋原理⾮常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和⽤户态之间的切换进⼊阻塞挂起状态,它们只需要等⼀等(⾃旋),等持有锁的线程释放锁后即可⽴即获取锁,这样就避免⽤户线程和内核的切换的消耗。
但是线程⾃旋是需要消耗 CPU 的,如果⼀直获取不到锁,那线程也不能⼀直占⽤ CPU ⾃旋做⽆⽤功,所以需要设定⼀个⾃旋等待的最⼤时间。 JVM 对于⾃旋周期的选择, JDK1.6 之后引⼊了适应性⾃旋锁,适应性⾃旋锁意味着⾃旋的时间不是固定的,⽽是由前⼀次在同⼀个锁上的⾃旋时间以及锁的拥有者的状态来决定。线程如果⾃旋成功了,则下次⾃旋的次数会更多,如果⾃旋失败了,则⾃旋的次数就会减少。
如果持有锁的线程执⾏的时间超过⾃旋等待的最⼤时间扔没有释放锁,就会导致其他争⽤锁的线程在最⼤等待时间内还是获取不到锁,⾃旋不会⼀直持续下去,这时争⽤线程会停⽌⾃旋进⼊阻塞状
态,该锁膨胀为重量级锁。
4. 重量级锁状态
竞争锁的线程很多轻量级锁会升级为重量级锁
,重量级锁会让其他申请的线程之间进⼊阻塞,性能降低。重量级锁也就叫做同步锁,这个锁对象 Mark Word 再次发⽣变化,会指向⼀个监视器(Monitor)对象,该监视器对象⽤集合的形式,来登记和管理排队的线程。
JDK1.6之前,内置锁都是重量级锁。重量级锁会造成 CPU 在⽤户态和核⼼态之间频繁切换,所以代价⾼、效率低。 JDK1.6为了减少获得锁和释放锁所带来的性能消耗,引⼊了“偏向锁”和“轻量级锁”实现。JDK1.6后内置锁⼀共有四种状态:⽆锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这些状态随着竞争情况逐渐升级。内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其⽬的是为了提⾼获得锁和释放锁的效率。
加锁后的等待和唤醒命令
- ntryList:双向队列:等待锁的多列
- Owner:正在执行的线程
- WaitSet:双向队列
- locko.wait:wait方法一定在synchronize代码块中,把当前线程放到waitset中
- locko.notify:把waitset队列中的一个随机线程放到entrylist中
- locko.notifyAll:把waitset队列中的所有线程都放到entrylist中、
下面是一个用wait和notify命令的案例
生产者消费者模式
消费者:
package Thred.PaC;
public class Consumers implements Runnable {
private Product product;
public Consumers(Product product) {
this.product = product;
}
@Override
public void run() {
while (true) {
synchronized (product) {
if (product.getCount() > 0) {
product.deleteCount();
System.out.println("吃货吃包子了");
product.notify();
} else {
System.out.println("请稍等,包子正在制作");
try {
product.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
生产者:
package Thred.PaC;
public class Producers implements Runnable {
private Product product;
public Producers(Product product) {
this.product=product;
}
@Override
public void run() {
while (true){
synchronized (product){
if (product.getCount() <= 0 ){
// 包子已经生产,等待消费者线程消费
product.addCount();
System.out.println("生成一个包子成功!");
product.notify();
}else {
try {
product.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}}}
}
产品类
package Thred.PaC;
public class Product {
private int count; //记录包子总数
public Product(int count) {
this.count = count;
}
public Product() {
}
public int getCount() {
return count;
}
public void addCount() {
count++;
}
public void deleteCount() {
count--;
}
}
测试类:
package Thred.PaC;
public class Test {
public static void main(String[] args) {
Product product = new Product();
Producers p1 =new Producers(product);
Consumers c1 =new Consumers(product);
Thread A =new Thread(p1);
Thread B =new Thread(c1);
A.start();
B.start();
}
}
juc显示锁
- 内置锁:synchronized
- 现实锁:juc锁
JDK5 版本引入了 java.util.concurrent 并发包,简称为 JUC 包,JUC 出自并发大师 Doug Lea 之手, Doug Lea 对 Java 并发性能的提升做出了巨大的贡献。
Lock接口
Java 对象锁还存在性能问题。在竞争稍微激烈的情况下, Java 对象锁会膨胀为重量级锁(基于操作系统的 Mutex Lock 实现),而重量级锁的线程阻塞和唤醒操作,需要进程在内核态和用户态之间来回切换,导致其性能非常低。JUC显示锁因为是纯粹Java语言实现,避免了这些问题,同时JUC显示锁具备了对象锁不具备的高级特性r如限时抢锁、可中断抢锁、多个等待队列。
显式锁不再作为 Java 内置特性来实现,而是作为 Java 语言可编程特性来实现
java.util.concurrent.locks.Lock接口的主要抽象方法如下
方 法 | 描 述 |
---|---|
void lock() | 抢锁。 成功则向下运行,失败则阻塞抢锁线程 |
void lockInterruptibly()throws InterruptedExceptio | 可中断抢锁,当前线程在抢锁的过程中可以响应中断信号 |
boolean tryLock() | 尝试抢锁, 线程为非阻塞模式,在调用 tryLock 方法后立即返回。抢锁成功返回 true, 抢锁失败返回 false |
boolean tryLock(long time, TimeUnit unit)throws InterruptedException | 限时抢锁,到达超时时间返回 false。并且此限时抢锁方法也可以响应中断信号 |
void unlock(); | 释放锁 |
Condition newCondition(); | 获取与显式锁绑定的 Condition 对象,用于“等待-通知”方式的线程间通信 |
从 Lock 提供的接口方法可以看出, 显式锁至少比 Java 内置锁多了以下优势:
1. 可中断获取锁
使用 synchronized 关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响应中断信号(interrupt)的;而使用 Lock.lockInterruptibly( )方法获取锁时,如果线程被中断,线程将抛出中断异常。
2. 可非阻塞获取锁
使用 synchronized 关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock( )方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回 false。
3. 可限时抢锁
使用 Lock.tryLock(long time, TimeUnit unit)方法, 显式锁可以设置限定抢占锁的超时时间。而在使用 synchronized 关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。
ReentrantLock的基本用法
public class ReenterLockTest implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
public void run() {
lock.lock();
lock.lock();
try {
for (int j = 0; j < 10000000; j++) {
i++;
}
} finally {
lock.unlock();
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockTest r1 = new ReenterLockTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
读写锁
读写锁的内部包含了两把锁:一把是为读锁,是一种共享锁;一把写锁,是一种独占锁。读写锁适用于读多写少的并发情况
- 读、读共享
- 读、写互斥
- 写、写互斥
public class ReadWriteLockDemo {
static CountDownLatch countDownLatch;
private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
lock.lock(); //阻塞
try {
Thread.sleep(1000);
System.out.println("read success");
countDownLatch.countDown();
return value;
} finally {
lock.unlock();
}
}
public void handleWrite(Lock lock, int index) throws InterruptedException {
lock.lock(); //阻塞
try {
Thread.sleep(1000);
value = index;
System.out.println("write success");
countDownLatch.countDown();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunnable = new Runnable() {
public void run() {
try {
demo.handleRead(readLock);
// demo.handleRead(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable() {
public void run() {
try {
demo.handleWrite(writeLock, new Random().nextInt());
// demo.handleWrite(lock, new Random().nextInt());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
countDownLatch = new CountDownLatch(20);
long t1 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
for (int i = 0; i < 18; i++) {
new Thread(readRunnable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunnable).start();
}
countDownLatch.await();
long t2 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
System.out.println(t2 - t1); //期待20s
}
}
Semaphore
Semaphore 是一个是许可管理器,可以用来控制在同一时刻访问共享资源的线程数量,Semaphore 维护了一组虚拟许可,其数量可以通过构造函数的参数指定。线程在访问共享资源前,必须使用 Semaphore 的 acquire 方法获得许可,如果许可数量为 0,该线程则一直阻塞。线程访问完成资源后,必须使用 Semaphore 的 release 方法去释放许可。
- Semaphore(permits):构造一个 Semaphore 实例,初始化其管理的许可数量为 permits 参数值。
- acquire( ):尝试获取 1 个许可。而当前线程被中断,则会抛出 InterruptedException 异常并终止阻塞
- release( ):释放 1 个可用的许可
Semaphore 示例
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
//线程池,用于多线程模拟测试
final CountDownLatch countDownLatch = new CountDownLatch(10);
//创建信号量,含有2个许可
final Semaphore semaphore = new Semaphore(2);
AtomicInteger index = new AtomicInteger(0);
//创建Runnable可执行实例
Runnable r = () ->
{
try
{
//抢占一个许可
semaphore.acquire(1);
//模拟业务操作: 处理排队业务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + ", 受理处理中...,服务号: " + index.incrementAndGet());
Thread.sleep(1000);
//释放一个信号
semaphore.release(1);
} catch (Exception e)
{
e.printStackTrace();
}
countDownLatch.countDown();
};
//创建4条线程
Thread[] tArray = new Thread[10];
for (int i = 0; i < 10; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动4条线程
for (int i = 0; i < 10; i++)
{
tArray[i].start();
}
countDownLatch.await();
}
}