Java 多线程入门

基本概念

  • 程序 :是指为完成特定任务 ,用某种编程语言编写的一组指令的集合 ,一段静态代码 。
  • 进程 :是指程序的一次执行过程 ,或正在执行的一个程序 。出生 -> 存在 -> 消亡 》生命周期
  • 线程 :程序内部的执行路径 。

例如 :360 木马查杀 、磁盘清理 、修复 。可以同时进行 支持多 线程 。

每个线程用于独立的 虚拟机栈 和 程序计数器 (pc)

一个 Java应用程序 java.exe 至少有三个线程 ,主线程 gc 垃圾回收线程,异常处理线程

并行与并发

  • 并行 :多个 CPU 同时执行多个任务 。例如 :多个人同时做不同的事。
  • 并发 :一个 CPU(采用时间片) 同时执行多个任务 。例如 :秒杀 ,多个人做同一件事 。

多线程优点等

1、提高应用程序的相应 。对图形化界面更有意义 ,可增强用户体验 。

2、提高计算机系统 CPU 的利用率

3、改善程序结构 ,将既长又复杂的进程分为多个线程 ,独立运行 ,利于理解和修改 。

何时需要多线程

  • 程序需要同时执行两个或多个任务
  • 程序需要实现一些需要等待的任务时,如用户输入 、文件读写 操作 、网络操作 、搜索等 。
  • 需要一些后台运行的程序时

创建多线程方式一

多线程的创建 ,方式一 :继承于Thread 类

1、创建一个继承于 Thread类 的子类

MyThread extends Thread

2、重写 Thread 类的 run() 方法 , 将此线程执行的操作声明在 run() 方法中 。

