javaSE- 多线程

1-进程和线程

1.1 什么是进程

  • 程序是静止的,运行中的程序就是进程,是系统的进行资源分配和调用的独立单位。
  • 每一个进程都有它自己的内存空间和系统资源。

1.1.1 进程的特征

1)动态性: 进程是运行中的程序,要动态的占用内存,CPU和网络等资源。

2)独立性 : 进程与进程之间是相关独立的,彼此有自己的独立内存区域。

3)并发和并行

并行: 某个时间段同时运行多个程序。

并发: 在某个时间点同时运行多个程序。

1.2 什么是线程

  • 线程是属于进程的。一个进程可以包含多个线程,这就是多线程。
  • 在同一个进程内可以执行多个任务,而这每一个任务就可以看成是一个线程。
  • 线程是程序的执行单位,执行路径是程序使用cpu的最基本单位。线程也支持并发性。

1.2.1 线程的作用

  • 可以提高程序的效率,可以有更多机会得到CPU。多线程可以解决很多业务模型。
  • 大型高并发技术的核心技术,设计到多线程的开发可能都比较难理解。

1.2.2 进程与线程的区别

  • 线程是在进程的基础上划分的。
  • 线程消失了,进程不会消失,进程如果消失了,则线程肯定消失。

2- 线程创建

2.1 继承Thread类

2.1.1Thread类基本概念

  • java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例。
  • Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。

定义一个线程类继承Thread类,然后重写run()方法,再创建线程对象,调用start()方法启动线程。

2.1.2 代码示例

package cn.guardwhy_01;
/**
方式一的步骤:
     a.定义一个线程类继承Thread类
     b.重写Thread类的run()方法
     c.创建一个子线程对象
     d.调用线程对象的start()方法启动线程(其实最终就是执行线程对象的run()方法)

线程的注意:
 1.启动线程不能直接调用run()方法,否则是普通对象的普通方法调用了,将失去线程特征。线程的启动必须调用start()
 2.一般是先创建子线程,再申明主线程的任务,否则主线程任务总是先执行完!

 优缺点:
 优点:编码简单。
 缺点:线程类已经继承了Thread类,不能再继承其他类,功能被削弱了。不能做线程池。无法直接返回线程执行的结果。
 */

//  a.定义一个线程类继承Thread类
class MyThread extends Thread{
    
    
    // 重写Thread类的run()方法
    @Override
    public void run() {
    
    
        for(int i=0; i < 10; i++){
    
    
            System.out.println("子线程执行:" + i);
        }
    }
}

public class ThreadDemo02 {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个子线程对象
        MyThread t = new MyThread();
        // 启动线程,线程的启动必须调用start()
        t.start();
        // 遍历操作
        for(int i=0; i<10; i++){
    
    
            System.out.println("main线程执行:" + i);
        }
    }
}

2.2 Runnable接口

2.2.1 Runnable基本概念

定义一个线程任务类实现Runnable接口,然后重写run()方法,创建线程任务对象,把线程任务对象包装成线程对象,调用start()方法启动线程。

2.2.2 代码示例(普通方式)

package cn.guardwhy_03;
/**
 方式二:
     a.定义一个线程任务类实现Runnable接口。重写run()方法
     b.创建一个线程任务对象
     c.把线程任务对象包装成一个线程对象
     -- public Thread(Runnable target)
     d.调用线程对象的start()方法启动线程。

 优缺点:
 缺点:编程相对复杂,不能直接返回线程的执行结果
 优点:
     1. 一个任务对象可以被反复包装成多个线程对象。
     2. 可以避免java中的单继承的局限性。因为线程任务对象只是实现了接口,还可以继续继承其他类和实现其他接口。
     3. 实现解耦操作,线程任务对象代码可以被多个线程共享,代码和线程独立。
     4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。适合做线程池。
 */

// 定义一个线程任务类实现Runnable接口。
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for(int i=0; i<10; i++){
    
    
            System.out.println(Thread.currentThread().getName() + "=>" + i);
        }
    }
}

public class ThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个线程任务对象
        Runnable target = new MyRunnable();
        // 将线程任务对象包装成线程对象
        Thread t1 = new Thread(target);
        // 启动线程
        t1.start();

        // 创建线程对象
        Thread t2 = new Thread(target);
        // 启动线程
        t2.start();

        for(int i=0; i<10; i++){
    
    
            System.out.println(Thread.currentThread().getName() + "=>"+i);
        }
    }
}

