NIO和Netty编程(一)之多线程编程

1、基本知识回顾

       线程是比进程更小的能独立运行的基本单位,它是进程的一部分,一个进程可以拥有多个线程,但至少要有一个线程,即主执行线程(Java 的main 方法)。我们既可以编写单线程应用,也可以编写多线程应用。一个进程中的多个线程可以并发(同时)执行,在一些执行时间长、需要等待的任务上(例如:文件读写和网络传输等),多线程就比较有用了。


怎么理解多线程呢?来两个例子:
1. 进程就是一个工厂,一个线程就是工厂中的一条生产线,一个工厂至少有一条生产线,只有一条生产线就是单线程应用,拥有多条生产线就是多线程应用。多条生产线可以同时运行。
2. 我们使用迅雷可以同时下载多个视频,迅雷就是进程,多个下载任务就是线程,这几个线程可以同时运行去下载视频。

       多线程可以共享内存、充分利用CPU,通过提高资源(内存和CPU)使用率从而提高程序的执行效率。CPU 使用抢占式调度模式在多个线程间进行着随机的高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而CPU 在多个线程间的切换速度相对我们的感觉要快很多,看上去就像是多个线程或任务在同时运行。Java 天生就支持多线程并提供了两种编程方式,一个是继承Thread 类,一个是实现Runnable 接口,接下来咱们通过两个案例快速复习回顾一下。

1.1 方式一:继承Thread

1.1.1 创建两个线程类

package com.zdw.thread;

/**
 * Create By zdw on 2019/7/19
 */
