Java并发编程基础

目录

普通方法调用和多线程区别

程序,进程-process,线程-Thread

创建线程方式一:继承Thread类

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

初识并发问题

Lambda表达式

线程五种状态

停止线程

线程休眠

线程等待--wait

线程礼让---yield

线程强制执行----join

观测线程状态

线程的优先级---priority

守护线程---daemon

线程同步机制

三大线程不安全案例

同步方法及同步块

CopyOnWriteArrayList

死锁

Lock锁

生产者消费者问题

管程法

信号灯法

线程池

四种线程池介绍

缓冲队列BlockingQueue和自定义线程池ThreadPoolExecutor



普通方法调用和多线程区别

程序,进程-process,线程-Thread

在操作系统中运行的程序就是进程,比如QQ,播放器,游戏,IDE等,一个进程可以有多个线程,比如视频中同时听声音,看图像,看弹幕等等。

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念;

进程则是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位;一个进程至少有一个线程,不然没有存在的意义;

线程是CPU调度和执行的单位。

注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。线程之间切换是因为每个线程都有一个时间片,时间片到期就会cpu被另一个线程抢占使用,也就是cpu允许一个线程运行多长时间,单位好像是纳秒。 

总结:线程就是独立的执行路径;在程序运行时,即使没有自己创建线程,后台也会又多个线程,如子线程,gc线程,也称之为守护线程,是jvm提供的;main()称之为主线程,为系统的入口,用于执行整个程序;在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的;对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;线程会带来额外的开销,如cpu调度时间、并发控制开销;每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

创建线程方式一:继承Thread类

创建线程方式:继承Thread类,重写run()方法,调用start开启线程
总结:注意,线程开启不一定立即执行,由cpu调度执行

public class Demo1 extends Thread{

    public void run(){
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我是run方法"+i);
        }
    }

    public static void main(String[] args) {
        //创建一个线程对象
        Demo1 demo1 = new Demo1();
        //调用start()方法开启线程
        demo1.start();

        //main线程,主线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主线程"+i);
        }
    }

}
//结果其中一块是这样,因为由于单核cpu,所以线程是交替执行的,线程在争抢时间片
/*
我是run方法2
我是主线程32
我是主线程33
我是主线程34
我是主线程35
我是run方法3
我是run方法4
我是run方法5
我是主线程36
 */

继续,如果按照下方的多线程写法呢,同时执行这个类里的方法。

public class Demo1 extends Thread{
    private String name;
    public Demo1(String name){
        this.name=name;
    }
    public void run(){
        //run方法线程体
        System.out.println(name);
    }
    public static void main(String[] args) {
        Demo1 demo1 = new Demo1("1");
        Demo1 demo2 = new Demo1("2");
        Demo1 demo3 = new Demo1("3");
        demo1.start();
        demo2.start();
        demo3.start();
    }
}
/*
1
3
2
 */

每次结果都不唯一,很明显,也没有按照预计的要求顺序输出,说明是同步执行。

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

代理:就是你把东西给我,我帮你执行

public class Demo2 implements Runnable{
    public void run(){
        //run方法线程体
        for (int i = 0; i < 5; i++) {
            System.out.println("线程编号"+i);
        }
    }

    public static void main(String[] args) {
        //创建runnable接口的实现类对象
        Demo2 demo2 = new Demo2();
        //创建线程对象,通过线程对象来开启我们的线程,这就是线程代理
        new Thread(demo2).start();   //把对象代理给线程使用
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程编号"+i);
        }
    }
}

总结:

继承Thread类

  • 子类继承Thread类具备多线程能力
  • 启动线程:子类对象.start()
  • 不建议使用:避免OOP单继承局限性,因为如果继承了Thread类就不允许继承其他类了

实现Runnable接口

  • 实现接口Runnable具有多线程能力
  • 启动线程:传如目标对象+Thread对象.start()
  • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用,解释下,比如下方代理,创建了一个对象,然后交给三个线程来处理,同时跑一个对象,这就是实现Runnable接口的好处。

