在Java多线程中可以使用synchronized隐式锁实现线程之间同步互斥,Java5中提供了Lock类(显示锁)也可以实现线程间的同步,而且在使用上更加方便。本文主要研究 ReentrantLock的使用。

  公平锁与非公平锁:公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁。从这个角度讲,synchronized其实就是一种非公平锁。

  ReentrantLock也是一种可重入锁,类似于synchronized,也就是在拥有锁的情况下可以调用其它需要本锁的方法或者代码块。lock.getHoldCount()可以获得当前线程拥有锁的层数,可以理解为重入了几层。当为0的时候代表当前线程没有占用锁,每重入一次count就加1.

  ReentrantLock具有嗅探锁定、多线路分路通知等功能,而且在使用上比synchronized更加灵活。功能上与synchronized一样实现了线程的互斥性与内存的可见性。

1 ReentrantLock的基本使用方法

  调用其lock()方法会占用锁,调用unlock()会释放锁,但是需要注意必须手动unlock释放锁,否则其他线程会永远阻塞。而且发生异常不会自动释放锁,所以编写程序的时候需要在finally中手动释放锁。

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* ReentrantLock的基本使用方法
* @author Administrator
*
*/
public class Demo1 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo1.class);
private Lock lock = new ReentrantLock(); public void testMethod(){
try {
LOGGER.info("threadName -> {} enter testMethod",Thread.currentThread().getName());
lock.lock();
LOGGER.info("threadName -> {} lock",Thread.currentThread().getName());
Thread.sleep(2*1000);
LOGGER.info("threadName -> {} unlock",Thread.currentThread().getName());
lock.unlock();
LOGGER.info("threadName -> {} exit testMethod",Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
final Demo1 demo1 = new Demo1();
new Thread(new Runnable() {
@Override
public void run() {
demo1.testMethod();
}
},"threadA").start();
new Thread(new Runnable() {
@Override
public void run() {
demo1.testMethod();
}
},"threadB").start();
}
}

结果:(实现了线程之间的互斥同步,threadB释放锁之后threadA才进入Lock,类似于synchronized同步锁的执行效果)

10:10:37 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadB enter testMethod
10:10:37 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadA enter testMethod
10:10:37 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadB lock
10:10:39 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadB unlock
10:10:39 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadB exit testMethod
10:10:39 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadA lock
10:10:41 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadA unlock
10:10:41 [cn.qlq.thread.eleven.Demo1]-[INFO] threadName -> threadA exit testMethod

测试异常发生不会释放锁:修改上面占用锁的方法

    public void testMethod(){
try {
LOGGER.info("threadName -> {} enter testMethod",Thread.currentThread().getName());
lock.lock();
int i =1/0;
LOGGER.info("threadName -> {} lock",Thread.currentThread().getName());
Thread.sleep(2*1000);
LOGGER.info("threadName -> {} unlock",Thread.currentThread().getName());
lock.unlock();
LOGGER.info("threadName -> {} exit testMethod",Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}

结果:(线程B中执行int i=1/0发生算数异常,但是没有释放锁,所以threadA也一直处于阻塞状态。)

正确的用法:finally中释放锁

    public void testMethod(){
try {
LOGGER.info("threadName -> {} enter testMethod",Thread.currentThread().getName());
lock.lock();
int i =1/0;
LOGGER.info("threadName -> {} lock",Thread.currentThread().getName());
Thread.sleep(2*1000); } catch (InterruptedException e) {
e.printStackTrace();
}finally {
LOGGER.info("threadName -> {} unlock",Thread.currentThread().getName());
lock.unlock();
}
LOGGER.info("threadName -> {} exit testMethod",Thread.currentThread().getName());
}

结果: (两个线程都发生算数异常,证明两个线程都可以占用锁,也就是锁被成功的释放)

2 使用Condition实现等待/通知

  关键字synchronized与wait()/notify()、notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock也可以实现类似的功能,但需要借助于Condition对象。Condition类是JDK5中出现的类,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(即对象监视器实例),线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

  在使用notify()/notifyAll()方法进行通知时,被通知的线程却是由JVM随机选择的。但使用ReentrantLock结合Condition类是可以实现前面介绍过的"选择性通知",这个功能是非常重要的,而且在Condition类中是默认提供的。

  而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的WATING线程,没有选择权,会出现相当大的效率问题。

例如:一个简单的等待/通知的例子

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* ReentrantLock結合Condition实现等待/通知
*
* @author Administrator
*
*/
public class Demo2 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo2.class);
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition(); public void await() {
try {
lock.lock();
LOGGER.info("threadName -> {} start await", Thread.currentThread().getName());
condition1.await();
LOGGER.info("threadName -> {} end await", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOGGER.info("threadName -> {} unlock", Thread.currentThread().getName());
lock.unlock();
}
LOGGER.info("threadName -> {} exit await 退出await方法", Thread.currentThread().getName());
} public void signal() {
try {
lock.lock();
LOGGER.info("threadName -> {} start signal", Thread.currentThread().getName());
condition1.signal();
LOGGER.info("threadName -> {} end signal", Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
LOGGER.info("threadName -> {} unlock", Thread.currentThread().getName());
lock.unlock();
}
LOGGER.info("exit signal 退出signal方法");
} public static void main(String[] args) {
final Demo2 demo2 = new Demo2();
new Thread(new Runnable() {
@Override
public void run() {
demo2.await();
}
}, "threadA").start();
new Thread(new Runnable() {
@Override
public void run() {
demo2.signal();
}
}, "threadB").start();
}
}

结果:

11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadA start await
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadB start signal
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadB end signal
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadB unlock
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] exit signal 退出signal方法
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadA end await
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadA unlock
11:00:12 [cn.qlq.thread.eleven.Demo2]-[INFO] threadName -> threadA exit await 退出await方法

注意:

  condition对象的await()\signal()\signalAll()必须在获得lock.lock()占用锁之后调用,而且最后必须手动释放锁。

  Object的wait()方法相当于Condition的await()方法,会释放锁;

  Object的wait(long)方法相当于Condition类的await(long)方法,可以指定多少秒后自动唤醒转入对象监视器的就绪队列;

  Object类的notify()方法相当于Condition的signal()方法,Object的notifyAll()方法相当于Condition类的signalAll()方法。

3 使用多个Condition实现等待/通知部分线程

  使用ReentrantLock创建多个Condition对象之后可以实现唤醒指定的线程,这是控制部分线程行为的方便方式。可以理解为将线程分组,每一组对应一个condition对象。

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* ReentrantLock結合Condition实现等待/通知,唤醒和等待部分线程
*
* @author Administrator
*
*/
public class Demo3 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo3.class);
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition(); public void awaitA() {
try {
lock.lock();
LOGGER.info("threadName -> {} start await", Thread.currentThread().getName());
Thread.sleep(1 * 1000);
conditionA.await();
LOGGER.info("threadName -> {} end await", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void signalA() {
try {
lock.lock();
LOGGER.info("threadName -> {} start signal", Thread.currentThread().getName());
Thread.sleep(1 * 1000);
conditionA.signal();
LOGGER.info("threadName -> {} end signal", Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void awaitB() {
try {
lock.lock();
LOGGER.info("threadName -> {} start await", Thread.currentThread().getName());
Thread.sleep(1 * 1000);
conditionB.await();
LOGGER.info("threadName -> {} end await", Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void signalB() {
try {
lock.lock();
LOGGER.info("threadName -> {} start signal", Thread.currentThread().getName());
Thread.sleep(1 * 1000);
conditionB.signal();
LOGGER.info("threadName -> {} end signal", Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) {
final Demo3 demo3 = new Demo3();
new Thread(new Runnable() {
@Override
public void run() {
demo3.awaitA();
}
}, "threadA").start();
new Thread(new Runnable() {
@Override
public void run() {
demo3.signalA();
}
}, "threadA1").start();
// 访问conditionB
new Thread(new Runnable() {
@Override
public void run() {
demo3.awaitB();
}
}, "threadB").start();
new Thread(new Runnable() {
@Override
public void run() {
demo3.signalB();
}
}, "threadB1").start();
}
}

结果: (多个线程还是公用一个锁,但是可以用多个Condition实现阻塞与唤醒部分线程。也就是多个Condition将对象阻塞到多个队列中)

11:18:31 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadA start await
11:18:32 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadA1 start signal
11:18:33 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadA1 end signal
11:18:33 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadB start await
11:18:34 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadB1 start signal
11:18:35 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadB1 end signal
11:18:35 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadA end await
11:18:35 [cn.qlq.thread.eleven.Demo3]-[INFO] threadName -> threadB end await

4 公平锁与非公平锁

  公平锁与非公平锁:公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁。从这个角度讲,synchronized其实就是一种非公平锁。

  ReentrantLock类有一个单一参数的构造方法,接受boolean类型的数据,传入true表示创建的是公平锁,传入false创建的是非公平锁(不带参数的默认创建非公平锁)

    public ReentrantLock() {
sync = new NonfairSync();
} /**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

(1)公平锁的测试:

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 公平锁与非公平锁
*
* @author Administrator
*
*/
public class Demo5 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class); private Lock lock = new ReentrantLock(true); public void testMethod() {
try {
lock.lock();
System.out.println("★ThreadName" + Thread.currentThread().getName() + "获得锁");
} finally {
lock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
final Demo5 demo5 = new Demo5();
Runnable runnable = new Runnable() {
public void run() {
System.out.println("☆线程" + Thread.currentThread().getName() + "运行了");
demo5.testMethod();
}
};
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++)
threads[i] = new Thread(runnable);
for (int i = 0; i < 5; i++)
threads[i].start(); }
}

结果: (先运行的先获得锁---只能说是基本上是FIFO,也并不是绝对的)

☆线程Thread-0运行了
☆线程Thread-4运行了
☆线程Thread-3运行了
☆线程Thread-2运行了
☆线程Thread-1运行了
★ThreadNameThread-0获得锁
★ThreadNameThread-4获得锁
★ThreadNameThread-3获得锁
★ThreadNameThread-2获得锁
★ThreadNameThread-1获得锁

(2)非公平锁的测试:

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 公平锁与非公平锁
*
* @author Administrator
*
*/
public class Demo5 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo5.class); private Lock lock = new ReentrantLock(false); public void testMethod() {
try {
lock.lock();
System.out.println("★ThreadName" + Thread.currentThread().getName() + "获得锁");
} finally {
lock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
final Demo5 demo5 = new Demo5();
Runnable runnable = new Runnable() {
public void run() {
System.out.println("☆线程" + Thread.currentThread().getName() + "运行了");
demo5.testMethod();
}
};
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++)
threads[i] = new Thread(runnable);
for (int i = 0; i < 5; i++)
threads[i].start(); }
}

结果: (不一定先运行的先获得锁)

☆线程Thread-1运行了
☆线程Thread-2运行了
☆线程Thread-3运行了
☆线程Thread-0运行了
☆线程Thread-4运行了
★ThreadNameThread-2获得锁
★ThreadNameThread-3获得锁
★ThreadNameThread-0获得锁
★ThreadNameThread-4获得锁
★ThreadNameThread-1获得锁

5  使用condition实现线程按顺序执行

  使用condition实现线程按顺序执行(比如创建10个线程,每个线程打印自己的名字,按照1-10打印)

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 使用condition实现线程按顺序执行(比如创建10个线程,每个线程打印自己的名字,按照1-10打印)
*
* @author Administrator
*
*/
public class Demo14 {
private ReentrantLock lock = new ReentrantLock();
private Condition newCondition = lock.newCondition();
private static final Logger LOGGER = LoggerFactory.getLogger(Demo14.class); private volatile int currentNum = 1;// 标记当前线程执行到第几个线程 public void printName() {
try {
lock.lock();
while (!String.valueOf(currentNum).equals(Thread.currentThread().getName())) {
newCondition.await();
}
LOGGER.info("threadName - > {} ", Thread.currentThread().getName());
newCondition.signalAll();
currentNum++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
final Demo14 demo8 = new Demo14();
Runnable runnable = new Runnable() {
@Override
public void run() {
demo8.printName();
}
}; Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(runnable, (i + 1) + "");
}
for (int i = 0; i < 10; i++) {
threads[i].start();
} }
}

结果:

6 其他方法研究

1. getHoldCount()、getQueueLength()方法、getWaitQueueLength(condition)方法

  • getHoldCount()  返回当前线程保持此锁定的个数,也就是调用lock方法的此时,可以理解为重入锁的次数
  • getQueueLength()方法  返回正等待此获取此锁定的线程的估计数,比如有5个线程,1个线程首先执行await(),那么在调用getQueueLength()方法后返回值是4,说明有4个线程等待lock的释放。(可以理解为等待锁的线程数)
package cn.qlq.thread.eleven;

import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
* 其他方法研究 getQueueLength--返回正等待此获取此锁定的线程的估计数
*
* @author Administrator
*
*/
public class Demo6 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo6.class); private ReentrantLock lock = new ReentrantLock(true); public void testMethod() {
try {
lock.lock();
LOGGER.debug("testMethod lock,getHoldCount()->{},getQueueLength->{}", lock.getHoldCount(),
lock.getQueueLength());
// 调用testMethod2(),模拟锁重入
testMethod2();
} finally {
lock.unlock();
LOGGER.debug("testMethod unlock,getHoldCount()->{},getQueueLength->{}", lock.getHoldCount(),
lock.getQueueLength());
}
} public void testMethod2() {
try {
lock.lock();
LOGGER.debug("testMethod2 lock,getHoldCount()->{},getQueueLength->{}", lock.getHoldCount(),
lock.getQueueLength());
} finally {
lock.unlock();
LOGGER.debug("testMethod2 unlock,getHoldCount()->{},getQueueLength->{}", lock.getHoldCount(),
lock.getQueueLength());
}
} public void testMethod3() {
try {
lock.lock();
LOGGER.debug("testMethod3 lock,getHoldCount()->{},getQueueLength->{}", lock.getHoldCount(),
lock.getQueueLength());
} finally {
lock.unlock();
LOGGER.debug("testMethod3 unlock,getHoldCount()->{},getQueueLength->{}", lock.getHoldCount(),
lock.getQueueLength());
}
} public static void main(String[] args) throws InterruptedException {
final Demo6 demo6 = new Demo6();
new Thread(new Runnable() {
public void run() {
demo6.testMethod();
}
}, "thread--t1").start(); new Thread(new Runnable() {
@Override
public void run() {
demo6.testMethod3();
}
}, "thread--t2").start();
}
}

结果:

15:22:47 [cn.qlq.thread.eleven.Demo6]-[DEBUG] testMethod lock,getHoldCount()->1,getQueueLength->1
15:22:47 [cn.qlq.thread.eleven.Demo6]-[DEBUG] testMethod2 lock,getHoldCount()->2,getQueueLength->1
15:22:47 [cn.qlq.thread.eleven.Demo6]-[DEBUG] testMethod2 unlock,getHoldCount()->1,getQueueLength->1
15:22:47 [cn.qlq.thread.eleven.Demo6]-[DEBUG] testMethod unlock,getHoldCount()->0,getQueueLength->1
15:22:47 [cn.qlq.thread.eleven.Demo6]-[DEBUG] testMethod3 lock,getHoldCount()->1,getQueueLength->0
15:22:47 [cn.qlq.thread.eleven.Demo6]-[DEBUG] testMethod3 unlock,getHoldCount()->0,getQueueLength->0

  • getWaitQueueLength(condition)方法返回此Condition对象阻塞队列的数量

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo7 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo7.class);
private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition(); public void awaitA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
LOGGER.info("threadName -> {},getWaitQueueLength(conditionA)->{} ", Thread.currentThread().getName(),
lock.getWaitQueueLength(conditionA));
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void signalA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
LOGGER.info("threadName -> {},getWaitQueueLength(conditionA)->{} ", Thread.currentThread().getName(),
lock.getWaitQueueLength(conditionA));
conditionA.signal();
LOGGER.info("threadName -> {},getWaitQueueLength(conditionA)->{} ", Thread.currentThread().getName(),
lock.getWaitQueueLength(conditionA));
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) {
final Demo7 demo7 = new Demo7();
Runnable await = new Runnable() {
public void run() {
demo7.awaitA();
}
};
new Thread(await, "threadA1").start();
new Thread(await, "threadA2").start();
new Thread(await, "threadA3").start(); // 访问signal
new Thread(new Runnable() {
@Override
public void run() {
demo7.signalA();
}
}, "threadS1").start();
}
}

结果:

15:40:27 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadA2,getWaitQueueLength(conditionA)->0
15:40:28 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadA3,getWaitQueueLength(conditionA)->1
15:40:29 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadA1,getWaitQueueLength(conditionA)->2
15:40:30 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadS1,getWaitQueueLength(conditionA)->3
15:40:30 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadS1,getWaitQueueLength(conditionA)->2

修改上面signalA的方法唤醒所有:

    public void signalA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
LOGGER.info("threadName -> {},getWaitQueueLength(conditionA)->{} ", Thread.currentThread().getName(),
lock.getWaitQueueLength(conditionA));
conditionA.signalAll();
LOGGER.info("threadName -> {},getWaitQueueLength(conditionA)->{} ", Thread.currentThread().getName(),
lock.getWaitQueueLength(conditionA));
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

结果:

15:41:58 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadA1,getWaitQueueLength(conditionA)->0
15:41:59 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadA2,getWaitQueueLength(conditionA)->1
15:42:00 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadA3,getWaitQueueLength(conditionA)->2
15:42:01 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadS1,getWaitQueueLength(conditionA)->3
15:42:01 [cn.qlq.thread.eleven.Demo7]-[INFO] threadName -> threadS1,getWaitQueueLength(conditionA)->0

2.hasQueuedThreads()、 hasQueuedThread(thread)、lock.hasWaiters(conditionA)方法

  • hasQueuedThreads()  方法返回此锁是否有线程在等待获取此锁
  • hasQueuedThread(thread)  查询指定的线程是否正在等待获取此锁
  • lock.hasWaiters(condition)  此方法必须在获取到lock(lock.lock())之后调用,查询指定的condition是否有等待的对象
package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo8 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo8.class);
private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition(); public void awaitA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void signalA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) {
final Demo8 demo8 = new Demo8();
Runnable await = new Runnable() {
public void run() {
demo8.awaitA();
}
};
new Thread(await, "threadA2").start();
new Thread(await, "threadA3").start();
Thread thread = new Thread(await, "threadA1");
thread.start(); // 访问signal
new Thread(new Runnable() {
@Override
public void run() {
demo8.signalA();
}
}, "threadS1").start(); // 返回thread是否在等待获取此锁
System.out.println(demo8.getLock().hasQueuedThread(thread));
// 获取是否有等待线程
System.out.println(demo8.getLock().hasQueuedThreads());
} public ReentrantLock getLock() {
return lock;
} public void setLock(ReentrantLock lock) {
this.lock = lock;
}
}