2.2.3 代码示例(匿名内部类)

package cn.guardwhy_03;
/**
匿名内部类简化写法!
*/
public class ThreadDemo02 {
    
    
   public static void main(String[] args) {
    
    
       // 直接创建Runnable的线程任务对象的匿名内部类形式
       /*
           Runnable target = new Runnable() {
               @Override
               public void run() {
                   for(int i=0; i<10; i++){
                       System.out.println(Thread.currentThread().getName() + "=>" + i);
                   }
               }
           };

           Thread t1 = new Thread(target);
           t1.start();
       */

       // 匿名形式..
       new Thread(new Runnable() {
    
    
           @Override
           public void run() {
    
    
               for(int i=0; i<10; i++){
    
    
                   System.out.println(Thread.currentThread().getName() + "=>"+i);
               }
           }
       }).start();

       // 遍历操作
       for(int i=0; i<10; i++){
    
    
           System.out.println(Thread.currentThread().getName()+"=>"+i);
       }
   }
}

注意

启动线程不能直接使用run()方法?

因为run()方法仅仅是封装线程执行代码,直接调用是普通方法。Start( )方法首先启动了线程,然后由JVM去调用该线程的run()方法。
JVM虚拟机的启动是多线程的,原因是垃圾回收线程要先启动,否则容易出现内存溢出。

2.2.4 Thread和Runnable区别

1) Thread类是Runnable接口的子类,但是Thread类中并且没有完全实现Runnable接口中的run( )方法。

2)如果一个类继承Thread类,不适合于多个线程共享资源,实现Runnable接口,就可以方便地实现资源共享。

2.3 Callable接口

2.3.1 基本概念

2.3.2 原理分析

2.3.3 代码示例(传统方式)

定义一个线程任务类实现Callable接口。

package cn.guardwhy_04;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 创建线程的方式三:
     a.定义一个线程任务类实现Callable接口。
     b.重写call()方法。
     c.把线程任务对象包装成一个未来任务对象。
     d.把未来任务对象包装成一个线程对象。
     e.调用线程对象的start()方法启动线程。
优缺点:
 缺点:编码复杂。
 优点:全是优点。
 可以继续继承其他类。可以做线程池。可以得到线程返回的结果。可以做资源共享操作。
*/

//a.定义一个线程任务类实现Callable接口,申明返回值类型
class MyCallable implements Callable<String>{
    
    
    @Override
    public String call() throws Exception {
    
    
        // 定义计数器
        int count = 0;
        for(int i=0; i<10; i++){
    
    
            // 1-10的和
            count += (i+1);
            System.out.println(Thread.currentThread().getName()+"=>"+i);
        }
        return Thread.currentThread().getName()+"求和结果:" + count;
    }
}

public class ThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 把线程任务对象包装成一个未来任务对象。
        MyCallable call = new MyCallable();
        /**
         * 未来任务对象: FutureTask
         *  1.可以通过未来任务对象去获取线程执行的结果。
         *  2.未来任务对象其实就是一个Runnable的对象。
         */
        FutureTask<String> target = new FutureTask<>(call);
        // 将未来任务对象包装成一个线程对象
        Thread t1 = new Thread(target);
        // 调用线程对象的start()方法启动线程
        t1.start();

        for(int i=0; i<10; i++){
    
    
            System.out.println(Thread.currentThread().getName() + "=>" + i);
        }

        try {
    
    
            // 线程的执行的结果
            String result = target.get();
            System.out.println("线程执行结果:" + result);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

    }
}

2.3.4 代码示例(jdk1.8)

Callable底层源码

package cn.guardwhy.List01;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableTest01 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        /*
        * 步骤:
        * 1. new Thread(new Runable()).start;
        * 2. new Thread(new FutureTask<V>()).start;
        * 3. new Thread(new FutureTask<V>(Callable)).start;
        */
        
        // 创建线程对象
        MyThread thread = new MyThread();
        // 适配类:未来任务对象 FutureTask
        FutureTask futureTask = new FutureTask(thread);
        // 线程操作
        new Thread(futureTask, "kobe").start();
        // 第二次调用执行,会有结果缓存,不用再次计算
        new Thread(futureTask, "curry").start();

        // 获取操作,get方法可能会产生阻塞,放到最后
        Integer result = (Integer) futureTask.get();
        System.out.println(result);
    }
}