StartThread st=new StartThread();   //一份资源
new Thread(st,"小明").start();
new Thread(st,"小黑").start();   //多个代理,st后面的是线程的名字
new Thread(st,"小白").start();

初识并发问题

多个线程抢票,创造一个并发问题

多个线程同时操作同一个对象,线程不安全,数据紊乱,如下方输出结果5,6两个票被多个人同时拿到,这就是并发问题。

public class Demo3 implements Runnable{
    private int ticketnums=10;
    public void run(){
        while (true){
            if(ticketnums==0){
                break;
            }
            //模拟延时,放置cpu执行过快看不到超卖现象
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢了第"+ticketnums--+"个票");
        }
    }

    public static void main(String[] args) {
        Demo3 demo3 = new Demo3();
        new Thread(demo3,"小明").start();
        new Thread(demo3,"小白").start();
        new Thread(demo3,"黄牛").start();
    }
}

/*
小白抢了第10个票
黄牛抢了第9个票
小明抢了第8个票
小白抢了第6个票
小明抢了第7个票
黄牛抢了第6个票
小明抢了第5个票
黄牛抢了第4个票
小白抢了第5个票
黄牛抢了第3个票
小白抢了第2个票
小明抢了第1个票
 */

模拟龟兔赛跑,这里不让兔子休眠了

//模拟龟兔赛跑
public class Race implements Runnable{
    //定义比赛跑到长100米
    public static String winnder;
    public void run(){
        for (int i = 0; i <= 100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
            boolean over = getWinnder(i);
            if (over){   //为true比賽結束
                break;
            }
        }
    }

    //判断比赛是否结束并输出赢家
    public boolean getWinnder(int steps){

        if(steps==100){
            winnder=Thread.currentThread().getName();
            System.out.println("胜利者是"+winnder);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }

}



Lambda表达式

new Thread( ()-> System.out.println("Hello world") ).start();

为什么要使用lambda表达式

  • 避免匿名内部类定义过多
  • 可以让你的代码看起来很简洁
  • 去掉了一堆没有意义的代码,只留下核心的逻辑

理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在

函数式接口的定义:接口里的方法默认是抽象的,是public abstract修饰的,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

例如:public interface Runnable{
                  void run();   //只有一个方法
             }

对于函数式接口,可以通过lambda表达式来创建该接口的对象,说白了就是为了简化一个实现类,不用再定义内部类了。

public class Lambda {
    public static void main(String[] args) {
//        ILike like = new Like();
//        like.lambda();
        
        ILike like;

        //匿名内部类,没有类的名称,必须借助接口或者父类
        like = new ILike(){
            @Override
            public void lambda() {
                System.out.println("I like lambda2");
            }
        };
        like.lambda();

        //使用lambda方式实现,根本不需要再去定义一个外部类或者匿名内部类
        like= () -> {
            System.out.println("I like lambda3");
        };
        like.lambda();
    }

}

//定义函数式接口
interface ILike{
    void lambda();
}

class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("I like lambda");
    }
}

在来一例有参数的,Lambda表达式只有在只有一行代码的情况下才能简化成一行代码,多行就用{}包裹。

public class Application {

    public static void main(String[] args) {
        ILove iLove;
        iLove = (int a) -> {
            System.out.println("I love you "+a+"天");
            System.out.println("I love you "+a+"天");
        };
        iLove.love(520);

        //简化
        iLove=(a) -> {
            System.out.println(("I love you "+a+"天"));
        };
        iLove.love(521);

        //再简化
        iLove=a -> {
            System.out.println(("I love you "+a+"天"));
        };
        iLove.love(522);

        //最后一次简化
        iLove=a -> System.out.println(("I love you "+a+"天"));
        iLove.love(523);
    }

