Java高级编程: 多线程基础


一、基本概念:程序、进程、线程

1. 程序、进程和线程
  1. 程序(Programmer):是为了完成特定任务、使用某种语言编写一组指令的集合。即指一段静态的代码
  2. 进程(Progress):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程。它由“创建”而产生,由”调用“而执行,因得不到系统资源而阻塞,最后”撤销“而消亡
    • 进程是作为资源分配的单位,系统在为每个进程分配不同的内存空间
  3. 线程(Thread):线程是一个程序内部的一条执行路径。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),线程切换 的开销小
    • 若一个进程同一时间并行执行多个线程,就是支持多线程的
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从一堆中分配对象,可以访问相同的变量和对象,这就使得线程间的通信更简洁、高效。但多个线程操作共享的系统西元可能就会带来安全的隐患。

虚拟机中的虚拟机栈和程序计数器,每个线程各自拥有一份,每个线程都共享虚拟机中的方法区和堆
一个Java应用程序,其实至少由三个线程:main()主线程,GC()垃圾回收线程,异常处理栈

2. 并行和并发
  • 并行:多个CPU同时执行多个任务。
  • 并发:一个CPU(采用时间片)同一时间段执行多个任务。
3. 多线程的优点:
  1. 提高应用程序的响应,对于图形用户化界面,可增强用户体验
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构,将复杂的进程分为多个线程,独立运行,利于理解和改善
4. 线程的分类
  • Java中的线程分为两类,一种是守护线程,一种是用户线程
    1. 它们几乎所有方面都是相同的,唯一的区别是判断JVM何时离开
    2. 守护线程是服务于用户线程的,通过在start()方法前调用 thread.setDaemon(true)可以把一个用户线程变成一个守护线程
    3. java垃圾回收就是一个典型的守护线程,当所有用户线程结束后,java垃圾回收线程也就结束
    4. 若JVM中都是守护线程,当前JVM将退出

狡兔死,走狗烹,飞鸟尽,良弓藏


二、线程的创建和使用

  • Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来提现
1. 线程的创建
[1] 方式一:继承于Thread类
  1. 创建一个继承于Thread类的子类
  2. 重写Thead类的run()方法:将此线程执行的操作写在run方法中
  3. 创建Thread类的对象
  4. 通过此对象调用start()方法:启动当前线程;调用当前线程的run()方法
/**
 * @Description: 测试继承Thread类实现多线程的方式:
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-16:53
 */
public class MyThreadTest extends Thread {
    @Override
    public void run() {
     	// 打印100以内的偶数
        for (int i = 0; i < 100; i+=2) {
            System.out.println(Thread.currentThread().getName()+": "+i);
        }
    }

    public static void main(String[] args) {
        MyThreadTest myThreadTest = new MyThreadTest();
        myThreadTest.start();
    }
}

  • 创建Thread类的匿名子类:创建后直接调用start启动
   new Thread(){
   			// 打印100以内的偶数
            @Override
            public void run() {
                for (int i = 0; i < 100; i += 2) {
                    System.out.println(i);
                }
            }
        }.start();
[2] 方式二:实现Runnable接口
  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()
  • 知识点
    1. 该测试中实现类MyRunnableTest的run()方法中的Thread.currentThread()是获得当前线程的实例,返回的是该实现类对象作为参数传递给Thread类的构造器所创建的线程对象的实例。例如:在该测试中,返回的是线程对象t1的实例
    2. start()的作用是启动当前线程,并调用当前线程对象的run()方法。背后原理:在源码中,Thread类由一个Runnable类型的成员变量,在Thread类的run()方法中,若该成员变量不为空,则调用该成员变量的run()方法。因此在将Runnable的实现类作为参数传递给Thread类的构造器后,thread类调用的run()方法即为Runnable实现类的run()方法
/**
 * @Description: 测试继承Runnable接口实现多线程
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-15:59
 */
public class MyRunnableTest 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) {
        MyRunnableTest runnableTest = new MyRunnableTest();
        Thread t1 = new Thread(runnableTest);
        t1.start(); // 启动t1线程,并调用t1线程的run()方法
    }
}
  • Thread类的部分源码:
class Thread implements Runnable {
    // 其他的成员变量........
    private Runnable target;
    // 其他的成员变量........