class MyThread implements Callable<Integer>{
    
    

    @Override
    public Integer call() throws Exception {
    
    
        System.out.println("call方法被调用");
        // 耗时操作
        return 666;
    }
}

2.3.5 Callable与Runnable区别

  • 是否有返回值, 是否抛异常。
  • 方法不一样,一个是call,一个是run。

3- 线程状态

3.1 线程的生命周期

3.1.1 图示生命周期

3.1.2 线程的状态

1)新建状态 :使用new关键字创建之后进入的状态,此时线程并没有开始执行。

2)就绪状态:调用start方法后进入的状态,此时线程还是没有开始执行。

3) 运行状态:使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完毕后任务没有完成时回到就绪状态。

4) 阻塞状态:当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法。阻塞状态解除后进入就绪状态。

5)死亡状态:线程调用stop( )方法时或者run( )方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

3.1.3 生命周期路线

新建-就绪-运行-死亡
新建-就绪-运行-就绪-运行-死亡
新建-就绪-运行-就绪-运行-死亡
新建-就绪-运行-其他阻塞-就绪-运行-死亡
新建-就绪-运行-同步阻塞-就绪-运行-死亡
新建-就绪-运行-等待阻塞-同步阻塞-就绪-运行-死亡

3.2 线程操作方法

2.2.1 常用方法API

方法声明 功能介绍
Thread( ) 使用无参的方式构造对象
Thread(String name) 根据参数指定的名称来构造对象
Thread(Runnable target,String name) 根据参数指定引用和名称来构造对象。
void start( ) 用于启动线程,Java虚拟机会自动调用该线程的run方法
long getId( ) 获取调用对象所表示线程的编号。
Thread currentThread( ) 获取当前正在执行线程的引用。
int getPriority( ) 发挥线程的优先级
boolean isInterrupted( ) 判断目前线程是否被中断
void join( ) 等待线程死亡
String getName( ) 返回线程的名称
void yield( ) 将目前正在执行的线程暂停

3.2.2 取得线程名称

package cn.guardwhy_02;
/**
Thread多线程常用API:

 线程是有默认名字的:子线程的名称规则Thread_索引, main线程的默认名称就是main

 1.public void setName(String name):给线程对象取名字。
 2.public String getName():返回线程对象的名字。
 3.public static Thread currentThread(): 获取当前线程对象,这个代码是哪个线程在执行就返回哪个线程对象。
 */

// 定义一个线程类继承Thread类,线程类不是线程对象,是用来创建线程对象的。
class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        for(int i=0; i < 10; i++){
    
    
            System.out.println(Thread.currentThread().getName() + "=>" + i);
        }
    }
}

public class ThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个子线程对象
        MyThread t1 = new MyThread();
        // 设置线程
        t1.setName("1号线程");
        // 启动线程
        t1.start();
        // 输出t1线程对象
        // System.out.println(t1.getName());

        // 创建t2子线程对象
        MyThread t2 = new MyThread();
        t2.setName("2号线程..");
        // 启动线程
        t2.start();
        // 输出t2线程对象
        // System.out.println(t2.getName());

        // 返回当前线程对象,这个代码是哪个线程在执行就返回哪个线程对象。
        Thread main = Thread.currentThread();
        // System.out.println(main.getName());

        main.setName("最牛逼的线程");
        for (int i=0; i< 10; i++){
    
    
            System.out.println(main.getName()+ "=>" + i);
        }
    }
}

3.2.3 有参构造器

通过有参数构造器为线程对象取名字

package cn.guardwhy_02;
/**
 目标:通过有参数构造器为线程对象取名字(拓展)
 Thread父类的有参数构造器: public Thread(String name):
 */

// 定义一个线程类继承Thread类
class MyThread02 extends Thread{
    
    
    // 代参构造器
    public MyThread02(String name) {
    
    
        super(name);
    }

    // 重写Thread类的run()方法
    @Override
    public void run() {
    
    
       for(int i=0; i<10; i++){
    
    
           System.out.println(Thread.currentThread().getName() + "=>" + i);
       }
    }
}

public class ThreadDemo02 {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个子进程对象
        MyThread02 t1 = new MyThread02("1号线程");
        // 启动线程
        t1.start();

        MyThread02 t2 = new MyThread02("2号线程");
        // 启动线程
        t2.start();