结果:

false
true

package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo9 {
private static final Logger LOGGER = LoggerFactory.getLogger(Demo9.class);
private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition(); public void awaitA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void signalA() {
try {
lock.lock();
Thread.sleep(1 * 1000);
System.out.println(lock.hasWaiters(conditionA));
conditionA.signalAll();
System.out.println(lock.hasWaiters(conditionA));
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) {
final Demo9 demo8 = new Demo9();
Runnable await = new Runnable() {
public void run() {
demo8.awaitA();
}
};
Thread thread = new Thread(await, "threadA1");
thread.start();
new Thread(await, "threadA2").start();
new Thread(await, "threadA3").start(); // 访问signal
new Thread(new Runnable() {
@Override
public void run() {
demo8.signalA();
}
}, "threadS1").start();
}
}

结果:

true
false

3.   isFair(),isLocked(),isHeldByCurrentThread()方法

  •   isFair()  判断一个锁是否是公平锁
  •   isLocked()  判断一个锁是否已经锁住,也就是判断是否被任意线程锁定
  •   isHeldByCurrentThread()  判断当前线程是否拥有指定的锁
package cn.qlq.thread.eleven;

import java.util.concurrent.locks.ReentrantLock;

/**
*
* @author Administrator
*
*/
public class Demo10 {
private ReentrantLock lock = new ReentrantLock(); public void awaitA() {
lock.lock();
System.out.println("isFair -> " + lock.isFair());
System.out.println("isLocked -> " + lock.isLocked());
System.out.println("isHeldByCurrentThread -> " + lock.isHeldByCurrentThread());
lock.unlock();
} public ReentrantLock getLock() {
return lock;
} public void setLock(ReentrantLock lock) {
this.lock = lock;
} public static void main(String[] args) {
final Demo10 demo8 = new Demo10();
demo8.awaitA();
}
}

结果:

isFair -> false
isLocked -> true
isHeldByCurrentThread -> true

4. lockInterruptibly()、tryLock()、tryLock(long, TimeUnit)---轮询锁与定时锁

  • lockInterruptibly()方法如果在获取锁的情况下如果收到中断信号会进入中断异常
package cn.qlq.thread.eleven;

import java.util.concurrent.locks.ReentrantLock;

/**
*
* @author Administrator
*
*/
public class Demo11 {
private ReentrantLock lock = new ReentrantLock(); public void awaitA() {
try {
lock.lockInterruptibly();
System.out.println("isFair -> " + lock.isFair());
System.out.println("isLocked -> " + lock.isLocked());
System.out.println("isHeldByCurrentThread -> " + lock.isHeldByCurrentThread());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) {
final Demo11 demo8 = new Demo11();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo8.awaitA();
}
});
thread.start();
// 发出中断信号
thread.interrupt();
}
}

