JAVAEE细细看 进阶 15 - 多线程

多线程

一. 进程

是正在运行的程序

  1. 是系统进行资源分配和调用的独立单位;
  2. 每个进程都有它自己的内存空间和系统资源
二. 线程

是进程中的耽搁顺序控制流,是一条执行路径
1.一个进程如果只有一条执行路径,则成为单线程程序
2.多线程,一个进程如果有多条执行路径,则成为多线程程序

多线程的实现方式

  • 方式1
  1. 定义一个类,继承于Thread类
  2. 在这个类中重写run()方法
  3. 创建类对象
  4. 启动线程

两个小问题

  1. 为什么要重写run()方法?
    因为run() 方法是为了封装被线程执行的代码
  2. run()和strart()的区别
    前者封装线程执行代码,直接调用,相当于普通方法的调用;后者启动线程,然后由JVM调用此线程的run()方法

设置和获取线程名称

  • setName()
  • getName()
  • currentThread().getName() 获取当前线程的名称
	 /*
        设置线程名称
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.setName("飞机");
        my2.setName("高铁");
        */

        /*
        带参数的设置线程名称
        MyThread my1 = new MyThread("飞机");
        MyThread my2 = new MyThread("高铁");
        */

//      my1.start();
//	my2.start();

        // 获取当前线程的名称  System.out.println(Thread.currentThread().getName());  // main

三. 线程调度

线程优先级高,仅仅表示它获取CPU时间片相对多一些,并不表示它一定最先执行,是概率事件。

线程的调度模型

  1. 分时调度模型:所有线程轮有使用CPU的使用权,平均分配每个线程占用CPU的时间片。
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果优先级相同,那么会随机选择一个。优先级高的获取的CPU时间片相对多一些。
    Java使用的是抢占式的调度模型

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

  • getPriority() int
  • setPriority(int newPriority)
    Thread.MAX_PRIORITY 最大10
    Thread.MIN_PRIORITY 最小1
    Thread.NORM_PRIORITY 默认值5
    优先级高不一定在最前面,只是获取CPU时间片相对多一些
ThreadPriority tp1 = new ThreadPriority();
ThreadPriority tp2 = new ThreadPriority();
ThreadPriority tp3 = new ThreadPriority();

tp1.setName("高铁");
tp2.setName("飞机");
tp3.setName("汽车");

// 获取优先级
System.out.println(tp1.getPriority());  // 5
System.out.println(tp2.getPriority());  // 5
System.out.println(tp3.getPriority());  // 5
System.out.println("--------");

// 查看优先级的区间
System.out.println(Thread.MAX_PRIORITY);  // 10
System.out.println(Thread.MIN_PRIORITY);  // 1
System.out.println(Thread.NORM_PRIORITY); // 5

// 设置优先级
tp1.setPriority(10);
tp2.setPriority(1);
tp3.setPriority(6);

// 运行
tp1.start();
tp2.start();
tp3.start();

四. 线程控制
  • sleep(long) 暂停
  • join() 这个线程结束,才能向下执行
  • setDaemon(boolean) 当运行的程序只剩下守护线程时,Java虚拟机也将退出,比如垃圾回收机制就是守护线程;
    a. 当java虚拟机中没有非守护线程在运行的时候,java虚拟机会关闭。
    b. 当所有常规线程运行完毕以后,守护线程不管运行到哪里,虚拟机都会退出运行。
    c. 所以你的守护线程最好不要写一些会影响程序的业务逻辑。否则无法预料程序到底 会出现什么问题
五. 线程的生命周期

在这里插入图片描述

方式二
实现Runnable

  1. 定义一个了类,实现Runnable接口
  2. 在类中重写run() 方法
  3. 创建类对象
  4. 创建Thread类对象,把类对象作为构造方法的参数
  5. 启动线程
    两种方式小结:
    方案:
  6. 继承Thread类
  7. 实现Runnable接口

相比于继承Thread类,实现Runnable接口的好处

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

案例

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

public class SellTicket implements Runnable {

    private int tickets = 100;

    @Override
    public void run() {
        int k = 110;

        while (k >= 0) {
            if (tickets >= 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 卖了票,目前剩票" + tickets--);
            } else {
                System.out.println("票已经卖光了");
            }
            k --;
        }
    }
}

测试类
// 创建卖票对象
SellTicket sellTicket = new SellTicket();

// 创建线程
Thread th1 = new Thread(sellTicket,"窗口1");
Thread th2 = new Thread(sellTicket,"窗口2");
Thread th3 = new Thread(sellTicket,"窗口3");

// 时间到,开始售票
th1.start();
th2.start();
th3.start();

但是测试出来会出现bug

六. 同步代码块

synchronized(任意对象){}
改进上面代码如下

public class SellTicket implements Runnable {

    private int tickets = 100;
    private int runTimes = 110;
    private Object obj = new Object();