public class MyThread01 extends Thread {
    @Override
    public void run() {
        for(int i =1;i<=50;i++){
            System.out.println(this.getName()+":"+i);
            try {
                Thread.sleep(300);//让线程睡眠300毫秒,是为了看到cpu的随机在线程之间切换的效果
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.zdw.thread;

/**
 * Create By zdw on 2019/7/19
 */
public class MyThread02 extends Thread {
    @Override
    public void run() {
        for(int i=100;i<=150;i++){
            System.out.println(this.getName()+":"+i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1.1.2 创建线程测试类

package com.zdw.thread;

/**
 * Create By zdw on 2019/7/19
 */
public class MyThreadTest {
    public static void main(String[] args) {
        //构建两个线程对象,并设置线程名称
        MyThread01 myThread01 = new MyThread01();
        myThread01.setName("线程myThread01");
        MyThread02 myThread02 = new MyThread02();
        myThread02.setName("线程myThread02");
        //启动两个线程
        myThread01.start();
        myThread02.start();
    }
}

1.1.3 控制效果

从控制台的打印消息可以看出,cpu在两个线程之间是随机切换的,哪个线程获取到了cpu的执行权就可以执行run()方法中的业务逻辑。但是平时我们肉眼看到和感觉到的是同时运行的。

 

1.2 方式二:实现Runnable接口

1.2.1 创建两个任务类,实现Runnable接口

package com.zdw.runnable;

/**
 * Create By zdw on 2019/7/19
 */
public class MyRunnable01 implements Runnable {
    @Override
    public void run() {
        for(int i =1;i<=50;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(300);//让线程睡眠300毫秒,是为了看到cpu的随机在线程之间切换的效果
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.zdw.runnable;

/**
 * Create By zdw on 2019/7/19
 */
public class MyRunnable02 implements Runnable {
    @Override
    public void run() {
        for(int i =100;i<=150;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(300);//让线程睡眠300毫秒,是为了看到cpu的随机在线程之间切换的效果
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1.2.2 创建测试类

package com.zdw.runnable;

/**
 * Create By zdw on 2019/7/19
 */
public class MyRunnableTest {
    public static void main(String[] args) {
        /**
         * 实现Runnable接口的类不是线程类,所以不能直接new出来,然后进行start启动
         * 我们可以把实现Runnable接口的类当作是任务类,而创建线程的时候还有一个构造方法
         * 就是要接收一个任务类的
         */
        Thread myRunnableThread01 = new Thread(new MyRunnable01());
        Thread myRunnableThread02 = new Thread(new MyRunnable02());
        myRunnableThread01.setName("线程myRunnableThread01");
        myRunnableThread02.setName("线程myRunnableThread02");

        myRunnableThread01.start();
        myRunnableThread02.start();
    }
}

1.2.3 控制台消息

线程myRunnableThread01:1
线程myRunnableThread02:100
线程myRunnableThread01:2
线程myRunnableThread02:101
线程myRunnableThread01:3
线程myRunnableThread02:102
线程myRunnableThread01:4
线程myRunnableThread02:103
线程myRunnableThread01:5
线程myRunnableThread02:104
线程myRunnableThread02:105
线程myRunnableThread01:6
线程myRunnableThread02:106
线程myRunnableThread01:7
线程myRunnableThread02:107
线程myRunnableThread01:8

可以看到这种方式的效果和之前的那种继承Threa类的效果是一样的,只是实现方式有不同。

 

2、线程安全

2.1 产生线程安全的原因

       在进行多线程编程时,要注意线程安全问题,我们先通过一个案例了解一下什么是线程安全问题。该案例模拟用两个售票窗口同时卖火车票,具体代码如下所示:

2.1.1 创建售票任务类

package com.zdw.window;

/**
 * Create By zdw on 2019/7/19
 * 这是一个售票任务的模拟类
 */
public class SaleWindow implements Runnable {
    private int piao = 10;//假设一共有10张票,这个十张票是线程之间共享的
    @Override
    public void run() {
        //执行卖10张票的业务逻辑
        for(int i=1;i<=10;i++){
            if(piao>0){
                System.out.println(Thread.currentThread().getName()+"窗口卖出了第"+piao+"张票");
            }
            piao--;//票的数量减一
            //为了演示效果,当前线程睡眠800毫秒,因为cpu执行起来很快,一个线程可能瞬间就卖掉了10张票,看不到效果
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2.1.2 线程测试类

package com.zdw.window;

/**
 * Create By zdw on 2019/7/19
 * 该类是来执行售票操作的。开启两个线程售票10张票
 */
public class SaleTest {
    public static void main(String[] args) {
        //构建任务类
        SaleWindow saleWindow = new SaleWindow();
        //创建两个线程
        Thread window01 = new Thread(saleWindow);
        Thread window02 = new Thread(saleWindow);
        window01.setName("window01");
        window02.setName("window02");

        window01.start();
        window02.start();
    }
}

2.1.3 控制台消息

window01窗口卖出了第10张票
window02窗口卖出了第9张票
window01窗口卖出了第8张票
window02窗口卖出了第8张票
window02窗口卖出了第6张票
window01窗口卖出了第6张票
window02窗口卖出了第4张票
window01窗口卖出了第4张票
window01窗口卖出了第2张票
window02窗口卖出了第2张票

从打印结果可以看出,同一张票被售卖了两次,这肯定是不行的。这就是我们的线程安全问题。

那么造成这种情况的原因是什么呢?那是因为这10张票是共享资源,而线程之间是彼此独立,相互隔绝的,,因此就会出现数据(共享资源)不能同步更新的情况,这就是线程安全问题。

       所以上述操作中,两个窗口都是卖共同的10张票,当window01获取到了cpu的执行权,执行了输出语句之后,这个时候代表的就是window01窗口已经售卖了第10张票。但是cpu的执行是随机,快速的,所以当window01窗口还没有执行piao--操作的时候,恰巧这个时候window02窗口获取到了cpu的执行权。这时,window02窗口拿到的piao的值还是10,它并不知道window01已经卖了第10张票,因此它也会执行售卖第10张票的操作。这样就造成了一张票售卖了两次。这也是多线程出现的线程安全问题。

 

2.2 解决线程安全问题

       Java 中提供了一个同步机制(锁)来解决线程安全问题即让操作共享数据的代码在某一时间段,只被一个线程执行(锁住),在执行过程中,其他线程不可以参与进来,这样共享数据就能同步了。简单来说,就是给某些代码加把锁。


       锁是什么?又从哪儿来呢?

       锁的专业名称叫监视器monitor,其实Java 为每个对象都自动内置了一个锁(监视器monitor),当某个线程执行到某代码块时就会自动得到这个对象的锁,那么其他线程就无法执行该代码块了,一直要等到之前那个线程停止(释放锁)。需要特别注意的是:多个线程必须使用同一把锁(对象)。


Java 的同步机制提供了两种实现方式:
       同步代码块:即给代码块上锁,变成同步代码块
       同步方法:即给方法上锁,变成同步方法
       接下来我们分别用这两种方式解决卖火车票案例的线程安全问题,其实这两种方式本质上差不多,都是通过synchronized 关键字来实现的。

2.2.1 同步代码块

package com.zdw.window;

/**
 * Create By zdw on 2019/7/19
 * 这是一个售票任务的模拟类
 */
public class SaleWindow implements Runnable {
    private int piao = 10;//假设一共有10张票,这个十张票是线程之间共享的
    @Override
    public void run() {
        //执行卖10张票的业务逻辑
        for(int i=1;i<=10;i++){
            synchronized (this) {
                if (piao > 0) {
                    System.out.println(Thread.currentThread().getName() + "窗口卖出了第" + piao + "张票");
                }
                piao--;//票的数量减一
                //为了演示效果,当前线程睡眠800毫秒,因为cpu执行起来很快,一个线程可能瞬间就卖掉了10张票,看不到效果
                try {
                    Thread.sleep(800);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

我们只是在for循环里面新增了一个synchronized关键字,加上了this这把锁,其他的跟之前的完全是一样的

然后再次执行测试类(测试类和之前的完全一模一样的),发现控制台打印的消息如下:

window01窗口卖出了第10张票
window02窗口卖出了第9张票
window01窗口卖出了第8张票
window01窗口卖出了第7张票
window01窗口卖出了第6张票
window02窗口卖出了第5张票
window01窗口卖出了第4张票
window02窗口卖出了第3张票
window02窗口卖出了第2张票
window01窗口卖出了第1张票

2.2.2 同步方法

创建一个新的任务类,里面用同步方法实现,其实它就是把上面的同步代码块抽取出来放到了一个方法中,然后方法上加上synchronized关键字:

注意:同步方法的锁对象默认是this;

package com.zdw.window;

/**
 * Create By zdw on 2019/7/19
 * 这是一个售票任务的模拟类
 */
public class SaleWindow2 implements Runnable {
    private int piao = 10;//假设一共有10张票,这个十张票是线程之间共享的
    @Override
    public void run() {
        //执行卖10张票的业务逻辑
        for(int i=1;i<=10;i++) {
            //调用卖票方法(同步方法)
            shoumai();
        }
    }

    public synchronized void shoumai(){
        if (piao > 0) {
            System.out.println(Thread.currentThread().getName() + "窗口卖出了第" + piao + "张票");
        }
        piao--;//票的数量减一
        //为了演示效果,当前线程睡眠300毫秒,因为cpu执行起来很快,一个线程可能瞬间就卖掉了10张票,看不到效果
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试类:

package com.zdw.window;

/**
 * Create By zdw on 2019/7/19
 * 该类是来执行售票操作的。开启两个线程售票10张票
 */
public class SaleTest {
    public static void main(String[] args) {
        //构建任务类
        SaleWindow2 saleWindow = new SaleWindow2();
        //创建两个线程
        Thread window01 = new Thread(saleWindow);
        Thread window02 = new Thread(saleWindow);
        window01.setName("window01");
        window02.setName("window02");

        window01.start();
        window02.start();
    }
}

控制台:

window01窗口卖出了第10张票
window01窗口卖出了第9张票
window02窗口卖出了第8张票
window02窗口卖出了第7张票
window02窗口卖出了第6张票
window01窗口卖出了第5张票
window01窗口卖出了第4张票
window01窗口卖出了第3张票
window01窗口卖出了第2张票
window02窗口卖出了第1张票

 

2.3 Java API 中的线程安全问题

       我们平时在使用Java API 进行编程时,经常遇到说哪个类是线程安全的,哪个类是不保证线程安全的,例如:StringBuffer / StringBuilder 和Vector / ArrayList ,谁是线程安全的?谁不是线程安全的?我们查一下它们的源码便可知晓。

通过查看源码,我们发现StringBuffer 和Vector 类中的大部分方法都是同步方法,所以证明这两个类在使用时是保证线程安全的;而StringBuilder 和ArrayList 类中的方法都是普通方法,没有使用synchronized 关键字进行修饰,所以证明这两个类在使用时不保证线程安全。线程安全和性能之间不可兼得,保证线程安全就会损失性能,保证性能就不能满足线程安全。

 

3、线程间通信

       多个线程并发执行时, 在默认情况下CPU 是随机性的在线程之间进行切换的,但是有时候我们希望它们能有规律的执行, 那么,多线程之间就需要一些协调通信来改变或控制CPU的随机性。

       Java 提供了等待唤醒机制来解决这个问题,具体来说就是多个线程依靠一个同步锁,然后借助于wait()和notify()方法就可以实现线程间的协调通信。
       同步锁相当于中间人的作用,多个线程必须用同一个同步锁(认识同一个中间人),只有同一个锁上的被等待的线程,才可以被持有该锁的另一个线程唤醒,使用不同锁的线程之间不能相互唤醒,也就无法协调通信。


Java 在Object 类中提供了一些方法可以用来实现线程间的协调通信,我们一起来了解一下:

 public final void wait(); 让当前线程释放锁
 public final native void wait(long timeout); 让当前线程释放锁,并等待xx 毫秒
 public final native void notify(); 唤醒持有同一锁的某个线程
 public final native void notifyAll(); 唤醒持有同一锁的所有线程

       需要注意的是:在调用wait 和notify 方法时,当前线程必须已经持有锁,然后才可以调用,否则将会抛出IllegalMonitorStateException 异常。接下来咱们通过两个案例来演示一下具体如何编程实现线程间通信。

 

3.1 案例一

需求:一个线程输出10次1,一个线程输出10次2,要求交替输出,例如:1 2 1 2 1 2 ..........

3.1.1 定义锁

package com.zdw.shizhan;

/**
 * Create By zdw on 2019/7/19
 * 该对象只是为了定义个锁,让任务类都拿到同一个锁
 */
public class MyLock {
    public static Object lock = new Object();
}

3.1.2 定义控制台输出1的任务类

package com.zdw.shizhan;

/**
 * Create By zdw on 2019/7/19
 * 该线程负责打出数字1
 */
public class PrintOneThread implements Runnable {

    @Override
    public void run() {
        for(int i=0;i<10;i++){
            synchronized (MyLock.lock){//拿到公共的锁
                System.out.print(1+" ");//打出数字1
                MyLock.lock.notify();//唤醒另一个线程
                try {
                    MyLock.lock.wait();//让自己休眠并释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.1.3 定义控制台输出2的任务类

package com.zdw.shizhan;

/**
 * Create By zdw on 2019/7/19
 * 该线程任务类用来向控制台输出2
 */
public class PrintTwoThread implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            synchronized (MyLock.lock){
                System.out.print(2+" ");
                MyLock.lock.notify();//唤醒另一个线程
                try {
                    MyLock.lock.wait();//让自己睡眠并释放掉锁对象
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.1.4 测试类

package com.zdw.shizhan;

/**
 * Create By zdw on 2019/7/19
 */
public class PrintTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new PrintOneThread());
        Thread thread2 = new Thread(new PrintTwoThread());
        thread.start();
        thread2.start();
    }
}

测试结果如下:

 

3.2 案例二:生产者消费者模式

       该模式在现实生活中很常见,在项目开发中也广泛应用,它是线程间通信的经典应用。生产者是一堆线程,消费者是另一堆线程,内存缓冲区可以使用List 集合存储数据。该模式的关键之处是如何处理多线程之间的协调通信,内存缓冲区为空的时候,消费者必须等待,而内存缓冲区满的时候,生产者必须等待,其他时候可以是个动态平衡。


       下面的案例模拟实现农夫采摘水果放到筐里,小孩从筐里拿水果吃,农夫是一个线程,小孩是一个线程,水果筐放满了,农夫停;水果筐空了,小孩停。

3.2.1 定义篮子的类

package com.zdw.shizhan2;

import java.util.ArrayList;
import java.util.List;

/**
 * Create By zdw on 2019/7/19
 * 该对象只是为了定义个锁,让任务类都拿到同一个锁,同时还起到篮子的作用
 */
public class  Kuang {
    public static List<String> lanzi = new ArrayList<>();//这个代表篮子,假设篮子装了10个果子就满了
}

3.2.2 农夫摘水果的任务类

package com.zdw.shizhan2;

/**
 * Create By zdw on 2019/7/19
 * 该线程任务类是农夫采摘水果放到篮子里面
 */
public class NongFu implements Runnable {
    @Override
    public void run() {
        while(true) {
            synchronized (Kuang.lanzi) {//农夫拿到了篮子,往里面采摘果子
                if (Kuang.lanzi.size() >= 10) {//如果篮子满了,就停下来,唤醒小孩子吃水果
                    try {
                        Kuang.lanzi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //往篮子里采摘水果
                Kuang.lanzi.add("水果");
                System.out.println("现在篮子里面一共有" + Kuang.lanzi.size() + "个水果");
                Kuang.lanzi.notify();//唤醒孩子吃水果
            }
            //控制摘水果的时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2.3 小孩吃水果的任务类

package com.zdw.shizhan2;

/**
 * Create By zdw on 2019/7/19
 * 该类是小孩子吃水果的任务类
 */
public class Child implements Runnable {
    @Override
    public void run() {
        while(true) {
            synchronized (Kuang.lanzi) {
                if (Kuang.lanzi.size() == 0) {
                    try {
                        Kuang.lanzi.wait();//篮子里面没有水果,等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Kuang.lanzi.remove("水果");
                System.out.println("小孩子刚吃了1个水果,篮子中还剩下" + Kuang.lanzi.size() + "个水果");
                Kuang.lanzi.notify();//唤醒农夫去摘水果
            }
            //控制吃水果的速度
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2.4 测试类

package com.zdw.shizhan2;

/**
 * Create By zdw on 2019/7/19
 */
public class Test {
    public static void main(String[] args) {
        Thread nongfu = new Thread(new NongFu());
        Thread child = new Thread(new Child());
        nongfu.start();
        child.start();
    }
}

控制台消息:

第一种情况,当像上述代码所示,农夫采摘水果的速度要于小孩吃水果的速度的时候,控制台如下:

现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
现在篮子里面一共有2个水果
现在篮子里面一共有3个水果
小孩子刚吃了1个水果,篮子中还剩下2个水果
现在篮子里面一共有3个水果
现在篮子里面一共有4个水果
小孩子刚吃了1个水果,篮子中还剩下3个水果
现在篮子里面一共有4个水果
现在篮子里面一共有5个水果
现在篮子里面一共有6个水果
小孩子刚吃了1个水果,篮子中还剩下5个水果
现在篮子里面一共有6个水果
现在篮子里面一共有7个水果
现在篮子里面一共有8个水果
小孩子刚吃了1个水果,篮子中还剩下7个水果
现在篮子里面一共有8个水果
现在篮子里面一共有9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果
小孩子刚吃了1个水果,篮子中还剩下9个水果
现在篮子里面一共有10个水果

第二种情况,当农夫采摘水果的速度于小孩吃水果的速度的时候,控制台打印如下:

现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果
现在篮子里面一共有1个水果
小孩子刚吃了1个水果,篮子中还剩下0个水果

参考文献:传智播客的资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值