        // 获取当前线程对象
        Thread main = Thread.currentThread();
        for(int i=0; i<10; i++){
    
    
            System.out.println(main.getName() + "=>"+i);
        }
    }
}

3.2.4 强制运行

1) 代码示例

package cn.thread.demo01;

class MyThread implements Runnable{
    
    

    @Override
    public void run() {
    
    
        // 覆写run()方法
        for(int i=0; i<10; i++){
    
    
            // 输出线程名称
            System.out.println(Thread.currentThread().getName() + "运行 -->" + i);
        }
    }
}

public class ThreadJoinDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 实例化对象
        MyThread mt = new MyThread();
        // 实例化Thread对象
        Thread t = new Thread(mt, "线程");
        // 线程启动
        t.start();
        // 循环10次
        for(int i=0; i<10; i++){
    
    
            // 判断变量内容
            if(i > 3){
    
    
                try {
    
    
                    // 线程t进行强制运行
                    t.join();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println("Main 线程运行 -->" + i);
        }
    }
}

2) 执行结果

3.2.5 线程休眠

package cn.guardwhy_10;
/**
 线程休眠
 Thread.sleep(5000):参数是毫秒,让当前所在线程对象休眠5s。
 */
public class ThreadDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 条件遍历
        for(int i=0; i<10; i++){
    
    
            System.out.println("输出:" + i);
            if(i == 5){
    
    
                try {
    
    
                    // 让当前线程休眠5s,休眠是不释放锁的。
                    Thread.sleep(5000);
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}

4- 线程同步机制

4.1 基本概念

  • 当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调。
  • 多个线程并发读写同一个临界资源时会发生线程并发安全问题。
  • 异步操作:多线程并发的操作,各自独立运行。同步操作:多线程串行的操作,先后执行的顺序。

4.2 线程安全问题

线程问题的核心原因:多个线程操作同一个共享资源的时候可能出现线程安全问题。

4.2.1代码示例

1)账户对象

package cn.guardwhy_05;

public class Account {
    
    
    // 卡号
    private String cardId;
    // 余额
    private double money;

    // 无参构造器
    public Account() {
    
    
    }
    // 代参构造器
    public Account(String cardId, double money) {
    
    
        this.cardId = cardId;
        this.money = money;
    }

    /*
        get/set方法
    */
    public String getCardId() {
    
    
        return cardId;
    }

    public void setCardId(String cardId) {
    
    
        this.cardId = cardId;
    }

    public double getMoney() {
    
    
        return money;
    }

    public void setMoney(double money) {
    
    
        this.money = money;
    }

    // 取钱地点方法
    public void drawMoney(double money){
    
    
        // 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
        String name = Thread.currentThread().getName();
        // 2.判断余额是否足够
        if(this.money >= money){
    
    
            // 钱够了
            System.out.println(name + "来取钱,余额足够,吐出:" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name + "取钱剩余:" + this.money);
        }else {
    
    
            // 钱不够
            System.out.println(name + "来取钱,余额不足..");
        }
    }
}

2) 取钱的线程类

package cn.guardwhy_05;
/**
 *  取钱的线程类
 */
public class DrawThread extends Thread{
    
    
    // 定义一个成员变量接收账户对象
    private Account acc;
    // 带参构造器
    public DrawThread(String name, Account acc) {
    
    
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
    
    
        // 去账户acc中取钱
        acc.drawMoney(10000);
    }
}

3)转账功能

package cn.guardwhy_05;
/**
 先模拟一个线程安全问题的案例:转账功能。

 分析:整存整取。
 (1)定义一个账户(余额,卡号)。
 (2)定义一个取钱的线程类
 (3)创建一个账户对象,创建2个线程对象,去这个账户对象取钱10000

总结:
 多个线程操作同一个共享资源的时候可能出现线程安全问题。
 */
public class ThreadSaveDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
        Account acc = new Account("ICBC-162", 10000);
        // 创建2个线程对象代表curry和james
        DrawThread curry = new DrawThread("curry", acc);
        // 启动线程
        curry.start();

        DrawThread james = new DrawThread("james", acc);
        // 启动线程
        james.start();
    }
}

4) 执行结果

4.2.2 解决方案

1)引发原因

  • 当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。
  • 线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。

2)解决方案

  • 让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。

4.3 线程同步