    @Override
    public void run() {

        while (runTimes >= 0) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 卖了票,目前剩票" + tickets--);
                } else {
                    System.out.println("票已经卖光了");
                }
                runTimes--;
            }
        }
    }
}

同步代码块的优缺点

优点:解决了多线程的数据安全问题。
缺点:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

七. 同步方法

把synchronized 加到方法上
格式:

  • 修饰符 synchronized 返回值类型 方法名(){}
    同步方法的锁对象是: this
八. 同步静态方法

格式

  • 修饰符 static synchronized 返回值类型 方法名(){}
    同步静态方法的锁对象是:类名.class
private static int tickets = 100;
private Object obj = new Object();

@Override
public void run() {
    while (true) {

        // 同步方法锁
        sellTickets();
        
        // 同步静态方法锁
        sellTickets_class();
    }
}

// 同步方法锁,锁对象是 this
private synchronized void sellTickets() {
    if (tickets > 0) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
        tickets--;
    }
}

// 同步今天方法锁的锁对象是 SellTicket.class
private static synchronized void sellTickets_class() {
    if (tickets > 0) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
        tickets--;
    }
}

九. 线程安全的类

在这里插入图片描述

/*
 以下是线程安全的替代类,如果不考虑同步,还是使用StringBuilder 、ArrayList 、HashMap
  */

StringBuilder sbu = new StringBuilder();
StringBuffer sbf = new StringBuffer();

ArrayList<String> arr = new ArrayList<>();
Vector<String> ve = new Vector<>();

HashMap<String, String> map = new HashMap<>();
Hashtable<String, String> table = new Hashtable<>();

/*
 但是数组和map的依然被替代了,替代的是Collections
 */

List<String> strings = Collections.synchronizedList(arr);
Set<Object> objects = Collections.synchronizedSet(new HashSet<>());
Map<String, String> map1 = Collections.synchronizedMap(map);

JDK5之后提供了新的锁对象 Lock

十. Lock

它是一个接口,所有要用他的实现类ReentrantLock的实例

  • lock() 加锁
  • unlock() 释放锁
private int tickets = 100;
private Lock lock = new ReentrantLock();

@Override
public void run() {
    while (true) {
        try {
            lock.lock();
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        } finally {
            lock.unlock();
        }
    }
}
十一. 线程等待

为了体现java生产者和消费者,在生产和消费过程中的等待和唤醒,在Object类中有等待和唤醒的方法

  • wait() 导致当前线程等待,直到另一个线程调用该对象的notify() 或者notifyAll()
  • notify() 唤醒单个线程
  • notifyAll() 唤醒正在等待监视器中的的所有线程

特殊的错误 IllegalMonitorStateException
在线程中调用wait方法的时候 要用synchronized锁住对象,确保代码段不会被多个线程调用

案例
送奶工人送牛奶到奶箱中,程序员从奶箱中拿牛奶,要求:工人送一瓶,程序员取一瓶,用多线程实现;

生产者消费者案例中包含的类:
    1:奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
    2:生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作
    3:消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
    4:测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下
        A:创建奶箱对象,这是共享数据区域
        B:创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
        C:创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
        D:创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
        E:启动线程

public class BoxDemo {
    public static void main(String[] args) {

        // 创建盒子对象
        Box box = new Box();

        // 创建生产者和消费者,因为他们都对盒子做操作,所以盒子作为参数
        Producter producter = new Producter(box);
        Consumer consumer = new Consumer(box);

        // 创建两个线程,开始存牛奶和取牛奶
        Thread t1 = new Thread(producter);
        Thread t2 = new Thread(consumer);

        // 开始
        t1.start();
        t2.start();
    }
}

public class Box {

    private int milk; // 记录第几瓶奶

    private boolean state = false; // 记录箱子中是否有奶

    // 盒子具有放入牛奶的功能
    public synchronized void put(int i){

        // 如果有奶,就等待
        if (state) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果没奶,就放入
        this.milk = i+1;
        System.out.println("生产者放入第" + this.milk + "瓶奶");

        // 通知,其他线程,已经有奶了,可以拿了
        state = true;
        notifyAll();

    }

    // 盒子具有被拿出牛奶的功能
    public synchronized void get() {

        // 如果没有奶了,就等待
        if (!state) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果有奶,就取走
        System.out.println("消费者拿走第" + milk + "瓶奶");

        // 通知其他线程,可以放奶了
        state = false;
        notifyAll();
    }
}

public class Producter implements Runnable{

    private Box box;

    public Producter(Box box) {
        this.box = box;
    }

    @Override
    public void run() {
        // 放入牛奶
        for (int i = 0; i < 5; i++) {
            box.put(i);
        }
    }
}

public class Consumer implements Runnable{

    private Box box;

    public Consumer(Box box) {
      this.box = box;
    }

    // 消费者 取走牛奶
    @Override
    public void run() {
        // 因为不知道什么时候放奶,所以一直取
        while (true) {
            box.get();
        }
    }
}

十二. 总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值