多线程的创建使用和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接口
 
方式一:
- 创建类(线程任务类)继承Thread,并重写run方法
 - 创建线程类对象
 - 调用线程对象的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来调用的
 - 不要重复启动同一个线程
 
方式二:
- 创建类(线程任务类)实现Runnable接口,并重写run方法
 - 创建线程类对象
 - 创建Thread对象,通过构造器传入线程任务对象
 - 调用线程对象的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 构造器
- 空参构造器:
public Thread() - 指定名字:
public Thread(String name) - 指定线程任务对象: 
public Thread(Runnable target); 
3.2 基本方法
run(): 定义线程执行任务isAlive(): 判断当前线程是否还在活动状态currentThread(): 返回当前线程对象的引用setName():设置线程名称- getName():获取线程名称
 - setPriority(): 设置线程优先级
 - getPriority(): 获取线程优先级
 
线程优先级最高10,最低1,主线程默认优先级为5,主线程创建的子线程默认优先级也为5.
3.3 线程控制方法
- start(): 启动线程
 - sleep(): 线程休眠
 - yield(): 线程礼让,抢到CPU资源后让出给其他线程。
 - join(): 线程插队,插入线程执行结束后,被插队线程再执行。
 stop(): 强迫线程终止,不安全,已废弃。- interrupt(): 给线程标记为中断状态
 - interrupted(): 判断线程的中断状态,判断后会清除中断标记。
 - isInterrupted(): 判断线程的中断状态,不会清除中断标记。
 
注: 礼让有可能并没有礼让成功,如果没有其他线程使用CPU资源,原线程还会继续执行。
3.4 守护线程
线程可以分为两类:用户线程和守护线程。用户线程都结束时,守护线程也会随着用户线程的结束而终止。
设置守护线程的方法:
- 必须在启动线程前设置
 - setDaemon(true)
 
JVM中的垃圾回收器就是一个守护线程,守护的就是程序就是用户线程,程序结束时,守护线程就好自动结束。
三、 线程的生命周期
线程生命周期的五种状态:新建、就绪、运行、阻塞、死亡

JDK将线程状态分为6种。其中就绪和运行合并为可运行状态,阻塞状态分为了三种:定时等待状态、无限等待状态、等待监视器锁。
 
四、线程安全
使用多个线程模拟多个窗口的卖票过程,保证卖票过程的正常运行。
方式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张卖了多次。
 
方式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("=====买完了=====");
    }
}
 
运行结果:同样出现线程安全问题,这里就不再贴图了。
出现线程安全问题的前提条件
- 多个线程访问
 - 访问的是共享数据
 - 操作共享数据的过程不是原子操作
 
解决方法:同步代码块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;
    }
}
                


