4.3.1 线程同步的作用

  • 用于解决线程安全问题,线程同步就是指线程安全了。线程同步解决线程安全问题的核心思想。

  • 线程同步就是让多个线程实现先后有序的访问共享资源,每次只能一个线程执行完毕,另一个线程才能进行。

4.3.2 同步代码块

1) 账户对象

package cn.guardwhy_06;

public class Account {
    
    
    // 卡号
    private String cardId;
    // 余额
    private double money;

    // 无参构造器
    public Account() {
    
    
    }
    // 代参构造器
    public Account(String cardId, double money) {
    
    
        this.cardId = cardId;
        this.money = money;
    }

    /*
        get/set方法
    */
    public String getCardId() {
    
    
        return cardId;
    }

    public void setCardId(String cardId) {
    
    
        this.cardId = cardId;
    }

    public double getMoney() {
    
    
        return money;
    }

    public void setMoney(double money) {
    
    
        this.money = money;
    }

    // 取钱地点方法
    public void drawMoney(double money){
    
    
        // 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
        String name = Thread.currentThread().getName();
        // 2.判断余额是否足够
        synchronized (this){
    
    
            if(this.money >= money){
    
    
                // 钱够了
                System.out.println(name + "来取钱,余额足够,吐出:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱剩余:" + this.money);
            }else {
    
    
                // 钱不够
                System.out.println(name + "来取钱,余额不足..");
            }
        }

    }
}

2) 取钱的线程类

package cn.guardwhy_06;

/**
 *  取钱的线程类
 */
public class DrawThread extends Thread{
    
    
    // 定义一个成员变量接收账户对象
    private Account acc;
    // 带参构造器
    public DrawThread(String name, Account acc) {
    
    
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
    
    
        // 去账户acc中取钱
        acc.drawMoney(10000);
    }
}

3)转账功能

package cn.guardwhy_06;
/**
同步代码块:
     思想:把“出现线程安全问题的核心代码”给锁起来,每次只能进入一个线程,
     其他线程必须在外面等这个线程执行完毕以后,才能进入执行,这样就线程安全了。
格式:
     synchronized(锁对象){
        出现线程安全问题的核心代码。
     }
锁对象:原则上可以是任意唯一的Java对象。
理论上:在实例方法中推荐用this作为锁.在静态方法中推荐用类名.class字节码文件作为锁对象
*/
public class ThreadSaveDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
        Account acc = new Account("ICBC-162", 10000);
        // 创建2个线程对象代表curry和james
        DrawThread curry = new DrawThread("curry", acc);
        // 启动线程
        curry.start();

        DrawThread james = new DrawThread("james", acc);
        // 启动线程
        james.start();
    }
}

4) 执行结果

4.3.3 同步方法

1) 账户对象

package cn.guardwhy_07;

public class Account {
    
    
    // 卡号
    private String cardId;
    // 余额
    private double money;

    // 无参构造器
    public Account() {
    
    
    }
    // 代参构造器
    public Account(String cardId, double money) {
    
    
        this.cardId = cardId;
        this.money = money;
    }

    /*
        get/set方法
    */
    public String getCardId() {
    
    
        return cardId;
    }

    public void setCardId(String cardId) {
    
    
        this.cardId = cardId;
    }

    public double getMoney() {
    
    
        return money;
    }

    public void setMoney(double money) {
    
    
        this.money = money;
    }

    // 取钱地点方法
    public synchronized void drawMoney(double money){
    
    
        // 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
        String name = Thread.currentThread().getName();
        // 2.判断余额是否足够
        if(this.money >= money){
    
    
            // 钱够了
            System.out.println(name + "来取钱,余额足够,吐出:" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name + "取钱剩余:" + this.money);
        }else {
    
    
            // 钱不够
            System.out.println(name + "来取钱,余额不足..");
        }
    }
}

2) 取钱的线程类

package cn.guardwhy_07;

public class DrawThread extends Thread{
    
    
    // 定义一个成员变量接收账户对象
    private Account acc;
    // 带参构造器
    public DrawThread(String name, Account acc) {
    
    
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
    
    
        // 去账户acc中取钱
        acc.drawMoney(10000);
    }
}

3) 转账功能