    interface ILove{
        void love(int a);
    }
}

线程五种状态

创建状态——就绪状态——阻塞状态——运行状态——死亡状态

  • Thread t=new Thread(),线程对象一旦创建就进入到了新生状态
  • 当调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行
  • 进入运行状态,线程才真正执行线程体的代码块
  • 当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待cpu调度执行
  • 线程中断或者结束,一旦进入死亡状态,就不能再次启动

停止线程

  • 不推荐使用JDK提供的stop()、destroy()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行。
public class Demo4 implements Runnable{

    private boolean flag = true;  //设置一个标志位

    @Override
    public void run() {
        int i=0;
        while (flag){
            System.out.println("run...Thread"+i++);
        }
    }

    //自定义一个公开的方法停止线程,转换标志位
    public void stop(){
        this.flag=false;
    }

    public static void main(String[] args) {
        Demo4 demo4 = new Demo4();
        new Thread(demo4).start();
        for (int i = 0; i < 1000; i++) {   //快捷键:1000.fori
            System.out.println("主线程"+i);
            if (i==900){
                demo4.stop();   //调用stop方法切换标志位,让线程停止
                System.out.println("线程该停止了");
            }
        }
    }
}

线程休眠

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁
//模拟倒计时
public class Demo5 {

    public static void main(String[] args) {
        try {
            tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void tenDown() throws InterruptedException{
        int num=10;
        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0){
                break;
            }
        }
    }
}

线程等待--wait

wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)

wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

wait必须用在同步代码块里.

// main(主线程)
synchronized(t1) {  
 try {
          t1.start();
          t1.wait();
   } catch(InterruptedException e) {
          e.printStackTrace();
   }
}

// 在 t1 线程中唤醒主线程  
 synchronized (this) {          //这里的 this 为 t1
          this.notify();
   }

线程礼让---yield

  • 线程礼让,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功,看cpu心情
public class Demo6 {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"小明").start();
        new Thread(myYield,"小黑").start();
    }

    static class MyYield implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"线程开始执行");
            Thread.yield();  //礼让,但不一定成功
            System.out.println(Thread.currentThread().getName()+"线程执行结束");
        }
    }
}
/* 
礼让成功的输出:
小黑线程开始执行
小明线程开始执行
小黑线程执行结束
小明线程执行结束

礼让失败的输出:
小明线程开始执行
小明线程执行结束
小黑线程开始执行
小黑线程执行结束
 */

线程强制执行----join

  • join合并线程,待此线程执行完成后执行其他线程,其他线程阻塞
  • 可以想象成插队

如下,结果当i=50的时候会阻塞主线程,然后执行插队的线程,插队线程执行完毕后再执行主线程

public class Demo7 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("我是来插队的"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo7 demo7 = new Demo7();
        Thread thread = new Thread(demo7);
        thread.start();

        for (int i = 0; i < 100; i++) {
            if (i == 50) {
                thread.join();
            }
            System.out.println("我是主线程" + i);
        }
    }
}

/*
我是主线程49
我是来插队的0
我是来插队的1
我是来插队的2
.....
我是来插队的99
我是主线程50
 */

观测线程状态

Thread.State state = thread.getState();
System.out.println(state);

public class Demo8 {

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            for (int i = 0; i < 5; i++) {  //先睡5秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("||||||||");
        });


        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);  //NEW

        //观察启动后
        thread.start();
        state=thread.getState();
        System.out.println(state); //Run

        while (state!=Thread.State.TERMINATED){ //只要线程不终止就一直输出状态
            Thread.sleep(100);
            state=thread.getState(); //更新线程状态
            System.out.println(state); //输出状态
        }
    }
}

线程的优先级---priority

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程, 线程调度器按照优先级决定应该调度哪个线程来执行。但是优先级高的不一定先执行,大多数情况下会先执行,与那个线程礼让yield一样的道理。优先级默认是5。优先级低只是意味着获得调度的概率低.并不是优先级低就不会被调用了.这都是看CPU的调度。

