天天看点

Java入门:多线程学习笔记与多线程案例:卖票

介绍:进程是正在运行的程序;

单线程:一个进程有一个执行路径;

多线程:一个进程有多条执行路径;

一、实现多线程的方法:

①.继承Thread类;

a.定义类继承Thread类;

b.在类中重写run()方法;

c.创建该类的对象;

d.启动线程;

❤两个小问题:

A.为什么重写run()方法?

    因为run()方法是用来封装被线程执行的代码

B.run()方法和start()方法的区别?

    run():分装线程执行的代码,直接调用,相当于普通方法

start():启动线程,由JVM调用此线程的run()方法1.a.定义类继承Thread类、b.在类中重写run()方法;

public class duothread01 extends Thread{
    @Override
    public void run() {
        for(int x=0;x<25;x++){
            System.out.println(getName() + ":" + x);
        }
    }
}

           

2.c.创建该类的对象、d.启动线程;

duothread01 dt01 = new duothread01();
duothread01 dt02 = new duothread01();
//启动线程
dt01.start();
dt02.start();
           

②.实现runnable接口;

a.定义一个类实现runnable接口;

b.类中重写run方法;

c.创建对象;

d.创建Thread类的对象,把声明的类对象作为参数

e.启动线程;第一步、a.定义一个类实现runnable接口、b.类中重写run方法;

package runnable01;

public class runnable01 implements Runnable {
    //重写方法
    @Override
    public void run() {
        for(int i= 0;i<20;i++){
            //这里因为与Thread没有直接关系,所以需要获取主线程再获取线程名称
            System.out.println(Thread.currentThread().getName()+ "," + i);
        }
    }
}
           

第二步、c.创建对象、d.创建Thread类的对象,把声明的类对象作为参数、e.启动线程;

package runnable01;

public class runnableDemo {
    public static void main(String[] args) {
        //创建实现类对象
        runnable01 rb = new runnable01();
        //创建线程:创建Thread类的对象,把runnable实现类对象作为参数,设置名称,不设置有默认值;
        Thread t1 = new Thread(rb,"火箭");
        Thread t2 = new Thread(rb,"飞船");
        //启动线程
        t1.start();
        t2.start();
    }
}
           

❤相比继承Thread类,实现runnable接口实现多线程的好处:

  1. 避免了Java单继承的局限性;
  2. 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序代码、数据有效分离,较好的体现了面向对象设计思想;

二、设置和获取线程名称

setName():

getName():

  1. 调用setName方法,设置名称,

//调方法给线程赋值

dt01.setName("飞机");

dt02.setName("高铁");

  1. 在自己定义的Thread的子类中添加无参和带参构造方法,带参方法内部用super访问父类带参方法。
**************************Thread类中***************************

public duothread01() {
}
public duothread01(String name) {
    super(name);
}

************************测试类中**************************
//通过带参构造方法
duothread01 dt03 = new duothread01("飞船");
duothread01 dt04 = new duothread01("火箭");
           

3.获取当前正在执行的线程的名称

Thread currentThread():返回对当前正在执行的线程对象的引用
String name = Thread.currentThread().getName();
System.out.println(name);
           

线程调度

线程有两种调度模型

  1. 分时调度模型:所有线程轮流获得CPU的使用权,平均分配每个线程占用CPU的时间片;
  2. 抢占调度模型(Java使用该模式):优先级高的线程先获得CPU使用权,如果优先级相同则随机选一个;

Thread类中设置和获取线程优先级的方法:

  1. getPriority():返回此线程的优先级;
  2. setPriority():修改此线程的优先级;
//获取线程优先级
System.out.println(dt1.getPriority());
System.out.println(dt2.getPriority());
System.out.println(dt3.getPriority());
System.out.println(dt4.getPriority());

//获取优先级参数的取值范围
System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);

//修改线程优先级
dt1.setPriority(1);
dt2.setPriority(10);
dt3.setPriority(9);
dt4.setPriority(2);
           

线程默认优先级是5,取值范围是1-10;

线程优先级高仅仅表示线程获取CPU时间片的几率高,该线程不一定会跑到最前面,可能在运行次数多的时候才能看到你想要的效果;

  • 线程控制

Sleep(long millis):使当前正在执行的线程停留(暂停)指定的毫秒数;

案例:刘备孙权曹操争天下,势均力敌,不能让一个先跑完;

Join():等待这个线程死亡;如果有一个线程调用了此方法,那么其他线程必须得等到这个线程执行完才能执行;

案例:康熙、四阿哥、八阿哥抢皇位,需等康熙挂掉,两位阿哥开始抢;

