Java多线程机制基础

1,937 阅读10分钟

Java多线程机制:

线程的基本概念

程序:是一段可执行的静态代码

进程:程序的一次动态加载过程(将程序加载到内存时,此时程序就转换为了进程)

线程: 进程可进一步细化为线程,是一个程序内部的一条执行路径,线程不能独立存在,必须依附于某个进程

举个例子:打开火绒,火绒作为一个进程存在于内存中,其中包含的病毒查杀和垃圾清理就是两个线程

一个Java应用程序,至少有三个线程:

main线程,垃圾回收线程,异常处理线程

多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈 image.png

程序计数器的作用:

1.控制代码的执行流程

2.记录当前线程执行的位置

程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

栈和本地方法栈的作用:

栈用于存储局部变量表等信息

栈和本地方法栈私有是为了保证线程中的局部变量不被别的线程访问到

堆主要用于存放新创建的对象,方法区主要用于存放已被加载的类信息,这些是每个线程共享的资源

使用多线程的优点

优点在于充分利用了CPU的空闲时间片,用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。

如果是单线程,那同时只能处理一个用户请求,多线程则可以同时处理多个用户的请求

上下文切换

上下文切换是指:一个工作的线程被另外一个线程暂停,另外一个线程占用处理器开始执行任务

CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。

多线程的创建方式

1.继承Thread类

实现步骤:

创建一个继承Thread类的子类

重写Thread类的run() 将线程需要执行的操作声明在run()

创建Thread类子类的对象

通过此对象调用start()

start()作用:启动当前线程 调用当前线程的run

2.实现Runnable接口

实现步骤:

创建一个实现了Runnable接口的类

实现类去实现Runnable中的抽象方法:run()

创建实现类的对象

将此对象作为参数传递到Thread类的构造器,创建Thread对象

//Thread类的构造器需要接收一个Runnable对象
public Thread(Runnable target)

通过Thread类的对象调用start()

以上两种方式的对比

1.实现接口没有单继承的局限性

2.Thread类需要使用static来修饰共享数据,而实现Runnable接口就是将实现类当作参数传递给Thread构造器,run中数据天然就是共享数据

3.实现Callable接口

与实现Runnable接口相比,实现Callable接口功能更强大 我们需要重写call方法,call方法可以抛出异常

实现步骤:

创建一个实现Callable的实现类。

实现call()方法,将此线程需要执行的操作声明在call()中。

创建Callable接口实现类的对象。

将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象。

将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。

4.线程池

优点:线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作

image.png

使用线程池中线程对象的步骤

(1)创建线程池对象

(2)创建Runnable接口子类对象

(3)提交Runnable接口子类对象

(4)关闭线程池

线程池方法:

Executors:线程池创建工厂类

public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象

ExecutorService:线程池类

Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

public class ThreadPool implements Runnable {
    @Override
    public void run() {
        System.out.println("获取一个线程");
    }
}

public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        //创建Runnable实例对象
        ThreadPool t = new ThreadPool();
        //从线程池中获取线程对象,然后调用ThreadPool中的run()
        service.submit(t);
        //再获取个线程对象,调用run()
        service.submit(t);
        //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
        //关闭线程池
        service.shutdown();
    }

线程的生命周期

1.新建 :new Thread对象此时线程就是新建状态

2.就绪 :新建的线程执行start方法就是就绪状态

3.运行 :就绪的线程获取cpu执行权就是运行状态

4.阻塞 :运行的线程遇到sleep wait 等待同步锁等情况变为阻塞状态

5.死亡 :运行的线程执行完run 或者遇到stop 异常就变成死亡状态

image.png

线程同步

首先引入锁的概念:当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码

1.同步代码块

1.1实现Runnable接口

synchronized(){//任何一个类的对象 都可以充当锁
//需要被同步的代码 操作共享数据的方法

}
private int ticket=100;
    
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "票号为" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }

多个线程必须要共用一把锁

由于是使用同一个runnable对象实例化出来的Thread对象,此时用this当前对象充当锁

1.2继承Thread类

继承与实现接口的方法基本一致,只需要确保锁是同一个锁即可

锁设置为static或者用当前类来充当锁

2.同步方法

