Java知识点08——多线程

声明:

  1. 该资料来自自己整理。
  2. 参考/摘录 书籍 疯狂 Java讲义(第五版) 李刚©著,仅用作学习,非盈利!!

一、概述

1.1 并发和并行

  • 并发是指一个处理器同时处理多个任务。
  • 并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
  • 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生

【注意】:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

1.2 线程和进程

  • 进程是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有2个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

1.3 线程调度

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度

二、线程创建和启动

Java使用java.lang.Thread类代表线程所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。

2.1 继承 Thread 类创建线程


自定义多线程类

/**
 * 创建线程类的步骤:
 */
//1. 定义Thread类的子类
public class Multithreading extends Thread{
    //定义指定线程名称的构造方法
    public Multithreading(String threadName){
        //调用父类的String参数的构造器,指定线程的名称
        super(threadName);
    }
    //2. 重写Thread类中的run方法(该方法的方法体就代表了线程需要完成的任务)
    //run() 方法也称为线程执行体
    @Override
    public void run() {
        //获取线程名称
        System.out.println(getName());
        for (int i = 0; i < 10; i++) {
            System.out.println("run()-->"+i);
        }
    }
}

多线程测试类

public class MultithreadingTest {
    public static void main(String[] args) {
        //3. 创建Thread子类的实例,即创建了线程对象
        Multithreading st = new Multithreading("A thread");
        //4. 调用线程对象的start()方法来启动该线程
        st.start();
        //同一个Thread不能重复调用start方法,会报错
        //st.start(); //java.lang.IllegalThreadStateException

        //main线程
        for (int i = 0; i < 10; i++) {
            System.out.println("main-->"+i);
        }
    }
}

2.2 实现 Runnable 接口创建线程类


使用匿名内部类方式创建线程

public class AnonymousInnerClassThread {
    /**
     * 匿名内部类作用:简化代码
     *    1.把子类继承父类,重写父类方法,创建子类对象合一步完成
     *    2.把实现接口,重写接口中的方法,创建接口实现类对象合成一步完成
     * 匿名内部类的最终产物:子类/接口实现类对象,而这个类没有名字。
     *
     * 格式:
     *   new 父类/接口(){
     *       重写父类/接口中的方法
     *   };
     */
    public static void main(String[] args) {
        //1.继承Thread类的匿名内部类
        new Thread("A线程"){
            //重写run方法,设置线程任务
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(getName()+"-->"+i);
                }
            }
        }.start();//链式编程

        //2.实现Runnable接口的匿名内部类
        Runnable r = new Runnable(){
            //重写run方法,设置线程任务
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+i);
                }
            }
        };
        new Thread(r,"B线程").start();

        //简化Runnable接口的匿名内部类
        new Thread(new Runnable(){
            //重写run方法,设置线程任务
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"-->"+i);
                }
            }
        },"C线程").start();//链式编程
    }
}

2.3 使用 Callable 和 Future创建线程

import java.util.concurrent.Callable;
/**
 * 实现 Callable 接口创建线程
 * @author chubo
 * @since 2022/9/16 11:50
 */
public class CallableThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+" 线程执行call()方法");
        return 11*11;
    }
}
import org.junit.jupiter.api.Test;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
 * @author chubo
 * @since 2022/9/16 11:54
 */
public class CallableThreadTest {

    //创建 Callable接口实现类示例对象
    CallableThread callableThread = new CallableThread();
    //使用 FutureTask 来包装 Callable 实现类示例对象
    //方式一
    FutureTask<Integer> task1 = new FutureTask<Integer>(callableThread);
    //方式二
    // (Callable<Integer>) () :强制类型转换
    FutureTask<Integer> task2 = new FutureTask<Integer>((Callable<Integer>) () -> {
        System.out.println(Thread.currentThread().getName()+" 线程执行call()方法");
        return 10*10;
    });

    @Test
    void executeCallable() throws ExecutionException, InterruptedException {
        new Thread(task1,"task1").start();
        System.out.println("task1 线程执行的返回值:"+task1.get());
        new Thread(task2,"task2").start();
        System.out.println("task2 线程执行的返回值:"+task2.get());
    }

}

