多线程学习和Thread类

多线程学习

  • 一、多线程相关概念
    • 1. 并行与并发
    • 2. 进程与线程
    • 3. 多线程的作用
    • 4. 线程调度
  • 二、多线程创建使用
    • 1. 经典的两种方式
    • 2. 匿名内部类实现
    • 3 Thread类
      • 3.1 构造器
      • 3.2 基本方法
      • 3.3 线程控制方法
      • 3.4 守护线程
  • 三、 线程的生命周期
  • 四、线程安全
    • 方式1:继承Thread类
    • 方式2:实现Runnable接口
    • 出现线程安全问题的前提条件
    • 解决方法:同步代码块synchronized
    • 锁对象
    • 编写多线程程序的一般原则
    • 单例模式之懒汉式的同步代码块
  • 五、等待唤醒机制

一、多线程相关概念

1. 并行与并发

  • 并行:同一个时间点,多个事件同时执行.
  • 并发:同一个微小的时间段,多个事件正在执行.

2. 进程与线程

  • 进程:是程序运行过程的描述,系统以进程为单位为每个程序分配独立的系统资源,比如内存。
  • 线程:线程是进程中的执行单元,主要负责执行程序的任务,每个进程至少有一个线程。
  • 每个线程的栈空间是独立的,堆空间和方法区是共享的。多个线程运行时都会开辟自己专属的栈空间。

3. 多线程的作用

两种使用方式:

  • 多个线程做相同的任务
  • 多个线程做不同的任务

主要优点:充分利用CPU空闲时间片,用尽可能短的时间完成用户的请求,即使程序的响应速度更快。

4. 线程调度

表示CPU计算资源在不同线程中切换。有不同的调度方式:

  • 分时调度:所有线程轮流使用CPU使用权,平均分配。
  • 抢占式调度:按照优先级抢占,同级的话随机分配。

二、多线程创建使用

1. 经典的两种方式

实现多线程有两种方式:

  • 继承Thread类
  • 实现Runnable接口

方式一:

  1. 创建类(线程任务类)继承Thread,并重写run方法
  2. 创建线程类对象
  3. 调用线程对象的start方法启动线程
public class Thread1Demo {
    public static void main(String[] args) {
        //2.创建线程对象
        MyThread myThread = new MyThread();
        //3.启动线程
        myThread.start();

        //主线程代码
        for (int i = 0; i < 30; i++) {
            System.out.println("======主线程=======" + i);
        }
    }
}

//1.编写线程类,继承Thread类
class MyThread extends Thread{
    //重写run方法,即线程的任务

    @Override
    public void run() {
        System.out.println("线程任务启动");
        for (int i = 0; i < 30; i++) {
            System.out.println("分支线程" + i);
        }
    }
}

注意事项

  • 不要直接调用run方法来启动线程,run方法是由jvm来调用的
  • 不要重复启动同一个线程

方式二:

  1. 创建类(线程任务类)实现Runnable接口,并重写run方法
  2. 创建线程类对象
  3. 创建Thread对象,通过构造器传入线程任务对象
  4. 调用线程对象的start方法启动线程
public class Thread2Demo {
    public static void main(String[] args) {
        A a = new A();
        Thread thread = new Thread(a);
        thread.start();

        //主线程代码
        //Thread thread = Thread.currentThread();
        for (int i = 0; i < 30; i++) {
            System.out.println("======主线程=======" + i);
        }
    }
}

class A implements Runnable{

    @Override
    public void run() {
        //System.out.println("线程任务启动");
        for (int i = 0; i < 30; i++) {
            System.out.println("分支线程" + i);
        }
    }
}

注:使用接口创建多个线程,方便共享数据,多个线程可以都使用同一个线程任务类启动线程,在后期线程的同步机制中也更为便于锁的实现。

2. 匿名内部类实现

当创建一个线程,只会使用一次后就不会再使用了,可以使用匿名内部类方式实现。

public class Demo3 {
    public static void main(String[] args) {
        //方式1匿名写法:
        new Thread(){
            @Override
            public void run() {
                System.out.println("线程任务A");
            }
        }.start();
        
        //方式2匿名写法:
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程任务B");
            }
        }).start();
    }
}

3 Thread类

3.1 构造器

  1. 空参构造器:public Thread()
  2. 指定名字:public Thread(String name)
  3. 指定线程任务对象: public Thread(Runnable target);