package cn.guardwhy_07;
/**
 同步方法:
 思想:把"出现线程安全问题的核心方法"给锁起来,每次只能进入一个线程,
 其他线程必须在外面等这个线程执行完毕以后,才能进入执行,这样就线程安全了。
 只需要在方法上加上一个 synchronized 关键字即可!

 原理:同步方法的原理与同步代码块的原理是一样的,只是同步方法是把整个方法体代码都锁起来,同步方法默认也是有锁对象的。
 如果同步的方法是实例方法,默认用this作为锁对象。如果同步的方法是静态方法,默认用类名.class作为锁对象。
 */
public class ThreadSaveDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
        Account acc = new Account("ICBC-162", 10000);
        // 创建2个线程对象代表curry和james
        DrawThread curry = new DrawThread("curry", acc);
        // 启动线程
        curry.start();

        DrawThread james = new DrawThread("james", acc);
        // 启动线程
        james.start();
    }
}

4) 执行结果

5- 死锁

5.1 产生死锁

5.1.1 基本概念

死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

基本结构

线程一执行的代码:
public void run(){
    
    
    synchronized(a){
    
     //持有对象锁a,等待对象锁b
        synchronized(b){
    
    
        编写锁定的代码;
        }
    }
}

线程二执行的代码:
public void run(){
    
    
    synchronized(b){
    
     //持有对象锁b,等待对象锁a
        synchronized(a){
    
    
        编写锁定的代码;
        }
    }
}
注意:在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!

5.1.2 死锁产生条件

互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

循环等待,即存在一个等待队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路。

5.1.3 代码示例

package cn.guardwhy_11;

/**
   实现死锁一般需要进行锁资源的嵌套才会出现死锁。
 */
public class ThreadDead {
    
    
    // 定义两个静态资源对象
    public static Object resources1 = new Object();
    public static Object resources2 = new Object();

    public static void main(String[] args) {
    
    
        // 实现死锁现象至少存在两个线程
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (resources1){
    
    
                    System.out.println("线程对象1对资源1上锁,占用资源1");
                    System.out.println("线程对象1开始请求资源2");
                    try {
    
    
                        // 线程休眠
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    synchronized (resources2){
    
    
                        System.out.println("线程对象1对资源2上锁,占用资源2");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (resources2){
    
    
                    System.out.println("线程对象2对资源2上锁,占用资源2");
                    System.out.println("线程对象2开始请求资源1");
                    try {
    
    
                        // 线程休眠
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    synchronized (resources1){
    
    
                        System.out.println("线程对象2对资源1上锁,占用资源1");
                    }
                }
            }
        }).start();
    }
}

5.1.4 执行结果

5.2 线程通信

5.2.1 Object类常用方法

方法声明 功能介绍
void wait( ) 用于使得线程进入等待状态,直到其它线程调用notify()或notifyAll()方法,此方法必须锁对象调用。
void wait(long timeout) 用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止。
void notify() 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用。
void notifyAll( ) 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用。

5.2.2 生产者及消费者

1)账户对象

package cn.guardwhy_09;
/**
 * 账户对象
 */
public class Account {
    
    
    // 卡号
    private String cardId;
    // 余额
    private int money;
    // 无参构造器
    public Account() {
    
    
    }
    // 代参构造器
    public Account(String cardId, int money) {
    
    
        this.cardId = cardId;
        this.money = money;
    }

    /**
     * get/set方法
     * @return
     */
    public String getCardId() {
    
    
        return cardId;
    }

    public void setCardId(String cardId) {
    
    
        this.cardId = cardId;
    }

    public double getMoney() {
    
    
        return money;
    }

    public void setMoney(int money) {
    
    
        this.money = money;
    }