    // 其他的构造器........
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    // 其他的构造器........

    // 其他的方法........
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    // 其他的方法........
}
[3] 两种实现方式的比较:
  • 比较:
    • 由于Java单继承的存在,如果继承了Thread类之后,就不能在继承其他的类。这种情况下,接口的实现方式较好。通过实现Runnable接口,既可以实现多线程,也可以继承相关的体系结构。
    • 如果存在资源共享的情况,对于一个Thread实现类来说,如果使用成员变量来存储共享数据,则需要使用static来修饰该成员变量。而通过实现Runnable则自然可以实现共享数据的效果
    • 因此:开发中:应优先选择实现Runnable接口的方式
  • 联系:
    • 两种方式都需要实现run()方法,将线程要实现的逻辑写在run()方法中
    • Thread类实现了Runnable接口,并重写了Runnable中的run()方法
[4] 注意:
  1. 如果直接调用run()方法,JVM不会启动线程,相当于直接调用一个普通的方法,所有的执行都在主线程main()中
  2. start方法对于一个线程对象只能执行一次,不可以让已经start的线程再启动一次,会报一个IllegalThreadStateException异常

调用start()方法

/**
 * @Description: 多线程的测试
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-11:51
 */
public class Main {
    public static void main(String[] args) {
        Hero galen = new Hero("盖伦",75,655);
        Hero miss_Fortune = new Hero("赏金猎人",69,580);
        KillHero thread1 = new KillHero(galen,miss_Fortune);
      	// 两个示例的不同点:
        thread1.start();

        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("主线程结束");
    }
}

效果:

  • 主线程和线程1一起执行
盖伦正在攻击赏金猎人
赏金猎人的血变成了:505

1
2
盖伦正在攻击赏金猎人
赏金猎人的血变成了:430

3
4
盖伦正在攻击赏金猎人
赏金猎人的血变成了:355

5
盖伦正在攻击赏金猎人
赏金猎人的血变成了:280

6
7
盖伦正在攻击赏金猎人
赏金猎人的血变成了:205

8
9
盖伦正在攻击赏金猎人
赏金猎人的血变成了:130

主线程结束
盖伦正在攻击赏金猎人
赏金猎人的血变成了:55

盖伦正在攻击赏金猎人
赏金猎人的血变成了:-20

直接调用run()方法

/**
 * @Description: 多线程的测试
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-11:51
 */
public class Main {
    public static void main(String[] args) {
        Hero galen = new Hero("盖伦",75,655);
        Hero miss_Fortune = new Hero("赏金猎人",69,580);
        KillHero thread1 = new KillHero(galen,miss_Fortune);
        // 两个示例的不同点:
        thread1.run();

        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("主线程结束");
    }
}

效果:

  • 无论运行多少次,main()方法中的输出,都是在run()方法运行结束后再执行。这表示直接调用run方法,相当于执行了一个普通方法,运行方式也是单线程,并没有实现多线程
盖伦正在攻击赏金猎人
赏金猎人的血变成了:505

盖伦正在攻击赏金猎人
赏金猎人的血变成了:430

盖伦正在攻击赏金猎人
赏金猎人的血变成了:355

盖伦正在攻击赏金猎人
赏金猎人的血变成了:280

盖伦正在攻击赏金猎人
赏金猎人的血变成了:205

盖伦正在攻击赏金猎人
赏金猎人的血变成了:130

盖伦正在攻击赏金猎人
赏金猎人的血变成了:55

盖伦正在攻击赏金猎人
赏金猎人的血变成了:-20

0
1
2
3
4
5
6
7
8
9
主线程结束
Process finished with exit code 0
[5] 方式三:实现Callable接口

*[实现Callable接口的方式]

[6] 方式四:创建线程池

*[使用线程池的方式实现多线程]

2.线程常用方法
  1. start():启动当前线程,调用当前线程的run方法
  2. run();通常需要重写Thread类中的方法,将创建的线程要执行的操作写在此方法中
  3. currentThread():静态方法:返回执行当前代码的线程(可以用来获取主线程)
  4. getName():获取当前线程的名字
  5. setName():设置当前线程的名字
  6. yield():释放当前CPU的执行权
  7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
  8. stop():已过时。当执行此方法时,强制结束当前线程
  9. sleep(long millitime):让当前线程"睡眠"指定的millitime毫秒,在指定的millitime毫秒时间内,当前线程是阻塞状态
  10. isAlive():判断当前线程是否存活