线程的优先级用数字表示,范围从1~10.

  • Thread.MIN_ PRIORITY = 1;
  • Thread.MAX_ PRIORITY= 10;
  • Thread.NORM_ PRIORITY = 5;

使用以下方式改变或获取优先级  getPriority() ,setPriority(int xxx)

public class Demo9 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"主线程优先级"+Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);

        t1.start(); //先设置优先级在启动

        t2.setPriority(Thread.MIN_PRIORITY);  //优先级最低,为1
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);  //优先级最高,为10
        t4.start();

//        t5.setPriority(-1);   //不可为负数,会报错
//        t5.start();
    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"子线程优先级"+Thread.currentThread().getPriority());
    }
}

/*
main主线程优先级5
Thread-3子线程优先级10
Thread-0子线程优先级5
Thread-2子线程优先级4
Thread-1子线程优先级1
 */

守护线程---daemon

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如后台记录操作日志,监控内存,垃圾回收等待

用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆:

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

下方代码执行结果说明了在用户线程执行完毕后,到JVM关闭的时候,这段时间守护线程还会跑一会才结束。

public class Demo10 {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread thread=new Thread(god);
        thread.setDaemon(true);  //默认是false表示为用户线程,正常的线程都是用户线程
        thread.start();  //守护线程启动
        /*
        上帝守护线程启动,注意 thread.setDaemon(true)必须在thread.start()之前设置,
        否则会跑出一个IllegalThreadStateException异常。
        因为你不能把正在运行的常规线程设置为守护线程。
        */
        
        new Thread(you).start();  //启动用户线程
    }
}

//上帝
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("上帝保佑着你");
        }
    }
}

//你
class You implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("开心的活了一生");
        }
        System.out.println("===GoodBye! world!===");
    }
}

这里有几点需要注意: 

(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。 
(3) 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。 

因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了。 

线程同步机制

多个线程操作一个资源,即并发,线程同步需要队列+锁这两个东西来解决线程不安全问题。

要保证安全就会牺牲部分性能,要保证性能就要牺牲部分安全。所谓鱼与熊掌不可兼得就是这个道理。

处理多线程问题时,多个线程访问同一一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步。
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

◆由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized ,当-个线程获得对象的排它锁,独占资源, 其他线程必须等待,使用后释放锁即可.存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一 个优先级低的线程释放锁会导致优先级倒置,引起性能问题.

三大线程不安全案例

1)不安全的买票

public class Demo1 {
    public static void main(String[] args) {
        BuyTickets buyTickets = new BuyTickets();
        new Thread(buyTickets,"我").start();
        new Thread(buyTickets,"你们").start();
        new Thread(buyTickets,"黄牛").start();
    }
}

class BuyTickets implements Runnable{

    private int ticketName=10;
    boolean flag = true;
    @Override
    public void run() {
        //买票
        while (flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void buy() throws InterruptedException {
        //判断是否有票
        if(ticketName<=0){
            flag=false;
            return;
        }
        //模拟延时
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"拿到"+ticketName--);
    }
}

2)两个人去银行取钱

public class Demo2 {
    public static void main(String[] args) {
        //账户
        Account account = new Account(100,"结婚基金");
        Drawing you = new Drawing(account,50,"自己");
        Drawing wife = new Drawing(account,100,"老婆");
        new Thread(you).start();
        new Thread(wife).start();
    }
}

//账户
class Account{
    int money; //余额
    String name;  //卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//银行:模拟取款
class Drawing extends Thread{
    Account account; //账户
    //取了多少钱
    int drawingMoney;
    //手里的钱
    int nowMoney;

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);  //调用父类Thread的name
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    // 取钱
    public void run(){
        //判断有没有钱
        if(account.money-drawingMoney<0){
            System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
            return;
        }
        try {  //休眠有助于放大问题
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //卡内余额=余额-取出的钱
        account.money=account.money-drawingMoney;
        //去除的钱
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.name+"余额为"+account.money);
        /*由于这里继承了Thread,所以可以直接用this.getName()
        等价于Thread.currentThread().getName()
         */
        System.out.println(this.getName()+"手里钱为"+nowMoney);
    }
}