    // 取钱方法
    public synchronized void drawMoney(int money){
    
    
        try {
    
    
            // 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
            String name = Thread.currentThread().getName();
            // 2.判断余额是否足够
            if(this.money >= money){
    
    
                // 更新余额
                this.money -= money;
                System.out.println(name + "来取钱,余额足够.吐出:" + money + "元,剩余" + this.money + "元");

                this.notify(); // 钱已经取完了,暂停自己唤醒其他线程!
                // 等待自己
                this.wait();
            }else {
    
    
                this.notify();  // 没钱了.唤醒其他线程!
                // 等待自己
                this.wait();
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    // 存钱方法
    public synchronized void saveMoney(int money){
    
    
        try {
    
    
            String name = Thread.currentThread().getName();
            // 判断是否有钱
            if(this.money == 0){
    
    
                // 没钱,需要存钱
                this.money += money;
                System.out.println(name + "来存钱" + money + "元成功, 剩余" + this.money + "元");

                this.notify(); // 钱已经取完了,暂停自己,唤醒其他线程
                this.wait(); // 等待自己
            }else {
    
    
                this.notify(); // 唤醒其他线程
                this.wait(); // 等待自己
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

2) 存钱的线程类

package cn.guardwhy_09;
/**
 存钱的线程类。
 */
public class SaveThread extends Thread{
    
    
    // 定义一个成员变量接收账户对象
    private Account acc;
    // 带参构造器
    public SaveThread(String name, Account acc) {
    
    
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
    
    
        // 3个爸爸来反复的存钱.
        while (true){
    
    
            try {
    
    
                // 线程休眠
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            acc.saveMoney(10000);
        }
    }
}

3) 取钱的线程类

package cn.guardwhy_09;
/**
 取钱的线程类。
 */
public class DrawThread extends Thread{
    
    
    // 定义一个成员变量接收账户对象
    private Account acc;
    // 带参构造器
    public DrawThread(String name, Account acc) {
    
    
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
    
    
        // curry和james来取钱
        while (true){
    
    
            // 线程休眠
            try {
    
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            // 去账户acc中取钱
            acc.drawMoney(10000);
        }
    }
}

4) 线程通信

package cn.guardwhy_09;
/**
 线程通信:多个线程因为在同一个进程中,所以互相通信比较容易。

 线程通信的经典模型:生产者与消费者问题。
 生产者负责生成商品,消费者负责消费商品。
 生成不能不剩,消费不能没有,是一个同步模型。

 线程通信必须先保证线程安全,否则代码会报出异常!!

模拟一个案例:
     curry和james有一个共同账户:共享资源
     他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。

     模型:curry和james去取钱,如果有钱就取出,等待自己,唤醒他们3个爸爸们来存钱
     他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。
     整存整取 10000元。
分析:
     生产者:亲爸,岳父,干爹
     消费者:curry,james
     共享资源:账户对象。
 */
public class ThreadCommunication01 {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个共享资源账户对象
        Account acc = new Account("ISBC-162530", 0);

        // 定义两个取钱线程代表curry和james
        new DrawThread("curry", acc).start();
        new DrawThread("james", acc).start();

        // 定义三个存钱线程,分别代表亲爸,岳父,干爹
        new SaveThread("亲爸", acc).start();
        new SaveThread("干爹", acc).start();
        new SaveThread("岳父", acc).start();
    }
}

5)执行结果

5.3 线程同步(Lock锁)

5.3.1 常用的方法

方法声明 功能介绍
ReentrantLock( ) 使用无参方式构造对象
void lock( ) 获取锁
void unlock( ) 释放锁

与synchronized方式的比较

  • Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动释放。
  • Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
  • 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好。

5.3.2 代码示例

1) 账户对象

package cn.guardwhy_08;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    
    
    // 卡号
    private String cardId;
    // 余额
    private double money;

    // 无参构造器
    public Account() {
    
    
    }
    // 代参构造器
    public Account(String cardId, double money) {
    
    
        this.cardId = cardId;
        this.money = money;
    }

    // 创建一把锁对象:必须保证这个对象唯一
    private final Lock lock = new ReentrantLock();

    /***
     * get/set方法
     * @return
     */
    public String getCardId() {
    
    
        return cardId;
    }

    public void setCardId(String cardId) {
    
    
        this.cardId = cardId;
    }

    public double getMoney() {
    
    
        return money;
    }

    public void setMoney(double money) {
    
    
        this.money = money;
    }

    // 取钱地点方法
    public void drawMoney(double money){
    
    
        // 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
        String name = Thread.currentThread().getName();

        // 2.判断余额是否足够,加锁操作
        lock.lock();
        try {
    
    
            if(this.money >= money){
    
    
                // 钱够了
                System.out.println(name + "来取钱,余额足够,吐出:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱剩余:" + this.money);
            }else {
    
    
                // 钱不够
                System.out.println(name + "来取钱,余额不足..");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();  // 解锁操作
        }
    }
}

2) 取钱的线程类

package cn.guardwhy_08;

/**
 *  取钱的线程类
 */
public class DrawThread extends Thread{
    
    
    // 定义一个成员变量接收账户对象
    private Account acc;
    // 带参构造器
    public DrawThread(String name, Account acc) {
    
    
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
    
    
        // 去账户acc中取钱
        acc.drawMoney(10000);
    }
}

3) 转账功能

package cn.guardwhy_08;
/**
 Lock锁
     java.util.concurrent.locks.Lock
     Lock锁也称同步锁,加锁与释放锁方法如下:
     - public void lock() :加同步锁。
     - public void unlock():释放同步锁。
 */
public class ThreadSaveDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
        Account acc = new Account("ICBC-162", 10000);
        // 创建2个线程对象代表curry和james
        DrawThread curry = new DrawThread("curry", acc);
        // 启动线程
        curry.start();