setDaemon(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机即将推出;

案例:刘备关羽张飞三结义,刘备为大哥,刘备挂了,关羽和张飞也会结束,可能不是立即,还会执行一点;

需设置刘备为主线程,关羽张飞为守护线程;

  • 线程的生命周期
Java入门:多线程学习笔记与多线程案例:卖票

案例1:卖票

需求:某电影院目前正在上映国产大片,共有100张,电影院有三个窗口卖票,请设计一个程序模拟该电影院卖票;

思路:

  • 定义一个类Move实现runnable接口,设置一个成员变量piao = 100;
  • 重写run()方法实现卖票:
  1. 判断票数大于0,就买票,并告知是哪个窗口卖的
  2. 卖了票之后总数要减1
  3. 票没有了也会有人来问,所以用死循环一直卖
  • 定义卖票测试类,
  1. 新建Thread对象
  2. 将Move对象作为参数传递给三个Thread的对象并给出窗口编号
  3. 启动线程

代码如下:

实现类:

package Moveticket_anli;

public class Move_ticket implements Runnable {
    //定义变量,总票数赋值给它
    private int ticket = 20;
    //重写run()方法
    @Override
    public void run() {
        //死循环实现一直卖票,即使卖完了也可以访问;
        while (true) {
            //判断票数大于0,就卖票
            if (ticket > 0) {
                //拼接:窗口名+第几张
                System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
               //卖出后总数减1
                ticket--;
            }
        }
    }
}
           

测试类:

package Moveticket_anli;

public class movewin {
    public static void main(String[] args) {
        //新建卖票类的对象
        Move_ticket mt = new Move_ticket();
        //创建三个线程(窗口)同时卖票
        Thread t1 = new Thread(mt,"wein01");
        Thread t2 = new Thread(mt,"wein02");
        Thread t3 = new Thread(mt,"wein03");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}
           

案例思考1:  

现实生活中,票卖出去,出片也需要时间,所以在每卖出一张票,需要一点时间的延迟,所以给卖票动作加一个100毫秒的延迟操作,用sleep()方法实现,实现接口类的if内部开头添加如下代码:

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

案例思考2:

修改代码后发现,同一张票会被出售多次,并且最后出现了第0和第-1张票;

出现此问题的原因是线程的随机性导致的,称为数据安全问题;

数据安全问题:如果符合下面全部三种情况,即可判定程序存在数据安全问题;

  1. 是否是多线程环境;
  2. 是否有共享数据;
  3. 是否有多条语句操作共享数据;

解决方案1:使用同步代码块sychronized(参数)

参数:在外部定义private Object obj = new Object();对象名作为参数;

数据安全问题的存在是因为同时满足了三个条件,那么要解决就得至少改变一条使其不满足,多线程和共享数据是无法避免的,所以只能是改变多条语句操作共享数据的方案,把多条语句操作共享数据的代码给锁起来,让任意时刻都只有一个线程能够执行这段代码;

Java提供了同步代码块的操作:

格式:

sychronized(任意对象){

多条语句操作共享数据的代码

}

sychronized(任意对象):任意对象就可以看作是一把锁;

最终代码如下:

package Moveticket_anli;

public class Move_ticket implements Runnable {
    //定义变量,总票数赋值给它
    private int ticket = 20;
    private Object obj = new Object();
    //重写run()方法
    @Override
    public void run() {
        //死循环实现一直卖票,即使卖完了也可以访问;
        while (true) {
            //把多条语句操作共享数据的代码给锁起来
            // 让任意时刻都只有一个线程能够执行这段代码
            synchronized (obj) {
                //判断票数大于0,就卖票
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //拼接:窗口名+第几张
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
                    //卖出后总数减1
                    ticket--;
                }
            }
        }
    }
}
           

以下图示是教程中的代码执行流程注释信息,加深理解

Java入门:多线程学习笔记与多线程案例:卖票

使用同步代码块sychronized(参数)的好处与弊端

好处:解决了多线程的数据安全问题;

弊端:当线程很多时,因为每个线程都会去判断线程上的锁,非常耗费资源,无形中降低程序的运行效率;

解决方案2:同步方法

同步方法:将sychronized写到方法的修饰符后面;

Public sychronized void XXX(){};

锁对象是:this

同步静态方法:将sychronized写到方法的静态修饰符后面;

Public static sychronized void XXX(){};

锁对象是:类名.class

用同步方法改进后代码如下:

卖票方法:

package Moveticket_anli;

public class Move_ticket implements Runnable {
    private int ticket = 20;
    private Object obj = new Object();
    @Override
    public void run() {
        //死循环实现一直卖票,即使卖完了也可以访问;
        while (true) {
            maipiao();
        }
    }
    //把多条语句操作共享数据的代码给锁起来
    // 让任意时刻都只有一个线程能够执行这段代码
    private synchronized void maipiao() {
        //判断票数大于0,就卖票
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //拼接:窗口名+第几张
            System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
            //卖出后总数减1
            ticket--;
        }
    }
}
           

卖票窗口:

package Moveticket_anli;

public class wicket {
    public static void main(String[] args) {
        //新建卖票类的对象
        Move_ticket mt = new Move_ticket();
        //创建三个线程(窗口)同时卖票
        Thread t1 = new Thread(mt,"windows01");
        Thread t2 = new Thread(mt,"windows02");
        Thread t3 = new Thread(mt,"windows03");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}