结果:

java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1219)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:340)
at cn.qlq.thread.eleven.Demo11.awaitA(Demo11.java:15)
at cn.qlq.thread.eleven.Demo11$1.run(Demo11.java:31)
at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
at cn.qlq.thread.eleven.Demo11.awaitA(Demo11.java:22)
at cn.qlq.thread.eleven.Demo11$1.run(Demo11.java:31)
at java.lang.Thread.run(Thread.java:745)

如果不发出中断信号:

    public static void main(String[] args) {
final Demo11 demo8 = new Demo11();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo8.awaitA();
}
});
thread.start();
}

结果:

isFair -> false
isLocked -> true
isHeldByCurrentThread -> true

  • tryLock可以获取有个仅仅没有被其他线程占用的锁,返回一个boolean类型的值代表是否获取锁成功
package cn.qlq.thread.eleven;

import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo11 {
private ReentrantLock lock = new ReentrantLock();
private static final Logger LOGGER = LoggerFactory.getLogger(Demo11.class); public void awaitA() {
if (lock.tryLock()) {
LOGGER.info("threadName -> {} , isFair -> " + lock.isFair(), Thread.currentThread().getName());
LOGGER.info("threadName -> {} ,isLocked -> " + lock.isLocked(), Thread.currentThread().getName());
LOGGER.info("threadName -> {} ,isHeldByCurrentThread -> " + lock.isHeldByCurrentThread(),
Thread.currentThread().getName());
lock.unlock();
} else {
LOGGER.info("threadName -> {} 没有获得锁 ", Thread.currentThread().getName());
}
} public static void main(String[] args) {
final Demo11 demo8 = new Demo11();
Runnable runnable = new Runnable() {
@Override
public void run() {
demo8.awaitA();
}
};
Thread thread = new Thread(runnable, "thread");
Thread thread2 = new Thread(runnable, "thread2");
thread.start();
thread2.start();
}
}

