JUC并发编程之Lock锁和synchrionzed
什么是JUC:即 java.util.concurrent 包的缩写,也就是以下三个包,主要包含了Java原生的并发包和一些常用的工具类。
也就是说,我们掌握了这三个包的内容,那么Java并发编程也就掌握了。
基础知识
进程:是指程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
线程:线程是CPU调度和执行的单位。通过在一个进程中可以包含若干线程,当然一个进程中至少有一个线程,不然这个进程就没有存在的意义了。
并发:多个线程操作同一个资源,也指一个处理器(一核)
并行:多个线程同时执行
公平锁:顾名思义,非常公平,线程不能插队。
非公平锁:顾名思义,非常不公平,线程可以插队,默认使用的都是非公平锁。
可重入锁:获得最外面的锁之后,里面的锁会自动获得。而释放的时候则需要先释放里面的锁之后才会释放外面的锁!
Java默认的两个线程:main 线程、GC线程
线程的几个状态
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
并发编程的本质:充分利用CPU资源
wait和sleep的区别
-
来自不同的类
wait 方法来自 Object类 sleep 方法来自 Thread
-
是否会释放锁
wait会释放锁,sleep不会释放锁
-
使用的范围
wait:必须在同步代码块使用,否则就抛出IllegalMonitorStateException(表示线程已尝试在对象的监视器上等待或通知其他线程等待对象的监视器,而不拥有指定的监视器)异常
sleep:可以在任意地方使用
Lock锁的使用
在使用Lock锁之前我们先看看传统的synchronized锁
传统的synchronized锁
public class Test {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "C").start();
}
}
// 资源类 OOP原则(面向对象程序设计)
class Ticket {
private int number = 30;
// synchronized本质:队列,锁(对象,class)
public synchronized void sale() {
if (number > 0) {
// 卖票操作
System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余:" + number);
}
}
}
Lock锁
Lock锁的实现类如下:
通过下面官方API和源码我们可以得知ReentrantLock默认是非公平锁,如果使用非公平锁则会带来性能上的损失!
API文档如下图:
源码如下图
这里使用的ReentrantLock(可重入锁,后面文章会细说)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock锁的使用
*/
public class SaleTicketDemo02 {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类,把资源类丢入线程
Ticket2 ticket = new Ticket2();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.sale();},"A").start();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.sale();},"B").start();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.sale();},"C").start();
}
}
// 资源类 OOP原则(面向对象程序设计)
class Ticket2 {
// 属性、方法
private int number = 30;
Lock lock = new ReentrantLock();
public void sale() {
lock.lock();// 加锁,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用
try {
// 业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余:" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();// 解锁,必须在finally中进行解锁
}
}
}
注意事项:在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。