@Override
public void run() {

3、创建 Thread类的子类的对象 实例

MyThread thread = new MyThread();

4、通过此对象调用 start() 方法

​ 1、启动当前线程 ,2、调用当前线程的 run() 方法

thread.start();

线程的常用方法

方法名称 作用
void start() 启动线程 ,并执行对象的 run() 方法
run() 线程在被调度时执行的操作
String getName() 返回线程的名称
void setName(String name) 设置线程名称
static Thread currentThread() 返回当前线程 。
static void yield() 线程让步
暂停当前正在执行的线程 ,把执行机会让给优先级更高(或相同)的线程
join() 当某个程序执行流中调用其他线程的 join() 方式时,调用线程将被阻塞,直到 join() 方法加入的join线程执行完为止 。
在线程a中调用 线程b的join(),此时线程b进入阻塞状态 ,直到 线程b完全执行完以后 ,线程a才结束阻塞状态
static void sleep(long millis) 等待时间 (指定时间 :毫秒)令当前活动线程在指定时间段内放弃对 CPU 的控制 ,时间到后重新排队 。
boolean isAlive() 返回 boolean ,判断线程是否存活
stop() 强制线程生命周期结束 (不推荐使用 )

线程优先级的设置

  • 线程的优先级等级

    • MAX_PRIORITY = 10

    • MIN_PRIORITY = 1

    • NORM_PRIORITY = 5

  • 涉及的方法

    • getPriority() :返回线程优先值
    • setPriority(int newPriority) :改变线程的优先级
  • 注意事项 :

    • 线程创建时 ,继承父线程的优先级 。
    • 低优先级只是获得调度的概率低 ,并非一定 高优先级之后才被调用 。

创建多线程方式二

创建多线程的方式二 :实现 Runnable 接口

  • 1、创建一个实现了 Runnable 接口的类
  • 2、实现类去 重写 Runnable 中的抽象方法 :run()
  • 3、创建实现类的对象 (实例化)
  • 4、将此对象参数传递到 Thread 类的构造器中 ,创建 Thread 类的对象
  • 5、通过 Thread 类的对象调用 start()

两种方式的比较 :

1、实现Runnable 没有类的单继承的局限性

2、实现 Runnable 更适合来处理多个线程有共享数据的情况 。

public class Thread implements Runnable {		// Thread类  实际上也是 实现 Runnable 接口 

线程的生命周期

  • JDK 中 用 Thread.State 类定义了线程的几种状态 。

完整的生命周期 经历五种状态

1、新建状态(NEW):线程被创建后 ,就进入了新建状态 。 比如 : Thread t1 = new Thread();

2、就绪状态(Runnable):也被称为 “可执行状态” 。线程被创建后 ,其他线程调用 start() 方法来启动线程 。

​ 例如 : t1.start() 处于就绪状态的线程 ,随时可能被CPU调度执行 。

3、运行状态(Running):线程获取CPU权限进行执行任务 。但需要注意 :线程只能从就绪状态进入到运行状态 。

4 、阻塞状态(Blocked):阻塞状态是因为线程因为某种原因放弃CPU使用权 ,暂时停止运行 。直到线程进入就绪状态,才有机会转到运行状态 。阻塞的情况分为三种 :

  • 1、等待阻塞 :通过调用线程的 wait() 方法 ,让线程等待某工作的完成 。
  • 2、同步阻塞 :线程在获取 synchronized 同步锁失败(因为锁被其他线程所占用) ,它会进入到同步阻塞状态 。
  • 3、其他阻塞 :通过调用线程的 sleep()join() 或发出了 I/O请求时 ,线程会进入到阻塞状态 。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时 ,线程重新转入就绪状态 。

5、死亡状态(Dead):线程执行完了或者因异常退出了 run() 方法 ,该线程结束生命周期 。

img

理解线程的安全问题

线程的同步

方式一 :同步代码块

synchronized (同步监视器) {
    // 需要被同步的代码
}

说明

​ 1、操作共享数据的代码 ,即视为需要被同步的代码

​ 2、共享数据 :多个线程共同操作的变量 。例如 > ticket(车票) 就是共享数据

​ 3、同步监视器 ,俗称 锁 ,任何一个类的对象都可以充当锁 。

​ 要求 :多个线程必须共有同一把锁 。

补充 :

​ 1、实现 Runnable ,同步监视器 可以使用 this 充当锁 ,代表当前调用run方法的对象 。

​ 2、继承 Thread ,同步监视器 可以使用 当前类 充当 锁 ,类 也是对象 当前类.class

方式二 :同步方法

public synchronized void show() {
    // 需要被同步的代码
}

同步方法总结 :

​ 1、同步方法依然涉及到同步监视器 ,但是不需要我们显式的声明

​ 2、非静态的同步方法 ,同步监视器为 this

​ 静态的同步方法 ,同步监视器 为 当前类本身

实现 Runnable ,同步方法不需要使用 static 关键字

继承 Thread ,同步方法 需要使用 static 关键字

死锁的问题

Lock锁方式解决线程安全问题

JDK 5.0新增

  • Lock 锁 是接口 ,是控制多个线程对共享资源进行访问的工具 。

  • ReentrantLock 类实现了 Lock ,可以显式加锁、释放锁 。【常用】与 synchronized相同的并发性 。

  • 1、实例化 ReentrantLock

Lock lock = new ReentrantLock();
  • 2、调用锁的反法 lock()
try{
    lock.lock()
	// 需要被同步的代码
}
  • 3、调用解锁方法 :unlock()
finally {
    lock.unlock();
}

面试题 :

1、synchronized 与 lock 的异同 ?

​ · synchronized 自动释放锁 ,代码块锁 和方法锁

​ · lock 手动释放锁 ,只有代码块锁 ,

​ · 使用顺序:

​ Lock --》同步代码块 --》 同步方法 。

同步机制练习

class Account{
    private double balance;
    public Account(double balance) {
        this.balance = balance;
    }
    public synchronized void deposit(double amt) {
        if (amt > 0) {
            balance += amt;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "存钱成功:账户余额:" + balance);
        }
    }
}

class MyThreadRunnable implements Runnable{
    private Account account;
    public MyThreadRunnable(Account account) {
        this.account = account;
    }
    @Override
    public void run() {
             for (int i = 0; i < 3; i++) {
                 account.deposit(1000);
        }
    }
}
public class ThreadRunnableTest {
    public static void main(String[] args) {
        Account account = new Account(0);
        MyThreadRunnable runnable = new MyThreadRunnable(account);
        Thread thread01 = new Thread(runnable);
        Thread thread02 = new Thread(runnable);
        thread01.setName("甲");
        thread02.setName("乙");
        thread01.start();
        thread02.start();
    }
}

线程的通信

涉及线程通信的 方法 ,只能够在 同步代码块 或者 同步方法中 使用 。

方法名称 作用
wait() 一旦执行此方法,当前线程就进入阻塞状态 ,并释放同步监视器
notify() 一旦执行此方法 ,就会唤醒被 wait 的一个线程 。如果有多个线程被 wait ,就唤醒优先级高的那个线程
notifyAll() 一旦执行此方法 ,就会唤醒所有被 wait 的线程

上述方法必须使用在 同步代码块 或 同步方法中 。

上诉方法的调用者 必须是同步代码代码块或同步方法中的同步监视器,否则会出现异常 IllegalMonitorStateException

都定义在 Object 类 中 。

面试题 :sleep() 和 wait() 的异同 ?

1、声明位置不同 。Thread类中 声明 sleep() ,Object类中声明 wait()

2、调用要求不同 。sleep() 可以在任何需要出现的地方调用 。wait() 只能在同步代码块或者同步方法 中使用 。

3、是否释放同步锁 。如果都在 同步代码块或同步方法中执行 ,sleep 不会释放锁 ,wait 会释放锁

生产者消费者 例题

Resource.java 【工厂】

public class Resource {
    int c = 0;
    public synchronized void t1() {
        if (c < 20) {
            c++;
            System.out.println("开始生产第" + c + "个商品");
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
    }

    public synchronized void put() {
        if (c > 0) {
            System.out.println("开始消费第" + c + "个商品");
            c--;
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
    }
}

producerExample.java 【生产者】

public class producerExample implements Runnable{
    private Resource resource;

    public producerExample(Resource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        while (true) {
            resource.t1();
        }
    }
}

consumerExample.java 【消费者】

public class consumerExample implements Runnable{
    private Resource resource;
    public consumerExample(Resource resource) {
        this.resource = resource;
    }
    @Override
    public void run() {
        while (true) {
            resource.put();
        }
    }
}

Cashier.java 【超市 ,收银员】

public class Cashier {
    private int number;
    private String merchandise;

    public void setMerchandise(String merchandise) {
        this.merchandise = merchandise;
    }

    public String getMerchandise() {
        return merchandise;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

测试类

public class Test01 {
    public static void main(String[] args) {
        Resource resource = new Resource();
        producerExample producerExample = new producerExample(resource);
        consumerExample consumerExample = new consumerExample(resource);
        new Thread(producerExample).start();
        new Thread(consumerExample).start();
    }
}

创建多线程方式三 :实现 Callable

JDK 5.0 新增

  • 1、创建一个实现 Callable 的实现类
class MyCallableTest implements Callable {
  • 2、Callable 类的 call() 方法 ,将此线程需要执行的操作声明在 call() 中 。
@Override
public Object call() throws Exception {
  • 3、创建 Callable 接口实现类的对象
MyCallableTest myCallableTest = new MyCallableTest();
  • 4、创建 FutureTask 对象 ,将 Callable 接口的实现类对象作为参数传递到 FutureTask 构造器中
FutureTask task = new FutureTask(myCallableTest);
  • 5、创建 Thread对象 ,将 FutureTask 的对象作为参数传递到 Thread 类的构造器中 ,并调用 start() 。
new Thread(task).start();
  • 6、如果需要获取返回值 ,使用 FutureTask 对象 调用 get() 方法 。【可选 】
Object o = task.get();

如何理解 Callable 与 Runnable 的区别 ?

1、call() 可以有返回值

2、call() 可以抛出异常 ,可以被捕获异常 ,获取异常信息 。

3、Callable 支持泛型 。

使用线程池的好处

使用线程池

  • 背景 :经常创建和销毁 ,使用量特别大的资源 。比如并发情况下的线程 ,对性能影响很大 。

  • 思路 :提前创建号多个线程 ,放入线程池中 ,使用时直接获取 ,用完放回池中 。可以避免频繁地创建和销毁 ,实现重复利用 。

  • 好处 :

    • 提高响应速度 (减少创建线程的时间)
    • 降低资源消耗 (重复利用线程 )
    • 便于线程管理
      • CorePoolSize :核心池的大小
      • MaximumPoolSize :最大线程数
      • KeepAliveTim :线程没有任务时 最多保持多长时间后会终止 。
  • JDK 5.0 起提供了 线程池相关的API :ExecutorServiceExecutors

  • ExecutorService :线程池接口 ,常见子类 ThreadPoolExecutor

    •   <T> Future<T> submit(Callable<T> task);// 执行任务/命令	有返回值 。 适用于 实现Callable接口
      
    •   void execute(Runnable command);	// 执行任务 ,无返回值 适用于 实现Runnable接口
      
    •   void shutdown();		// 关闭连接池
      
  • Executors :工具类 、线程池的工厂类 。用于创建并返回不同类型的线程池 。

    •   Executors.newFixedThreadPool(int nThreads)	//创建一个可重用固定线程数的线程池 
      
    •   Executors.newCachedThreadPool()	// 创建一个可根据需要创建新线程的线程池
      
    •   Executors.newSingleThreadExecutor()		// 创建一个只有一个线程的线程池
      
    •   Executors.newScheduledThreadPool(int corePoolSize)	// 创建一个线程池 ,它可安排在给定延迟后运行命令或者定期地执行 
      

使用 :

  • 1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
  • 2、执行指定的线程的操作 ,需要提供实现 Runnable接口 或Callable 接口的实现类的对象
service.execute(myCallableTest); // 适合 实现Runnable接口
service.submit(myCallableTest);	// 适合 实现Callable接口
  • 关闭连接池
service.shutdown();

写一个线程安全的懒汉式 【使用同步机制 修改为线程安全 。】饿汉式

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

小结释放锁的操作

image-20201202191525624

小结不释放锁的操作

image-20201202191618014

入门完结,相信你对多线程有了一个概念

posted @ 2020-12-02 19:26  san只松鼠  阅读(85)  评论(0)    收藏  举报