并发编程(三)---共享模型之管程
三、共享模型之管程
3.1临界区(Critical Section)
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读取共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如如下的临界区
static int counter = 0;static void increment(){counter++;
}static void decrement(){counter--;
}
3.2synchronized解决方案
互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的
- 阻塞式的解决方案:synchronized、Lock
- 非阻塞式的解决方案:原子变量
synchronized,俗称【对象锁】
本次课使用阻塞式的解决方案: synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意:虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
3.2.1synchronized
语法
synchronized(对象){ //线程1、线程2(blocked)临界区
}
实例
@Slf4j
public class SynchronizedTest {static int count = 0;static Object object = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (object){count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (object){count--;}}});t1.start();t2.start();log.debug("{}",count);}
运行结果
08:18:53.134 [main] DEBUG com.example.Test.SynchronizedTest - 0
可以看到,运行结果为0,说明加锁之后可以确保在一个线程执行的时候,另一个线程会被阻塞,所以无论运行多少次,结果都是0,但是如果不加锁,那么运行结果会有很多种结果
思考
synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
为了加深理解:
如果把synchronized(obj)放在for循环外面,如何理解
- 可以
如果把t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎么样运作?
- 不会,必须是同一个对象
如果t1 synchronize(obj) 而t2没有加会怎么样?如何理解?
- 不会,数据仍然不对应
3.3方法上的synchronized
class Test{public synchronized void test(){}
}//等价于class Test{public void test(){synchronized(this){}}
}
class Test{public synchronized static void test(){}
}//等价于class Test{public static void test(){synchronized(this){}}
}
上面的案例优化
@Slf4j
public class SynchronizedTest {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(() -> {log.debug("{}","线程1run");for (int i = 0; i < 5000; i++) {room.add();}});Thread t2 = new Thread(() -> {log.debug("线程2run");for (int i = 0; i < 5000; i++) {room.decrease();}});t1.start();t2.start();t1.join();t2.join();log.debug("{}",room.getCount());}
}class Room{private int count = 0;public synchronized void add(){count++;}public synchronized void decrease(){count--;}public synchronized int getCount(){return count;}}
运行结果
08:46:26.954 [Thread-0] DEBUG com.example.Test.SynchronizedTest - 线程1run
08:46:26.954 [Thread-1] DEBUG com.example.Test.SynchronizedTest - 线程2run
08:46:26.958 [main] DEBUG com.example.Test.SynchronizedTest - 0
“线程八锁”
其实就是考察synchronized锁住的是哪个对象
案例1 互斥 12或21
@Slf4j
class Number{public synchronized void a(){log.debug("1");}public synchronized void b(){log.debug("2");}
}public static void main (String[] args){Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.a(); }).start();
}
结果
07:46:46.556 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:46:46.558 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
锁住的是同一个对象(this) 所以会有互斥,可能是线程1先执行,也有可能是2先执行
案例2 互斥 12或21
@Slf4j
class Number{public synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b(){log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
结果
07:49:30.848 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:49:30.852 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
同案例一,看谁先调度
案例3
@Slf4j
class Number{public synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b(){log.debug("2");}public void c(){log.debug("3");}
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();new Thread(()->{ n1.c(); }).start();}
结果
07:51:46.658 [Thread-2] DEBUG com.example.EightSynchroizedDemo.Number - 3
07:51:47.660 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
07:51:47.660 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
//3 1s 12
//23 1s 1
//32 1s 1
线程3和其他两个线程并行执行,但是线程1和线程2互斥
案例4
@Slf4j
class Number{public synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b(){log.debug("2");}
}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();}
结果
07:56:06.328 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
07:56:07.340 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
锁住的对象不同,无互斥,所以总是线程2先执行,因为线程1睡眠1s
案例5
@Slf4j
class Number{public static synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b(){log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
结果
07:58:07.288 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
07:58:08.282 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
同案例5,但是a方法加了static,所以此时a方法实际上锁住的是类对象,但是此时都是n1对象调用的a方法和b方法。
案例6
@Slf4j
class Number{public static synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public static synchronized void b(){log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();
// Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();}
结果
08:00:53.791 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
08:00:53.797 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
此时a方法和b方法都是对类对象加锁,但是类对象只有一份,所以此时两线程会互斥,看谁先被调度
案例7
@Slf4j
class Number{public static synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public synchronized void b(){log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
结果
08:03:00.603 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
08:03:01.605 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
不互斥,a方法是类对象,b方法是this对象,通过不同的对象实例调用,不是对同一个方法生效,所以不互斥
案例8
@Slf4j
class Number{public static synchronized void a(){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("1");}public static synchronized void b(){log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
结果
08:06:45.100 [Thread-0] DEBUG com.example.EightSynchroizedDemo.Number - 1
08:06:45.104 [Thread-1] DEBUG com.example.EightSynchroizedDemo.Number - 2
a方法和b方法都是锁住类对象,但是类对象只有一个,此时在运行时,即使调用对象不同,但是也是互斥。
说明
用static修饰的加锁方法,为什么锁住的是类对象,可以这样理解,在调用静态方法时,可以通过对象名.方法名(Number.a())调用,静态方法的调用基本都是这样,所以没吃调用静态方法实际上都是对象名.方法名(Number.a()),长的是不是都一样,只是后面方法名不通过罢了,所以静态变量锁会互斥
3.4变量的线程安全分析
3.4.1成员变量和静态变量是否线程安全
如果没共享,那么安全
如果被共享,根据他们状态是否能改变,有分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
3.4.2局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
3.4.3常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
这里说的线程安全是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为
- 他们的每个方法都是原子的
- 但注意他们多个方法的组合不是原子的,如果想要保证继续线程安全,还是需要上锁
3.5卖票练习
@Slf4j
public class SellTest {private static Random random = new Random();public static void main(String[] args) throws InterruptedException {TicketWindow ticketWindow = new TicketWindow(1000);List<Thread> list = new ArrayList<>();List<Integer> amountList = new Vector<>();for (int i = 0; i < 4000; i++) {Thread thread = new Thread(()->{int sell = ticketWindow.sell(random(5));amountList.add(sell);});list.add(thread);thread.start();}for (Thread thread : list) {thread.join();}log.debug("余票{}",ticketWindow.getCount());log.debug("卖出的票数{}",amountList.stream().mapToInt(i -> i).sum());}public static int random(int amount){return random.nextInt(5)+1;}
}
class TicketWindow{private int count;public TicketWindow(int count) {this.count = count;}public int getCount() {return count;}//售票public int sell(int amount){if(this.count >= amount){this.count-=amount;return amount;}else {return 0;}}
}
运行结果
20:30:51.321 [main] DEBUG com.example.demo.SellTest - 余票0
20:30:51.330 [main] DEBUG com.example.demo.SellTest - 卖出的票数1000
或
20:32:11.534 [main] DEBUG com.example.demo.SellTest - 余票0
20:32:11.541 [main] DEBUG com.example.demo.SellTest - 卖出的票数1004
很显然,第二种情况卖出的票数超过了总量1000,造成这样原因的就是因为,多个线程一起访问时,没有加锁,所以对同一个资源进行访问,使得结果有误。
怎么优化呢?
其实就是在sell()方法是加上synchronized即可
//售票public synchronized int sell(int amount){if(this.count >= amount){this.count-=amount;return amount;}else {return 0;}}
因为都是对售票操作,所以加在这即可,运行结果中每个都是1000
3.6Monitor(锁)概念
Monitor被翻译为监视器或者管理
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
Monitor结构如下
- 刚开始Monitor中Owner为null
- 当Thread-2执行synchronized(obj)将会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
3.7synchronized原理进阶
3.7.1轻量级锁
应用场景
如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1(){synchronized(obj){//同步块 Amethod2();}
}
public static void method2(){synchronized(obj){//同步块 B}
}
3.7.2锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
static Object obj = new Object();
public static void method1(){synchronized(obj){//同步块}
}
3.7.3自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
什么是自旋?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting,适用于多核cpu
自旋重试成功的情况
线程1(cpu1上) | 对象Mark | 线程2(cpu2上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取monitor | 10(重量锁)重置锁指针 | - |
成功(加锁) | 10(重量锁)重置锁指针 | - |
执行同步块 | 10(重量锁)重置锁指针 | - |
执行同步块 | 10(重量锁)重置锁指针 | 访问同步块,获取monitor |
执行同步块 | 10(重量锁)重置锁指针 | 自旋重试 |
执行完毕 | 10(重量锁)重置锁指针 | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁)重置锁指针 | 成功(加锁) |
- | 10(重量锁)重置锁指针 | 执行同步块 |
- | … | … |
自旋失败的情况
线程1(cpu1上) | 对象Mark | 线程2(cpu2上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取monitor | 10(重量锁)重置锁指针 | - |
成功(加锁) | 10(重量锁)重置锁指针 | - |
执行同步块 | 10(重量锁)重置锁指针 | - |
执行同步块 | 10(重量锁)重置锁指针 | 访问同步块,获取monitor |
执行同步块 | 10(重量锁)重置锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重置锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重置锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重置锁指针 | 自旋重试 |
执行同步块 | 10(重量锁)重置锁指针 | 阻塞 |
- | … | … |
注意
- 在java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认定为这次自旋成功的可能性会高,就会多自旋几次;反之,就少自旋甚至不自旋。
- 自旋会占用CPU时间,单核CPU自旋就是浪费资源,多核CPU自旋才能发挥优势
- java7之后不能控制是否开启自旋功能
3.7.4偏向锁
轻量级锁在竞争时(就自己这个线程),每次重入仍然需要执行CAS操作
java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程id设置到对象的Mark Word头,之后发现这个线程id是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
CAS概念
CAS的全称是 Compare-and-Swap,也就是比较并交换,是并发编程中一种常用的算法。它包含了三个参数:V,A,B。
其中,V表示要读写的内存位置,A表示旧的预期值,B表示新值
CAS指令执行时,当且仅当V的值等于预期值A时,才会将V的值设为B,如果V和A不同,说明可能是其他线程做了更新,那么当前线程就什么都不做,最后,CAS返回的是V的真实值。
而在多线程的情况下,当多个线程同时使用CAS操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。正是基于这样的原理,CAS即时没有使用锁,也能发现其他线程对当前线程的干扰,从而进行及时的处理。
3.7.5锁消除
3.8wait notify
- wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
- wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
- wait(long,int):对于超时时间更细力度的控制,单位为纳秒。
- notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
- notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
3.8.1原理
- Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时片
- BLOCKED线程会在Owner线程释放锁时唤醒
- WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍然需要进入EntryList重新竞争
3.8.2API介绍
- obj.wait()让进入object监视器的线程到waitSet等待
- obj.notify()在object上正在waitSet等待的线程中挑一个唤醒
- obj.notifyAll()让object上正在waitSet等待的线程全部唤醒
他们都是线程之间进行协作的手段,都属于Object对象的方法,必须获得此对象的锁,才能调用这几个方法
例
@Slf4j
public class demo01 {final static Object obj = new Object();public static void main(String[] args) {new Thread(()->{synchronized (obj){log.debug("执行....");try {obj.wait(); //线程一直等待} catch (InterruptedException e) {e.printStackTrace();}log.debug("其他代码...");}},"t1").start();new Thread(()->{synchronized (obj){log.debug("执行....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}log.debug("其他代码...");}},"t2").start();//主线程休息2stry {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("唤醒obj上的其他线程");synchronized (obj){obj.notify(); //唤醒obj上的一个线程
// obj.notifyAll(); //唤醒obj上的所有线程}}
}
运行结果
//obj.notify();
20:50:01.174 [t1] DEBUG com.example.demo.demo01 - 执行....
20:50:01.176 [t2] DEBUG com.example.demo.demo01 - 执行....
20:50:03.174 [main] DEBUG com.example.demo.demo01 - 唤醒obj上的其他线程
20:50:03.174 [t1] DEBUG com.example.demo.demo01 - 其他代码...
//注意此时方法并未结束,因为t2线程还没被唤醒// obj.notifyAll();
//20:51:18.327 [t1] DEBUG com.example.demo.demo01 - 执行....
//20:51:18.330 [t2] DEBUG com.example.demo.demo01 - 执行....
//20:51:20.338 [main] DEBUG com.example.demo.demo01 - 唤醒obj上的其他线程
//20:51:20.338 [t2] DEBUG com.example.demo.demo01 - 其他代码...
//20:51:20.338 [t1] DEBUG com.example.demo.demo01 - 其他代码...
//此时方法结束了
wait() 方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就有机会获取对象的锁。无限制等待,知道notify为止
wait(long n)有时间限制的等待,到n毫秒之后结束等待,或是被notify
3.9wait notify的正确使用姿势
sleep(long n)和wait(long n)的区别
1)sleep是Thread方法,而wait是Object的方法
2)sleep不需要强制和synchronized配合使用,但wait需要和synchronized配合使用
3)sleep在睡眠期间,不会释放对象锁的,但wait在等待的时机会释放对象锁
4)他们的状态都是TIMED_WAITING,都是有时限的等待
例
package com.example.demo;import lombok.extern.slf4j.Slf4j;@Slf4j
public class demo02 {static final Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) {new Thread(()->{synchronized (room){log.debug("有烟没?[{}]",hasCigarette);if (!hasCigarette){log.debug("没烟,先歇会");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?[{}]",hasCigarette);if(hasCigarette){log.debug("可以开始干活了");}else {log.debug("没干成活");}}},"小南").start();new Thread(()->{synchronized (room){log.debug("外卖送到没?[{}]",hasTakeout);if (!hasTakeout){log.debug("外卖没送到,先歇会");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("外卖送到没?[{}]",hasTakeout);if(hasTakeout){log.debug("可以开始干活了");}else {log.debug("没干成活");}}},"小女").start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{synchronized (room){hasTakeout = true;log.debug("外卖到了哦!");room.notifyAll();}},"送外卖的").start();}
}
运行结果:
15:05:57.882 [小南] DEBUG com.example.demo.demo02 - 有烟没?[false]
15:05:57.887 [小南] DEBUG com.example.demo.demo02 - 没烟,先歇会
15:05:57.887 [小女] DEBUG com.example.demo.demo02 - 外卖送到没?[false]
15:05:57.887 [小女] DEBUG com.example.demo.demo02 - 外卖没送到,先歇会
15:05:58.889 [送外卖的] DEBUG com.example.demo.demo02 - 外卖到了哦!
15:05:58.889 [小女] DEBUG com.example.demo.demo02 - 外卖送到没?[true]
15:05:58.889 [小女] DEBUG com.example.demo.demo02 - 可以开始干活了
15:05:58.890 [小南] DEBUG com.example.demo.demo02 - 有烟没?[false]
15:05:58.890 [小南] DEBUG com.example.demo.demo02 - 没干成活
可以看到,使用if判断时,如果当叫醒时,如果叫醒的不是需要的线程(虚假唤醒),那么仍然会处于等待,干不成活。
所以为了确保能正确唤醒需要的线程,其他的线程被唤醒后任然需要处于等待,所以if可以改为while
更改之后
package com.example.demo;import lombok.extern.slf4j.Slf4j;@Slf4j
public class demo02 {static final Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) {new Thread(()->{synchronized (room){log.debug("有烟没?[{}]",hasCigarette);//更改if->whilewhile (!hasCigarette){log.debug("没烟,先歇会");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?[{}]",hasCigarette);if(hasCigarette){log.debug("可以开始干活了");}else {log.debug("没干成活");}}},"小南").start();new Thread(()->{synchronized (room){log.debug("外卖送到没?[{}]",hasTakeout);//更改if->whilewhile (!hasTakeout){log.debug("外卖没送到,先歇会");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("外卖送到没?[{}]",hasTakeout);if(hasTakeout){log.debug("可以开始干活了");}else {log.debug("没干成活");}}},"小女").start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{synchronized (room){hasTakeout = true;log.debug("外卖到了哦!");room.notifyAll();}},"送外卖的").start();}
}
运行结果:
15:11:00.461 [小南] DEBUG com.example.demo.demo02 - 有烟没?[false]
15:11:00.464 [小南] DEBUG com.example.demo.demo02 - 没烟,先歇会
15:11:00.464 [小女] DEBUG com.example.demo.demo02 - 外卖送到没?[false]
15:11:00.465 [小女] DEBUG com.example.demo.demo02 - 外卖没送到,先歇会
15:11:01.464 [送外卖的] DEBUG com.example.demo.demo02 - 外卖到了哦!
15:11:01.464 [小女] DEBUG com.example.demo.demo02 - 外卖送到没?[true]
15:11:01.464 [小女] DEBUG com.example.demo.demo02 - 可以开始干活了
15:11:01.464 [小南] DEBUG com.example.demo.demo02 - 没烟,先歇会//此时程序未结束,仍然处于等待中,需要被唤醒
synchronized(lock){while(条件不成立){lock.wait();}//干活
}//另一个线程
synchronized(lock){lock.notifyAll();
}
3.10 Park & Unpark
3.10.1基本使用
//暂停当前线程
LockSupport.park();//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
简单使用
package com.example.MessageQueue;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;/*** @author 我见青山多妩媚* @date Create on 2022/7/2 16:30*/
@Slf4j
public class Demo22 {public static void main(String[] args) {Thread t1 = new Thread(()->{log.debug("start..");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("park...");//实际上是wait LockSupport.park();log.debug("resume...");},"t1");t1.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("unpark...");LockSupport.unpark(t1);}
}
运行结果:
16:32:47.766 [t1] DEBUG com.example.MessageQueue.Demo22 - start..
16:32:48.776 [t1] DEBUG com.example.MessageQueue.Demo22 - park...
16:32:49.769 [main] DEBUG com.example.MessageQueue.Demo22 - unpark...
16:32:49.769 [t1] DEBUG com.example.MessageQueue.Demo22 - resume...
当先调用unpark,再调用park时,运行结果如下:
package com.example.MessageQueue;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;/*** @author 我见青山多妩媚* @date Create on 2022/7/2 16:30*/
@Slf4j
public class Demo22 {public static void main(String[] args) {Thread t1 = new Thread(()->{log.debug("start..");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.debug("park...");LockSupport.park();log.debug("resume...");},"t1");t1.start();// try {// Thread.sleep(1000);
// } catch (InterruptedException e) {// e.printStackTrace();
// }log.debug("unpark...");LockSupport.unpark(t1);}
}
15:23:07.926 [main] DEBUG com.example.MessageQueue.Demo22 - unpark...
15:23:07.926 [t1] DEBUG com.example.MessageQueue.Demo22 - start..
15:23:08.942 [t1] DEBUG com.example.MessageQueue.Demo22 - park...
15:23:08.942 [t1] DEBUG com.example.MessageQueue.Demo22 - resume...
这是为什么?不是先调用的unpark吗?
3.10.2特点
与Object的wait & notify相比
- wait,notify和notifyAll必须配合Object Monitor一起使用,而unpark不必
- park & unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就没有那么精确
- park & unpark 可以先unpark,而wait & notify不能先notify
3.10.3原理
每个线程都有自己的一个Parker对象,由三部分组成_counter、_cond、_mutex。比如:
- 线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好像背包中的帐篷,_counter就好比背包中的备用干粮(0为耗尽,1为充足)
- 调用park就是看需不需要停下来休息
- 如果备用干粮耗尽,那么需要进帐篷休息
- 如果没有干粮充足,那么不需要停留,继续前进
- 调用unpark,就好比让干粮充足
- 如果这时线程还在帐篷(park时),那么就唤醒他让他前进
- 如果这时线程还在运行,那么下次他调用park时,仅仅是消耗掉备用干粮,不需停留继续前进(不会park)
- 因为背包空间有限,多次调用unpark仅仅会补充一份备用干粮
- 当前线程调用Unsafe.park()方法
- 检查_counter,本例情况为0,这时,获得_mutex互斥锁
- 线程进入_cond条件变量阻塞器
- 设置_counter=0
- 调用Unsafe.unpark(Thread_0)方法,设置_counter为1
- 唤醒_cond条件变量中的Thread_0
- Thread_0恢复运行
- 设置_counter为0
3.11 多把锁
3.11.1 多把不相干的锁
有小南小女两个线程,他们想同时执行学习和睡觉的方法,该怎么办呢?
先看看下面的例子:
例1
package com.example.MessageQueue;import com.example.tools.Sleep;
import lombok.extern.slf4j.Slf4j;/*** @author 我见青山多妩媚* @date Create on 2022/7/4 15:55*/
public class Demo23 {public static void main(String[] args) {BigRoom bigRoom = new BigRoom();new Thread(()->{bigRoom.sleep();},"小南").start();new Thread(()->{bigRoom.study();},"小女").start();}
}@Slf4j
class BigRoom{public void sleep(){synchronized(this){log.debug("sleeping 2小时");Sleep.sleep(2);}}public void study(){synchronized (this){log.debug("study 1小时");Sleep.sleep(1);}}
}
运行结果:
15:59:42.056 [小南] DEBUG com.example.MessageQueue.BigRoom - sleeping 2小时
15:59:44.074 [小女] DEBUG com.example.MessageQueue.BigRoom - study 1小时
可以看到,是在小南线程睡眠2s后,小女线程才开始运行,这个时候仍然是串行执行,并不是并行执行,并发度并不高
**改进方法:**加入两个小房间,对锁进行细粒度的划分
private static final Object studyRoom = new Object();private static final Object bedRoom =new Object();
更改后代码如下:
package com.example.MessageQueue;import com.example.tools.Sleep;
import lombok.extern.slf4j.Slf4j;/*** @author 我见青山多妩媚* @date Create on 2022/7/4 15:55*/
public class Demo23 {public static void main(String[] args) {BigRoom bigRoom = new BigRoom();new Thread(()->{bigRoom.sleep();},"小南").start();new Thread(()->{bigRoom.study();},"小女").start();}
}@Slf4j
class BigRoom{private static final Object studyRoom = new Object();private static final Object bedRoom =new Object();public void sleep(){synchronized(bedRoom){log.debug("sleeping 2小时");Sleep.sleep(2);}}public void study(){synchronized (studyRoom){log.debug("study 1小时");Sleep.sleep(1);}}
}
运行结果:
16:03:39.677 [小女] DEBUG com.example.MessageQueue.BigRoom - study 1小时
16:03:39.677 [小南] DEBUG com.example.MessageQueue.BigRoom - sleeping 2小时
**思考:**如果线程请求过多,会不会造成死锁?
3.11.2活跃性
3.11.2.1死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程获得A对象锁,接下来想获取B对象锁
t2线程获取B对象锁,接下来想获取A对象锁
例:
package com.example.MessageQueue;import com.example.tools.Sleep;
import lombok.extern.slf4j.Slf4j;/*** @author 我见青山多妩媚* @date Create on 2022/7/4 16:12*/
@Slf4j
public class Demo24 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(()->{synchronized (A){log.debug("lock A");Sleep.sleep(1);synchronized (B){log.debug("lock B");log.debug("操作..");}}},"t1");Thread t2 = new Thread(()->{synchronized (B){log.debug("lock B");Sleep.sleep(1);synchronized (A){log.debug("lock A");log.debug("操作..");}}},"t2");t1.start();t2.start();}
}
运行结果:
16:15:25.626 [t2] DEBUG com.example.MessageQueue.Demo24 - lock B
16:15:25.626 [t1] DEBUG com.example.MessageQueue.Demo24 - lock A
发生死锁, log.debug("操作..")
始终未打印
3.11.2.2定位死锁
检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstack定位死锁
使用jps定位
点击idea工具的Terminal
E:\IDEA举例项目\并发编程>jps
11808 KotlinCompileDaemon
16960 Launcher
17648 Demo24
25072
2684 Jps
可以看到,我们的对象编号是17648
E:\IDEA举例项目\并发编程>jstack 17648
2022-07-04 16:20:24
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.271-b09 mixed mode):"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000015b4bba8000 nid=0x6ef0 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"t2" #13 prio=5 os_prio=0 tid=0x0000015b68bf1800 nid=0x6fac waiting for monitor entry [0x000000361a2ff000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.MessageQueue.Demo24.lambda$main$1(Demo24.java:32)- waiting to lock <0x000000076be7f0f0> (a java.lang.Object)- locked <0x000000076be7f100> (a java.lang.Object)at com.example.MessageQueue.Demo24$$Lambda$2/2143192188.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)"t1" #12 prio=5 os_prio=0 tid=0x0000015b68bf0800 nid=0x6f70 waiting for monitor entry [0x000000361a1ff000]java.lang.Thread.State: BLOCKED (on object monitor)at com.example.MessageQueue.Demo24.lambda$main$0(Demo24.java:21)- waiting to lock <0x000000076be7f100> (a java.lang.Object)- locked <0x000000076be7f0f0> (a java.lang.Object)at com.example.MessageQueue.Demo24$$Lambda$1/110718392.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x0000015b6884b800 nid=0x81c runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x0000015b68820800 nid=0x6f74 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000015b6881a800 nid=0x7300 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000015b68816800 nid=0x71a0 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000015b68816000 nid=0x67bc waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000015b68814000 nid=0x3428 runnable [0x0000003619afe000]java.lang.Thread.State: RUNNABLEat java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)at java.net.SocketInputStream.read(SocketInputStream.java:171)at java.net.SocketInputStream.read(SocketInputStream.java:141)at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)- locked <0x000000076b854dc8> (a java.io.InputStreamReader)at java.io.InputStreamReader.read(InputStreamReader.java:184)at java.io.BufferedReader.fill(BufferedReader.java:161)at java.io.BufferedReader.readLine(BufferedReader.java:324)- locked <0x000000076b854dc8> (a java.io.InputStreamReader)at java.io.BufferedReader.readLine(BufferedReader.java:389)at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:61)"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000015b6683e000 nid=0x662c waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000015b667cd000 nid=0x4dec runnable [0x0000000000000000]java.lang.Thread.State: RUNNABLE"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000015b667b2000 nid=0x76d8 in Object.wait() [0x00000036197fe000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x000000076b588ee0> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)- locked <0x000000076b588ee0> (a java.lang.ref.ReferenceQueue$Lock)at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000015b667ab000 nid=0x69b4 in Object.wait() [0x00000036196ff000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x000000076b586c00> (a java.lang.ref.Reference$Lock)at java.lang.Object.wait(Object.java:502)at java.lang.ref.Reference.tryHandlePending(Reference.java:191)- locked <0x000000076b586c00> (a java.lang.ref.Reference$Lock)at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)"VM Thread" os_prio=2 tid=0x0000015b66783800 nid=0x7158 runnable"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000015b4bbc0800 nid=0x7094 runnable"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000015b4bbc2000 nid=0x6dc8 runnable"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000015b4bbc3800 nid=0x7084 runnable"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000015b4bbc4800 nid=0x7304 runnable"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000015b4bbc6800 nid=0x7598 runnable"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000015b4bbc7800 nid=0x243c runnable"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000015b4bbca800 nid=0x6db8 runnable"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000015b4bbcb800 nid=0x71cc runnable"VM Periodic Task Thread" os_prio=2 tid=0x0000015b68850800 nid=0x6aec waiting on conditionJNI global references: 316Found one Java-level deadlock:
=============================
"t2":waiting to lock monitor 0x0000015b667af368 (object 0x000000076be7f0f0, a java.lang.Object),which is held by "t1"
"t1":waiting to lock monitor 0x0000015b667b1518 (object 0x000000076be7f100, a java.lang.Object),which is held by "t2"Java stack information for the threads listed above:
===================================================
"t2":at com.example.MessageQueue.Demo24.lambda$main$1(Demo24.java:32)- waiting to lock <0x000000076be7f0f0> (a java.lang.Object)- locked <0x000000076be7f100> (a java.lang.Object)at com.example.MessageQueue.Demo24$$Lambda$2/2143192188.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)
"t1":at com.example.MessageQueue.Demo24.lambda$main$0(Demo24.java:21)- waiting to lock <0x000000076be7f100> (a java.lang.Object)- locked <0x000000076be7f0f0> (a java.lang.Object)at com.example.MessageQueue.Demo24$$Lambda$1/110718392.run(Unknown Source)at java.lang.Thread.run(Thread.java:748)Found 1 deadlock.
更细致:
使用jconso
点击JDK/bin目录下的jconsole.exe
启动
找到刚刚的进程,点击连接
点击之后,切换到线程界面
找到刚刚的线程,t1、t2,点击检测死锁
t1:
t2:
出错行数也都显示了
3.11.3哲学家就餐问题
有5位哲学家,围坐在圆桌旁
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭之后接着思考
- 吃饭的时候要用两根筷子吃,桌子上一共有5跟筷子,每位哲学家左右手边都各有一根筷子
- 如果筷子被别人使用,自己就需要等待
代码实现:
筷子类
class Chopstick{String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "Chopstick{" +"name='" + name + '\'' +'}';}
}
哲学家类
@Slf4j
class Philosopher extends Thread{//左手边的筷子final Chopstick left;//右手边的筷子final Chopstick right;public Philosopher(String name,Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}@Overridepublic void run() {while (true){// 尝试获取左边筷子synchronized (left){//尝试获取右边筷子synchronized (right){eat();}}}}private void eat(){log.debug("eating");Sleep.sleep(1);}
}
测试
/*** @author 我见青山多妩媚* @date Create on 2022/7/4 16:50*/
public class Demo25 {public static void main(String[] args) {Chopstick c1 = new Chopstick("1");Chopstick c2 = new Chopstick("2");Chopstick c3 = new Chopstick("3");Chopstick c4 = new Chopstick("4");Chopstick c5 = new Chopstick("5");new Philosopher("苏格拉底",c1,c2).start();new Philosopher("柏拉图",c2,c3).start();new Philosopher("亚里士多德",c3,c4).start();new Philosopher("赫拉克利特",c4,c5).start();new Philosopher("阿基米德",c5,c1).start();}
}
运行结果:
16:58:15.200 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
16:58:15.200 [苏格拉底] DEBUG com.example.MessageQueue.Philosopher - eating
16:58:16.209 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
16:58:16.209 [阿基米德] DEBUG com.example.MessageQueue.Philosopher - eating
16:58:17.215 [阿基米德] DEBUG com.example.MessageQueue.Philosopher - eating
16:58:18.224 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
吃了一会后,发生了死锁,哲学家各拿着一根筷子(如果没有死锁,会一直运行下去)
使用jconso查看线程运行状态:
五个线程全部进入死锁
------------------------------------------------------------------------------------------------------------
名称: 阿基米德
状态: com.example.MessageQueue.Chopstick@38c7b2a3上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 4, 总等待数: 2堆栈跟踪:
com.example.MessageQueue.Philosopher.run(Demo25.java:48)- 已锁定 com.example.MessageQueue.Chopstick@335704ea
------------------------------------------------------------------------------------------------------------
名称: 苏格拉底
状态: com.example.MessageQueue.Chopstick@6d93340f上的BLOCKED, 拥有者: 柏拉图
总阻止数: 7, 总等待数: 2堆栈跟踪:
com.example.MessageQueue.Philosopher.run(Demo25.java:48)- 已锁定 com.example.MessageQueue.Chopstick@38c7b2a3
------------------------------------------------------------------------------------------------------------
名称: 柏拉图
状态: com.example.MessageQueue.Chopstick@951fff6上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0堆栈跟踪:
com.example.MessageQueue.Philosopher.run(Demo25.java:48)- 已锁定 com.example.MessageQueue.Chopstick@6d93340f
------------------------------------------------------------------------------------------------------------
名称: 亚里士多德
状态: com.example.MessageQueue.Chopstick@5fc07890上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 10, 总等待数: 2堆栈跟踪:
com.example.MessageQueue.Philosopher.run(Demo25.java:48)- 已锁定 com.example.MessageQueue.Chopstick@951fff6
------------------------------------------------------------------------------------------------------------
名称: 赫拉克利特
状态: com.example.MessageQueue.Chopstick@335704ea上的BLOCKED, 拥有者: 阿基米德
总阻止数: 3, 总等待数: 1堆栈跟踪:
com.example.MessageQueue.Philosopher.run(Demo25.java:48)- 已锁定 com.example.MessageQueue.Chopstick@5fc07890
------------------------------------------------------------------------------------------------------------
一人拿一根筷子都不放下,等待对方放下筷子,造成死锁
解决方法:使用锁超时解决哲学家就餐问题
3.11.4活锁
活锁出现在两个线程互相改变对方的结束条件,导致谁也无法结束
例
package com.example.MessageQueue;import com.example.tools.Sleep;
import lombok.extern.slf4j.Slf4j;/*** @author 我见青山多妩媚* @date Create on 2022/7/4 17:10*/
@Slf4j
public class Demo26 {/*** Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量* 是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。*/static volatile int count = 10;static final Object lock = new Object();public static void main(String[] args) {new Thread(()->{while (count > 0){Sleep.sleep(0.2);count--;log.debug("count{}",count);}},"t1").start();new Thread(()->{while (count < 20){Sleep.sleep(0.2);count++;log.debug("count{}",count);}},"t2").start();}
}
运行结果:
17:16:01.283 [t2] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:01.283 [t1] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:01.500 [t1] DEBUG com.example.MessageQueue.Demo26 - count10
17:16:01.500 [t2] DEBUG com.example.MessageQueue.Demo26 - count10
17:16:01.705 [t2] DEBUG com.example.MessageQueue.Demo26 - count10
17:16:01.705 [t1] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:01.908 [t1] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:01.908 [t2] DEBUG com.example.MessageQueue.Demo26 - count10
17:16:02.112 [t2] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:02.112 [t1] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:02.315 [t1] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:02.315 [t2] DEBUG com.example.MessageQueue.Demo26 - count9
17:16:02.521 [t1] DEBUG com.example.MessageQueue.Demo26 - count8
17:16:02.521 [t2] DEBUG com.example.MessageQueue.Demo26 - count8
17:16:02.725 [t1] DEBUG com.example.MessageQueue.Demo26 - count7
17:16:02.725 [t2] DEBUG com.example.MessageQueue.Demo26 - count7
17:16:02.928 [t2] DEBUG com.example.MessageQueue.Demo26 - count7
17:16:02.928 [t1] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:03.133 [t1] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:03.133 [t2] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:03.337 [t2] DEBUG com.example.MessageQueue.Demo26 - count5
17:16:03.337 [t1] DEBUG com.example.MessageQueue.Demo26 - count5
17:16:03.542 [t1] DEBUG com.example.MessageQueue.Demo26 - count4
17:16:03.542 [t2] DEBUG com.example.MessageQueue.Demo26 - count4
17:16:03.748 [t1] DEBUG com.example.MessageQueue.Demo26 - count5
17:16:03.748 [t2] DEBUG com.example.MessageQueue.Demo26 - count5
17:16:03.951 [t1] DEBUG com.example.MessageQueue.Demo26 - count4
17:16:03.951 [t2] DEBUG com.example.MessageQueue.Demo26 - count5
17:16:04.153 [t2] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:04.153 [t1] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:04.358 [t2] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:04.358 [t1] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:04.559 [t1] DEBUG com.example.MessageQueue.Demo26 - count6
17:16:04.559 [t2] DEBUG com.example.MessageQueue.Demo26 - count7
互相改变对方的停止条件,都不会终止,造成活锁
解决办法:
让睡眠时间不同,或者增加随机的睡眠时间,让两个线程交错开,避免活锁的产生
3.11.5饥饿
很多教程把饥饿定义为,一个线程由于优先级太低,始终得不到cpu调度执行,也不能够得到结果,饥饿的情况不易演示,到读写锁时会涉及饥饿问题
一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
线程A先获取对象A,然后获取对象B
线程B先获取对象B,然后获取对象A(详见3.11.2.1死锁)
顺序加锁的解决方案
演示:
3.11.3哲学家就餐问题,将
new Philosopher("阿基米德",c5,c1).start();
//改为
new Philosopher("阿基米德",c1,c5).start();
再次运行代码,结果为
17:30:29.248 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:29.248 [苏格拉底] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:30.257 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:31.271 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:31.271 [阿基米德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:32.273 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:33.280 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:34.286 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:35.300 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:36.301 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:37.302 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:38.317 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:39.322 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:40.322 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:41.329 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:42.332 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:43.332 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:44.335 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:45.343 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:46.349 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:47.351 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:48.353 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:49.360 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:50.375 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:50.375 [柏拉图] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:51.389 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:52.397 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:53.408 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:54.413 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:55.418 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:56.432 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:57.446 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:58.462 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:30:59.465 [亚里士多德] DEBUG com.example.MessageQueue.Philosopher - eating
17:31:00.472 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
17:31:01.480 [赫拉克利特] DEBUG com.example.MessageQueue.Philosopher - eating
使用工具查看是否死锁
可以看到并没有死锁
但是对于运行结果,可以看到,基本都是[赫拉克利特]
线程在吃法,其他线程很少,这就是饥饿问题
3.12 ReentrantLock
ReentrantLock主要用到unsafe的CAS(原子性)和park(阻塞)两个功能实现锁(CAS + park )
相对于synchronized它具备以下几点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与synchronized一样,都可支持可重入
基本语法:
//获取锁
reentrantLock.lock();
try{//需要上锁的代码
}finally{//释放锁reentrantLock.unlock();
}
与synchronized()不同的是,synchronized(obj) 可以指定锁住的对象,但是ReentrantLock仅仅只能锁住从lock() -> unlock()(这样说不是很严谨,因为上锁的方法不止一个,总之就是从上锁到解锁的那段代码都是被锁上的)内的代码,并且要保证ReentrantLock是线程间共享。
3.12.1可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获得这把锁,如果是不可重入的,那么第二次获得锁时,自己也会被锁挡住
例
package com.example.MessageQueue;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;/*** @author 我见青山多妩媚* @date Create on 2022/7/4 19:20*/
@Slf4j
public class Demo27 {static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {lock.lock();try {log.debug("进入主方法");m1();} finally {lock.unlock();}}//模拟锁重入public static void m1(){lock.lock();try {log.debug("进入m1");m2();} finally {lock.unlock();}}public static void m2(){lock.lock();try {log.debug("进入m2");} finally {lock.unlock();}}}
运行结果:
19:25:16.549 [main] DEBUG com.example.MessageQueue.Demo27 - 进入主方法
19:25:16.552 [main] DEBUG com.example.MessageQueue.Demo27 - 进入m1
19:25:16.552 [main] DEBUG com.example.MessageQueue.Demo27 - 进入m2
3.12.2可打断
package com.example.MessageQueue;import com.example.tools.Sleep;
import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;/*** @author 我见青山多妩媚* @date Create on 2022/7/4 19:28*/
@Slf4j
public class Demo28 {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(()->{try {//如果没有竞争,此方法会获取lock对象的锁//如果有竞争,进入阻塞队列,可以被其他线程使用interrupt方法打断log.debug("尝试获取到锁");lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("没有获取到锁");return;}try {log.debug("获取到锁");} finally {lock.unlock();}},"t1");lock.lock();t1.start();Sleep.sleep(1);log.debug("打断t1");t1.interrupt();}
}
运行结果:
19:46:14.254 [t1] DEBUG com.example.MessageQueue.Demo28 - 尝试获取到锁
19:46:15.255 [main] DEBUG com.example.MessageQueue.Demo28 - 打断t1
19:46:15.255 [t1] DEBUG com.example.MessageQueue.Demo28 - 没有获取到锁
java.lang.InterruptedExceptionat java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)at com.example.MessageQueue.Demo28.lambda$main$0(Demo28.java:22)at java.lang.Thread.run(Thread.java:748)
使用打断操作,是为了防止死锁的发生,如果使用lock.loc(),会一直等待,不会抛异常信息
3.12.3锁超时
可以让锁立刻失败
package com.example.MessageQueue;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;/*** @author 我见青山多妩媚* @date Create on 2022/7/5 16:36*/
@Slf4j
public class Demo29 {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {Thread t1 = new Thread(()->{//尝试获得锁log.debug("尝试获得锁");if(!lock.tryLock()){log.debug("获取不到锁");return;}//给定等待时间获取锁
// try {// if(!lock.tryLock(2,TimeUnit.SECONDS)){// log.debug("获取不到锁");
// return;
// }
// } catch (InterruptedException e) {// e.printStackTrace();
// return;
// }try {//获取到锁log.debug("获取到锁");}finally {lock.unlock();}},"t1");log.debug("主线程获取到锁");lock.lock();t1.start();//等待时间时让主线程释放锁
// Sleep.sleep(1);
// log.debug("主线程释放了锁");
// lock.unlock();}
}
运行结果:
16:42:12.654 [main] DEBUG com.example.MessageQueue.Demo29 - 主线程获取到锁
16:42:12.656 [t1] DEBUG com.example.MessageQueue.Demo29 - 尝试获得锁
16:42:12.657 [t1] DEBUG com.example.MessageQueue.Demo29 - 获取不到锁//等待时间后的运行结果
16:49:01.580 [main] DEBUG com.example.MessageQueue.Demo29 - 主线程获取到锁
16:49:01.584 [t1] DEBUG com.example.MessageQueue.Demo29 - 尝试获得锁
16:49:02.597 [main] DEBUG com.example.MessageQueue.Demo29 - 主线程释放了锁
16:49:02.597 [t1] DEBUG com.example.MessageQueue.Demo29 - 获取到锁
用锁超时解决哲学家就餐问题
3.11.3哲学家就餐问题
代码实现:
筷子类
//筷子类继承ReentrantLock
class Chopsticks extends ReentrantLock {String name;public Chopsticks(String name) {this.name = name;}@Overridepublic String toString() {return "Chopstick{" +"name='" + name + '\'' +'}';}
}
哲学家类
@Slf4j
class Philosophers extends Thread{//左手边的筷子final Chopsticks left;//右手边的筷子final Chopsticks right;public Philosophers(String name,Chopsticks left, Chopsticks right) {super(name);this.left = left;this.right = right;}@Overridepublic void run() {while (true) {//与synchronized ()不同的是,如果获取锁失败(无论左手筷子还是右手筷子),会把当前的锁对象释放掉,不会一直等下去// 尝试获取左边筷子if(left.tryLock()){try {//尝试获取右边筷子if(right.tryLock()){try {//左手筷子,有手筷子都拿到之后,就可以吃饭了eat();}finally {right.unlock();}}}finally {//确保锁可以释放掉left.unlock();}}}}private void eat(){log.debug("eating");Sleep.sleep(0.2);}
}
测试
/*** @author 我见青山多妩媚* @date Create on 2022/7/5 16:55*/
public class Demo25_Solve {public static void main(String[] args) {Chopsticks c1 = new Chopsticks("1");Chopsticks c2 = new Chopsticks("2");Chopsticks c3 = new Chopsticks("3");Chopsticks c4 = new Chopsticks("4");Chopsticks c5 = new Chopsticks("5");new Philosophers("苏格拉底",c1,c2).start();new Philosophers("柏拉图",c2,c3).start();new Philosophers("亚里士多德",c3,c4).start();new Philosophers("赫拉克利特",c4,c5).start();new Philosophers("阿基米德",c5,c1).start();}
}
运行结果:
17:07:38.884 [苏格拉底] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:38.884 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.090 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.090 [柏拉图] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.300 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.300 [柏拉图] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.510 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.512 [苏格拉底] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.723 [苏格拉底] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.725 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.943 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:39.943 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.151 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.159 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.365 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.370 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.578 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.578 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.789 [亚里士多德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.789 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:40.999 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:41.000 [柏拉图] DEBUG com.example.MessageQueue.Philosophers - eating
17:07:41.210 [阿基米德] DEBUG com.example.MessageQueue.Philosophers - eating
可以看到,大家吃的都很快乐,没有产生死锁问题
3.12.5公平锁
ReentrantLock默认是不公平的
/***创建一个 {@code ReentrantLock} 的实例。这相当于使用 {@code ReentrantLock(false)}。* Creates an instance of {@code ReentrantLock}.* This is equivalent to using {@code ReentrantLock(false)}.*/public ReentrantLock() {sync = new NonfairSync();}/*** 使用给定的公平策略创建 {@code ReentrantLock} 的实例。 * Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} 如果这个锁应该使用公平的排序策略* @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
测试不好实现,以后有例子
3.12.6条件变量
synchronized中也有条件变量,就是我们讲原理时哪个waitSet休息室,当条件不满足时进入waitSet等待
ReentrantLock的条件变量比synchronized强大之处在于,他是支持多个条件变量的,这就好比
- synchronized是那些不满足条件的线程都在一间休息室等消息
- 而ReentrantLock支持多间休息室,有专门等烟的休息室,专门等早餐的休息室,唤醒时也是按休息室来唤醒
使用流程
- await 前需要获得锁
- await执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(或打断、超时等)重新竞争lock锁
- 竞争lock锁成功后,await后继续执行
package com.example.MessageQueue;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*** @author 我见青山多妩媚* @date Create on 2022/7/5 17:30*/
public class test {static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {//创建一个新的条件变量(休息室),同一把锁可以有多个锁对象Condition condition1 = lock.newCondition();Condition condition2 = lock.newCondition();lock.lock();//进入休息室等待try {//释放当前锁持有的锁condition1.await();//叫醒condition1等待的某一个线程condition1.signal();//叫醒所有等待的线程condition1.signalAll();} catch (InterruptedException e) {e.printStackTrace();}}
}
例子
小南小女送干活
- 小南需要烟才能干活
- 小女需要外卖才能干活
package com.example.MessageQueue;import com.example.tools.Sleep;
import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*** @author 我见青山多妩媚* @date Create on 2022/7/5 17:30*/
@Slf4j
public class Demo30 {static ReentrantLock room = new ReentrantLock();//等烟的休息室static Condition waitCigaretteSet = room.newCondition();//等外面的休息室static Condition waitTakeoutSet = room.newCondition();//有烟没static boolean hasCigarette = false;//有外卖没static boolean hasTakeout = false;public static void main(String[] args) {new Thread(()->{room.lock();try{log.debug("有烟没{}",hasCigarette);while (!hasCigarette){log.debug("没烟,先歇会");try {//进入等烟的休息室waitCigaretteSet.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没{}",hasCigarette);log.debug("可以开始干活了");}finally {//释放锁room.unlock();}},"小南").start();new Thread(()->{room.lock();try{log.debug("外卖到了没{}",hasTakeout);while (!hasTakeout){log.debug("没到,先歇会");try {//进入等外卖的休息室waitTakeoutSet.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("外卖到了没{}",hasTakeout);log.debug("可以开始干活了");}finally {//释放锁room.unlock();}},"小女").start();Sleep.sleep(1);new Thread(()->{room.lock();try{log.debug("外卖送到了");//唤醒等外卖室waitTakeoutSet.signal();hasTakeout = true;}finally {room.unlock();}},"送外卖的").start();Sleep.sleep(1);new Thread(()->{room.lock();try{log.debug("烟送到了没");//唤醒等烟室waitCigaretteSet.signal();hasCigarette = true;}finally {room.unlock();}},"送烟的").start();}
}
运行结果
17:54:58.326 [小南] DEBUG com.example.MessageQueue.Demo30 - 有烟没false
17:54:58.330 [小南] DEBUG com.example.MessageQueue.Demo30 - 没烟,先歇会
17:54:58.330 [小女] DEBUG com.example.MessageQueue.Demo30 - 外卖到了没false
17:54:58.330 [小女] DEBUG com.example.MessageQueue.Demo30 - 没到,先歇会
17:54:59.330 [送外卖的] DEBUG com.example.MessageQueue.Demo30 - 外卖送到了
17:54:59.330 [小女] DEBUG com.example.MessageQueue.Demo30 - 外卖到了没true
17:54:59.330 [小女] DEBUG com.example.MessageQueue.Demo30 - 可以开始干活了
17:55:00.340 [送烟的] DEBUG com.example.MessageQueue.Demo30 - 烟送到了没
17:55:00.340 [小南] DEBUG com.example.MessageQueue.Demo30 - 有烟没true
17:55:00.340 [小南] DEBUG com.example.MessageQueue.Demo30 - 可以开始干活了
3.13小结
需要掌握的是:
基础方面:
- 分析多线程资源时,哪些代码属于临界区
- 使用synchronized互斥解决临界区的线程安全问题
- 掌握synchronized锁对象语法
- 掌握synchronized加载成员方法和静态方法语法
- 掌握wait\notify同步方法
- 使用lock互斥锁解决临界区的线程安全问题
- 掌握lock的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
应用方面:
- 互斥:使用synchronized或Lock达到共享资源互斥效果
- 同步:使用wait\notify或Lock的条件变量来达到线程间通信效果
原理方面
- monitor(管程)、synchronized、wait\notify原理
- synchronized进阶原理
- park & unpark原理
模式方面:
- 同步模式之保护性暂停(4.1)
- 异步模式之生产者消费者(4.2)
- 同步模式之顺序控制(4.3)
并发编程(三)---共享模型之管程相关推荐
- java并发编程(7) 共享模型之工具 - stampedLock、semaphore、CountdownLatch、CyclicBarri
文章目录 前言 1. stampedLock 1. 概述 2. 代码 1. 读读 2. 读写 3. 注意 2. Semaphore 1. 基本使用 2. 应用场景 3. 原理 3. Countdown ...
- 【并发编程】(学习笔记-共享模型之管程)-part3
文章目录 并发编程-共享模型之管程-3 1.共享带来的问题 1-1 临界区 Critical Section 1-2 竞态条件 Race Condition 2.synchronized 解决方案 2 ...
- JUC笔记-共享模型之管程 (Monitor)
JUC-共享模型之管程( Monitor) 一.线程安全问题(重点) 1.1 同步 1.2 线程出现问题的根本原因分析 1.3 synchronized 解决方案 1.3.1 同步代码块 1.3.2 ...
- Java 多线程共享模型之管程(下)
共享模型之管程 wait.notify wait.notify 原理 Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 BLOCKED 和 W ...
- 【云风skynet】详解skynet的多核高并发编程丨actor模型丨游戏开发丨游戏服务端开发丨多线程丨Linux服务器开发丨后端开发
skynet中多核高并发编程给我们的启发 1. 多核并发编程 2. actor模型详解 3. 手撕一个万人同时在线游戏 视频讲解如下,点击观看: [云风skynet]详解skynet的多核高并发编程丨 ...
- php三要素,并发编程三要素:原子性,有序性,可见性
并发编程三要素 **原子性:**一个不可再被分割的颗粒.原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性: 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) ...
- 【并发编程三】C++进程通信——管道(pipe)
[并发编程三]C++实现通信--管道(pipe) 一.管道(pipe) 二.匿名管道 1.简介 2.父子进程:匿名管道的通信过程? 3.相关函数 3.1.创建管道CreatePipe 3.2.写入管道 ...
- 并发操作之——并发编程三要素
并发操作 并发操作之--并发编程三要素. 并发操作之--并发编程三要素 并发操作 前言 一.原子性 二.有序性: 三.可见性: 总结 前言 并发操作之--并发编程三要素. 一.原子性 一个不可再被分割 ...
- Cpython解释器下实现并发编程——多进程、多线程、协程、IO模型
一.背景知识 进程即正在执行的一个过程.进程是对正在运行的程序的一个抽象. 进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一.操作系统的其他所有内容都 ...
最新文章
- Mysql Cluster 集群 windows版本
- D1net阅闻:思科占全球交换路由器53%市场份额
- docker history 27f1068ca9da --no-trunc查看镜像dockerfile内容
- python的心得体会200字_python_学习心得
- matlab波特图带延迟的传递函数,matlab实现波特图
- mysql8 修改权限_MySQL8修改重置root密码,远程连接权限设置
- oracle π,plsql 计算π
- C语言解决累加和累乘问题
- Atitit java读取tif文件为空null的解决 图像处理
- 获取音频频响和失真_专业音响设备_音频功放失真的四大要点及改善方法
- mysql用户管理设置权限_mysql 用户管理和权限设置
- 浅谈5类过零检测电路
- 3Dmax读取丢失的贴图的方法
- Caml 多表关联查询
- 计算机网络类别(按照作用范围分类)
- 股票量化分析(11)——第二个策略(5日移动均线、双均线、MACD策略)
- 爱情就像是免杀,连鞋都没脱,就悄无声息的走进了你的心里
- php获取蓝凑云文件列表,PHP获取蓝奏云直链下载地址
- Android 客户端路由框架的整理和思考
- linux使用vmware虚拟机玩LOL
热门文章
- 阿里云服务器申请流程
- 2019最新升级全能版vbox硬件级虚拟机系统 vm去虚拟化修改信息工具 批量启动克隆 virtualbox CPA网赚挂机电商
- 微信小程序开发一个简单的摇骰子游戏
- /dev/sda1 is mounted:will not make a filesystem here!
- hadoop+Spark+hbase集群动态增加节点
- VScode中txt文件乱码解决
- C++11 多线程之 packaged_task
- springboot整合jett实现模板excel数据导出
- ExecutorService的shutdownNow方法注意事项
- 基本磁盘与所谓动态磁盘区别