JAVA多线程

一个程序最少需要一个进程,而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最小单位,而进程是系统进行资源分配和调度的一个独立单位。

进程、线程

  • 程序:开发写的代码称之为程序。程序就是一堆代码,一组数据和指令集,是一个静态的概念
  • 进程(Process):将程序运行起来,我们称之为进程。进程是执行程序的一次执行过程,它是动态的概念。进程存在生命周期,也就是说程序随着程序的终止而销毁。进程之间是通过TCP/IP端口实现交互的。
    • 进程就是正在运行中的程序(进程是驻留在内存中的)
    • 是系统执行资源分配和调度的独立单位
    • 每一进程都有属于自己的存储空间和系统资源
    • 注意:进程A和进程B的内存独立不共享。
  • 线程(Thread):线程是进程中的实际运作的单位,是进程的一条流水线,是程序的实际执行者,是最小的执行单位。通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程。线程是CPU调度和执行的最小单位。
    • 线程就是进程中的单个顺序控制流,也可以理解成是一条执行路径
    • 单线程:一个进程中包含一个顺序控制流(一条执行路径)
    • 多线程:一个进程中包含多个顺序控制流(多条执行路径)
    • 注意:很多多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

JAVA多线程_第1张图片

并发、并行、串行

  • 并发:同一个对象被多个线程同时操作。(这是一种假并行。即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉)。
  • 并行:多个任务同时进行。并行必须有多核才能实现,否则只能是并发。
  • 串行:一个程序处理完当前进程,按照顺序接着处理下一个进程,一个接着一个进行。

在java语言中:线程A和线程B,堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。

java中之所以有多线程机制,目的就是为了提高程序的处理效率。

对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是多个事情同时在做。

线程实现方式

1、继承Thread类

步骤:

  1. 自定义线程类继承Thread类
  2. 重写run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程(启动后不一定立即执行,抢到CPU资源才能执行)
// 自定义线程对象,继承Thread,重写run()方法
public class MyThread extends Thread {

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

    @Override
    public void run() {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
    }

    public static void main(String[] args) {
        // main线程,主线程

        // 创建线程实现类对象
        MyThread thread = new MyThread("线程1");
        MyThread thread2 = new MyThread("线程2");
        // 调用start()方法启动线程
        thread.start();
        thread2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程--" + i);
        }
    }
}

2、实现Runnable接口

步骤:

  • 自定义线程类实现Runnable接口
  • 实现run()方法,编写线程体
  • 创建线程对象,调用start()方法启动线程(启动后不一定立即执行,抢到CPU资源才能执行)

// 自定义线程对象,实现Runnable接口,重写run()方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
    }

    public static void main(String[] args) {
        // main线程,主线程

        // 创建实现类对象
        MyRunnable myRunnable = new MyRunnable();
        // 创建代理类对象
        Thread thread = new Thread(myRunnable,"线程1");
        Thread thread2 = new Thread(myRunnable,"线程2");
        // 调用start()方法启动线程
        thread.start();
        thread2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程--" + i);
        }
    }
}

3、实现Callable接口(不常用)

步骤:

  • 实现Callable接口,先要返回值类型
  • 重写call()方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务:ExecutorService ser = Executor.newFixedThreadPool(1);
  • 提交执行:Future res = ser.submit(t1);
  • 获取结果:boolean r1 = res.get();
  • 关闭服务:ser.shutdownNow();
import java.util.concurrent.*;

// 自定义线程对象,实现Callable接口,重写call()方法
public class MyThread implements Callable {

    @Override
    public Boolean call() throws Exception {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }

        return true;
    }

    public static void main(String[] args) throws ExecutionException,
        InterruptedException {
        // main线程,主线程

        // 创建线程实现类对象
        MyThread thread = new MyThread();
        MyThread thread2 = new MyThread();

        // 创建执行服务,参数是线程池线程数量
        ExecutorService ser = Executors.newFixedThreadPool(2);
        // 提交执行
        Future res = ser.submit(thread);
        Future res2 = ser.submit(thread2);
        // 获取结果
        boolean r1 = res.get();
        boolean r2 = res2.get();
        // 关闭服务
        ser.shutdownNow();
    }
}

线程的状态

多线程的在争夺资源时的状态,现在是单线程在执行过程中的状态

  • 新建状态(NEW):线程已创建,尚未调用start()方法启动之前。
  • 运行状态(RUNNABLE):线程对象被创建后,调用该对象的start()方法,并获取CPU权限进行执行。
  • 阻塞状态(BLOCKED):线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 等待状态(WAITING ):等待状态。正在等待另一个线程执行特定动作来唤醒该线程的状态。
  • 超时等待状态(TIME_WAITING):有明确结束时间的等待状态。
  • 终止状态(TERMINATED ):当线程结束完成之后就会变成此状态。

JAVA多线程_第2张图片

同步(解决并发问题)

synchronized锁

解决线程并发问题的方法是线程同步,线程同步就是让线程排队,就是操作共享资源要有先后顺序,一个线程操作完之后,另一个线程才能操作或者读取。

  • 好处:解决了线程同步的数据安全问题
  • 弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率

同步语句块

  • synchronized(this){方法体}  (synchronized括号后的数据必须是多线程共享的数据,才能达到多线程排队)
  • Obj称之为同步监视器,可以是任何对象,但是推荐使用共享资源作为同步监视器。
  • 不同方法中无需指定同步监视器,因为同步方法中的同步监视器就是this,就是这个对象本身,或者是class。