操作共享数据的方法可以设置为同步方法

public synchronized void show(){
//操作共享数据的代码
}

非静态的同步方法 同步监视器是this

静态的同步方法 同步监视器是当前类本身

3.新增方式:Lock锁

lock中的方法

locke()方法: 加锁

unlock()方法: 释放锁

private ReentrantLock lock=new ReentrantLock();
//上锁
lock.lock();
//释放锁
lock.unlock();

synchronized和lock不同之处:

synchronized机制在执行完同步代码之后,自动释放锁

lock需要手动启动锁,手动关闭锁

死锁问题

所谓死锁是指多个进程因竞争资源而相互等待,若无外力作用,这些进程都无法向前推进

死锁产生的四个必要条件

  1. 互斥条件:资源一次只允许一个进程访问
  2. 不剥夺条件:已被占用的资源只能由属主释放,不允许被其它进程剥夺
  3. 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  4. 循环等待条件:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源

如何解决死锁问题:

破坏死锁产生的四个必要条件中的一个或多个

  1. 破坏互斥:允许多个进程同时访问资源
  2. 破坏不剥夺:必须释放以保持的资源
  3. 破坏请求和保持:一次性分配所有资源
  4. 破坏循环等待:定义资源类型的线性顺序来预防

银行家算法是一个典型的解决死锁方案: 银行家算法

线程通信

wait():一旦执行此方法,当前线程进入阻塞状态,并释放锁

notify():一旦执行此方法,就会唤醒一个wait的线程, wait方法释放锁,notify方法不释放锁

notifyAll(): 会唤醒所有被wait的线程

wait(),notify(),notifyAll()只能在同步代码块或者同步方法中执行

以上三个方法都是Object中的方法

sleep和wait方法的不同之处:

(1)方法声明的位置不同,sleep是Thread类中的方法,wait是Object类中的方法

(2)wait只能使用在同步代码块或者同步方法中

(3)wait会释放锁,而sleep不会释放锁

生产者消费者问题

问题分析:系统中有一组生产者进程和一组消费者进程,

生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取走一个产品并使用。

代码实现:

//定义Resource作为线程需要的资源
public class Resource {
    //当前资源的数量
    int num=0;
    //当前资源的上限
    int size=10;
    //消费资源
    public synchronized void remove(){
        while (num==0){
            try {
                System.out.println("消费者等待");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        System.out.println("消费者线程为:"+Thread.currentThread().getName()+"资源数量"+num);
        notify();
    }
    //生产资源
    public synchronized void put(){
        while (num==size){
            try {
                System.out.println("生产者等待");
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num++;
        System.out.println("生产者线程为:"+Thread.currentThread().getName()+"资源数量"+num);
        notify();
    }

}

//定义Consumer使用remove消费资源
public class Consumer implements Runnable {
    private Resource resource;
    public Consumer(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true){
            resource.remove();
        }
    }
}
//定义Producer使用put生产资源
public class Producer implements Runnable {
    private Resource resource;
    public Producer(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true){
            resource.put();
        }
    }
}

线程安全的单例模式

单例设计模式

//有共享数据,有多个线程,会出现线程安全问题
class Bank{
    private Bank(){}

    private static volatile Bank instance=null;

    public static Bank getInstance() {
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

分析一下代码:

假设有两个线程同时到达 synchronized 语句块,那么实例化代码只会由其中先抢到锁的线程执行一次,而后抢到锁的线程会在第二个 if 判断中发现 instance 不为 null,所以跳过创建实例的语句。

再后面的其他线程再来调用 getInstance 方法时,只需判断第一次的 if (instance == null) ,然后会跳过整个 if 块,直接 return 实例化后的对象。

第一个if语句的作用:当已经有对象实例化时,线程不需要排队等待其他线程释放锁,效率更快

第二个if语句的作用:使单例模式不被破坏

在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 instance = new Bank() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

(1)给 instance 分配内存空间

(2)调用 Bank 的构造函数等,来初始化 instance

(3)将 instance 对象指向分配的内存空间(执行完这步 instance 就不是 null 了)

这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。

1-3-2时程序执行顺序:

image.png

在使用了 volatile 后,会一定程度禁止相关语句的重排序