java多线程进阶(上)


说在前面,我自己是不喜欢看特别长篇的博客。我能理解看长篇博客的那种难受,因此,想直接入正题看编码的不妨跳过前两块内容,直接进入“线程的创建”。

线程概述

什么是线程?

之前看到过某大神这样一句话:比较糟糕的技术文档的特征:用专业名词来解释专业名词 

从这句话中,我受益匪浅,因此以后的文章尽量都避免这样。相信大家也不喜欢看一些概念性的东西吧。

介绍线程之前,首先得解释一下进程,因为要用进程来解释线程。

进程:是程序的一次动态执行过程。比如打开谷歌浏览器,从它打开的时刻开始就启动了一个进程。

多进程:多进程就像打开了多个程序,比如边玩QQ,边听歌,还可以浏览网页,多个任务同时运行。

线程:比进程更小的执行单位,通常一个进程拥有1-n个线程。

多线程:指在同一程序(进程)中能够同时处理多个任务,而这些任务就对应多个线程,比如浏览器下载图片能够同时下载多张图片。比如ABC三个用户同时访问淘宝网,淘宝网的服务器收到A用户的请求后,为A用户创建了一个线程,BC用户同样拥有自己对应的线程。注意:多线程不是为了提高程序执行速度(性能甚至更低),而是提高应用程序的使用效率。

线程的生命周期

每一个生命周期也是一种状态。

1、新建:创建一个新的线程对象

2、就绪:线程对象创建后,其他线程调用了该对象的start()方法,该线程被放入可运行的线程池中,等待获取CPU的使用权。

(这里可以这样解释,百米赛跑上,每个运动员对应一个线程,裁判员大喝“预备”这声预备相当于调用了start()方法,所有运动员进入就绪状态,开枪这个动作则对应了获取CPU使用权,运动员得到命令开跑)注意:一个CPU核心在某一时刻只能运行一个线程,这里的CPU相当于多核CPU,不扯远了。。。

3、运行:就绪状态的线程获取了CPU,执行。(运动员开跑)

4、阻塞:阻塞情况比较复杂,这里简单考虑一下就不分类了,即线程因为某种原因放弃CPU使用权,线程暂停。(某运动员抢跑,此次比赛暂停,所有运动员回起跑线重新准备就绪,注意不能继续跑!也就是说不能到运行状态)

5、死亡:线程执行完成或异常退出,该线程生命周期结束。

状态转换图如下:



JAVA线程的创建

好了,上面啰嗦了这么多,不过相信大家已经对线程有了一定理解吧,回到也许是大家最想看到的部分,coding...

1、继承Thread类

这里写一个简单的类,主线程输出1-5,两个子线程输出1-10

 
 
public class FirstThread extends Thread {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + " " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String args[]) {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        new FirstThread().start();
        new FirstThread().start();
    }
}

运行截图(部分):


可以看出,线程的执行是无序的,线程的执行是根据CPU分配情况来决定的。

2、实现Runnable接口

package cn.thread;