3.2 基本方法

  1. run(): 定义线程执行任务
  2. isAlive(): 判断当前线程是否还在活动状态
  3. currentThread(): 返回当前线程对象的引用
  4. setName():设置线程名称
  5. getName():获取线程名称
  6. setPriority(): 设置线程优先级
  7. getPriority(): 获取线程优先级

线程优先级最高10,最低1,主线程默认优先级为5,主线程创建的子线程默认优先级也为5.

3.3 线程控制方法

  1. start(): 启动线程
  2. sleep(): 线程休眠
  3. yield(): 线程礼让,抢到CPU资源后让出给其他线程。
  4. join(): 线程插队,插入线程执行结束后,被插队线程再执行。
  5. stop (): 强迫线程终止,不安全,已废弃。
  6. interrupt(): 给线程标记为中断状态
  7. interrupted(): 判断线程的中断状态,判断后会清除中断标记。
  8. isInterrupted(): 判断线程的中断状态,不会清除中断标记。

注: 礼让有可能并没有礼让成功,如果没有其他线程使用CPU资源,原线程还会继续执行。

3.4 守护线程

线程可以分为两类:用户线程和守护线程。用户线程都结束时,守护线程也会随着用户线程的结束而终止。

设置守护线程的方法:

  1. 必须在启动线程前设置
  2. setDaemon(true)

JVM中的垃圾回收器就是一个守护线程,守护的就是程序就是用户线程,程序结束时,守护线程就好自动结束。

三、 线程的生命周期

线程生命周期的五种状态:新建、就绪、运行、阻塞、死亡

多线程学习和Thread类_第1张图片

JDK将线程状态分为6种。其中就绪和运行合并为可运行状态,阻塞状态分为了三种:定时等待状态、无限等待状态、等待监视器锁。
多线程学习和Thread类_第2张图片

四、线程安全

使用多个线程模拟多个窗口的卖票过程,保证卖票过程的正常运行。

方式1:继承Thread类

public class Demo {
    public static void main(String[] args) {
        new TicketSell("窗口1").start();
        new TicketSell("窗口2").start();
        new TicketSell("窗口3").start();
    }
}


class TicketSell extends Thread{
    static int num = 100;
    public TicketSell(String name) {
        super(name);
    }

    @Override
    public void run() {
        //int num = 100;//局部变量
        while(num > 0) {
            System.out.println(Thread.currentThread().getName()+"卖出了第"+num+"张票");
            num -- ;
        }
        System.out.println("=====买完了=====");
    }
}

运行结果:出现了重票问题,第100张卖了多次。
多线程学习和Thread类_第3张图片

方式2:实现Runnable接口

public class Demo2 {
    public static void main(String[] args) {
        TicketSell2 sell2 = new TicketSell2();
        new Thread(sell2, "窗口1").start();
        new Thread(sell2, "窗口2").start();
        new Thread(sell2, "窗口3").start();
    }
}

class TicketSell2 implements Runnable{
    static int num = 100;

    @Override
    public void run() {
        //int num = 100;//局部变量
        while(num > 0) {
            System.out.println(Thread.currentThread().getName()+"卖出了第"+num+"张票");
            num -- ;
        }
        System.out.println("=====买完了=====");
    }
}

运行结果:同样出现线程安全问题,这里就不再贴图了。

出现线程安全问题的前提条件

  1. 多个线程访问
  2. 访问的是共享数据
  3. 操作共享数据的过程不是原子操作

解决方法:同步代码块synchronized

出现线程安全问题的前两个条件无法改变,我们就是想让多线程去访问共享数据。故只能对第三个条件进行处理,其中sychronized关键字来将访问共享数据的操作变成原子操作。同步代码块要越小越好,同步代码块越大,效率越低。代码块最小时,应该包含所有操作共享数据的代码,其中本问题中的num就是共享数据。必须将判断num大小和对num--的代码都包含在代码块中才能解决线程安全问题。

public class Demo2 {
    public static void main(String[] args) {
        TicketSell2 sell2 = new TicketSell2();
        new Thread(sell2, "窗口1").start();
        new Thread(sell2, "窗口2").start();
        new Thread(sell2, "窗口3").start();
    }
}

class TicketSell2 implements Runnable{
    static int num = 100;


    @Override
    public void run() {
        //int num = 100;//局部变量
        while(true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (this) {//this就是锁对象
                if(num > 0)//共享数据
                    System.out.println(Thread.currentThread().getName()+"卖出了第"+num--+"张票");//共享数据
            }
            if(num <= 0) {
            	break;
            }
        }
        System.out.println("=====买完了=====");
    }
}

锁对象