        DrawThread james = new DrawThread("james", acc);
        // 启动线程
        james.start();
    }
}

4) 执行结果

6- 线程池

6.1 线程池概述

6.1.1 什么是线程池

线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用。省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源。

6.1.2 线程池好处

1.降低资源消耗,减少了创建和销毁线程的次数。每个工作线程都可以被重复利用,可执行多个任务。

2.提高响应速度,线程池的核心思想:线程复用,同一个线程可以被重复使用。

3.提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机)

6.2 创建线程池

6.2.1 创建方式一

package cn.guardwhy_12;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
线程池在Java中的代表: ExecutorService

创建线程池的API:
     java.util.concurrent.Executors类下:
     -- public static ExecutorService newFixedThreadPool(int nThreads):
     返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
总结:
     线程池启动后是不会死亡的,因为后续还要重复使用的。
     void shutdown():会等全部线程执行完毕才关闭。比较友好!
     List<Runnable> shutdownNow():立即关闭,不管是否执行完毕
 */
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for(int i=0; i<3; i++){
    
    
            System.out.println("线程" + Thread.currentThread().getName()+" => " + i);
        }
    }
}

public class ThreadPoolsDemo01 {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个线程池:线程池固定放置三个线程
        ExecutorService pools = Executors.newFixedThreadPool(3);
        // 给线程池提交任务,提交任务的时候会自动创建线程对象
        Runnable target = new MyRunnable();
        // 提交任务会自动创建线程对象,并自动启动
        pools.submit(target);
        pools.submit(target);
        pools.submit(target);
        //  这里不会再创建线程了,因为线程池已经满了,这里会复用之前的线程。
        pools.submit(target);   

        // 全部线程执行完毕才关闭
        pools.shutdown();
    }
}

6.2.2 创建方式二

package cn.guardwhy_12;

import java.util.concurrent.*;

/**
线程池的创建方式二。
线程池在Java中的代表: ExecutorService

创建线程池的API:
     java.util.concurrent.Executors类下:
     -- public static ExecutorService newFixedThreadPool(int nThreads):
        返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
往线程池中创建线程的API:
     1.public Future<?> submit(Runnable task)
     2.<T> Future<T> submit(Callable<T> task)
总结: Callable接口创建线程对象是可以返回线程执行的结果的。
 */
class Mycallable implements Callable<String>{
    
    
    // 定义变量
    private int n;

    // 带参构造器
    public Mycallable(int n) {
    
    
        this.n = n;
    }

    @Override
    public String call() throws Exception {
    
    
        // 定义计数器
        int count = 0;
        for(int i=1; i<=n; i++){
    
    
            count += i;
        }
        return Thread.currentThread().getName()+ "=> 1-" + n + "和是: " + count;
    }
}

public class ThreadPoolsDemo02 {
    
    
    public static void main(String[] args) {
    
    
        // 1.创建一个线程池:线程池固定放置三个线程
        ExecutorService pools = Executors.newFixedThreadPool(3);
        // 2.提交任务给线程池
        Mycallable t1 = new Mycallable(10);
        Mycallable t2 = new Mycallable(20);
        Mycallable t3 = new Mycallable(30);
        Mycallable t4 = new Mycallable(40);

        Future<String> rs1 = pools.submit(t1);
        Future<String> rs2 = pools.submit(t2);
        Future<String> rs3 = pools.submit(t3);
        Future<String> rs4 = pools.submit(t4);

        try {
    
    
            System.out.println(rs1.get());
            System.out.println(rs2.get());
            System.out.println(rs3.get());
            System.out.println(rs4.get());
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

        pools.shutdown();   // 关闭线程池
    }
}

6.2.3 执行结果

猜你喜欢

转载自blog.csdn.net/hxy1625309592/article/details/113705821