public class SecondThread implements Runnable {
    private int money=500;//卡上还剩500元
    @Override
    public void run() {
        while(true) {
            String name = Thread.currentThread().getName();
            if (name.equals("Son")) {
                if (money <= 0) {
                    System.out.println("爸,我没钱了!");
                    return;//Son线程的run方法结束
                }
                money = money - 300;//儿子一次取300元
                System.out.println(name + "卡上还剩:" + money + "元");
            }
            if (name.equals("Father")) {
                if (money >= 500) {
                    System.out.println("卡上钱还够用!");
                    return;//Father线程的run方法结束
                }
                money = money + 1000;//父亲一次存1000
                System.out.println(name + "卡上还剩:" + money + "元");
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String args[]) {
        SecondThread st = new SecondThread();
        new Thread(st, "Son").start();
        new Thread(st, "Father").start();
    }
}


运行截图:


继承Thread类和实现Runnable接口该用哪一个?

结论网上一大堆,都可以搜到:继承Thread类不适合资源共享,实现Runnable接口更具有灵活性。

我这里就来解释一下这个结论,深入理解一下它实现的原理

分析一下这两种方法创建线程的原理,看Thread类的源码,如图:



Thread类实现了Runnable接口并重写了run()方法,如果target(目标)为空,则不执行代码。

所以我们这里继承Thread类重写run()方法的原因就在此。代码如下:

public class FirstThread extends Thread {
    public void run() { //该线程要执行的操作       
        }
    public static void main(String args[]) {
        new FirstThread().start();
    }
}

继续看,当target不为空时,运行target的run()方法,而变量target看源码

是一个Runnable对象,也就是说只要target这个对象不为空,则调用Runnable对象的run()方法,再把Runnable对象传递给Thread类。代码如下:

public class SecondThread implements Runnable {
    @Override
    public void run() {
    }
    public static void main(String args[]) {
        SecondThread st = new SecondThread();
        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();
    }
}

注意看这两行代码:

        new Thread(st, "线程1").start();
        new Thread(st, "线程2").start();

传给Thread的是同一个Runnable对象,这也就是为什么说实现Runnable接口,可以资源共享,因为操作的都是同一个对象啊。而继承Thread每次都要new一个新的对象,对象自然就不同了。并且,实现Runnable接口这种方式更体现了面向对象这种思维,new一个线程,线程里面传一个对象,这个对象封装了一系列操作。


JAVA线程同步的与通信

主要是以实际例子,应用来说明。

这里先解释一下互斥和同步(之后的缓存池也要用到)

互斥:是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。

同步:上一段代码没的完成,下一段必须等到上一段代码完成后才可以执行(必须严格按照规定的 某种先后次序来运行)。如买票排队

(同步是一种更为复杂的互斥,而互斥是一种特殊的同步。)

异步:上一段代码没的完成,下一段不必等到上一段代码完成就可以执行。如手机发送短信。

1、synchronized同步

线程安全问题这里举个例子用火车购票来解释,库存100张票,三个窗口同时售票。

方法一:直接给方法加上synchronized关键字修饰

package cn.thread;

public class SynchronizedSaleTickets implements Runnable {
    private static int tickets = 100;

    @Override
    public void run() {
        while (tickets>0) {//余票充足
            try {
                Thread.sleep(100);//延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sale();
        }
        if(tickets==0){
            System.out.println("票已售完!");
        }
    }

    synchronized static void sale() {
        tickets--;
        if(tickets>0) {
            System.out.println(Thread.currentThread().getName() + "当前余票:" + tickets);
        }
    }
    public static void main(String[] args){
        SynchronizedSaleTickets sst=new SynchronizedSaleTickets();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();

    }
}

运行结果(部分):


方法二:定义一个共同的“锁”

代码和上面大致相同,修改了一点:

package cn.thread;

public class SynchronizedSaleTickets implements Runnable {
    private static int tickets = 100;
    /*
    锁住一个“对象”(门栓),这个对象可以是一个引用对象,也可以是此对象内部的某个数据
    这里定义了一个对象
     */
    private static Object lock=new Object();
    @Override
    public void run() {
        while (tickets>0) {//余票充足
            try {
                Thread.sleep(100);//延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sale();
        }
        if(tickets==0){
            System.out.println("票已售完!");
        }
    }

    static void sale() {
        synchronized (lock) {//“锁住”要执行的操作
            tickets--;
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "当前余票:" + tickets);
            }
        }
    }
    public static void main(String[] args){
        SynchronizedSaleTickets sst=new SynchronizedSaleTickets();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();
        new Thread(sst).start();

    }
}

注意:锁是上在要操作的资源内部方法中,而不是上在线程代码中!

2、wait、notify通信

这里举一个例子,也是一道java线程面试题

子线程循环10次,接着主线程循环100次,又接着回到子线程循环10次,再接着回到主线程又循环100次,如此循环50次。

代码如下:

package cn.thread;

public class ThreadCommunication {
    public static void main(String[] args) {
        final Business business = new Business();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 50; i++) {
                    business.sub(i);
                }
            }
        }).start();
        for (int i = 1; i <= 50; i++) {
            business.main(i);
        }
    }
//互斥代码放在资源内部方法中,而不是放在线程代码中!
    static class Business {
        private boolean shouldSub = true;
        //子线程业务逻辑
        public synchronized void sub(int i) {
            //这里用while比用if好,用if表示如果被通知唤醒,则继续执行后续的代码,而用while表示被通知唤醒过后
            //再回来检查参数是否该执行,逻辑更严密
            while (!shouldSub) {//不该子线程执行
                try {
                    this.wait();//等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int j = 1; j <= 10; j++) {
                System.out.println("sub thread:" + j + ",loop of " + i);
            }
            //子线程执行完通知主线程
            shouldSub = false;
            this.notify();
        }
        //主线程业务逻辑
        public synchronized void main(int i) {
            while (shouldSub) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int j = 1; j <= 100; j++) {
                System.out.println("main thread:" + j + ",loop of" + i);
            }
            //主线程执行完通知子线程
            shouldSub = true;
            this.notify();
        }
    }
}

运行结果(部分):


3、线程间共享数据

这个例子和火车售票不一样,四个线程,两个线程对每次J+2,两个线程每次对J-1。

注意:共享的数据封装到了一个单独的类中

package cn.thread;

public class MultiThreadShareData {
    public static void main(String[] args) {
        final ShareData1 data2 = new ShareData1();//共享数据data2
        //Thread-0和Thread-1每次-1
        new Thread(new MyRunnable1(data2)).start();
        new Thread(new MyRunnable1(data2)).start();
        ////Thread-2和Thread-3每次+2
        new Thread(new MyRunnable2(data2)).start();
        new Thread(new MyRunnable2(data2)).start();
    }
}

class MyRunnable1 implements Runnable {//减法
    private ShareData1 data1;

    public MyRunnable1(ShareData1 data1) {
        this.data1 = data1;
    }

    public void run() {
        while (true) {
            data1.decrement();
            System.out.println(Thread.currentThread().getName() + "线程每次-1,当前:" + data1.getJ());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyRunnable2 implements Runnable {//加法
    private ShareData1 data1;

    public MyRunnable2(ShareData1 data1) {
        this.data1 = data1;
    }

    public void run() {
        while (true) {
            data1.increment();
            System.out.println(Thread.currentThread().getName() + "线程每次+2,当前:" + data1.getJ());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class ShareData1 {//共享数据存放在此类的对象中,还有基本的操作
    private int j = 0;

    public int getJ() {
        return j;
    }

    public void setJ(int j) {
        this.j = j;
    }

    public synchronized void increment() {
        j = j + 2;
    }

    public synchronized void decrement() {
        j--;
    }
}

下篇继续讲解:

线程池(缓存池)

线程相关类

线程面试经典题目

......



原文出自:https://my.csdn.net/qq_37094660(如需转载请注明出处)


猜你喜欢

转载自blog.csdn.net/qq_37094660/article/details/80472657
今日推荐