3)线程不安全集合

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(
                ()->{
                    list.add(Thread.currentThread().getName());
                }
            ).start();
        }
        Thread.sleep(3000);   //睡眠三秒,让一万个线程有足够的时间加到集合里
        System.out.println(list.size());
    }
}

同步方法及同步块

◆由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块.

同步方法: public synchronized void method(int args) {}

◆synchronized方法控制对 “对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行 ,
就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷:若将一个大的方法申明为synchronized将会影响效率。

方法里面需要修改的内容才需要锁,锁的太多,浪费资源。

同步块: synchronized (Obj){}

Obj称之为同步监视器 
       
◆Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
       ◆同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身,或者是class

同步监视器的执行过程
       1. 第一个线程访问,锁定同步监视器,执行其中代码
       2.第二个线程访问 ,发现同步监视器被锁定,无法访问
       3.第一个线程访问完毕,解锁同步监视器
       4.第二个线程访问, 发现同步监视器没有锁,然后锁定并访问

使用synchronize来修改三大线程不安全案例里的买票,只需在buy方法里加个关键字synchronized相当于修饰符

private synchronized void buy() throws InterruptedException {
    //判断是否有票
    if(ticketName<=0){
        flag=false;
        return;
    }
    //模拟延时
    Thread.sleep(100);
    System.out.println(Thread.currentThread().getName()+"拿到"+ticketName--);
}

使用使用synchronize来修改三大线程不安全案例里的取钱,这个要用同步块

默认锁的是this,即synchronized(this),this表示这个对象,这个类,也就是Drawing银行这个类,所以没有作用,锁的东西取决于进程同时执行的对象(变化的),锁的对象是唯一的。

