想了解更多JUC的知识
——JUC并发编程合集
1. Lock接口
- Lock接口有三个实现类:ReentrantLock,ReentrantLockReadWriteLock.ReadLock,ReentrantLockReadWrite.WriteLock
-
ReentrantLock:
-
Lock接口的主要方法:
- void lock():获取锁,如果锁被暂用则一直等待
- void unlock():释放锁
- boolean tryLock(): 尝试获取锁,如果获取锁的时候锁被占用就返回false,否则返回true
- boolean tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
- void lockInterruptibly():获取锁,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
2. synchronized和lock演示
-
传统的synchronized
public class SaleTicketDemo01 { public static void main(String[] args) { //并发:多线程操作操作同一个资源类,把资源列放入线程 Ticket ticket = new Ticket(); //@FunctionalInterface:函数式接口,jdk8 lambda表达式(参数)->{ 代码 } new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"A").start(); new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"B").start(); new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"C").start(); } } //资源类 OOP面向对象编程 class Ticket{ //属性、方法 private int number = 30; //卖票的方式 public synchronized void sale(){ if (number > 0){ System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余:" + number); } } }
-
lock锁
public class SaleTicketDemo02 { public static void main(String[] args) { //并发:多线程操作操作同一个资源类,把资源列放入线程 Ticket2 ticket = new Ticket2(); //@FunctionalInterface:函数式接口,jdk8 lambda表达式(参数)->{ 代码 } new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"A").start(); new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"B").start(); new Thread(()->{ for (int i = 1; i < 40; i++) ticket.sale(); },"C").start(); } } //lock三部曲 //1.Lock lock=new ReentrantLock(); //2.lock.lock() 加锁 //3.finally=> 解锁:lock.unlock(); //资源类 OOP class Ticket2{ //属性、方法 private int number = 30; Lock lock = new ReentrantLock(); //卖票的方式 public void sale(){ lock.lock(); try{ //业务代码 if (number > 0){ System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余:" + number); } }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } }
3. Synchronized与Lock的区别
- 存在的层次
- synchronized是Java的一个关键字,在jvm层面上
- Lock是一个接口
- 锁的释放
- synchronized会自动释放锁
- 获取这个锁的线程执行完同步代码,释放锁
- 线程执行发生异常,jvm会让线程释放锁
- Lock必须手动加锁和手动释放锁(在try/catch/finally的finally中释放锁),如果不手动释放锁,容易造成死锁
- synchronized会自动释放锁
- 锁的状态
- synchronized无法判断和获取锁的状态
- Lock可以判断锁的状态
- 锁的获取
- synchronized,如果有两个线程,线程1获取锁,线程2必须等待线程1释放锁;如果线程1阻塞,线程2会一直等待
- Lock不一定会一直等待,可以通过tryLock去尝试获取锁,不会造成过久的等待
- 锁的类型
- synchronized是可重入、不可中断、非公平的锁
- Lock可重入、可判断的锁,而且可以根据需要自己设置公平锁和非公平锁
- 性能
- synchronized适合锁少量的代码同步问题
- Lock适合锁大量的同步代码
4. 生产者消费者问题
4.1 synchronized版本
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
class Data {
private int num = 0;
// +1
public synchronized void increment() throws InterruptedException {
// 判断等待
if (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 判断等待
if (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
如果上述的流程从创建两个线程变为创建四个线程,则会出现虚假唤醒的问题
解决的方式:将if判断换成while判断,即:
... while(num != 0){ this.wait(); } ... while(num == 0){ this.wait(); } ...
用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码;而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
4.2 Lock版本
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0;i<10;i++){
data.increment();
}
},"A").start();
new Thread(() -> {
for (int i = 0;i<10;i++){
data.decrement();
}
},"B").start();
new Thread(() -> {
for (int i = 0;i<10;i++){
data.increment();
}
},"C").start();
new Thread(() -> {
for (int i = 0;i<10;i++){
data.decrement();
}
},"D").start();
}
}
class Data{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment(){
//加锁
lock.lock();
try {
while (number != 0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + ":" + number);
//通知其他线程
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
}
public void decrement(){
lock.lock();
try {
while (number == 0){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + ":" + number);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
从中可以得出:
synchronized是自动释放锁的,而lock是通过lock.lock()来手动加锁和lock.unlock()来手动释放锁;
synchronize是通过wait()来等待,而lock是lock.newCondition()创建一个Condition对象,通过Condition的await()方法来等待
synchronize是通过notifyAll()来通知其他线程,lock是通过Condition的condition.signalAll()来通知其他线程
4.3 Condition的优势
-
Condition可以精准的通知和唤醒的线程
//案例,让A执行完调用B,B执行完调用C,C执行完调用A public class ConditionDemo { public static void main(String[] args) { Data data = new Data(); new Thread(() -> { for (int i = 0; i < 10; i++) { data.printA(); } },"A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { data.printB(); } },"B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { data.printC(); } },"C").start(); } } class Data { private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); private int num = 1; // 1A 2B 3C public void printA() { lock.lock(); try { while (num != 1) { condition1.await(); } System.out.println(Thread.currentThread().getName() + "==> A" ); num = 2; condition2.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printB() { lock.lock(); try { while (num != 2) { condition2.await(); } System.out.println(Thread.currentThread().getName() + "==> B" ); num = 3; condition3.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public void printC() { lock.lock(); try { while (num != 3) { condition3.await(); } System.out.println(Thread.currentThread().getName() + "==> C" ); num = 1; condition1.signal(); }catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } }
5. 八锁现象(彻底理解)
-
如何判断的是谁?(锁住对象?锁住Class)
-
案例一:两个同步方法,先执行发短信还是打电话?
public class dome01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { phone.sendMsg(); },"A").start(); //睡眠一秒 TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); },"B").start(); } } class Phone { public synchronized void sendMsg() { System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
案例一的输出结果总是先输出"发短信",后输出"打电话"。
思考:是因为顺序执行吗?
结论:并不是因为顺序执行,Java的线程调度是抢占式的,即线程A和线程B都有可能抢占到CPU。在线程A和线程B调用了start方法后,A和B都进入了就绪状态,但是TimeUnit.SECONDS.sleep(1)的存在会让A和B不是同时进入就绪态,即A总会比B先抢占到CPU。而synchronized锁住的是对象的调用者,两个方法共用同一把锁,谁先拿到谁先用,另外一个等待。所以输出结果总是先输出"发短信",后输出"打电话"。
-
案例二:在案例一的基础上,让发短信延时4s
public class demo02 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { phone.sendMsg(); },"A").start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); },"B").start(); } } class Phone { public synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
案例二的输出结果总是先输出"发短信",后输出"打电话"。
思考:为什么结果与案例一一样?
结论:原因与案例一一致,在方法sendMsg里延时4s不会影响线程A先获取锁的使用权
-
案例三:在案例二的基础上再加一个普通方法
public class demo03 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { phone.sendMsg(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.hello(); }).start(); } } class Phone { public synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } public void hello(){ System.out.println("hello!"); } }
案例三的输出结果总是先输出"hello!“,后输出"发短信”。
结论:因为hello()只是一个普通方法,不受synchronized锁的影响,不用等待锁的释放。(间接证明了此时synchronized锁锁住的是对象对方法的调用)
-
案例四:如果我们使用的是两个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?
public class demo04 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone1.sendMsg(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
案例四的输出结果总是先输出"打电话",后输出"发短信"。
结论:两个对象用了两把锁,线程A与线程B获取两个不同的锁互不影响,不会出现等待的情况,但是方法sendMsg在输出"发短信"前睡眠了4s,所以会先输出"打电话"。
-
案例五:如果我们把synchronized的方法加上static变成静态方法,那么顺序又是怎么样的呢?
public class demo05 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { phone.sendMsg(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); }).start(); } } class Phone { public static synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public static synchronized void call() { System.out.println("打电话"); } }
案例五的输出结果总是先输出"发短信",后输出"打电话"。
结论:原因还是线程A先获得CPU,也就先获得了锁,但是这里的锁已经发生了变化。
-
案例六:在案例五的基础上使用两个对象调用两个方法
public class demo06 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone1.sendMsg(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public static synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public static synchronized void call() { System.out.println("打电话"); } }
案例六的输出结果总是先输出"发短信",后输出"打电话"。
思考:为什么不是和案例四一样的结果?
结论:分析后我们不难得知,两个线程仍共用一把锁,原因也很简单,我们在两个方法前加了static关键字。对于static静态方法来说,整个类Class只有一份,对于不同的实例对象使用的是同一份方法,相当于这个方法是属于这个类的,如果静态static方法使用synchronized锁定,那么这个synchronized锁会锁住整个Class对象,不管多少个实例对象,对于静态的锁都只有一把锁,谁先拿到这个锁就先执行,其他的进程都需要等待。
-
案例七:如果使用一个静态同步方法、一个同步方法、一个对象调用顺序是什么
public class demo07 { public static void main(String[] args) throws InterruptedException { Phone phone = new Phone(); new Thread(() -> { phone.sendMsg(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone.call(); }).start(); } } class Phone { public static synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
案例七的输出结果总是先输出"打电话",后输出"发短信"。
结论:此时有两把锁,一把锁锁的是Class类这个模板,另一把锁锁的是对象的调用,故不存在等待情况,而方法sendMsg里睡眠了4s,故先输出"打电话"。
-
案例八:如果我们使用一个静态同步方法、一个同步方法、两个对象调用顺序是什么
public class demo01 { public static void main(String[] args) throws InterruptedException { Phone phone1 = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { phone1.sendMsg(); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { phone2.call(); }).start(); } } class Phone { public static synchronized void sendMsg(){ try { //睡眠4s TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("发短信"); } public synchronized void call() { System.out.println("打电话"); } }
案例八的输出结果总是先输出"打电话",后输出"发短信"。
结论:道理与案例七一样,两把不同的锁。