普通同步方法

  • 修饰符 synchronized 返回值类型 方法名(形参列表){方法体}
  • synchronized出现在实例方法上,一定锁的是this(此方法)。不能是其他的对象了。 所以这种方式不灵活。
  • 另外还有一个缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。

静态同步方法

  • 修饰符 synchronized static 返回值类型 方法名(形参列表){方法体}
  • synchronized 方法控制对(synchronized修饰的方法所在的对象,就是this)“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的锁才能执行,否则线程会阻塞,synchronized所在方法一旦执行,就独占该锁,直到方法执行完才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
  • 缺陷:若将一个大的方法声明为synchronized 将会影响效率。需要修改的内容才需要锁,锁的太多,浪费资源。

Lock(锁)

Lock 锁也称同步锁,java.util.concurrent.locks.Lock 机制提供了⽐ synchronized 代码块和 synchronized ⽅法更⼴泛的锁定操作,同步代码块 / 同步⽅法具有的功能 Lock 都有,除此之外更强⼤,更体现⾯向对象。

创建对象 Lock lock = new ReentrantLock() ,加锁与释放锁⽅法如下:

  • public void lock() :加同步锁
  • public void unlock() :释放同步锁

优先选择线程同步吗?

不是,synchronized会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。

其他方案

  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

synchronized和Lock的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭),synchronized是隐式锁,除了作用域就自动释放。
  • Lock只是代码块锁(执行体放在开启锁和关闭锁中间),synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
  • 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)。
  • 应用场景不同,不一定要在同一个方法中进行解锁,如果在当前的方法体内部没有满足解锁需求时,可以将lock引用传递到下一个方法中,当满足解锁需求时进行解锁操作,方法比较灵活。

线程协作

  • 关于Object类中的wait和notify方法。

  1. 第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。
    1. wait方法和notify方法不是通过线程对象调用,不是这样的:t.wait(),也不是这样的:t.notify()..不对。
  2. 第二:wait()方法作用:
    1. 表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
    2. o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。
  3. 第三:notify()方法作用:
    1. 表示:唤醒正在o对象上等待的线程。
    2. 还有一个notifyAll()方法:
    3. 这个方法是唤醒o对象上处于等待的所有线程。
    4. 注意:wait方法和notify方法需要建立在synchronized线程同步的基础之上。
  4. 重点:
    1. o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁;
    2. o.notify()方法只会通知,不会释放之前占有的o对象的锁。
Object o = new Object();
o.wait();
o.notify();

生产者和消费者模式

 生产者与消费者模式是并发、多线程编程中经典的设计模式,通过wait和notifyAll方法实现。

例如:生产满了,就不能继续生产了,必须让消费线程进行消费。

           消费完了,就不能继续消费了,必须让生产线程进行生产。

import java.util.ArrayList;
import java.util.List;
 

public class ThreadTest16 {
    public static void main(String[] args) {
        // 创建1个仓库对象,共享的。
        List list = new ArrayList();
        // 创建两个线程对象
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));
 
        t1.setName("生产者线程");
        t2.setName("消费者线程");
 
        t1.start();
        t2.start();
    }
}
 
// 生产线程
class Producer implements Runnable {
    // 仓库
    private List list;
 
    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直生产(使用死循环来模拟一直生产)
        while(true){
            // 给仓库对象list加锁。
            synchronized (list){
                if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
                    try {
                        // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}
 
// 消费线程
class Consumer implements Runnable {
    // 仓库
    private List list;
 
    public Consumer(List list) {
        this.list = list;
    }
 
    @Override
    public void run() {
        // 一直消费
        while(true){
            synchronized (list) {
                if(list.size() == 0){
                    try {
                        // 仓库已经空了。
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到此处说明仓库中有数据,进行消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒生产者生产。
                list.notifyAll();
            }
        }
    }
}

死锁

死锁形成的原因:多个线程各自占有一个资源,并且相互等待其他线程占有的资源才能运行,从而导致另个或者多个线程都在等待对方释放资源,都停止了执行。某一个同步代码块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

// 死锁例子:鱼和熊不可兼得
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Person personA = new Person(0, "猎人A");
        Person personB = new Person(1, "猎人B");
        personA.start();
        personB.start();
    }
}

// 熊掌
class Bear {}

// 鱼
class Fish {}

// 人
class Person extends Thread {
    // 保证资源只有一份
    public static Bear bear = new Bear();
    public static Fish fish = new Fish();

    int choose;
    String personName;

    public Person (int choose, String personName) {
        this.choose = choose;
        this.personName = personName;
    }

    @Override
    public void run() {
        // 捕猎
        try {
            this.hunting();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 捕猎方法
    private void hunting() throws InterruptedException {
        if (choose == 0) {
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
                Thread.sleep(1000);
                synchronized (fish) {
                    System.out.println(personName + "想捕捉鱼");
                }
            }
        } else {
            synchronized (fish) {
                System.out.println(personName + "想捕捉鱼");
                Thread.sleep(1000);
                synchronized (bear) {
                    System.out.println(personName + "想捕捉熊");
                }
            }
        }
    }
}

解决死锁的方法:同步代码块中不要相互嵌套,即,不要相互嵌套锁。

你可能感兴趣的:(JAVA基础,java)