三、线程生命周期

3.1 线程状态转换



3.1.1 新建状态、就绪状态

  当程序使用 new关键字 创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

备注

  1. 只能兑处于新建状态的线程调用 start()方法,否则将会引发 IllegalThreadStateException异常。
  2. 处于就绪状态的线程什么时候运行取决于JVM里线程调度器的调度。
  3. 启动线程使用start()方法,而不是run()方法。调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。

3.1.2 运行和阻塞状态

3.1.3 线程死亡

四、控制线程

4.1 join线程(线程合并)

线程的合并就是:线程A在运行期间,可以调用线程B的join()方法,这样线程A就必须等待线程B执行完毕后,才能继续执行。

join()方法有如下三种重载形式:

  • join() :等待被join的线程执行完成,才可以执行调用该线程的线程。
  • join(long millis) :等待被join 的线程的时间长为millis 毫秒。如果在millis 毫秒内被join的线程还没有执行结束,则不再等待。
  • join(long millis,int nanos) :等待被join 的线程的时间最长为millis 毫秒加 nanos 毫微秒。
public class JoinThread extends Thread{
    //提供一个有参构造器,用于设置线程名称
    public JoinThread(String name) {
        super(name);
    }
    
    //run方法为线程的执行体
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+" "+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //启动子线程
        new JoinThread("A线程").start();
        for (int i = 0; i < 10; i++) {
            if (i == 6){
                JoinThread jt = new JoinThread("Join线程");
                jt.start();
                //main线程调用了jt线程的join()方法,main线程必须等jt线程执行结束才会向下执行!!!
                jt.join();//当执行jt线程的join()方法时,main线程处于阻塞状态
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

4.2 后台线程

后台线程(Daemon Thread)。JVM的垃圾回收线程就是典型的后台线程。

后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。

调用Thread对象的 setDaemon(true) 方法可将指定线程设置成后台线程。

public class DaemonThread extends Thread{
    //定义后台线程的线程执行体与普通线程没有任何区别
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        DaemonThread dt = new DaemonThread();
        //将此线程设置成后台线程
        dt.setDaemon(true);
        //启动后台线程
        dt.start();
        //判断dt线程是否是后台线程
        System.out.println(dt.isDaemon());//true
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        /**
         * --------程序执行到此处,前台线程(main线程)结束-------
         *                 后台线程也应该随之结束
         *
         * dt线程本应该执行到999,但是前台线程先执行完,
         * 前台线程执行完后,后台线程也跟着结束执行,所以到不了999
         */
    }
}

注意:

  • setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。

4.3 线程睡眠(sleep)

import java.util.Date;
public class SleepThread {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println("当前时间:"+new Date());
            //调用sleep()方法让当前线程暂停1s
            Thread.sleep(1000);
        }
    }
}

4.4 改变线程优先级

package com.ccbx.monitoringsys.thread;

import org.junit.jupiter.api.Test;

/**
 * 线程优先级测试
 * @author chubo
 * @since 2022/10/17 10:10
 */
public class ThreadPriorityTest {