public void run(){
    //锁的对象得是变化的量,就是增删改的量
    synchronized (account){
        //判断有没有钱
        if(account.money-drawingMoney<0){
            System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
            return;
        }
        try {  //休眠有助于放大问题
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //卡内余额=余额-取出的钱
        account.money=account.money-drawingMoney;
        //去除的钱
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.name+"余额为"+account.money);
    /*由于这里继承了Thread,所以可以直接用this.getName()
    等价于Thread.currentThread().getName()
     */
        System.out.println(this.getName()+"手里钱为"+nowMoney);
    }
}

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {

            new Thread(
                ()->{
                    synchronized (list){  //锁住list集合
                        list.add(Thread.currentThread().getName());
                    }
                }
            ).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

CopyOnWriteArrayList

CopyOnWriteArrayList使用了一种叫写时复制的方法,

  • 当有新元素添加到CopyOnWriteArrayList时,
  • 先从原有的数组中拷贝一份出来,然后在新的数组做写操作,
  • 写完之后,再将原来的数组引用指向到新数组。
    • 创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。
    • 当元素在新数组添加成功后,将array这个引用指向新数组。
  • CopyOnWriteArrayList的整个add操作都是在的保护下进行的。
    • 这样做是为了避免在多线程并发add的时候,
    • 复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

add源码如下:

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。juc下的包非常多,举个CopyOnWriteArrayList例子

测试JUC安全类型的集合

import java.util.concurrent.CopyOnWriteArrayList;
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()-> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

死锁

多个线程各自占有一-些共享资源 ,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形. 某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题.

产生死锁的必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

任何线程进入同步代码块、同步方法之前,必须获得同步监视器的锁定,那么何时会释放这个锁定呢?在程序中,是无法显式释放对同步监视器的锁的,而会在如下几个情况下释放锁。

1、当前线程的同步方法、代码块执行结束的时候释放

2、当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。

3、出现未处理的error或者exception导致异常结束的时候释放

4、程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁

如下情况不会释放锁

1、程序调用 Thread.sleep()  Thread.yield() 这些方法暂停线程的执行,不会释放

2、线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend 和 resume 来控制线程。

死锁代码示例,两个线程是同步进行的,第一个线程先获得t1,然后停一毫秒,目的是为了让第二个线程有时间拿到t2对象,防止cpu把两个对象都只分配给其中一个线程。然后第一个线程继续执行里面的代码块,即要获得t2对象,此时由于获取t2的代码块写在synchronized (t1){}里面,所以t1对象并没有释放,因为里面的代码块没有执行结束
同理第二个线程先获得t2对象,然后sleep要获取t1对象,由于t1对象仍然在第一个线程的手里没有释放,不可能拿到,t2对象又不可能被第一个线程拿到,所以两边相互等待,成了死锁。

public class DieLock {

    public static Object t1 = new Object();
    public static Object t2 = new Object();

    public static void main(String[] args){
        new Thread(){
            @Override
            public void run(){
                synchronized (t1){
                    System.out.println("Thread1 get t1");
                    try {
                        Thread.sleep(100);
                    }catch (Exception e){

                    }

                    synchronized (t2){
                        System.out.println("Thread2 get t2");
                    }
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run(){
                synchronized (t2){
                    System.out.println("Thread2 get t2");

                    try {
                        Thread.sleep(100);
                    }catch (Exception e){

                    }

                    synchronized (t1){
                        System.out.println("Thread2 get t1");
                    }
                }
            }
        }.start();
    }
}

Lock锁

从JDK 5.0开始,Java提供了更强大的线程同步机制一通过 显式定义同步锁对象来实现同步。同步锁使用Lock对象充当java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是Reentrantlock,可以显式加锁、释放锁。作用与synchronized一致。

class A{
    private final ReentrantLock lock = new ReentrantLock();
    public void m(){
        lock.lock();
        try {
            //这里放保证线程安全的代码
        }finally {
            lock.unlock();
            //如果同步代码有异常,要将unlock()写入finally语句块
        }
    }
}

synchronized与Lock的对比

  • Lock是 显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁, 出了作用域自动释放。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方法体之外)
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}

class TestLock2 implements Runnable{

    int ticketNums = 10;
    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            //在这个if代码块加锁
            try {
                lock.lock(); //加锁,一般要用try-finally包围起来,因为涉及到手动解锁

                //代码块开始
                if(ticketNums>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+ticketNums--);
                }else{
                    break;
                }
                //代码块结束

            } finally {
                lock.unlock();  //解锁
            }
        }
    }
}

生产者消费者问题

应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费.
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止.
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止.

线程通信分析
这是一个线程同步问题,生产者和消费者共享同-个资源,并且生产者和消费者之间相互依赖,互为条件.

  • 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者, 在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费.
  • synchronized 可阻止并发更新同一个共享资源,实现了同步
  • synchronized不能用来实现不同线程之间的消息传递(通信)

Java提供了几个方法解决线程之间的通信问题

注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常legalMonitorStateException

管程法

并发协作模型“生产者/消费者模式”-->管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区,生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

思路:

1.首先有一个生产者,消费者、生产者只顾生产,消费者只管消费、

2.利用了一个缓冲区,缓冲了一个10个大小的数组

3.有个方法叫放入产品,产品丢进来的时候,我们判断一下缓冲区有没有满,如果满了的话,生产者就要等待了,

如果没有满,就将产品放进去,放进去之后有产品了,赶紧通知消费者消费

4.消费者就判断下能不能消费呢,有没有东西,有东西的话,我就可以直接消费,消费完了,就赶紧通知生产者生产。

如果没有东西呢,消费者就等待。等待生产者去通知他,生产者通知了,他就可以解除等待了。