结果:

16:53:03 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread 没有获得锁
16:53:03 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread2 , isFair -> false
16:53:03 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread2 ,isLocked -> true
16:53:03 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread2 ,isHeldByCurrentThread -> true

  • lock.tryLock(long, TimeUnit.MILLISECONDS) 如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定
package cn.qlq.thread.eleven;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo11 {
private ReentrantLock lock = new ReentrantLock();
private static final Logger LOGGER = LoggerFactory.getLogger(Demo11.class); public void awaitA() {
try {
if (lock.tryLock(2000, TimeUnit.MILLISECONDS)) {
LOGGER.info("threadName -> {} , isFair -> " + lock.isFair(), Thread.currentThread().getName());
LOGGER.info("threadName -> {} ,isLocked -> " + lock.isLocked(), Thread.currentThread().getName());
LOGGER.info("threadName -> {} ,isHeldByCurrentThread -> " + lock.isHeldByCurrentThread(),
Thread.currentThread().getName());
// 释放锁
lock.unlock();
} else {
LOGGER.info("threadName -> {} 没有获得锁 ", Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
final Demo11 demo8 = new Demo11();
Runnable runnable = new Runnable() {
@Override
public void run() {
demo8.awaitA();
}
};
Thread thread = new Thread(runnable, "thread");
Thread thread2 = new Thread(runnable, "thread2");
thread.start();
thread2.start();
}
}

结果:

16:56:14 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread2 , isFair -> false
16:56:14 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread2 ,isLocked -> true
16:56:14 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread2 ,isHeldByCurrentThread -> true
16:56:14 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread , isFair -> false
16:56:14 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread ,isLocked -> true
16:56:14 [cn.qlq.thread.eleven.Demo11]-[INFO] threadName -> thread ,isHeldByCurrentThread -> true

5. Condition.awaitUninterruptibly()、Condition.awaitUntil(date)方法

  • Condition.awaitUninterruptibly()是在await的过程中如果线程收到中断信号不会抛出异常(可中断的锁获取)
package cn.qlq.thread.eleven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo12 {
private ReentrantLock lock = new ReentrantLock();
private Condition newCondition = lock.newCondition();
private static final Logger LOGGER = LoggerFactory.getLogger(Demo12.class); public void awaitA() {
LOGGER.info("threadName -> {} 进入方法,等待锁 ", Thread.currentThread().getName());
try {
lock.lock();
LOGGER.info("threadName -> {} begain await ", Thread.currentThread().getName());
newCondition.awaitUninterruptibly();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
final Demo12 demo8 = new Demo12();
Runnable runnable = new Runnable() {
@Override
public void run() {
demo8.awaitA();
}
};
Thread thread = new Thread(runnable, "thread"); Thread.sleep(1 * 1000);
thread.interrupt(); }
}

结果:

如果修改为await之后再次中断:

    public void awaitA() {
LOGGER.info("threadName -> {} 进入方法,等待锁 ", Thread.currentThread().getName());
try {
lock.lock();
LOGGER.info("threadName -> {} begain await ", Thread.currentThread().getName());
newCondition.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

结果:

  • Condition.awaitUntil(date)是停止到指定时间如果没有被唤醒自动唤醒
package cn.qlq.thread.eleven;

import java.util.Date;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /**
*
* @author Administrator
*
*/
public class Demo13 {
private ReentrantLock lock = new ReentrantLock();
private Condition newCondition = lock.newCondition();
private static final Logger LOGGER = LoggerFactory.getLogger(Demo13.class); public void awaitA() {
LOGGER.info("threadName -> {} 进入方法,等待锁 ", Thread.currentThread().getName());
try {
lock.lock();
LOGGER.info("threadName -> {} begain await ", Thread.currentThread().getName());
Date deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 3);
newCondition.awaitUntil(deadline);
LOGGER.info("threadName -> {} end await ", Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
final Demo13 demo8 = new Demo13();
Runnable runnable = new Runnable() {
@Override
public void run() {
demo8.awaitA();
}
};
Thread thread = new Thread(runnable, "thread");
thread.start();
}
}

结果: (3秒钟后自己唤醒,这个方法是停止到某一时间点)

17:16:18 [cn.qlq.thread.eleven.Demo13]-[INFO] threadName -> thread 进入方法,等待锁
17:16:18 [cn.qlq.thread.eleven.Demo13]-[INFO] threadName -> thread begain await
17:16:21 [cn.qlq.thread.eleven.Demo13]-[INFO] threadName -> thread end await

总结: 关于Lock和Synchronized两种同步方式的比较:

1.性能方面,两者实际是差不多的,JVM不断的对synchronized进行优化,所以性能基本没多大差别
2.synchronized是关键字,就和if...else...一样,是语法层面的实现,因此synchronized获取锁以及释放锁都是Java虚拟机帮助用户完成的;ReentrantLock是类层面的实现,因此锁的获取以及锁的释放都需要用户自己去操作。而且synchronized遇到错误会释放锁,而ReentrantLock不会自动释放。类和关键字最大的区别就是类使用更加灵活。

3.synchronized是不公平锁,而ReentrantLock可以指定锁是公平的还是非公平的。公平和非公平体现的就是获取锁的顺序是否是FIFO的顺序获取。

4.synchronized实现等待/通知机制通知的线程是随机的,ReentrantLock结合Condition可以实现等待/通知机制可以有选择性地通知,这点有时候便于理解。

5.和synchronized相比,ReentrantLock提供给用户多种方法用于锁信息的获取,比如可以获取是否有线程在等待锁、锁重入的层数等信息;而且显示锁增加了可中断的锁获取方式,以及tryLock轮询锁或者定时锁等方法

  学习完ReentrantLock+Condition的使用之后可以完成一个简单的生产者消费者的例子,参考我的另一篇:https://www.cnblogs.com/qlqwjy/p/10115756.html

补充:重入锁也可能造成死锁:

package Thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* 测试死锁 思路:两个线程,每个线程占有不同的资源,等待其他资源
*
* @author: qlq
* @date : 2018年6月14日上午10:37:24
*/
public class DeadLockTest1 { public static void main(String[] args) {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
MyThread1 t1 = new MyThread1(true,lock1,lock2);
MyThread1 t2 = new MyThread1(false,lock1,lock2);
t1.start();
t2.start();
} } class MyThread1 extends Thread {
private boolean flag;// 标记走哪个线路
private Lock lock1;//第一把锁
private Lock lock2;//第二把锁 public boolean isFlag() {
return flag;
} public void setFlag(boolean flag) {
this.flag = flag;
} protected MyThread1(boolean flag, Lock lock1, Lock lock2) {
super();
this.flag = flag;
this.lock1 = lock1;
this.lock2 = lock2;
} @Override
public void run() {
if (flag) {// 占有资源A,等待资源B
if(lock1.tryLock()){
try {
Thread.sleep(2*1000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"占有第一把锁,等待第二把锁");
try {
if(lock2.tryLock(50*1000,TimeUnit.SECONDS)){
try {
Thread.sleep(2*1000);
System.out.println(Thread.currentThread().getName()+"占有第二把锁");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"释放第二把锁");
lock2.unlock();
System.out.println(Thread.currentThread().getName()+"释放第一把锁");
lock1.unlock();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
} else {// 占有第二把锁,等待第一把锁
if(lock2.tryLock()){
try {
Thread.sleep(2*1000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"占有第二把锁,等待第一把锁");
try {
if(lock1.tryLock(50*1000,TimeUnit.SECONDS)){
try {
Thread.sleep(2*1000);
System.out.println(Thread.currentThread().getName()+"占有第一把锁");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"释放第一把锁");
lock1.unlock();
System.out.println(Thread.currentThread().getName()+"释放第二把锁");
lock2.unlock();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
}
}
}

结果:

Thread-0占有第一把锁,等待第二把锁
Thread-1占有第二把锁,等待第一把锁

解决办法:在正确的地方释放锁。。。。

注意:锁必须是同一把锁才会生效,如果锁作为局部变量是不会生效的,局部变量是每个线程一把锁。。。。。

Lock类-ReentrantLock的使用的更多相关文章

  1. (四)Lock,ReentrantLock,ReentrantReadWriteLock类的使用以及相关api---synchronized进阶

    这篇博客记录了Lock,ReentrantLock,ReentrantReadWriteLock类的使用以及其一些api: 码字不易~~另外<java多线程编程核心技术>这本书读着很爽 前 ...

  2. Lock、ReentrantLock、synchronized、ReentrantReadWriteLock使用

    先来看一段代码,实现如下打印效果: 1 2 A 3 4 B 5 6 C 7 8 D 9 10 E 11 12 F 13 14 G 15 16 H 17 18 I 19 20 J 21 22 K 23 ...

  3. 《深入浅出 Java Concurrency》—锁紧机构(一)Lock与ReentrantLock

    转会:http://www.blogjava.net/xylz/archive/2010/07/05/325274.html 前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最 ...

  4. Java多线程基础——Lock类

    之前已经说道,JVM提供了synchronized关键字来实现对变量的同步访问以及用wait和notify来实现线程间通信.在jdk1.5以后,JAVA提供了Lock类来实现和synchronized ...

  5. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

    原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...

  6. Lock、ReentrantLock、ReentrantReadWriteLock区别

    Lock Lock相比于synchronized具有更强大的功能,在jdk1.6之前,锁竞争激烈的情况下使用lock的实现类ReentrantLock甚至比synchronized具有更好的性能,1. ...

  7. 016-并发编程-java.util.concurrent.locks之-Lock及ReentrantLock

    一.概述 重入锁ReentrantLock,就是支持重进入的锁 ,它表示该锁能够支持一个线程对资源的重复加锁.支持公平性与非公平性选择,默认为非公平. 以下梳理ReentrantLock.作为依赖于A ...

  8. Java并发编程:synchronized、Lock、ReentrantLock以及ReadWriteLock的那些事儿

    目录 前言 synchronized用法 修饰方法 修饰实例方法 修饰静态方法 同步代码块 引出Lock Lock用法 子类:ReentrantLock 读写分离锁:ReadWriteLock Loc ...

  9. synchronized、Lock、ReentrantLock、ReadWriteLock

    synchronized:同步锁,是java内置的关键字.当一个线程A执行到被synchronized修饰的方法时,其他线程B如果也要执行这个方法,那么B只能等A执行完方法释放锁后才能获取资源锁执行s ...

随机推荐

  1. (sort 排序)P1583 魔法照片 洛谷

    题目描述 一共有n(n≤20000)个人(以1--n编号)向佳佳要照片,而佳佳只能把照片给其中的k个人.佳佳按照与他们的关系好坏的程度给每个人赋予了一个初始权值W[i].然后将初始权值从大到小进行排序 ...

  2. django 跨域解决方案

    使用django-cors-headers模块 github:https://github.com/ottoyiu/django-cors-headers 官方文档中有详细说明 简要配置 1.安装 p ...

  3. 剑指Offer_编程题_12

    题目描述 给定一个double类型的浮点数base和int类型的整数exponent.求base的exponent次方. class Solution { public: double Power(d ...

  4. script weixin app / weixin xiao chen xu

    s 开发人员: 1.设置权限程序员管理人员-管理体验者-配置测试人员的真实微信号2.微信小程序的后台-上传体验版代码-生成体验码二维码测试人员: 1.你的台式机/笔记本申请内外网.有usb权限2.有W ...

  5. Rancher之Pipeline JAVA demo

    Rancher Pipeline Pipeline,简单来说,就是一套运行于Rancher上的工作流框架,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂发布流程. Ranc ...

  6. nginx的下载、编译安装和启动

    一.nginx简介 nginx(“engine x”)是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP代理服务器.nginx是由Igor Sysoev为俄罗斯访问量第二的R ...

  7. Linux 内核里的数据结构:位图(bitmap)

    注: 本文由 LCTT 原创翻译,Linux中国 荣誉推出 Linux 内核中的位数组和位操作 除了不同的基于链式和树的数据结构以外,Linux 内核也为位数组(或称为位图(bitmap))提供了 A ...

  8. sqlalchemy外键和relationship查询

    前面的文章中讲解了外键的基础知识和操作,上一篇文章讲解了sqlalchemy的基本操作.前面两篇文章都是作为铺垫,为下面的文章打好基础.记得初一时第一次期中考试时考的不好,老爸安慰我说:“学习是一个循 ...

  9. Centos配置ARP和Tomcat Native

    Tomcat Native是用于Tomcat的一个可选组件,能够允许Tomcat使用一定的本地资源.性能.兼容性. 具体来说,Tomcat Native给了Tomcat访问 Apache Portab ...

  10. jspdf简单使用

    安装 npm install jspdf --save 英文输出 import jsPDF from 'jspdf-customfonts' let doc = new jsPDF() doc.tex ...