三、共享模型之管程

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)

并发编程(三)---共享模型之管程相关推荐

  1. java并发编程(7) 共享模型之工具 - stampedLock、semaphore、CountdownLatch、CyclicBarri

    文章目录 前言 1. stampedLock 1. 概述 2. 代码 1. 读读 2. 读写 3. 注意 2. Semaphore 1. 基本使用 2. 应用场景 3. 原理 3. Countdown ...

  2. 【并发编程】(学习笔记-共享模型之管程)-part3

    文章目录 并发编程-共享模型之管程-3 1.共享带来的问题 1-1 临界区 Critical Section 1-2 竞态条件 Race Condition 2.synchronized 解决方案 2 ...

  3. JUC笔记-共享模型之管程 (Monitor)

    JUC-共享模型之管程( Monitor) 一.线程安全问题(重点) 1.1 同步 1.2 线程出现问题的根本原因分析 1.3 synchronized 解决方案 1.3.1 同步代码块 1.3.2 ...

  4. Java 多线程共享模型之管程(下)

    共享模型之管程 wait.notify wait.notify 原理 Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 BLOCKED 和 W ...

  5. 【云风skynet】详解skynet的多核高并发编程丨actor模型丨游戏开发丨游戏服务端开发丨多线程丨Linux服务器开发丨后端开发

    skynet中多核高并发编程给我们的启发 1. 多核并发编程 2. actor模型详解 3. 手撕一个万人同时在线游戏 视频讲解如下,点击观看: [云风skynet]详解skynet的多核高并发编程丨 ...

  6. php三要素,并发编程三要素:原子性,有序性,可见性

    并发编程三要素 **原子性:**一个不可再被分割的颗粒.原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性: 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) ...

  7. 【并发编程三】C++进程通信——管道(pipe)

    [并发编程三]C++实现通信--管道(pipe) 一.管道(pipe) 二.匿名管道 1.简介 2.父子进程:匿名管道的通信过程? 3.相关函数 3.1.创建管道CreatePipe 3.2.写入管道 ...

  8. 并发操作之——并发编程三要素

    并发操作 并发操作之--并发编程三要素. 并发操作之--并发编程三要素 并发操作 前言 一.原子性 二.有序性: 三.可见性: 总结 前言 并发操作之--并发编程三要素. 一.原子性 一个不可再被分割 ...

  9. Cpython解释器下实现并发编程——多进程、多线程、协程、IO模型

    一.背景知识 进程即正在执行的一个过程.进程是对正在运行的程序的一个抽象. 进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一.操作系统的其他所有内容都 ...

最新文章

  1. Mysql Cluster 集群 windows版本
  2. D1net阅闻:思科占全球交换路由器53%市场份额
  3. docker history 27f1068ca9da --no-trunc查看镜像dockerfile内容
  4. python的心得体会200字_python_学习心得
  5. matlab波特图带延迟的传递函数,matlab实现波特图
  6. mysql8 修改权限_MySQL8修改重置root密码,远程连接权限设置
  7. oracle π,plsql 计算π
  8. C语言解决累加和累乘问题
  9. Atitit java读取tif文件为空null的解决 图像处理
  10. 获取音频频响和失真_专业音响设备_音频功放失真的四大要点及改善方法
  11. mysql用户管理设置权限_mysql 用户管理和权限设置
  12. 浅谈5类过零检测电路
  13. 3Dmax读取丢失的贴图的方法
  14. Caml 多表关联查询
  15. 计算机网络类别(按照作用范围分类)
  16. 股票量化分析(11)——第二个策略(5日移动均线、双均线、MACD策略)
  17. 爱情就像是免杀,连鞋都没脱,就悄无声息的走进了你的心里
  18. php获取蓝凑云文件列表,PHP获取蓝奏云直链下载地址
  19. Android 客户端路由框架的整理和思考
  20. linux使用vmware虚拟机玩LOL

热门文章

  1. 阿里云服务器申请流程
  2. 2019最新升级全能版vbox硬件级虚拟机系统 vm去虚拟化修改信息工具 批量启动克隆 virtualbox CPA网赚挂机电商
  3. 微信小程序开发一个简单的摇骰子游戏
  4. /dev/sda1 is mounted:will not make a filesystem here!
  5. hadoop+Spark+hbase集群动态增加节点
  6. VScode中txt文件乱码解决
  7. C++11 多线程之 packaged_task
  8. springboot整合jett实现模板excel数据导出
  9. ExecutorService的shutdownNow方法注意事项
  10. 基本磁盘与所谓动态磁盘区别