1:了解 2:重点 3:首次引入–要清楚,记住 4:重点难点 5:比4小一点 6:补充2的两种,一共四种

8.1、基本概念:

线程的同步来解决安全隐患

注:JVM简介:


每个线程都有对应相匹配的虚拟机栈程序计数器,并且线程共用同一个方法区-共享.
每个进程有独立的方法区和堆空间



8.2、线程的创建和使用




查看ThreadAPI:



方式一:继承Thread类

/*** 多线程的创建,方式一:继承于Thread类* 1.创建一个继承于Thread类的子类* 2.重写Thread类的run()方法--> 将此线程执行的操作声明在run()中* 3.创建Thread类的子类对象* 4.通过此对象调用start()** 例子:遍历100以内的偶数** @author pflik-* @create 2021-05-29 13:50*///1.创建一个继承于Thread类的子类
class MyThread extends Thread {//2.重写Thread类的run()方法@Overridepublic void run() {for(int i = 1;i < 100;i++) {if(i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}
public class ThreadTest {public static void main(String[] args) {//3.创建Thread类的子类对象MyThread t1 = new MyThread();//4.通过此对象调用start():1.启动当前线程  2.调用当前线程的run()t1.start();//问题一:我们不能通过直接调用run()的方式启动线程
//      t1.run();//问题二:在启动一个线程,遍历100以内的偶数(这样是不可以的)//不可以让已经start()的线程去执行
//      t1.start();//不过,可以再重新创建一个线程对象,在启动一个新的线程MyThread t2 = new MyThread();t2.start();//如下操作仍然是在main线程中执行for(int i = 1;i < 100;i++) {if(i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":" + i + "***main()***");}}}
}

一个练习:

/*** 练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数* @author pflik-* @create 2021-05-29 14:18*/
public class ThreadDemo {public static void main(String[] args) {MyThread1 m1 = new MyThread1();MyThread2 m2 = new MyThread2();m1.start();m2.start();}
}class MyThread1 extends Thread {@Overridepublic void run() {for (int i = 1;i < 100;i++) {if(i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}class MyThread2 extends Thread {@Overridepublic void run() {for(int i = 1;i < 100;i++) {if(i % 2 != 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}

创建Thread类的匿名子类的方式:

/*** 练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数* @author pflik-* @create 2021-05-29 14:18*/
public class ThreadDemo {public static void main(String[] args) {//MyThread1 m1 = new MyThread1();//MyThread2 m2 = new MyThread2();//m1.start();//m2.start();//创建Thread类的匿名子类的方式--因为只会用到一次//new的是一个Thread类的匿名子类的对象(没有名字,所以直接用Thread来充当)new Thread() {@Overridepublic void run() {for (int i = 1;i < 100;i++) {if(i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}}.start();new Thread() {@Overridepublic void run() {for(int i = 1;i < 100;i++) {if(i % 2 != 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}}.start();}
}

关于Thread类的有关方法




给线程改名字的方法二:–通过写一个Thread类的含参构造器


注:run方法没有抛异常,所以在我们使用Thread类重写run()方法时也不能往上抛异常(也就是不能throws)

知识点:子类重写抛出的异常不能比父类的更大,如果父类没有抛出异常,那么子类重写的方法同样是不能够抛异常的。

线程的调度


package com.pfl.java;/*** 例子:创建是三个窗口卖票*/class Window extends Thread {//这样就是每个对象都会有一个ticket属性//private int ticket = 100;//静态变量,共享一个静态变量--会有线程的安全问题(同步会解决-后面会讲)private static int ticket = 100;//可不可以不加static来解决多个线程共享一个属性的问题--创建线程的第二种方式@Overridepublic void run() {while(true) {if(ticket > 0) {System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);ticket--;} else {break;}}}
}
public class WindowTest {public static void main(String[] args) {Window t1 = new Window();Window t2 = new Window();Window t3 = new Window();//启动之前起个名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}

方式二:实现Runnable接口的方式

创建多线程的方式二:实现Runnable接口

  1. 创建一个实现了Runnable接口的类
  2. 实现类(重写)去实现Runnable类中的抽象方法 :run()方法
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()方法
 //1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{//直接显示接口的抽象方法:alt + enter//2. 实现类(重写)去实现Runnable类中的抽象方法 :run()方法@Overridepublic void run() {for (int i = 0;i < 100;i++) {if (i % 2 == 0) {//注:因为没有继承Thread,所以就没法直接用于获取线程的名字方法,// 需要加上Thread.currentThread().getName()System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}public class ThreadTest1 {public static void main(String[] args) {//3. 创建实现类的对象(直接new MThread(),然后快捷键:alt + enter)MThread mThread = new MThread();//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象//这是一个多态的形式,将实现类的对象作为参数传递给形参Thread t1 = new Thread(mThread);//5. 通过Thread类的对象调用start()方法:// start()方法:①.启动线程②.调用当前线程的run方法(调用的是Thread类种的run方法)//调用了Runnable类型的target的run()方法---就是Runnable target = mThread;t1.start();//使用Runnable方式来创建了一个线程--t1//在启动一个线程Thread t2 = new Thread(mThread);t2.start();}
}

一个例子:用继承Runnable接口的方式实现多线程

/*** 例子:创建是三个窗口卖票,使用继承Thread类接口的方式来实现(WindowTest.java)* 方法:现在使用Runnable接口的方式来实现**/class Window1 implements Runnable {//注:没有加static总的票数还是100张票//原因:只是造了一个对象;三个线程都是用一个对象,那么就是同一个ticketprivate int ticket = 100;//重写Run方法@Overridepublic void run() {while(true) {if (ticket > 0) {System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);ticket--;} else {break;}}}
}
public class WindowTest1 {public static void main(String[] args) {//造一个Window1的对象Window1 w = new Window1();//上面的对象作为参数,传递给Thread类的构造器中//因为卖票的时候会有三个窗口Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();}
}

方式一与方式二的比较

比较创建(多)线程的两种方式:
开发中:优先选择:实现Runnable接口的方式
原因:

  1. 实现的方式没有类的单继承性的局限性限制(接口出现的原因就是解决类的单继承性的问题)
  2. 实现的方式更适合处理,多个线程有共享的数据情况(景区的票数)

联系: public class Threa implements Runnable
相同点: 两种方式都需要重写run()方法,将创建的线程要执行的逻辑声明在run()中

class MThread implements Runnable{//直接显示接口的抽象方法:alt + enter//2. 实现类(重写)去实现Runnable类中的抽象方法 :run()方法@Overridepublic void run() {for (int i = 0;i < 100;i++) {if (i % 2 == 0) {//注:因为没有继承Thread,所以就没法直接用于获取线程的名字方法,// 需要加上Thread.currentThread().getName()System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}public class ThreadTest1 {public static void main(String[] args) {//3. 创建实现类的对象(直接new MThread(),然后快捷键:alt + enter)MThread mThread = new MThread();//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象//这是一个多态的形式,将实现类的对象作为参数传递给形参Thread t1 = new Thread(mThread);//5. 通过Thread类的对象调用start()方法:// start()方法:①.启动线程②.调用当前线程的run方法(调用的是Thread类种的run方法)//调用了Runnable类型的target的run()方法---就是Runnable target = mThread;t1.start();//使用Runnable方式来创建了一个线程--t1//在启动一个线程Thread t2 = new Thread(mThread);t2.start();}
}

每日一练

程序:为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态的对象(在存储空间中)。
进程:正在运行的一个程序,是一个动态的过程:有它自身的产生,存在和消亡的过程。–生命周期
线程(Thread):进程进一步细化为线程,是一个程序内部的一条执行路径。


多线程的创建,方式一:继承于Thread类

  • 1.创建一个继承于Thread类的子类
  • 2.重写Thread类的run()方法–> 将此线程执行的操作声明在run()中
  • 3.创建Thread类的子类对象
  • 4.通过此对象调用start()


创建多线程的方式二:实现Runnable接口

  1. 创建一个实现了Runnable接口的类
  2. 实现类(重写)去实现Runnable类中的抽象方法 :run()方法
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()方法

比较创建(多)线程的两种方式:
开发中:优先选择:实现Runnable接口的方式
原因:

  1. 实现的方式没有类的单继承性的局限性限制(接口出现的原因就是解决类的单继承性的问题)
  2. 实现的方式更适合处理,多个线程有共享的数据情况(景区的票数)

联系: public class Threa implements Runnable
相同点: 两种方式都需要重写run()方法,将创建的线程要执行的逻辑声明在run()中

8.3、线程的生命周期

线程的状态:


8.4 、线程的同步

注:主要是解决线程的安全的问题
例:卖票的时候会有重票的问题,还有错票的问题–都是线程的安全的问题

案例:

1.问题:卖票过程中,出现了重票,错票 -->> 出现了线程的安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也来操作车票
3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket的时候。
线程才可以开始操作ticket。这种情况,即使线程a出现了阻塞,也不能改变
4.在java中,我们通过同步机制,来解决线程的安全问题
方式一: 同步代码块
synchronized(同步监视器) {
//需要被同步的代码
}
说明:1.操作共享数据的代码,即为需要别同步的代码
2.共享数据:多个线程共同操作的变量,比如ticket就是共享数据
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用一把锁。
方式二: 同步方法
5.同步的方式,解决了线程的安全的问题。—好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低–局限性

同步代码块解决使用Runnable接口的方式来实现线程


/*** 例子:创建是三个窗口卖票,使用继承Thread类接口的方式来实现(WindowTest.java)* 方法:现在使用Runnable接口的方式来实现**//*** 1.问题:卖票过程中,出现了重票,错票  -->> 出现了线程的安全问题* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也来操作车票* 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket的时候。* 线程才可以开始操作ticket。这种情况,即使线程a出现了阻塞,也不能改变* 4.在java中,我们通过同步机制,来解决线程的安全问题**  方式一: 同步代码块*      synchronized(同步监视器) {*          //需要被同步的代码*      }*      说明:1.操作共享数据的代码,即为需要别同步的代码*          2.共享数据:多个线程共同操作的变量,比如ticket就是共享数据*          3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。*              要求:多个线程必须要共用一把锁。**  方式二: 同步方法*** 5.同步的方式,解决了线程的安全的问题。---好处*      操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低--局限性*/class Window1 implements Runnable {//注:没有加static总的票数还是100张票//原因:只是造了一个对象;三个线程都是用一个对象,那么就是同一个ticketprivate int ticket = 100;//同步监视器(锁)--任意一个对象Object obj = new Object();//重写Run方法@Overridepublic void run() {while(true) {synchronized(obj) {//下面就是操作共享数据的代码了if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);ticket--;} else {break;}}}}
}public class WindowTest1 {public static void main(String[] args) {//造一个Window1的对象Window1 w = new Window1();//上面的对象作为参数,传递给Thread类的构造器中//因为卖票的时候会有三个窗口Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();}
}



注意:一定是同一把锁,需要我们把锁制成static
private static Object obj = new Object();
2. 区分一下下面两个代码的情况,为什么可以用this,–因为只是new了一个Windows对象,而Window2会有三个;

使用’同步代码块’解决继承Thread类的方式线程安全问题

package com.pfl.exer;/*** 使用'同步代码块'解决继承Thread类的方式线程安全问题** 例子:创建是三个窗口卖票**/class Window2 extends Thread {//这样就是每个对象都会有一个ticket属性//private int ticket = 100;//静态变量,共享一个静态变量--会有线程的安全问题(同步会解决-后面会讲)private static int ticket = 100;//可不可以不加static来解决多个线程共享一个属性的问题--创建线程的第二种方式//同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。private static Object obj = new Object();@Overridepublic void run() {while(true) {//正确的//synchronized(obj) {synchronized(Window2.class){ //类也是对象(反射:Class clazz = Window2.class)//Window2.class只会加载一次//错误的方式:this代表着t1,t2,t3三个对象//synchronized (this) {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);ticket--;} else {break;}}}}
}
public class WindowTest2 {public static void main(String[] args) {Window2 t1 = new Window2();Window2 t2 = new Window2();Window2 t3 = new Window2();//启动之前起个名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}
/*** 例子:创建是三个窗口卖票,使用继承Thread类接口的方式来实现(WindowTest.java)* 方法:现在使用Runnable接口的方式来实现**//*** 1.问题:卖票过程中,出现了重票,错票  -->> 出现了线程的安全问题* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也来操作车票* 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket的时候。* 线程才可以开始操作ticket。这种情况,即使线程a出现了阻塞,也不能改变* 4.在java中,我们通过同步机制,来解决线程的安全问题**  方式一: 同步代码块*      synchronized(同步监视器) {*          //需要被同步的代码*      }*      说明:1.操作共享数据的代码,即为需要别同步的代码。 -->不能包含代码多了也不能代码少了*          2.共享数据:多个线程共同操作的变量,比如ticket就是共享数据*          3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。*              要求:多个线程必须要共用一把锁。**        补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器*  方式二: 同步方法*        如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的** 5.同步的方式,解决了线程的安全的问题。---好处*      操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低--局限性*/class Window implements Runnable {//注:没有加static总的票数还是100张票//原因:只是造了一个对象;三个线程都是用一个对象,那么就是同一个ticketprivate int ticket = 100;//同步监视器(锁)--任意一个对象//Object obj = new Object();//重写Run方法@Overridepublic void run() {while(true) {synchronized(this) {//此时的this:唯一的Windows的对象  //方式二:synchronized(obj) {//下面就是操作共享数据的代码了if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);ticket--;} else {break;}}}}
}public class WindowTest1 {public static void main(String[] args) {//造一个Window1的对象Window w = new Window();//上面的对象作为参数,传递给Thread类的构造器中//因为卖票的时候会有三个窗口Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();}

使用’同步方式’解决实现Runnable接口的线程安全问题

/*** 使用'同步方式'解决实现Runnable接口的线程安全问题** @author pflik-* @create 2021-06-26 16:34*/
class Window3 implements Runnable {//注:没有加static总的票数还是100张票//原因:只是造了一个对象;三个线程都是用一个对象,那么就是同一个ticketprivate int ticket = 100;//重写Run方法@Overridepublic void run() {while(true) {show();}}private synchronized void show() {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);ticket--;}}
}public class WindowTest3 {public static void main(String[] args) {//造一个Window1的对象Window3 w = new Window3();//上面的对象作为参数,传递给Thread类的构造器中//因为卖票的时候会有三个窗口Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();}
}

使用’同步方法’解决继承Thread类的方式线程安全问题

package com.pfl.exer;/*** 使用'同步方法'解决继承Thread类的方式线程安全问题*** 关于同步方法的总结:* 1.同步方法仍然设计到同步监视器,只是不需要我们显示的声明* 2.非静态的同步方法,同步监视器是:this*   静态同步方法,同步监视器是:当前类本身** 例子:创建是三个窗口卖票**/class Window4 extends Thread {//这样就是每个对象都会有一个ticket属性//private int ticket = 100;//静态变量,共享一个静态变量--会有线程的安全问题(同步会解决-后面会讲)private static int ticket = 100;//可不可以不加static来解决多个线程共享一个属性的问题--创建线程的第二种方式@Overridepublic void run() {while(true) {show();}}private static  synchronized void show() {//同步监视器:Window4.class//private  synchronized void show() {//同步监视器:t1,t2,t3;此种方法是错误的if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);ticket--;}}
}
public class WindowTest4 {public static void main(String[] args) {Window4 t1 = new Window4();Window4 t2 = new Window4();Window4 t3 = new Window4();//启动之前起个名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}

使用同步机制将单例模式中的懒汉式改写为线程安全的

package com.pfl.java1;/*** 使用同步机制将单例模式中的懒汉式改写为线程安全的*/
public class BankTest {}
//懒汉式
//这个类只能是单例的,相当于只能创建一个实例
class Bank {//必须的:构造器的问题private Bank(){}private static Bank instance = null;//同步的方式://锁是当前类的对象:类本身充当了对象//public static synchronized Bank getInstance() {public static Bank getInstance() {//代码块的方式://方式一:效率稍差
//        synchronized (Bank.class) {//            if (instance == null) {//                instance = new Bank();
//            }
//            return instance;
//        }//方式二:if(instance == null) {synchronized (Bank.class) {if (instance == null) {instance = new Bank();}}}return instance;}
}

线程的死锁的问题

package com.pfl.java1;/*** 演示线程的死锁问题*/
public class ThreadTest {public static void main(String[] args) {//常用类StringBuffer s1 = new StringBuffer();StringBuffer s2 = new StringBuffer();//匿名的方式来造//方式一:继承Thread类的方式建线程new Thread() {  //继承@Overridepublic void run() {//synchronized (s1) {s1.append("a");s2.append("1");synchronized (s2) {s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();//方式二:实现Runnable接口建线程new Thread(new Runnable() {@Overridepublic void run() {synchronized (s2) {s1.append("c");s2.append("3");synchronized (s1) {s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}).start();}
}

运行结果:

对上面的代码修改一下,可以增加产生死锁的可能:

package com.pfl.java1;/*** 演示线程的死锁问题*  1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,形成线程的死锁**  2.说明:*  1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续*  2)我们使用同步时,要避免出现死锁。*  **/
public class ThreadTest {public static void main(String[] args) {//常用类StringBuffer s1 = new StringBuffer();StringBuffer s2 = new StringBuffer();//匿名的方式来造//方式一:继承Thread类的方式建线程new Thread() {  //继承@Overridepublic void run() {//synchronized (s1) {s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2) {s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}}.start();//方式二:实现Runnable接口建线程new Thread(new Runnable() {@Overridepublic void run() {synchronized (s2) {s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1) {s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}}).start();}
}

一个例子:

package com.pfl.java1;
//死锁的演示class A {public synchronized void foo(B b) {  //同步监视器:A类的对象:aSystem.out.println("当前线程名:" + Thread.currentThread().getName()+ " 进入了A实例的foo方法");  //1try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("当前线程名:" + Thread.currentThread().getName()+ " 企图调用B实例的last方法");  //3b.last();}public synchronized void last() {System.out.println("进入了A类的last方法内部");}
}class B {public synchronized void bar(A a) {  //同步监视器:bSystem.out.println("当前线程名:" + Thread.currentThread().getName()+ " 进入了B实例的bar方法");try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("当前线程名:" + Thread.currentThread().getName()+ " 企图调用A实例的last方法");a.last();}public synchronized void last() {  //同步监视器:b这个对象System.out.println("进入了B类的last方法内部");}}public class DeadLock implements Runnable{A a = new A();B b = new B();public void init() {Thread.currentThread().setName("主线程");//调用A线程的foo方法a.foo(b);System.out.println("进入了主线程之后");}@Overridepublic void run() {Thread.currentThread().setName("副线程");//调用B对象的bar方法b.bar(a);System.out.println("进入了副线程之后");}public static void main(String[] args) {DeadLock d1 = new DeadLock();new Thread(d1).start();d1.init();}
}

举个栗子:同样是以卖票的方式

package com.pfl.java1;import java.util.concurrent.locks.ReentrantLock;/*** 前面的方式一:同步代码块;方式二:同步方法;也可以统称为synchornized方法* 解决线程安全问题的方式三: Lock锁(Lock本来是个接口,我们用到他的子类ReentrantLock)  ---- JDK5.0新增** 1.面试题:synchronized 与Lock的异同* 相同:二者都可以解决线程安全问题* 不同:synchronized机制在执行完相应的同步代码块以后,自动的释放同步监视器*      Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())*  2.面试题:如何解决线程安全的问题?有几种方式*   答:可以说是两种也可以说是三种*   前面的方式一:同步代码块;方式二:同步方法;也可以统称为synchornized方法*   解决线程安全问题的方式三: Lock锁(Lock本来是个接口,我们用到他的子类ReentrantLock)  ---- JDK5.0新增
注:如果创建多线程的方式是继承Thread类的话:要保证Lock针对线程是唯一的,要用同一个锁去解锁
继承Thread类会造好几个线程,Lock要是静态的;--“private static ReentrantLock lock = new ReentrantLock(true);”*/
//先出现线程的安全问题--创建多线程(继承Thread类+实现Runnable接口)
//开始写的代码线程安全问题:会出现重票和错票的问题class Window implements Runnable {private int ticket = 100;//1.实例化ReentrantLock//在Runnable具体实现类中,声明一个属性,造一个ReentrantLock这个对象//参数为:true;说明这是一个公平的Lock:先进先出;private ReentrantLock lock = new ReentrantLock(true);@Overridepublic void run() {while(true) {try {//2. 调用了lock()--类似于:线程获取了同步监视器//下面的代码类似于同步代码块中的代码一样,是个单线程的lock.lock();if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ": 售票,票号为:" + ticket);ticket--;} else {break;}} finally { //因为没有异常,所以只需要finally//3.调用解锁的放法unlock-解锁一下lock.unlock();}}}
}
public class LockTest  {public static void main(String[] args) {Window w = new Window();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");}
}

练习一:

package com.pfl.exer;/*** 分析问题:* 1. 是否是多线程的问题?  是,两个储户线程* 2. 是多线程是否会涉及到线程安全的问题?  不一定,取决于是否有共享数据* 3. 是否有共享数据?   有,账户(账户余额)* 4. 需要考虑如何解决线程安全问题? 同步机制:有三种方式。**/class Account {private double balance; //账户余额public Account(double balance) {this.balance = balance;}//存钱public synchronized void deposit(double amt) {if(amt > 0) {balance += amt;try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("存钱成功!余额为:" + balance);}}
}
/*** 注意:用到了继承Thread类的方式实现多线程,然而上面我们用到了同步的方法;* 之前讲过,继承的方式,用同步的方法要慎用!!!* --需要同步的方法要是静态的,----也就是说同步监视器用的是this,要慎用,因为有可能this不唯一;** 然而这个例子为什么可以用:* 此时的this不是多个customer,而是共用的Account,Account使用的是同一个,所以就没事了,可以保证线程的安全性**/class Customer extends Thread {private Account acct;//注:Alt+Insert 生成代码(如get,set方法,构造函数等)//在构造器中对属性进行实例化public Customer(Account acct) {this.acct = acct;}@Overridepublic void run() {for (int i = 0;i < 3; i++) {acct.deposit(1000);}}
}public class AccountTest {public static void main(String[] args) {//实现两个线程共用一个账户Account acct = new Account(0);Customer c1 = new Customer(acct);Customer c2 = new Customer(acct);c1.setName("甲");c2.setName("乙");c1.start();c2.start();}
}

8.5、线程的通信

难度比线程的同步低一些。
算是在"交流"吧!–都可以叫通信
比如:两个线程交替打印数(1-100),其实这就是在"交流"了,“你一下,我一下!”
线程的通信

一个例子

没有实现交替打印,只是多线程打印

package com.pfl.java2;/*** 线程通信的一个例子**/
//通过实现Runnable接口的方式来创建多线程
class Number implements Runnable {//就这样执行的话,就会出现线程的安全问题;同时,number就是共享数据。//处理安全的问题:用同步代码块的方式来处理//注意:同步锁就可以用This,因为都是唯一的Number对象 Number number = new Number();private int number = 1;@Overridepublic void run() {while(true) {//注:自动生成try...catch或者synchronized等快捷键:ctrl + alt + Tsynchronized (this) {if(number <= 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":" + number);number++;} else {break;}}}}
}
public class CommunicationTest {public static void main(String[] args) {//先new一下Number对象Number number = new Number();//new两个线程Thread t1 = new Thread(number);Thread t2 = new Thread(number);t1.setName("线程一");t2.setName("线程二");t1.start();t2.start();}
}

结果(随机性特别大,也就是说结果不定):

实现交替打印的代码:

package com.pfl.java2;/*** 线程通信的一个例子**/
//通过实现Runnable接口的方式来创建多线程
class Number implements Runnable {//就这样执行的话,就会出现线程的安全问题;同时,number就是共享数据。//处理安全的问题:用同步代码块的方式来处理//注意:同步锁就可以用This,因为都是唯一的Number对象 Number number = new Number();private int number = 1;@Overridepublic void run() {while(true) {//注:自动生成try...catch或者synchronized等快捷键:ctrl + alt + Tsynchronized (this) {//2.需要唤醒//注:唤醒一个(notify)还是唤醒很多个(notifyAll)的问题,// 理解:// 如果两个线程AB:假如A线程执行notify,那么就会唤醒B线程,// 如果三个线程ABC:加入A线程执行notifyAll,那么就会把BC线程都唤醒。// 因为只有两个线程,所以只需要notify即可;notify();if(number <= 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":" + number);number++;try {//1.使得调用wait()方法的线程进入阻塞状态//同时需要注意:wait之后要释放同步锁,这样就可以被另外一个进程拿到,实现交替执行//就和sleep有区别了,在sleep中不会释放同步锁,直到sleep时间结束才会释放同步锁。wait();} catch (InterruptedException e) {e.printStackTrace();}} else {break;}}}}
}
public class CommunicationTest {public static void main(String[] args) {//先new一下Number对象Number number = new Number();//new两个线程Thread t1 = new Thread(number);Thread t2 = new Thread(number);t1.setName("线程一");t2.setName("线程二");t1.start();t2.start();}
}

实现交替打印的代码的终版:

package com.pfl.java2;/*** 线程通信的一个例子* 线程通信涉及到的三个方法:*      wait()方法:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。*      notify()方法--只会唤醒一个:一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。*      notifyAll()方法--唤醒所有的被wait的方法:一旦执行此方法,就会唤醒所有被wait的线程。** 说明:* 1.wait(),notify(),notifyAll()这些方法必须使用在同步代码块或同步方法中。* 2.wait(),notify(),notifyAll()这些方法的调用者必须是同步代码块或同步方法中的同步监视器。*  否则,会出现“java.lang.IllegalMonitorStateException”* 3.wait(),notify(),notifyAll()这些方法是定义在java.lang.Object类中;不是在Thread类中*  任何类都可以被当做是同步监视器,同时同步监视器和这三个方法都要被同一个类所调用,也就是任何对象都要有这三个方法;*  那么这三个方法只能是定义在Object类中。** 面试题:sleep()和wait()的异同?*  1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。*  2.不同点: 1) 两个方法声明的位置不同;Thread类中声明sleep(),Object类中声明wait()*           2) 调用范围不同:sleep()可以再任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中*           3) 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。***/
//通过实现Runnable接口的方式来创建多线程
class Number implements Runnable {//就这样执行的话,就会出现线程的安全问题;同时,number就是共享数据。//处理安全的问题:用同步代码块的方式来处理//注意:同步锁就可以用This,因为都是唯一的Number对象 Number number = new Number();private int number = 1;private Object obj = new Object();@Overridepublic void run() {while(true) {//注:自动生成try...catch或者synchronized等快捷键:ctrl + alt + Tsynchronized (obj) {  //注:同步监视器可以是任意对象来充当,不过在线程通信时,上面三个方法的调用者必须一致//2.需要唤醒//注:唤醒一个(notify)还是唤醒很多个(notifyAll)的问题,// 理解:// 如果两个线程AB:假如A线程执行notify,那么就会唤醒B线程,// 如果三个线程ABC:加入A线程执行notifyAll,那么就会把BC线程都唤醒。// 因为只有两个线程,所以只需要notify即可;obj.notify();if(number <= 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":" + number);number++;try {//1.使得调用wait()方法的线程进入阻塞状态//同时需要注意:wait之后要释放同步锁,这样就可以被另外一个进程拿到,实现交替执行//就和sleep有区别了,在sleep中不会释放同步锁,直到sleep时间结束才会释放同步锁。obj.wait();} catch (InterruptedException e) {e.printStackTrace();}} else {break;}}}}
}
public class CommunicationTest {public static void main(String[] args) {//先new一下Number对象Number number = new Number();//new两个线程Thread t1 = new Thread(number);Thread t2 = new Thread(number);t1.setName("线程一");t2.setName("线程二");t1.start();t2.start();}
}

结果:

一个面试题:sleep()和wait()的异同?

面试题:sleep()和wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:

  1. 两个方法声明的位置不同;Thread类中声明sleep(),Object类中声明wait()
  2. 调用范围不同:sleep()可以再任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中
  3. 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

线程通信的一个经典的问题:生产者和消费者的问题

package com.pfl.java2;/***  线程通信的应用:经典例题:生产者/消费者问题**  分析:*  1.是不是多线程的问题? 是  ,生产者线程 + 消费者线程*  2.是否有线程安全的问题?是否有共享数据?   是,店员(或产品)*  3.处理线程的安全问题--如何解决线程的安全问题?  同步机制,三种方法*  4.是否涉及到线程的通信? 是, ** @author pflik-* @create 2021-07-08 17:10*/
//店员
class Clerk {private int productCount = 0;//生产产品public synchronized void produceProduct() {if(productCount < 20) {productCount++;System.out.println(Thread.currentThread().getName() +":开始生产第" + productCount + "个产品");//只要是生产了产品,那么就可以被消费,这样就要唤醒一下消费者了;notify();} else {//等待try {wait();} catch (InterruptedException e) {e.printStackTrace();}}}//消费产品public synchronized void consumeProduct() {if(productCount > 0) {System.out.println(Thread.currentThread().getName() +":开始消费第" + productCount + "个产品");productCount--;//同理,只要是消费了产品,就可以生产了!就可以唤醒生产者了!notify();} else {//等待try {wait();} catch (InterruptedException e) {e.printStackTrace();}}}//注:两个线程都会对productCount进行操作,所以就会出现线程的安全的问题;//为了保证,在同一时间段,只会有一个线程对共享数据进行操作,需要处理线程的安全问题;//1.首先不能用同步代码块对两个方法进行处理//2.可以把这两个方法同步一下即可;//最后注意:对于wait()方法的前边没有写"对象.",其实是省略了"this."
}//生产者
//以继承Thread类的方式来创建多线程
class Producer extends Thread{//因为生产者,或者消费者都会共用Clerk(店员)private Clerk clerk;//构造器 : alt + insertpublic Producer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {//注:使用的继承Thread类的方式,可以省略"Thread.currentThread()."System.out.println(Thread.currentThread().getName() + ":开始生产产品......");while(true) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//生产东西在店员中实现clerk.produceProduct();}}
}
//消费者
//同样是使用继承Thread类的方式创建多线程(同一个项目中,继承就都继承,实现就都实现)
class Consumer extends Thread {private Clerk clerk;public Consumer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {//注:使用的继承Thread类的方式,可以省略"Thread.currentThread()."System.out.println(Thread.currentThread().getName() + ":开始消费产品......");while(true) {try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}//生产东西在店员中实现clerk.consumeProduct();}}
}public class ProductTest {public static void main(String[] args) {Clerk clerk = new Clerk();Producer p1 = new Producer(clerk);p1.setName("生产者1");Consumer c1 = new Consumer(clerk);c1.setName("消费者1");Consumer c2 = new Consumer(clerk);c2.setName("消费者1");p1.start();c1.start();c2.start();}
}

8.6、JDK5.0新增线程创建方式

新增方式:实现Callable接口+使用线程池

方式三:关于实现Callable接口:

重写的是call方法;
会用到泛型–在第12章讲

package com.pfl.java2;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** @author pflik-* @create 2021-07-09 16:40* 创建线程的方式三:实现Callable接口。 ---JDK5.0新增** 实现call()抽象方法,并且还可以抛出异常,并且还有返回值** 问题:如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?(Callable的优点)* 1.call()可以有返回值的* 2.call()可以抛出异常,被外面的操作捕获,获取异常的信息* 3.Callable是支持泛型的**/
//1.创建一个实现Callable的实现类
class NumThread implements Callable {//2.实现call方法,将此线程需要执行的操作声明在call()中//可以抛出异常@Overridepublic Object call() throws Exception {//遍历100以内的int sum = 0;for(int i = 0;i <= 100;i++) {if(i % 2 == 0) {System.out.println(i);sum += i;}}return sum;//注:需要转换类型}
}public class ThreadNew {public static void main(String[] args) {//3.创建Callable接口实现类的对象NumThread numThread = new NumThread();//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象FutureTask futureTask = new FutureTask(numThread);//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()// FutureTask实现了Runnable接口new Thread(futureTask).start();//注:也可以分两步(一般写法)//Thread t = new Thread(futureTask);//t.start();//如果想要结果,就调用get方法,否则可以不用调用,这个不是必须的步骤://通过调用get方法,就可以获取形参Callable接口实现类对象的回调(call())方法//get()方法返回值即为FutureTask构造器参数Callable实现类重写的call()返回值try {//6.获取Callable中call方法的返回值Object sum = futureTask.get();System.out.println("总和为:" + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}

方式四:关于使用线程池:

真正在开发中,不会自己亲自一个一个的造线程,效率其实比较差,真正用到的都是线程池
一个案例:在手机端页面刷新的时候,图片和文字的刷新情况。

一个例子:

package com.pfl.java2;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;/****  创建线程的范式四:使用线程池**  好处:*  1.提高响应速度(减少了创建新线程的时间)*  2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)*  3.便于线程管理*      corePoolSize:核心池的大小*      maximumPoolSize:最大线程数*      keepAliveTime:线程没有任务时最多保持多长时间后会终止**  面试题:创建多线程有几种方式?  四种!*  * @author pflik-* @create 2021-07-10 11:16*/class NumberThread implements Runnable {@Overridepublic void run() {for (int i = 0;i <= 100;i++) {if(i % 2 ==0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}class NumberThread2 implements Runnable {@Overridepublic void run() {for (int i = 0;i <= 100;i++) {if(i % 2 !=0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}}
}public class ThreadPool {public static void main(String[] args) {//1. 提供指定线程数量的线程池//返回的是一个接口,这是一个多态的形式,是接口实现类的对象ExecutorService service = Executors.newFixedThreadPool(10);//设置线程池的属性--便于线程管理
//        System.out.println(service.getClass());//结果是:class java.util.concurrent.ThreadPoolExecutor//强制转换一下格式ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;//接口实现类的对象(类中的属性可以是变量,在接口中设置肯定是不可以的,只能需要转一下格式)service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象NumberThread numberThread = new NumberThread();NumberThread2 numberThread2 = new NumberThread2();//执行,重写run()方法,也没有返回值--适合适用于Runnableservice.execute(numberThread);  //需要把实现Runnable类的对象传进来service.execute(numberThread2);  //需要把实现Runnable类的对象传进来//提交,--适合适用于Callable//service.submit(Callable callable);//注:需要自己指定想要干什么--在实现类中//3.关闭连接池:线程池不用了之后需要执行关闭操作service.shutdown();}
}

每日一练:


后面的总结会在继续总结!

注:同步监视器首先是一个对象,而且任何一个类的对象都可以作为同步监视器;
唯一的要求就是多个线程要用同一个监视器,也就是同一把锁。

共享数据:多个线程共同操作的数据;
需要注意的是:一个线程操作共享数据的时候就不能让其他线程继续操作共享数据,否则就会出现线程的安全的问题;
在同步中,大括号里面包着的就是操作共享数据的代码;(包括的多了之后,就会出现:一个线程一直执行,这就成了单线程了;包括的少了,依然会有线程的安全问题)

在同步方法中:
方法体依然是:操作共享数据的代码;
同步监视器是默认指定的,不需要显示的去声明;需要理解的是:同步方法中的同步监视器是谁?(要保证多个线程共用同一个同步监视器)
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身(唯一的)

  1. 存在的位置不同:sleep()在Thread类中,wait()是在Object中
  2. 声明的位置不同:sleep()在任何地方,wait()生明在同步代码块或同步方法中
  3. sleep()需要指定时间一到时间就会结束阻塞,wait()需要notify()或者notifyAll()来结束,唤醒
  4. 关于是否释放锁:sleep()不会主动地释放锁,wait()会释放锁,被其他线程使用


使用同步机制将单例模式中的懒汉式改写为线程安全的

package com.pfl.java1;/*** 使用同步机制将单例模式中的懒汉式改写为线程安全的*/
public class BankTest {}
//懒汉式
//这个类只能是单例的,相当于只能创建一个实例
class Bank {//必须的:构造器的问题private Bank(){}private static Bank instance = null;//同步的方式://锁是当前类的对象:类本身充当了对象//public static synchronized Bank getInstance() {public static Bank getInstance() {//代码块的方式://方式一:效率稍差
//        synchronized (Bank.class) {//            if (instance == null) {//                instance = new Bank();
//            }
//            return instance;
//        }//方式二:if(instance == null) {synchronized (Bank.class) {if (instance == null) {instance = new Bank();}}}return instance;}
}

Java笔记14-Java高级编程部分-第八章-多线程相关推荐

  1. iOS读书笔记之Objective-C高级编程(GCD)

    本文主要对GCD的概念.API以及实现进行梳理. 一.CCD的概念. 1.GCD,全称是Grand Central Dispatch,它是C语言的API. GCD的核心 : 将block(任务)添加到 ...

  2. 侯捷C++课程笔记01: 面向对象高级编程(上)

    本笔记根据侯捷老师的课程整理而来:C++面向对象高级编程(上) pdf版本笔记的下载地址: 笔记01_面向对象高级编程(上),排版更美观一点(访问密码:3834) 侯捷C++课程笔记01: 面向对象高 ...

  3. 侯捷C++课程笔记02: 面向对象高级编程(下)

    本笔记根据侯捷老师的课程整理而来:C++面向对象高级编程(下) pdf版本笔记的下载地址: 笔记02_面向对象高级编程(下),排版更美观一点(访问密码:3834) 侯捷C++课程笔记02: 面向对象高 ...

  4. 【狂神Java笔记】Java网络编程实战详细笔记整理(附代码)

    1.1 概述 计算机网络: 计算机网络是指将地理位置不同 的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递 ...

  5. 学java怎么做笔记?如何整理java笔记?java学习笔记

    许多开始学Java编程的朋友,都是跟着视频学习的,知道要记笔记,但又不知如何下手.其实笔记主要的还是记框架以及自己能感觉到不懂得地方方便巩固加深印象,笔记不要记得密密麻麻的看着就让人感觉头疼,学习编程 ...

  6. Python学习笔记:面向对象高级编程(上)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  7. Java笔记:Java SE —— 核心类库(下)

    数据来源:拉勾教育Java就业急训营 核心类库(下) 一. 异常机制和File类 1. 异常机制(重点) 1.1 概念 1.2 异常的分类 1.3 异常的避免 1.4 异常的捕获 笔试考点 1.5 异 ...

  8. java 笔记(4) —— java I/O 流、字节流、字符流

    Java中使用流来处理程序的输入和输出操作,流是一个抽象的概念,封装了程序数据于输入输出设备交换的底层细节.JavaIO中又将流分为字节流和字符流,字节流主要用于处理诸如图像,音频视频等二进制格式数据 ...

  9. 【读书笔记】JavaScript高级编程(二)

    2019独角兽企业重金招聘Python工程师标准>>> 书中第3章 基本概念摘要(一) 3.3 变量 使用var操作符定义的变量将成为定义该变量的作用域中的局部变量.也就是说,如果在 ...

最新文章

  1. [maven] 使用问题及思考汇总
  2. Redis 基数统计:HyperLogLog 小内存大用处
  3. obj: object是什么意思_面试官问你JavaScript基本类型时他想知道什么?
  4. 使用Activator.CreateInstance完善简单工厂
  5. hbase源码系列(八)从Snapshot恢复表
  6. 使用Javascript / jQuery下载文件
  7. python_selenium简单的滑动验证码
  8. HDU 2076 夹角有多大
  9. float和int转换
  10. iPhone X (XS XR XSMAX)如何根据状态栏上的图标 获取设备的联网状态(不是单个应用的)
  11. zemax验证高斯公式_Zemax中高斯光束设置的相关问题
  12. pc模仿移动端滚动条样式,好看就对了
  13. Keras中的深度学习的模型:序列模型(Sequential)和通用模型(Model)
  14. 国内公有云对比(1.5)- 功能篇之青云
  15. mysql模糊查询语句怎么不区分大小写
  16. github 开源android项目
  17. 估值最高的自媒体达38亿,不是咪蒙、一条,也不是逻辑思维,而是他..
  18. CAD批量打图精灵更新至9.5.3,支持使用Adobe PDF虚拟打印机以创建高质量的PDF文件。
  19. 百度api使用:文字识别(OCR)、长图文字识别、姓名识别
  20. python社区微信群_30行Python代码,打造一个简单的微信群聊助手,简单方便

热门文章

  1. python函数笔记_小白学习笔记之Python函数(一)
  2. 驻云科技完成C轮近亿元融资 成为撬起云计算的“支点”
  3. 三维点云数据特征检测
  4. Android 监听系统语言切换
  5. 15个最实用的数据可视化工具
  6. 计算机cct证书含金量排名,省考CCT和国考NCRE的区别?
  7. 从家里到阿里,学弟求职的一年
  8. 外贸企业邮箱多少钱? 企业内部邮箱用什么安全? 企业邮箱怎么申请?
  9. 专业能力一般的应届本科生,该如何准备面试
  10. 北极熊扫描器4.0发布,无需过多介绍的国产安全工具