方法测试

  1. 测试currentThread()
  2. 测试getName()
  3. 测试setName()
/**
 * @Description: 测试继承Thread类实现多线程的方式:
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-16:53
 */
public class MyThreadTest extends Thread {
    @Override
    public void run() {
        System.out.println("这里是"+getName()+"的run方法");
    }
    /**
     * Description:主方法:测试多种线程的常用方法
     * @param args
     */
    public static void main(String[] args) {
        MyThreadTest myThreadTest = new MyThreadTest();
        // setName()方法测试
        myThreadTest.setName("线程1"); 
        myThreadTest.start();
        // getName()方法测试
        System.out.println("myThreadTest的线程名:"+myThreadTest.getName());
        System.out.println("主线程的线程名:"+Thread.currentThread().getName()); // currentThread()方法测试
    }
}
  1. 测试sleep()方法
  2. 测试join()方法
  • 没有添加join方法的代码:
/**
 * @Description: 方法测试 sleep(),join()
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-17:05
 */
public class Sleep_JoinMethodTest implements Runnable{
    @Override
//    public void run()  throws InterruptedException{ // error: 因为Runnable接口的run方法没有抛出异常,则子类也不能抛出异常(重写方法异常抛出的规范)
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次执行run()");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Runnable testMethod = new Sleep_JoinMethodTest();
        Thread thread1 = new Thread(testMethod);
        thread1.setName("线程1");
        thread1.start();
        // 让thread1调用join方法后,thread1执行完才开始执行主方法
        Thread.currentThread().setName("主线程");
        try {
//            thread1.join();
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "第" + (i + 1) + "次执行run()");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  • 没有添加join方法的实现效果:
主线程第1次执行run()
线程1第1次执行run()
线程1第2次执行run()
主线程第2次执行run()
线程1第3次执行run()
线程1第4次执行run()
主线程第3次执行run()
  • 添加join方法之后的代码:
package ThreadMethodTest;

/**
 * @Description: 方法测试 sleep(),join()
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-17:05
 */
public class Sleep_JoinMethodTest implements Runnable{
    @Override
//    public void run()  throws InterruptedException{ // error: 因为Runnable接口的run方法没有抛出异常,则子类也不能抛出异常(重写方法异常抛出的规范)
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println(Thread.currentThread().getName()+"第"+(i+1)+"次执行run()");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Runnable testMethod = new Sleep_JoinMethodTest();
        Thread thread1 = new Thread(testMethod);
        thread1.setName("线程1");
        thread1.start();
//         让thread1调用join方法后,thread1执行完才开始执行主方法
        Thread.currentThread().setName("主线程");
        try {
            thread1.join();
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "第" + (i + 1) + "次执行run()");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
  • 添加join方法之后的实现效果:
线程1第1次执行run()
线程1第2次执行run()
线程1第3次执行run()
线程1第4次执行run()
线程1第5次执行run()
线程1第6次执行run()
线程1第7次执行run()
线程1第8次执行run()
线程1第9次执行run()
线程1第10次执行run()
线程1第11次执行run()
线程1第12次执行run()
线程1第13次执行run()
线程1第14次执行run()
线程1第15次执行run()
主线程第1次执行run()
主线程第2次执行run()
主线程第3次执行run()
主线程第4次执行run()
  1. 测试yield()方法

3. 线程调度
  • 调度策略:
    1. 时间片:将CPU运行时间划分成多个时间片,按时间片将CPU资源分配给不同的线程
    2. 抢占式:高优先级的线程抢占CPU
  • java的调度方法:
    • 同优先级线程组成先进先出队列,使用时间片策略
    • 对高优先级,使用优先级调度的抢占式策略

线程的优先级等级:

  1. MAX_PRIOPITY:10 最大优先级
  2. MIN_PRIOPITY:1 最小优先级
  3. NORM_PRIOPITY:5 默认优先级
  • 高优先级的线程要抢占低优先级的线程CPU的执行权。但只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才被执行

涉及的方法

  1. getPriority():获取线程的优先级
  2. setPriority():设置线程的优先级

三、线程的生命周期