    public class PriorityTest extends Thread{
        public PriorityTest(String name) {
            super(name);
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName()+",优先级是:"+getPriority()+",i-->"+i);
            }
        }
    }

    @Test
    void changePriority(){
        //改变主线程的优先级
        Thread.currentThread().setPriority(6);
        for (int i = 0; i < 30; i++) {
            if (i == 10){
                PriorityTest lowPt = new PriorityTest("lowPt");
                lowPt.start();
                //lowPt创建之初的优先级:6
                System.out.println(lowPt.getName()+"创建之初的优先级:"+lowPt.getPriority());
                //设置该线程为最低优先级
                lowPt.setPriority(Thread.MIN_PRIORITY);
            }
            if (i == 20){
                PriorityTest highPt = new PriorityTest("highPt");
                highPt.start();
                System.out.println(highPt.getName()+"创建之初的优先级:"+highPt.getPriority());
                //设置该线程的优先级
                highPt.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }

}

备注:改变优先级只是执行的概率变大/变小,并不一定先/后执行。

五、线程同步

5.1 取钱案例

账户类

@Data
public class Account {
    private String accountNo;
    private double balance;
}

取钱类

public class DrawThread extends Thread{

    //模拟用户账户
    private Account account;
    //当前线程所希望取的钱数
    private double drawAmount;
    
    public DrawThread(String name, Account account, double drawAmount){
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    
    //当多个线程修改一个共享数据时,将涉及数据安全问题
    //run()方法不具有同步安全性!!
    @Override
    public void run() {
        //账户余额大于取钱数目
        if (account.getBalance() >= drawAmount){
            //吐出钞票
            System.out.println(getName()+",取钱 "+drawAmount+" 成功!");
            //修改金额
            account.setBalance(account.getBalance() - drawAmount);
            System.out.println("\t余额为:"+account.getBalance());
        }else{
            System.out.println(getName()+" 取钱失败!余额不足!");
        }
    }
}

测试类

public class DrawTest {
    public static void main(String[] args) {
        //创建一个账户
        Account account = new Account("123456789",1000);
        //模拟两个线程对同一个账户取钱
        new DrawThread("甲",account,800).start();
        new DrawThread("乙",account,800).start();
    }
}

5.2 同步代码块

//synchronized  美 [ˈsɪŋkrənaɪzd] 已同步
//同步代码块的语法格式
synchronized(obj){ //obj 为同步监视器
    ...
    //此处就是同步代码块
}


同步监视器的目的:
    阻止两个或多个线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。
    
同步监视器可以理解为就是一把锁,锁住了别的线程就进不去了,直到该线程释放掉这个锁(释放锁是指持锁线程退出了synchronized同步代码块)。

同步监视器注意事项:
    1、同步监视器必须是引用数据类型,不能是基本数据类型。
    2、多个线程必须共用同一把锁
    3、线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
    4、任何时刻只有一个线程可以获得同步监视器的锁定,当同步代码块执行完成后该线程就会释放对同步监视器的锁定。

修改存取钱案例:

public class DrawThread extends Thread{
    //模拟用户账户
    private Account account;
    //当前线程所希望取的钱数
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount){
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    //当多个线程修改一个共享数据时,将涉及数据安全问题
    @Override
    public void run() {
        /**
         * 同步监视器的目的:
         *      阻止两个或多个线程对同一个共享资源进行并发访问,
         *      通常推荐使用可能被并发访问的共享资源充当同步监视器。
         *
         * 使用account作为同步监视器,任何线程进入下面同步代码块之前
         * 必须先获得对account账户的锁定----其它线程无法获得锁,也就无法修改它
         * 这种做法符合:”加锁-->修改-->释放锁“ 的逻辑
         */
        synchronized (account){ //account对象作为同步监视器
            //账户余额大于取钱数目
            if (account.getBalance() >= drawAmount){
                //吐出钞票
                System.out.println(getName()+",取钱 "+drawAmount+" 成功!");
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //修改金额
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t余额为:"+account.getBalance());
            }else{
                System.out.println(getName()+" 取钱失败!余额不足!");
            }
        }
    }
}

5.3 同步方法

不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。

使用同步方法的方式实现取钱案例:

//将前面的Account类设置为线程安全的类
public class AccountSafe {
    private String accountNo;
    private double balance;
    public AccountSafe(){ }
    public AccountSafe(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    public String getAccountNo() {
        return accountNo;
    }
    //balance get和set方法
    ···
    //提供一个线程安全的draw()方法来完成取钱操作
    public synchronized void draw(double drawAmount){
        //账户余额大于取钱数目
        if (drawAmount <= balance){
            //吐出钞票
            System.out.println(Thread.currentThread().getName()+",取钱 "+drawAmount+" 成功!");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //修改金额
            balance -= drawAmount;
            System.out.println("\t余额为:"+balance);
        }else{
            System.out.println(Thread.currentThread().getName()+" 取钱失败!余额不足!");
        }
    }
    //根据accountNo来重写hashCode()和equals()方法
    ···
}
public class DrawThreadSafe extends Thread{
    //模拟用户账户
    private AccountSafe account;
    //当前线程所希望取的钱数
    private double drawAmount;
    public DrawThreadSafe(String name, AccountSafe account, double drawAmount){
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    //当多个线程修改一个共享数据时,将涉及数据安全问题
    @Override
    public void run() {
        /**
         * 直接调用account对象的draw()方法执行取钱操作
         * 同步方法的同步监视器是this,this代表调用draw()方法的对象
         * 也就是说,线程进入draw()方法之前,必须先对account对象加锁
         */
        account.draw(drawAmount);
    }
}
public class DrawTest {
    public static void main(String[] args) {
        System.out.println("---------Account线程安全类----------------");
        //创建一个账户
        AccountSafe accountSafe = new AccountSafe("67890",1000);
        //模拟两个线程对同一个账户取钱
        new DrawThreadSafe("甲",accountSafe,800).start();
        new DrawThreadSafe("乙",accountSafe,800).start();
    }
}

备注:可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全带来的负面影响,程序可以采用如下策略:

5.4 释放同步监视器的锁定

5.5 同步锁


reentrant 美 [ˌriˈɛntrənt] 可重入的

class X{
    //定义锁对象
    private final RenntrantLock lock = new ReentrantLock();
    //..
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try{
            //需要保证线程安全的代码
            //... method body
        }
        //使用finally块来保证释放锁
        finally{
            lock.unlock();
        }
    }
}

使用 ReentrantLock 对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally 块来确保在必要时释放锁。

5.6 死锁


如下案例:

class A{
    public synchronized void foo(B b){
        System.out.println("当前线程名:"+Thread.currentThread().getName()+" 进入了A实例的foo()方法");//1
        try{
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程名:"+Thread.currentThread().getName()+" 企图调用B实例的last()方法");//3
        b.last();//此时当前线程(主线程)还没有释放同步监视器
    }
    public synchronized void last(){
        System.out.println("进入了A类的last()方法内部");
    }
}

class B{
    public synchronized void bar(A a){
        System.out.println("当前线程名:"+Thread.currentThread().getName()+" 进入了B实例的bar()方法");//2
        try{
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程名:"+Thread.currentThread().getName()+" 企图调用A实例的last()方法");//4
        a.last();//此时当前线程(副线程)还没有释放同步监视器;a对象(也是a同步监视器)此时访问a的同步last方法,但是访问同步方法前先要对a加锁,但是a同步监视器还没解锁。
        //此时就出现了,副线程保持着b的锁,等待主线程对a加锁;主线程保持着a的锁,等待副线程对a加锁;出现死锁!!
    }
    public synchronized void last() {
        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("进入了主线程之后");
    }
    @Override
    public void run() {
        Thread.currentThread().setName("副线程");
        //调用b对象的bar()方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        //以deadLock启动新线程
        new Thread(deadLock).start();
        //调用init()方法
        deadLock.init();
    }
}


六、线程通信

6.1 传统线程通信

借助 Objects 类提供的 wait()notify()notifyAll() 三个方法。这三个方法必须由同步监视器对象来调用。

/**
 * 实现存钱取钱交替进行(不能连续存两次,也不能连续取两次)
 */
public class AccountWaitNotify {
    private String accountNo;
    private double balance;
    //标识账户中是否已有存款的开关,false表示未存款
    private boolean flag = false;
    public AccountWaitNotify(){ }
    public AccountWaitNotify(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    public String getAccountNo() {
        return accountNo;
    }
    public double getBalance() {
        return balance;
    }
    //设置取钱的同步方法
    public synchronized void draw(double drawAmount){
        try{
            //如果falg为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag){
                wait();
            }else{
                //执行取钱操作
                System.out.println(Thread.currentThread().getName()+" 取钱:"+drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:"+balance);
                //将标识符设置为已存款
                flag = false;
                //唤醒此同步监视器上的其它线程
                notifyAll();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    //设置存钱的同步方法
    public synchronized void deposit(double depositAmount){
        try {
            //如果flag为真,表明账户已有人存钱进去,存钱方法阻塞
            if (flag){
                wait();
            }else{
                //执行存款操作
                System.out.println(Thread.currentThread().getName()+" 存款:"+depositAmount);
                balance += depositAmount;
                System.out.println("账户余额:"+balance);
                //改变状态为已存钱
                flag = true;
                //唤醒此同步监视器上的其它线程
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

取钱线程

public class WaitNotifyDrawThread extends Thread{
    private AccountWaitNotify account;
    private double drawAmount;
    public WaitNotifyDrawThread(String name, AccountWaitNotify account, double drawAmount){
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    //重复20次执行取钱操作
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            //account对象调用同步方法,则account对象就是同步监视器
            account.draw(drawAmount);
        }
    }
}

存钱线程

public class WaitNotifyDepositeThread extends Thread{
    private AccountWaitNotify account;
    private double depositeAmount;
    public WaitNotifyDepositeThread(String name, AccountWaitNotify account, double depositeAmount){
        super(name);
        this.account = account;
        this.depositeAmount = depositeAmount;
    }
    //重复20次执行存钱操作
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            //account对象调用同步方法,则account对象就是同步监视器
            account.deposit(depositeAmount);
        }
    }
}
public class WaitNotifyThreadTest {
    public static void main(String[] args) {
        //创建一个账户
        AccountWaitNotify acc = new AccountWaitNotify("2020520",0);
        new WaitNotifyDrawThread("取钱者",acc,800).start();
        new WaitNotifyDepositeThread("存钱者甲",acc,800).start();
        new WaitNotifyDepositeThread("存钱者乙",acc,800).start();
        new WaitNotifyDepositeThread("存钱者丙",acc,800).start();
    }
}


注意
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。


总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;

6.2 使用 Condition 控制线程通信

在 Condition 对象中,与 waitnotifynotifyAll 方法对应的分别是 awaitsignalsignalAll

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AccountCondition {
    //创建锁对象
    private final Lock lock = new ReentrantLock();
    //获得指定Lock对象的Condition
    private final Condition cond = lock.newCondition();
    
    private String accountNo;
    private double balance;
    //有参、无参构造器
    //···
    public String getAccountNo() {
        return accountNo;
    }
    public double getBalance() {
        return balance;
    }
    
    //账户中是否已有存款默认false
    private boolean flag = false;
    //取钱方法
    public void draw(double drawAmount){
        //加锁
        lock.lock();
        try{
            if (!flag){
                //Condition类的await()方法使当前线程阻塞;
                //当前线程释放对此Lock对象的锁定!!!
                cond.await();
            }else{
                //取钱
                System.out.println(Thread.currentThread().getName()+" 取钱:"+drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额:"+balance);
                flag = false;
                //唤醒当前锁对象上的其它所有阻塞线程
                cond.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
    //存钱方法
    public void deposit(double depositAmount){
        //加锁
        lock.lock();
        try{
            if (!flag){
                //Condition类的await()方法使当前线程阻塞;
                //当前线程释放对此Lock对象的锁定!!!
                cond.await();
            }else{
                //取钱
                System.out.println(Thread.currentThread().getName()+" 存钱:"+depositAmount);
                balance += depositAmount;
                System.out.println("账户余额:"+balance);
                flag = true;//变为已有存款
                //唤醒当前锁对象上的其它所有阻塞线程
                cond.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
    //根据accountNo来重写hashCode()和equals()方法
    //···
}

显式地使用Lock对象来充当同步监视器时,需要使用Condition对象来暂停( await() )、唤醒( signal() )指定线程。

6.3 使用阻塞队列(BlockingQueue)控制线程通信(待完善)

待完善

6.4 线程组和未处理异常(待完善)

待完善

七、线程池(Executors)

简单理解:线程池就是存放了很多可以复用线程的一个“容器”。可以很大程度上降低创建线程带来的开销。

7.1 使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

7.2 创建线程

Java 5 新增了一个 Executors 工厂类来创建线程池,通过该类的几个静态工厂方法来创建线程池。

静态方法解释
newCachedThreadPoll()创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将被缓存在线程池中。
newFixedThreadPoll(int nThreads)创建一个可重用的、具有固定线程数的线程池。
newSingleThreadExecutor()创建一个只有单线程的线程池,它相当于调用 newFixedThreadPoll(1)。
newScheduledThreadPoll(int corePoolSize)创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
newSingleThreadScheduledExecutor()创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
newWorkStealingPool(int parallelism)创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。
newWorkStealingPool()如果当前机器有四个cpu,则目标并行级别被设置为4,也就相当于 Executors.newWorkStealingPool(4)。

7.2.1 ExecutorService

ExecutorService 代表尽快执行线程的线程池(只要线程池中有空闲线程,就立即执行线程任务),程序只要将一个 Runnable 对象或 Callable 对象(代表线程任务)提交给该线程池,该线程池就会尽快执行该任务。



7.2.2 ScheduledExecutorService

ScheduledExecutorService 代表可在指定延迟后或周期性地执行线程任务的线程池。




7.2.3 线程池执行线程任务步骤(重点)

  1. 调用 Executors 类的静态工厂方法来创建一个 ExecutorService 对象,该对象代表一个线程池。
  2. 创建 Runnable 实现类或 Callable 实现类的实例,作为线程执行任务。
  3. 调用 ExecutorService 对象的 submit()方法来提交 Runnable 实例或 Callable 实例。
  4. 当不想提交任何任务时,调用 ExecutorService 对象的 shutdown() 方法来关闭线程池。

案例

@Test
void createPool(){
    // 创建一个具有固定线程数的线程池
    ExecutorService pool = Executors.newFixedThreadPool(4);
    // 创建Runnable接口的示例
    Runnable target = new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + " 的 i 值为:" + i);
            }
        }
    };
    // 向线程池中提交三个线程
    pool.submit(target);
    pool.submit(target);
    pool.submit(target);
    // 关闭线程池
    pool.shutdown();
}

备注

  • 用完一个线程池后,应该调用该线程池的 shutdown() 方法,该方法将会启动线程池的关闭序列,调用shutdown() 方法后的线程池不再接收新任务,但会将以前的所有已提交的任务执行完成。
  • 当线程池中的所有任务都执行完成后,池中的所有线程都会死亡;另外也可以调用线程池的 shutdownNow()方法来关闭线程池,该方法视图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

7.2.4 ForkJoinPool(待完善)

待完善

八、线程相关类

8.1 ThreadLocal(待完善)

待完善
用途:隔绝多个线程之间的共相冲突时使用 ThreadLocal。

8.2 线程安全/不安全集合

8.2.1 举例

分类示例
常见的线程安全集合ConcurrentHashMapConcurrentSkipListMapConcurrentSkipListSetConcurrentLinkedQueueConcurrentLinkedDequeCopyOnWriteArrayListCopyOnWriteArraySet、等。
常见的线程不安全集合ArrayListLinkedListHashSetTreeSetHashMapTreeMap、等。

线程不安全集合:当多个并发线程向这些集合中存、取元素时,可能破坏这些集合的数据完整性。

8.2.2 包装线程不安全集合为线程安全集合

使用 Collections 提供的类方法把线程不安全的集合包装成线程安全的集合。

Collections静态方法解析
<T> Collection<T> synchronizedCollection(Collection<T> c)返回指定 collection 对应的线程安全的 collection。
<T> List<T> synchronizedList(List<T> list)返回指定 List 对象对应的线程安全的 List对象。
<K,V> Map<K,V> synchronizedMap(Map<K,V> map)返回指定 Map 对象对应的线程安全的 Map对象。
<T> Set<T> synchronizedSet(Set<T> set)返回指定 Set 对象对应的线程安全的 Set 对象。
<K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> map)返回指定 SortedMap 对象对应的线程安全的 SortedMap 对象。
<T> SortedSet<T> synchronizedSortedSet(SortedSet<T> set)返回指定 SortedSet 对象对应的线程安全的 SortedSet 对象。
@Test
void wrapCollections(){
    // 使用 Collections 的方法将线程不安全的集合包装成线程安全的集合
    List<String> synchronizedList = Collections.synchronizedList(new ArrayList<String>());
}

备注:如果想要把某个集合包装成线程安全的集合,则在创建之后应该立即包装。

8.3 发布-订阅框架(待完善JDK9)

待完善JDK9的功能

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值