Java08-多线程

NiuMT 2020-06-03 20:58:30
Java

基本概念

程序 (program) 是为完成特定任务、用某种语言编写的一组指令的集合 。即指 一段静态的代码,静态对象。

进程 (process) 是程序的一次执行过程,或是正在运行的一个程序。是 一个动态的过程 :有它自身的产生、存在和消亡的过程 。——生命周期

线程 (thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

并行与并发:

使用多线程的优点

背景:以单核 CPU 为例, 只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统 CPU 的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

何时需要多线程:

线程的创建和使用

java.lang.Thread

Thread 类的特性

创建方式一:继承Thread类

  1. 创建继承于Thread的子类
  2. 重写run()方法,将此线程执行的操作声明在run()中
  3. 创建子类的对象
  4. 通过此对象调用start()方法:启动线程,并自动调用run()方法

注意点:

  1. 如果 自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式。
  2. run 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU
    调度决定 。
  3. 想要启动多线程,必须调用 start 方法 。
  4. 一 个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出的异常“ “IllegalThreadStateException”。

线程的优先级、调度

线程的优先级等级:

调度策略:

Java 的调度方法:

创建方式二:实现 Runnable 接口

  1. 定义子类 ,实现 Runnable 接口。
  2. 子类中重写 Runnable 接口中的 run 方法。
  3. 通过 Thread 类含参构造器创建线程对象。
  4. 将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造器中 。
  5. 调用 Thread 类的 start 方法:开启线程调用,Runnable 子类接口的 run 方法。

比较两种创建方式

开发中,优先选择实现Runnable接口的方式;

两者联系:Thread也是 implements Runnable;都需要重写run方法

补充:线程的分类:

Java
中的线程分为两类:一种是 守护线程 ,一种是 用户线程 。

线程的生命周期


想实现 多 线程 必须在主线程中创建新的线程对象 。 Java 语言使用 Thread 类
及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

image-20201016163804776

线程的同步

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误 。

解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

方式一:同步代码块

synchronized(同步监视器){
    // 需要同步的代码,即操作共享数据的代码
}

方式二:同步方法

// synchronized 还可以放在方法声明中,表示整个方法为同步方法
public synchronized void show (String name){
    …  
}

implements Runnable的方式

class Window3 implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show(){ //同步监视器:this
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

继承Thread的方法

同步方式要求是==静态==的

class Window4 extends Thread {
    private static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    private static synchronized void show(){//同步监视器:Window4.class
        //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

同步的方式解决了线程的安全问题;但操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低.

同步方法仍然涉及同步监视器,只是不需要显式地声明;非静态的同步方法,同步监视器为this;静态的同步方法,同步监视器为 当前类本身 (类.class)

使用同步机制将单例模式中的懒汉式改写为线程安全的:

class Bank{
    private Bank(){}
    private static Bank instance = null;
    public static Bank getInstance(){
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if(instance == null){
            synchronized (Bank.class) {
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

死锁

//死锁的演示
class A {
    public synchronized void foo(B b) { //同步监视器:A类的对象:a
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了A实例的foo方法"); // ①
//        try {
//            Thread.sleep(200);
//        } catch (InterruptedException ex) {
//            ex.printStackTrace();
//        }
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用B实例的last方法"); // ③
        b.last();
    }

    public synchronized void last() {//同步监视器:A类的对象:a
        System.out.println("进入了A类的last方法内部");
    }
}

class B {
    public synchronized void bar(A a) {//同步监视器:b
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了B实例的bar方法"); // ②
//        try {
//            Thread.sleep(200);
//        } catch (InterruptedException ex) {
//            ex.printStackTrace();
//        }
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用A实例的last方法"); // ④
        a.last();
    }

    public synchronized void last() {//同步监视器:b
        System.out.println("进入了B类的last方法内部");
    }
}

public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主线程");
        // 调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    public void run() {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }
}
/*
当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 主线程 企图调用B实例的last方法
进入了B类的last方法内部
进入了主线程之后
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法
进入了A类的last方法内部
进入了副线程之后
*/

// 取消注释后
/*
当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法
当前线程名: 主线程 企图调用B实例的last方法   // 这里卡住了
*/

Lock (锁)

从 JDK 5.0 开始 Java 提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当 。

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,==线程开始访问共享资源之前应先获得 Lock 对象==。

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是 ReentrantLock 可以显式加锁、释放锁。对多个线程,lock应是唯一的。

class A { 
    private final ReentrantLock lock = new ReenTrantLock();
    // 对多个线程应是唯一的
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码
        }
        finally{
            lock.unlock();
        }
    }
}
// 注意:如果同步代码有异常,要将unlock() 写入 finally 语句块

synchronized 与 Lock 的对比

  1. Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized 是隐式锁,出了作用域自动释放
  2. Lock 只有代码块锁, synchronized 有代码块锁和方法锁
  3. 使用 Lock 锁, JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  4. 优先使用顺序:① Lock ②同步代码块(已经进入了方法体,分配了相应资源) ③同步方法(在方法体之外)

线程的通信

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                obj.notify();
                // notify()唤醒正在排队等待同步资源的线程中优先级最高者结束等待
                // notifyAll()唤醒正在排队等待资源的所有线程结束等待
                if(number <= 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        //自动释放锁
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}


public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

wait () 与 notify() 和 notifyAll()

sleep()和wait()方法异同

  1. 相同点:都可以使当前线程进入阻塞状态

  2. 不同:

    a. 两个方法声明的位置不同,Thread类中声明sleep()方法;Object类中声明wait()方法。

    b. 调用范围不同,sleep()可以在任何场景下调用;wait()必须使用在同步代码块或同步方法中

    c. sleep不会释放锁,wait释放锁

JDK5.0新增线程的创建方式

新增方式一:实现Callable 接口

  1. 创建一个实现Callable的实现类
  2. 重写call方法,将此线程需要执行的操作声明在call()中
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callable中call方法的返回值。get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大:

  1. call()可以有返回值
  2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
  3. Callable是支持泛型的

新增方式二:使用线程池

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

思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复 利用。类似生活中的公共交通工具。

好处:

JDK 5.0 起提供了线程池相关 API:ExecutorService 和 Executors

ExecutorService :真正的线程池接口。常见子类 ThreadPoolExecutor

Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池

class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable 返回值可以用FutureTask接收 
        //3.关闭连接池
        service.shutdown();
    }
}