public class guancheng {
    //生产者消费者模式,主要是用来借助一个缓冲区,管程法.
    public static void main(String[] args) {
        huanchong hc =new huanchong();
        new shengchan(hc).start();
        new xiaofei(hc).start();
    }
}
class shengchan extends Thread{
    huanchong hc;
    public shengchan(huanchong hc) {
        this.hc =hc;
    }
    public void run() {
        for(int i=0; i<10; i++) {
            System.out.println("生产了"+i+"个馒头");
            hc.push(new mantou(i));
        }
    }
}
class xiaofei extends Thread{
    huanchong hc;
    public xiaofei(huanchong hc) {
        this.hc =hc;
    }
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println("消费了"+hc.pop().id+"个馒头");
        }
    }
}
class huanchong{
    mantou[] mt =new mantou[10];
    int count=0;
    //存取
    public synchronized void push(mantou m) {
        if(count==mt.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        mt[count++]=m;
        //有了空间之后。通知消费者消费
        this.notifyAll();
    }
    //消费
    public synchronized mantou pop() {
        if(count==0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        count--;
        mantou m=mt[count];
        //没有空间,通知生产者生产
        this.notifyAll();
        return m;
    }
}
class mantou{
    int id;
    public mantou(int id) {
        this.id=id;
    }
}

信号灯法

来判断一个标志位flag,如果为true,就让他等待、如果为false,就让他去通知另外一个人、把两人衔接起来,就像咱们的信号灯红灯停,绿灯行,通过这样一个判断方式,只要来判断什么时候让他等待,什么时候将他唤醒即可。

package com.thread.gaoji;

//测试生产者消费者问题2:信号灯法,通过标志位解决

public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者-->演员
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快乐大本营播放中");
            } else {
                this.tv.play("抖音:记录美好生活");
            }
        }
    }
}

//消费者-->观众
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//产品-->节目
class TV {
    //演员表演,观众等待 T
    //观众观看,演员等待 F
    String voice; // 表演的节目
    boolean flag = true;