  1. 创建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新创建的状态
  2. 就绪:当处于创建状态的线程被start()后或已经使用完一个CPU时间片,但运行并没有结束,将进如线程队列等待CPU时间片,此时他已经具备了运行的条件(此状态下的线程已获得除CPU以外的所有的系统资源)
  3. 运行:当就绪态的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
  4. 阻塞:在某种特殊情况下,被人为挂起和执行输入输出操作时,让出CPU并临时中断执行,进入阻塞状态
  5. 死亡:线程完成了他的全部工作或线程被提前强制中止或出现异常导致结束
    在这里插入图片描述

四、线程的同步

1. 线程安全问题
  • 多个线程执行顺序的不确定性会引起执行结果的不同
  • 多个线程对数据的共享,会造成操作的不完整性,会破坏数据

安全问题代码测试:

对共享资源total(初始值为10000)分别进行一万次 +1/-1操作

package ThreadSafe;

/**
 * @Description: 展示线程的安全性问题
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-19:42
 */
public class ThreadSafeTest extends Thread{
    private SharingData data ;
    private boolean flag ;
    public ThreadSafeTest(SharingData data,boolean flag){
        this.data = data;
        this.flag = flag;
    }
    @Override
    public void run() {
        try {
            if(flag){

                    this.data.addOne();
            }
            else{
                this.data.subtractOne();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        SharingData sharingData = new SharingData();
        ThreadSafeTest thread1 = new ThreadSafeTest(sharingData,true);
        ThreadSafeTest thread2 = new ThreadSafeTest(sharingData,false);
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sharingData.total = " + sharingData.getTotal());
    }
}

/**
 * @Description: 定义一个类,该类由一个成员变量表示共享资源。该类有两个方法分别对这个数据进行一万次+1/-1操作
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-19:42
 */
class SharingData {
    private int total = 10000;
    public void addOne() throws InterruptedException {
        for(int i = 0 ; i < 10000; i++){
            this.total += 1 ;
        }
    }
    public void subtractOne() throws InterruptedException {
        for (int i = 0; i < 10000; i++) {
            this.total -= 1 ;
        }
    }
    public int getTotal() {
        return total;
    }
}

运行结果:

  • 每次运行的结果都不相同,这是由线程执行顺序的不确定性造成的
sharingData.total = 10884
  • 出现问题的原因:当一个线程对共享资源进行操作,但尚未操作完成时,又有其他的线程也参与进来操作共享资源。
  • 解决方案:当一个线程操作共享资源时,其他线程不能参与进来。直到该线程操作完毕,其他线程才可以操作共享资源,即使该进程阻塞了。
  • 在Java中使用同步机制来解决线程的安全问题
2. 解决方法
[1] 方式一:同步代码块
sychronized(同步监视器){
	// 需要被同步的代码
	// 说明:此操作共享资源的代码,即为需要被同步的代码 --->不能包含多余的代码,也不能包含的代码不够
}
  • 同步监视器:俗称锁;任何一个类的对象都可以充当这个锁。多个线程必须要使用同一把锁

共享资源存在于线程类中:

1. 对实现Runnable接口方式实现同步

  • 因为实现Runnable接口的方式可以天然的共享资源,因此可以将共享的资源作为Runnable实现类的成员变量
  • 同样,可以创建一个对象作为成员变量,当作一把锁。或者使用this当前对象作为锁。

2. 对继承Thread类方式实现同步

  • 继承Thread类的方式的类所创建的对象不具有共享资源的性质,因此可以使用static修饰作为成员变量的数据和锁
  • 因为继承Thread类的线程类创建后互相独立,因此不能使用this作为锁。但是可以使用 MyClassName.class 将子类作为一个锁,即将类作为一个对象看待
  • 继承Thread类使用当前类充当同步监视器的代码
package ThreadSafe;

/**
 * @Description: 对共享资源存在于线程类中,使用多个线程对共享资源进行相减操作直到为0
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-22:17
 */
package ThreadSafe;

/**
 * @Description: 对共享资源存在于线程类中,使用多个线程对共享资源进行相减操作直到为0
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-22:17
 */
public class MyCountThread extends Thread{
    private static int count = 100 ;
    @Override
    public void run() {
        while(true){
            synchronized(MyCountThread.class){
                if(count>0){
                	System.out.println(Thread.currentThread().getName() + ": " + count--);
                }
                else
                    break;
            }
            // 释放资源后睡眠0.1s,便于观察线程对CPU执行权的"掌握和释放"
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        MyCountThread thread1 = new MyCountThread();
        MyCountThread thread2 = new MyCountThread();
        MyCountThread thread3 = new MyCountThread();
        thread1.setName("线程1");
        thread2.setName("线程2");
        thread3.setName("线程3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:线程安全

线程1:100
线程3:99
线程2:98
线程1:97
线程2:96
线程3:95
线程1:94
// 中间结果省略
线程1:7
线程2:6
线程3:5
线程3:4
线程2:3
线程1:2
线程3:1

  • 小问题:如何将递减操作单独封装为一个方法,在该方法的共享资源区添加同步监视器,在run方法设置一个循环并进行调用该递减方法,从而实现同步,且要求输出的共享资源大于0
    *[解决方法]
    问题模板:
    @Override
    public void run() {
        while (count>0){
            subtract();
        }
    }
    public  void subtract(){
        synchronized(MyCountThread.class){
            System.out.println(Thread.currentThread().getName() + ": " + count--);
        }
    }

该方式输出错误,原因:当count 等于1时,线程3获得共享资源,此时线程1和线程2进行了对count进行了判断并进入了循环内等待锁的释放从而获取共享资源。线程3对共享资源进行了-1后释放锁。线程1和线程2一次获取锁,并进行了-1操作(线程1和线程2已经进入循环内)
(可在共享资源释放后让当前线程睡眠一会规避这个问题,但没有真正解决)

线程3: 4
线程3: 3
线程3: 2
线程3: 1
线程2: 0
线程1: -1
  • *[ 解决方法 ]:
    • 不光在循环头部判断count是否大于0(方便为了跳出循环结束线程的运行),并且在方法体内判断是否大于0,不成立则不执行
public synchronized void subtract(){
        if(count>0)
            System.out.println(Thread.currentThread().getName() + ": " + count--);
    }

共享资源封装为一个类:

  • 在该类中对操作共享资源的代码区添加锁,因为锁可以是任意的一个对象,因此可将给类实例化的对象作为锁。从而达到不用static修饰实现共享资源和使用用一把锁
  • 对封装成类的共享资源实现同步的测试代码
// 对操作共享资源的代码添加一个锁,锁可以是任意类的对象,则该测试例中的锁为存储共享资源的类的当前对象
 public void addOne() throws InterruptedException {
        for (int i = 0; i < 10000; i++) {
            synchronized (this) {
                this.total += 1;
            }
        }
    }
    public void subtractOne() throws InterruptedException {
        for (int i = 0; i < 10000; i++) {
            synchronized (this) {
                this.total -= 1;
            }
        }
    }

添加同步方法块后的运行结果:

sharingData.total = 10000

[2] 方式二:同步方法
  • 如果操作共享数据的代码声明在一个方法中,就可以将该方法声明为同步的
  • 对共享资源作为Runnable实现类的成员变量,使用同步方法实现同步的测试代码:
/**
 * @Description: 对共享资源作为Runnable实现类的成员变量,使用同步方法实现同步的测试代码
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-22:40
 */
public class MyCountRunnableSubOfSynchronizedMethod implements Runnable {
    private static int count = 100;

    @Override
    public void run() {
        while (count>0) {
            try {
                subtract();
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
	// 没有显式的声明同步监视器,实际上同步监视器为当前对象this 
    public synchronized void subtract(){ 
        if(count>0)
            System.out.println(Thread.currentThread().getName() + ": " + count--);
    }
}
  • 对共享资源作为Thread子类的成员变量,使用同步方法实现同步的测试代码:
/**
 * @Description: 对共享资源作为Thread子类的成员变量,使用同步方法实现同步的测试
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-23:16
 */
public class MyCountThreadOfSynchronizedMehtod extends Thread{
    public static int count = 100 ;
    @Override
    public void run() {
        while(count>0){
            this.subtract();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void subtract(){
        if(count>0)
            System.out.println(Thread.currentThread().getName() + ": " + count--);
    }
}

  • 共享资源单独封装为一个类,使用同步方法实现同步的测试代码:
/**
 * @Description: 共享资源单独封装为一个类,使用同步方法实现同步的测试
 * @Author :QianZhiSanyao
 * @Date: 2021-05-23-23:20
 */
class SharingSource{
    private int count = 100 ;
    public synchronized void subtract(){
        if(this.count > 0)
            this.count-- ;
    }
    public int getCount() {
        return count;
    }
}
public class MyThreadRun extends Thread{
    private SharingSource sharingSource ;
    public MyThreadRun(SharingSource sharingSource){
        this.sharingSource = sharingSource;
    }
    @Override
    public void run() {
        while(sharingSource.getCount() >0 ){
            sharingSource.subtract();
            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 对使用同步方法实现同步的总结:
  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
  2. 非静态的同步方法,同步监视器:this
    静态的同步方法,同步监视器:当前类本身
[3] 方式三:Lock锁
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReentrantLock,可以显示加锁、释放锁
public class ReentrantLockTest {
    public static void main(String[] args) {
        countSubtract subRunnable = new countSubtract();
        Thread t1 = new Thread(subRunnable);
        Thread t2 = new Thread(subRunnable);
        Thread t3 = new Thread(subRunnable);
        t1.start();
        t2.start();
        t3.start();
    }
}

class countSubtract implements Runnable {
    private int count = 100;
    private ReentrantLock lock = new ReentrantLock(true); // 实例化ReentrantLock

    @Override
    public void run() {
        while (this.count > 0) {
        	// lock()与unlock()之间的代码则被同步
            try {
                lock.lock(); // 调用lock方法加锁
                if (this.count <= 0)
                    break;
                System.out.println(Thread.currentThread().getName() + ":  " + count);
                this.count--;
            } finally {
                lock.unlock(); // 调用unlock方法解锁
            }
        }
    }
}
ReentrantLock锁和关键字synchronized的区别:
  • synchronized执行完相应的同步代码后自动释放锁,而ReentrantLock需要手动调用lock()和unlock()进行加锁和解锁
  • synchronized是一个java关键字,而ReentrantLock是一个对象
[4] 使用同步的优劣
  1. 同步的方式,解决了线程的安全性问题
  2. 同步操作代码时,只能由一个线程参与,其他线程等待,相当于是一个单线程的过程,效率较低
3. 使用同步更改单例模式中的懒汉式使其线程安全
  • 将懒汉式改写为线程安全的。
4. 同步机制造成的死锁问题
  • 死锁: 不同的线程分别占有对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 引起死锁的原因:
    - 竞争不可抢占性资源
    - 竞争可消耗资源
    - 线程推进顺序不合理
  • 产生死锁的必要条件:
  • 死锁的解决方法:
    • 专门的算法:原则
    • 尽量减少同步资源的定义
    • 尽量避免嵌套同步

五、线程的通信

1. wait()、notify()、notifyAll()
  • 线程间使用wait()、notify()、notifyAll()进行线程间的阻塞和唤醒
  1. wait():执行此方法,当前线程进入阻塞状态,并释放同步监视器
  2. notify():执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个
  3. notifyAll():执行此方法,会唤醒所有被wait的线程
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description: 线程间通信的测试: 两个进程交替打印100以内的数
 * @Author :QianZhiSanyao
 * @Date: 2021-05-24-22:15
 */
public class ThreadCommunication {
    public static void main(String[] args) {
//        ReentrantLock lock = new ReentrantLock();
        MySubRunnable subRunnable = new MySubRunnable();
        Thread t1 = new Thread(subRunnable);
        Thread t2 = new Thread(subRunnable);
        t1.setName("王也");
        t2.setName("周圣");
        t1.start();
        t2.start();
    }
}

class MySubRunnable implements Runnable {
    private int count = 100;
    private static Object obj = new Object();
    @Override
    public void run() {
        while (count > 0) {
            synchronized (obj) {
                obj.notify();
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " count = " + count--);
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

-运行结果:

王也 count = 100
周圣 count = 99
王也 count = 98
- - - - - - - 
周圣 count = 3
王也 count = 2
周圣 count = 1 

注意

  1. 以上三个方法都必须在同步方法、和同步代码块中,不能使用lock进行同步的方式调用这三个方法
  2. 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现IllegalMonitorStateException 异常
  3. 这三个方法都是定义在Object类的方法,
    在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。

1. 使用lock锁进行同步时进行线程间通信:

import java.util.concurrent.locks.ReentrantLock;
/**
 * @Description: 使用lock进行同步并进行线程间通信测试
 * @Author :QianZhiSanyao
 * @Date: 2021-05-27-10:53
 */
public class MyThread extends Thread {
    private static int count = 100;
    private ReentrantLock lock;
    public MyThread(ReentrantLock lock) {
        this.lock = lock;
    }
    @Override
    public void run() {
        while (count > 0) {
            lock.lock();
            notify();
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + " count = " + count--);
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        MyThread t1 = new MyThread(lock);
        MyThread t2 = new MyThread(lock);
        t1.setName("王也");
        t2.setName("周圣");
        t1.start();
        t2.start();
    }
}

运行结果:
抛出 IllegalMonitorStateException 异常

2. 充当同步监视器的对象和调用线程通信方法的对象不是一个:

class MySubRunnable implements Runnable {
    private int count = 100;
    private static Object obj = new Object();
    @Override
    public void run() {
        while (count > 0) {
            synchronized (obj) {
                notify(); 
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " count = " + count--);
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
// 代码中更改为 obj.notify() 和 obj.wait() 则会正常运行

抛出 IllegalMonitorStateException 异常

2. sleep()和wait()的异同
  • 相同点:
    • 这两个方法一旦被执行,都可以使当前的线程进入阻塞状态
  • 不同点:
    1. 定义两个方法的位置不同:Thread类中定义sleep,Object类中定义wait()
    2. 调用要求不同:sleep()可以在任何需要的场景下调用,sleep()必须使用在同步代码块/同步方法中
    3. 是否释放锁:如果两个方法都使用在同步方法/同步代码块中,sleep()方法不会释放锁;而wait()方法会使当前线程阻塞并释放锁
3. 生产者消费者问题

*[什么生产者消费者问题?]

定义共享资源:缓冲区类:

/**
 * @Description: 缓冲区类:即共享资源,暂时存储生产和消费的产品
 * @Author :QianZhiSanyao
 * @Date: 2021-05-27-11:16
 */
public class Buffer {
    private int MAX_COUNT;
    private int curProduceCount = 0;

    public Buffer(int MAX_COUNT) {
        this.MAX_COUNT = MAX_COUNT;
    }

    public boolean isEmpty() {
        return this.curProduceCount == 0;
    }

    public boolean isFull() {
        return this.curProduceCount == this.MAX_COUNT;
    }

    // 生产一个产品
    public synchronized void produceProduct() throws InterruptedException {
        if (isFull()) {
            wait();
        } else {
            int produceNum = ++this.curProduceCount;
            System.out.println(Thread.currentThread().getName() + " 生产了 产品" + produceNum);
            notifyAll();
        }
    }

    // 消费一个产品
    public synchronized void customProduct() throws InterruptedException {
        if (isEmpty()) {
            wait();
        } else {
            int produceNum = this.curProduceCount--;
            System.out.println(Thread.currentThread().getName() + " 消费了 产品" + produceNum);
            notifyAll();
        }
    }
}

定义生产者类:

/**
 * @Description: 生产者线程类:进行产品的生产
 * @Author :QianZhiSanyao
 * @Date: 2021-05-27-11:15
 */
public class Productor extends Thread{
    private Buffer buffer;

    public Productor(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        while(true){
            try {
                this.buffer.produceProduct();
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定义消费者类:

/**
 * @Description: 消费者线程类:使用生产者生产的产品
 * @Author :QianZhiSanyao
 * @Date: 2021-05-27-11:15
 */
public class Customer extends Thread {
    private Buffer buffer;

    public Customer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        while (true) {
            try {
                this.buffer.customProduct();
                 sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定义场景类:

/**
 * @Description: 场景类:对生产者-消费者问题进行测试
 * @Author :QianZhiSanyao
 * @Date: 2021-05-27-11:13
 */
public class Client {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(10);
        // 创建五个消费者进行消费
        for (int i = 0; i < 5; i++) {
            Customer customer = new Customer(buffer);
            customer.setName("消费者"+i);
            customer.start();
        }
        // 创建三个生产者进行生产
        for(int i = 0 ; i < 3 ;i++){
            Productor producer = new Productor(buffer);
            producer.setName("生产者"+i);
            producer.start();
        }
    }
}
  • 运行结果
生产者1 生产了 产品1
生产者0 生产了 产品2
生产者2 生产了 产品3
消费者0 消费了 产品3
消费者1 消费了 产品2
生产者2 生产了 产品2
消费者3 消费了 产品2
消费者2 消费了 产品1
生产者0 生产了 产品1
生产者1 生产了 产品2
生产者0 生产了 产品3
生产者2 生产了 产品4
生产者1 生产了 产品5
消费者2 消费了 产品5
消费者1 消费了 产品4
消费者4 消费了 产品3

注意:

在唤醒时使用notify()在某种极端情况下会造成假死

  • 缓冲区满:假设所有生产者被阻塞。此时有消费者进行消费,进行唤醒时应当唤醒生产者,但因为nofity()唤醒是随机的。因此,有可能会唤醒一个被阻塞的消费者。如果所有的消费者都在唤醒一个被阻塞的消费者,这种极端情况下,等到所有的产品被消费完毕,所有的消费者也会因为没有产品而被阻塞,造成假死。因此测试代码使用notifyAll()来唤醒所有被阻塞的进程来避免极端状况的出现。
  • 缓冲区空,同理

六、新增的线程创建方式

1. *[实现Callable接口]
  1. 相比run方法,可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要结束FutureTesk类,比如获取返回结果
  • FutureTask类:
    • 可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
    • FutureTask是Future接口的唯一实现类
    • FutureTask同时实现了Runnable、Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

测试实现Callable接口实现多线程:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
 * @Description: 创建方式三:实现Callable接口实现多线程
 * @Author :QianZhiSanyao
 * @Date: 2021-05-27-16:02
 *
 * 1. 创建一个实现了Callable接口的实现类
 * 2. 重写call方法
 * 3. 创建Callable接口实现类的对象
 * 4. 将Callable接口实现类的对象作为参数传递给FutureTask的构造器从而创建一个FutureTask类的对象
 * 5. 将FutureTask类的对象作为参数传递给Thread类构造器创建一个线程对象
 * 6. 启动线程
 * (如果要获取Callable中可以调用FutureTask类的对象的get方法获取)
 */
public class TestCallable {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();
        FutureTask futureTask = new FutureTask(numThread);
        // 因为FutureTask接口实现了Runnable接口,因此它也可以作为实参传递给Thread构造器
        // 如果不创建Thread对象,不调用start方法,则线程无法启动s
        Thread t1 = new Thread(futureTask);
        t1.start();
        try {
            System.out.println("sum = "+futureTask.get()); // 通过futureTask调用get方法可以得到Callable接口实现类的call方法的返回值
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class NumThread implements Callable{
    // 相当于run()方法
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 0 ; i <= 100 ;i+=2 ){
            System.out.println(i);
            sum += i ;
        }
        return (Integer)sum;
    }
}

运行结果:

92
94
96
98
100
sum = 2550

Runnable和Callable的区别:

  1. Runnable中的run()没有返回值,而Callable中的call()有返回值
  2. Runnable中的run()不能抛出异常,而Callable中的call()可以抛出异常
  3. Callable接口支持泛型
2. *[线程池]
  • 提前创建好几个多线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用

线程池的好处:

  1. 提高响应速度(提前创建好一些线程)
  2. 可以重复利用资源(使用完毕的线程重新放回线程池中)
  3. 便于线程管理(设置线程池的一些属性)

线程池的一些属性:

  • corePoolSize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAilveTime:线程没有任务时最长保持多长时间后会中止
  • 等等

线程池相关API:

  • ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
  1. void execute(Runnable command); 执行任务/命令,没有返回值,一般用来执行Runnable
  2. Future<?> submit(Runnable task); 执行任务,有返回值,一般用来执行Callable
  • Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池

常见创建线程池的方法:

    Executors.newCachedThreadPool(); // 创建一个可根据需要创建不同类型的线程池
    Executors.newFixedThreadPool(n); // 创建一个可重用固定线程数的线程池
    Executors.newSingleThreadExecutor(); // 创建一个只有一个线程的线程池
    Executors.newScheduledThreadPool(n); // 创建一个线程池,它可安排给定延迟后运行命令或定期地执行

创建一个线程池并进行测试:

  • 添加线程池测试代码

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值