当线程获取到锁对象才能执行同步代码,在执行完同步代码时会持有锁对象,执行完同步代码后才会释放锁对象。

因此,在使用同步代码块解决线程安全问题时,必须保证锁对象是同一个实例。如果上面代码中的synchronized (this) 更换为new Object()时就仍然会出现线程安全问题。

  • 非静态静态方法,默认的锁对象就是this
  • 静态同步代码块建议使用当前类的Class实例作为锁对象

编写多线程程序的一般原则

把共享数据和操作共享数据的方法封装到一个类中,作为共享资源类,与线程类解耦合。还是以买票问题为例子,将该问题中的买票和操作共享数据分成两个类来处理。

共享资源类:

public class Ticket{
	private int num = 100;//共享数据
	//操作共享数据的方法
	public synchronized void sale(){
		if(num > 0)
			System.out.println("卖出了第" +(num--)+"张票");
	}
	//get-set方法
}

线程类:

public class TicketThread extend Thread{
	private Ticket ticket;//共享资源
	public TicketThread(String name, Ticket ticket){
		super(name);
		this.ticket = ticket;
	}
	@Override
	public void run(){
		while(true){
			ticket.sale();
			if (ticket.getNum() <= 0){
				return ;
			}
		}
	}
}

单例模式之懒汉式的同步代码块

在实现单例模式时,懒汉式是先判断当前对象实例是否已经创建了,如果为空就创建一个新的对象,否则就new一个唯一的实例。如果在多线程的情况下, 由于判断是否为空和创建对象实例不是原子操作,在CPU资源切换时有可能导致单例模式失效的问题,也需要用到同步代码块。

public class Singleton {
    //私有的静态变量
    private static Singleton instance = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
    //如果目前对象还未创建,就进入同步代码块,
        if(instance == null){//双重校验
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        //否则直接返回实例对象即可
        return instance;
    }
}

五、等待唤醒机制

多个线程间的一种协作机制,最经典的就是生产消费者模型,当生产者的商品还没生产出来时,消费者进入等待状态,当商品生产好后再将消费者线程唤醒。

其中等待和唤醒的方法是Object类下面的方法:

  • 等待使用wait():会释放锁, sleep()方法不释放锁
  • 唤醒使用notify(): 还有另外一个唤醒所有等待的线程的方法,notifyAll()
    为什么这两个方法不在Thread类里面,因为该方法是通过锁对象来调用的,而锁对象可以是任意对象,所有只能放在Object类中。

这里使用一个厨师和服务员的情景来应用生产者消费者模型。

共享资源类(锁对象):

public class WorkBench {
    private int num; //工作台当前餐数
    private final int MAX = 10;
    private final int MIN = 0;

    //放一份快餐到工作台
    public synchronized void put(){
        while(num >= MAX) {
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println(Thread.currentThread().getName()+"放了一份快餐到工作台,当前共"+(++num)+"份");

        //休息一下
        try {
            int random = (int)(Math.random() * 1000);
            Thread.sleep(random);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        this.notify();
    }

    //取快餐
    public synchronized void take(){

        while(num<=MIN){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

	    System.out.println(Thread.currentThread().getName()+"取走了一份快餐,当前共"+(--num)+"份");
        //随机休息一下
        try {
            int random = (int)(Math.random() * 1000);
            Thread.sleep(random);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //随机唤醒在此监视器上等待的一个线程
        this.notify();
    }
}

线程任务类
厨师类

public class Cooker extends Thread{
    private WorkBench workBench;

    public Cooker(String name, WorkBench workBench) {
        super(name);
        this.workBench = workBench;
    }

    @Override
    public void run() {
        while (true){
            try {
                int random = (int)(Math.random() * 1000);
                Thread.sleep(random);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            workBench.put();
        }
    }
}

服务员类

public class Waiter extends Thread{

    private WorkBench workBench;

    public Waiter(String name, WorkBench workBench) {
        super(name);
        this.workBench = workBench;
    }

    @Override
    public void run() {
        while (true){
            try {
                int random = (int)(Math.random() * 1500);
                Thread.sleep(random);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            workBench.take();
        }
    }
}

测试类:

public class Demo {
    public static void main(String[] args) {
        WorkBench wb = new WorkBench();
        new Cooker("大厨",wb).start();
        new Waiter("翠花",wb).start();
    }
}

这个版本的代码如果修改为多个生产者多个消费者模型,可以将notify方法改为notifyAll,当服务员取走快餐时,唤醒所有厨师类进行工作。

你可能感兴趣的:(学习,java,开发语言)