    //表演
    public synchronized void play(String voice) {

        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + voice);
        //通知观众观看
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:" + voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:◆提高响应速度(减少了创建新线程的时间) 
        ◆降低资源消耗(重复利用线程池中线程,不需要每次都创建
        ◆便于线程管理
                 ◆corePoolSize: 核心池的大小
                 ◆maximumPoolSize:最大线程数
                 ◆keepAliveTime: 线程没有任务时最多保持多长时间后会终止

1. 线程池的概念:
        线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

2. 线程池的工作机制
    2.1 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
    2.1 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3. 使用线程池的原因:
    多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

四种线程池介绍

1. 线程池的返回值ExecutorService简介:ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程。
2. 具体的4种常用的线程池实现如下:(返回值都是ExecutorService)

  • 1)Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务,线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。代码例如
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewCachedThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个可缓存线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            try {
                // sleep可明显看到使用的是线程池里面以前的线程,没有创建新的线程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    // 打印正在执行的缓存线程信息
                    System.out.println(Thread.currentThread().getName()
                            + "正在被执行");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
  • 2)Executors.newFixedThreadPool(int n):创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors(),代码例如
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewFixedThreadPoolTest {

    public static void main(String[] args) {
       //1.创建服务,创建线程池
        //newFixedThreadPool参数为:线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        //执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        System.out.println(Runtime.getRuntime().availableProcessors());
        //2.关闭连接
        service.shutdown();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
  • 3)Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class NewScheduledThreadPoolTest {

    public static void main(String[] args) {
        //创建一个定长线程池,支持定时及周期性任务执行——延迟执行
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        //延迟1秒执行
                 /*scheduledThreadPool.schedule(new Runnable() {
                     public void run() {
                        System.out.println("延迟1秒执行");
                     }
                 }, 1, TimeUnit.SECONDS);*/
                 
        //延迟1秒后每3秒执行一次
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("延迟1秒后每3秒执行一次");
            }
        }, 1, 3, TimeUnit.SECONDS);

    }
}
  • 4)Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewSingleThreadExecutorTest {

    public static void main(String[] args) {
        //创建一个单线程化的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        //结果依次输出,相当于顺序执行各个任务
                        System.out.println(Thread.currentThread().getName()+"正在被执行,打印的值是:"+index);
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

缓冲队列BlockingQueue和自定义线程池ThreadPoolExecutor

1. 缓冲队列BlockingQueue简介:

          BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。

2. 常用的几种BlockingQueue:

  • ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。

  • LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小有Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。

  • PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。

  • SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。

3. 自定义线程池(ThreadPoolExecutor和BlockingQueue连用):

     自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池。

    常见的构造函数:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ZiDingYiThreadPoolExecutor {

    static class TempThread implements Runnable {

        @Override
        public void run() {
            // 打印正在执行的缓存线程信息
            System.out.println(Thread.currentThread().getName() + "正在被执行");
            try {
                // sleep一秒保证3个任务在分别在3个线程上执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建数组型缓冲等待队列
        BlockingQueue<Runnable> bq = new ArrayBlockingQueue<Runnable>(10);
        // ThreadPoolExecutor:创建自定义线程池,池中保存的线程数为3,允许最大的线程数为6
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(3, 6, 50, TimeUnit.MILLISECONDS, bq);
        // 创建3个任务
        Runnable t1 = new TempThread();
        Runnable t2 = new TempThread();
        Runnable t3 = new TempThread();
        Runnable t4 = new TempThread();
        Runnable t5 = new TempThread();
        Runnable t6 = new TempThread();
        // 3个任务分别在3个线程上执行
        tpe.execute(t1);
        tpe.execute(t2);
        tpe.execute(t3);
        tpe.execute(t4);
        tpe.execute(t5);
        tpe.execute(t6);
        // 关闭自定义线程池
        tpe.shutdown();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java并发编程是指在Java程序中使用多线程实现并发执行的编程技术。它能有效利用多核处理器的优势,提升程序的性能和响应能力。以下是Java并发编程基础知识: 1. 线程与进程:Java中的线程是程序中执行的最小单位,线程共享进程的资源,包括内存空间和文件等。多线程可以同时执行不同的任务,相比单线程能更高效地利用系统资源。 2. 线程创建:Java中创建线程有两种方式,一种是继承Thread类,实现run()方法;另一种是实现Runnable接口,重写run()方法。通过调用start()方法启动线程。 3. 线程同步:多个线程在访问共享资源时可能会产生竞争条件,可能会导致数据不一致或者出现死锁等问题。通过使用同步机制来保证线程安全,例如使用synchronized关键字实现对共享资源的互斥访问。 4. 线程通信:线程之间可以通过共享变量来进行通信。使用wait()、notify()和notifyAll()方法实现线程的等待和唤醒。 5. 线程池:线程池是一种管理线程的机制,可以有效控制线程的数量和复用线程资源,避免频繁创建销毁线程的开销。 6. 并发容器:Java提供了一些线程安全的数据结构,如ConcurrentHashMap和ConcurrentLinkedQueue等,用于在多线程环境下安全地操作数据。 7. 原子操作:Java提供了一些原子操作类,如AtomicInteger和AtomicLong等,它们能够保证对共享数据的操作是原子的,不会发生数据不一致的情况。 8. 同步工具类:Java提供了一些同步工具类,如CountDownLatch和CyclicBarrier等,用于控制线程的执行顺序和线程之间的同步。 以上是Java并发编程基础知识,掌握了这些知识可以更好地利用多线程来提高程序的性能和并发能力。同时也需要注意并发编程可能带来的线程安全问题,合理使用同步机制和并发容器等工具类来保证程序的正确运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值