JAVA基础深化提高(六) | 多线程和并发编程

一、多线程概念

1.程序、进程、线程

1.1 什么是程序?

程序(Program)是一个静态的概念,一般对应于操作系统中的一个可执行文件。

1.2 什么是进程?

执行中的程序叫做进程(Process),是一个动态的概念。其实进程就是一个在内存中独立运行的程序空间 。

现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

在这里插入图片描述

1.3 什么是线程?

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些
“子任务”称为线程(Thread)。

在这里插入图片描述

1.4 进程、线程的区别

在这里插入图片描述
4 调度和切换:线程上下文切换比进程上下文切换要快得多。

总结:

  1. 进程和进程之间是完全独立的但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),进程内的线程在其它进程不可见;
  2. 每个程序在执行时都会有对应的进程
  3. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  4. 一个进程之间包括三个内容(CPU运行时间,data ,code)
  5. 调度和切换:线程上下文切换比进程上下文切换要快得多

1.5 什么是并发?

在这里插入图片描述

  1. 串行(serial):一个CPU上,按顺序完成多个任务
  2. 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的
  3. 并发(concurrency):是指多个事件、任务或操作在同一时间段内交替进行的情况。一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

1.6 什么是主线程以及子线程?

主线程

当Java程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread),即main方法对应的线程,它是程序开始时就执行的。
Java应用程序会有一个main方法,是作为某个类的方法出现的。当程序启动时,该方法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main方法是一个应用的入口,也代表了这个应用的主线程。JVM在执行main方法时,main方法会进入到栈内存,JVM会通过操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程
它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。

子线程

在主线程中创建并启动的线程,一般称之为子线程。

二、线程的创建

2.1 通过继承Thread类实现多线程

/**
 * 通过继承Thread类实现多线程
 */
public class TestThread extends Thread{

    public TestThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(this.getName()+"执行开始");
        for (int i=0 ;i<10;i++){
            System.out.println(this.getName()+":"+i);
        }
        System.out.println(this.getName()+"执行结束");
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"执行开始");
        TestThread t1 = new TestThread("TestThread---1");
        t1.start();
        TestThread t2 = new TestThread("TestThread---2");
        t2.start();
        TestThread t3 = new TestThread("TestThread---3");
        t3.start();

        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
        
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        System.out.println(Thread.currentThread().getName()+"执行结束");
    }
}

2.2 通过Runnable接口实现多线程

/**
 * 通过Runnable接口实现多线程
 */
public class TestThread2 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) {
        Thread t1 = new Thread(new TestThread2(),"TestThread1");
        t1.start();
        Thread t2 = new Thread(new TestThread2(),"TestThread2");
        t2.start();
    }
}

2.3 使用Lambda表达式实现多线程

/**
 * 使用landon表达式实现多线程
 */
public class TestThread3 {

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        },"Lambda1");
        t1.start();

        Thread t2 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        },"Lambda2");
        t2.start();
    }
}

三、线程的使用

3.1 线程终止

在这里插入图片描述

  • interrupt():发送中断信号(true)
    • 如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
    • 只是发送信号,不会对线程产生影响
  • static interrupted():得到中断信号(true),然后把中断信号设置成false
  • isInterrupted():得到中断信号,不会清除中断信号
public class TestStopThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始");
        int i = 0;
        while (!Thread.currentThread().interrupted()) {
        i++;
         System.out.println(Thread.currentThread().getName() + "线程执行 " + i + " 次");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("线程被中断");
                break;//退出循环
            }
        }
        System.out.println(Thread.currentThread().getName() + "线程结束");
    }

    public static void main(String[] args) {
        TestStopThread2 testStopThread2 = new TestStopThread2();
        Thread t = new Thread(testStopThread2, "TestStopThread2");
        t.start();
        try {
            System.in.read();//一个IO操作是线程是线程进入堵塞
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        t.interrupt();
    }
}

3.2 线程睡眠

sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。sleep方法的参数为休眠的毫秒数。

public class MyTask implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                //Thread.currentThread().getName():得到线程的名字
                System.out.println(Thread.currentThread().getName() + "\t" + i);
                try {
                    Thread.sleep(1000);
                    Thread.yield();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}

3.3 线程让步

yield()让当前正在运行的线程回到就绪状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

使用yield方法时要注意的几点:

  • yield是一个静态的方法。
  • 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。
  • yield不能保证,当前线程迅速从运行状态切换到就绪状态。
  • yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。
/**
 * 线程让步
 */
public class TestYieldThread implements Runnable {
    @Override
    public void run() {

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

    public static void main(String[] args) {
        TestYieldThread testYieldThread = new TestYieldThread();
        Thread t1 = new Thread(testYieldThread, "TestYieldThread1");
        Thread t2 = new Thread(testYieldThread, "TestYieldThread2");
        t1.start();
        t2.start();
    }
}

3.4 线程联合

当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。

join()方法就是指调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,即将两个线程合并,用于实现同步控制。

/**
 * 父亲的线程
 */
class FatherThread implements Runnable{

    @Override
    public void run() {
        System.out.println("爸爸想抽烟,发现烟抽完了");
        System.out.println("爸爸让儿子去买一包红塔山");
        //启动儿子买烟线程
        Thread s = new Thread(new SonThread());
        s.start();

        try {
            s.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("爸爸出去找儿子");
        }
        System.out.println("爸爸高兴的接过烟,并把零钱给了儿子");
    }
}

/**
 * 儿子的买烟线程
 */
class SonThread implements Runnable{

    @Override
    public void run() {
        System.out.println("儿子出门买烟");
        System.out.println("儿子买烟需要10分钟");
        for(int i=0;i<10;i++){
            System.out.println("第"+i+"分钟");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("儿子买烟回来了");
    }
}

public class TestJoinDemo {
    public static void main(String[] args) {
        Thread f = new Thread(new FatherThread());
        f.start();
    }
}

3.5 Thread类中的其他常用方法

3.5.1 获取当前线程名称

(1) this.getName()获取线程名称,该方法适用于继承Thread实现多线程方式。
(2)Thread.currentThread().getName()获取线程名称,该方法适用于实现Runnable接口实现多线程方式。

3.5.2 设置线程的名称

(1)通过构造方法设置线程名称。
(2)通过setName()方法设置线程名称。

3.5.3 判断线程是否存活

isAlive()方法: 判断当前的线程是否处于活动状态。
活动状态是指线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是存活的。

class Alive implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class TestAliveThread {
    public static void main(String[] args) {
        Thread thread =new Thread(new Alive());
        System.out.println("线程启动前"+thread.isAlive());
        thread.start();
        System.out.println("线程刚启动"+thread.isAlive());
        try {
            Thread.sleep(5000);
            System.out.println("线程堵塞状态"+Thread.currentThread().isAlive());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName()+"线程结束"+thread.isAlive());
    }

四、线程的原理

4.1 线程的执行流程

在这里插入图片描述

4.2 线程的调度与时间片

由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态
在这里插入图片描述

(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型

4.3 线程状态和生命周期

4.3.1 操作系统的线程状态

在这里插入图片描述

1. 新生状态
用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

2. 就绪状态
处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,
它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
有4种原因会导致线程进入就绪状态:

  • 新建线程:调用start()方法,进入就绪状态;
  • 阻塞线程:阻塞解除,进入就绪状态;
  • 运行线程:调用yield()方法,直接进入就绪状态;
  • 运行线程:JVM将CPU资源从本线程切换到其他线程。

3. 运行状态
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

4. 阻塞状态
在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
有4种原因会导致线程阻塞:

1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

5. 死亡状态
死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。
当一个线程进入死亡状态以后,就不能再回到其它状态了。

4.3.2 Java的线程状态

//返回当前线程的执行状态,一个枚举类型值
public Thread.State getState(); 

Thread.State 是一个内部枚举类,定义了 6 个枚举常量,分别代表 Java 线程的 6 种状态,具体如下:

public static enum State {
     NEW, //新建
     RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态
     BLOCKED, //阻塞  -> 操作系统线程中的阻塞
     WAITING, //等待  -> 操作系统线程中的阻塞
     TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞
     TERMINATED; //终止
}

在Thread.State 定义的 6 种状态中,有四种是比较常见的状态,它们是: NEW 状态、RUNNABLE状态、 TERMINATED 状态、 TIMED_WAITING 状态。

接下来,将线程的 6 种状态以及各种状态的进入条件,做一个总结。

1. NEW 状态
通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式,本质上都是通过new Thread( )创建的线程,仅仅是创建了不同的 target 执行目标实例(如 Runnable 实例)。
2. RUNNABLE 状态
Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的 start()实例方法后,线程就处于就绪状态;此线程获取到 CPU 时间片后,开始执行 run( )方法中的业务代码,线程处于执行状态。
(1)就绪状态就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状态;当前线程进入就绪状态的条件,大致包括以下几种:

  • 调用线程的 start()方法,此线程进入就绪状态。
  • 当前线程的执行时间片用完。
  • 线程睡眠(sleep)操作结束。
  • 对其他线程合入(join)操作结束。
  • 等待用户输入结束。
  • 线程争抢到对象锁(Object Monitor)。
  • 当前线程调用了 yield 方法出让 CPU 执行权限。
    (2)执行状态
    线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。

3. BLOCKED 状态 处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源,以下情况会让线程进入阻塞状态:
(1)线程等待获取锁 等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
(2) IO 阻塞 线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘 IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。

4. WAITING 状态
处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:

  • Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
  • Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
  • LockSupport.park() 方法,对应的唤醒方式为: LockSupport.unpark(Thread)。

5. TIMED_WAITING 状态
处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:

  • Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。
  • Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify() /Object.notifyAll()去主动唤醒,或者限时结束。
  • LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
    进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。
    6. TERMINATED 状态 线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

五、线程的优先级

使用下列方法获得或设置线程对象的优先级。

  • int getPriority();
  • void setPriority(int newPriority);

注意: 优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。

/**
 * 线程的优先级
 */
class Priority implements Runnable{
    private int num = 0;
    private boolean flag = true;

    @Override
    public void run() {
        while(this.flag){
            System.out.println(Thread.currentThread().getName()+" "+num++);
        }
    }
    public void stop(){
        this.flag = false;
    }
}

public class TestPriorityThread {
    public static void main(String[] args) {
        Priority p1 = new Priority();
        Priority p2 = new Priority();
        Thread t1 = new Thread(p1,"线程1");
        Thread t2 = new Thread(p2,"线程2");
        System.out.println(t1.getPriority());

        //线程优先级一定是在线程启动之前来设置的。
        //Thread.MAX_PRIORITY = 10
        t1.setPriority(Thread.MAX_PRIORITY);
        //Thread.MIN_PRIORITY
        t2.setPriority(Thread.MIN_PRIORITY);

        t1.start();
        t2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        p1.stop();
        p2.stop();
    }
}

六、守护线程

在Java中有两类线程:

  • User Thread(用户线程):就是应用程序里的自定义线程。
  • Daemon Thread(守护线程):比如垃圾回收线程,就是最典型的守护线程。

守护线程(即Daemon Thread),是一个服务线程,准确地来说就是服务其他的线程,这是它的作用,而其他的线程只有一种,那就是用户线程。

守护线程特点:
守护线程会随着用户线程死亡而死亡。

守护线程与用户线程的区别:
用户线程,不随着主线程的死亡而死亡。用户线程只有两种情况会死掉,1在run中异常终止。2正常把run执行完毕,线程死亡。
守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡。

/**
 * 守护线程
 */
class Daemon implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<20;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class UserThread implements Runnable{

    @Override
    public void run() {
        Thread t = new Thread(new Daemon(),"Daemon");
        //设置该线程为守护线程
        t.setDaemon(true);
        t.start();
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TestDaemonThread {
    public static void main(String[] args) {
        Thread t = new Thread(new UserThread(),"UserThread");
        t.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束");
    }
}

七、线程同步

7.1 线程同步的概念

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

7.2 线程同步案例演示

我们以银行取款经典案例来演示线程冲突现象。
银行取钱的基本流程基本上可以分为如下几个步骤。
(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。
(2)用户输入取款金额
(3)系统判断账户余额是否大于或等于取款金额
(4)如果余额大于或等于取款金额,则取钱成功;如果余额小于取款金额,则取钱失败。

class Account{
    private String Ano;
    private Double balance;

    public Account(String ano, Double balance) {
        Ano = ano;
        this.balance = balance;
    }

    public String getAno() {
        return Ano;
    }

    public void setAno(String ano) {
        Ano = ano;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Acount{" +
                "Ano='" + Ano + '\'' +
                ", balance=" + balance +
                '}';
    }
}

/**
 * 取钱的线程
 */
class DrawThread implements Runnable{
    private Account account;

    private Double drawMoney;

    public DrawThread(Account acount, Double drawMoney) {
        this.account = account;
        this.drawMoney = drawMoney;
    }

    @Override
    public void run() {
        synchronized (this.account) {
            double v = this.account.getBalance() - this.drawMoney;
            if (v >= 0) {
                System.out.println(Thread.currentThread().getName() + "取了" + this.drawMoney);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                this.account.setBalance(v);
                System.out.println("账户的余额为: " + v);
            } else {
                System.out.println("账户余额不足");
            }
        }
    }
}


/**
 * 取钱案例
 */
public class TestDrawMoneyThread {
    public static void main(String[] args) {
        Account account = new Account("1234",1000.0);
        Thread t1 =new Thread(new DrawThread(account,600.0),"父亲");
        Thread t2 =new Thread(new DrawThread(account,600.0),"母亲");
        t1.start();
        t2.start();
    }
}

7.3 synchronized内置锁

1. 临界区资源(共同数据)
表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程必须等待。
2. 临界区代码段(Critical Section)
是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源。临界区代码段(Critical Section)的进入和退出
在这里插入图片描述
在 Hotspot 虚拟机中, Monitor 是由 C++类 ObjectMonitor 实现, ObjectMonitor 类定义在ObjectMonitor.hpp 文件中,其构造器代码大致如下:

/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
    _header = NULL;
    _count = 0;
    _waiters = 0,
    
    //线程的重入次数
    _recursions = 0;
    _object = NULL;
    
    //标识拥有该 monitor 的线程
    _owner = NULL;
    
    //等待线程组成的双向循环链表
    _WaitSet = NULL;
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    
    //多线程竞争锁进入时的单向链表
    cxq = NULL ;
    FreeNext = NULL ;
    
    //_owner 从该双向循环链表中唤醒线程节点
    _EntryList = NULL ;
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
}

在这里插入图片描述

在这里插入图片描述

7.4 synchronized语法结构

synchronized(锁对象){ 
   //同步代码
}

synchronized关键字使用时需要考虑的问题:

  • 需要对那部分的代码在执行时具有线程互斥的能力(线程互斥:并行变行)。
  • 需要对哪些线程中的代码具有互斥能力(通过synchronized锁对象来决定)。

synchronized 方法和 synchronized 块。
synchronized 方法
通过在方法声明中加入 synchronized关键字来声明,语法如下:

public  synchronized  void accessVal(intnewVal);

synchronized 在方法声明时使用:放在访问控制符(public)之前或之后。这时同一个对象下synchronized方法在多线程中执行时,该方法是同步的,即一次只能有一个线程进入该方法,其他
线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

synchronized块
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
Java 为我们提供了更好的解决办法,那就是 synchronized 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。

7.5 线程同步的使用

使用this作为线程对象锁

synchronized(this){
      //同步代码
}public  synchronized  void accessVal(int newVal){
//同步代码
}

使用字符串作为线程对象锁

synchronized("字符串"){
      //同步代码
}

使用Class作为线程对象锁

synchronized(XX.class){
      //同步代码
}

使用自定义对象作为线程对象锁

synchronized(自定义对象){
      //同步代码
}

7.6 死锁及解决方案

7.6.1 死锁的概念

在这里插入图片描述
“死锁”指的是:
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

7.6.2 死锁案例演示

public class DeadLock {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (String.class) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (Integer.class) {
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (Integer.class) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (String.class) {
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

7.6.3 死锁问题的解决

死锁是由于 “同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。

public class DeadLock {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (String.class) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }           
                }
                synchronized (Integer.class) {
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (Integer.class) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }                    
                }
                synchronized (String.class) {
                
                }
            }
        });
        t1.start();
        t2.start();
    }
}

7.7 CountDownLatch

juc的公共锁

  • 构造函数new CountDownLatch(perms 10)授权数量
  • countDown():授权数量-1
  • await():执行后线程进入阻塞,知道授权数量=0
public class Plus {

    private int count = 0;

    public void selfPlus() {
        count ++;
    }

    public int getCount() {
        return count;
    }
}
import java.util.concurrent.CountDownLatch;

public class MyTask implements Runnable {

    Plus plus;
    CountDownLatch countDownLatch;

    public MyTask() {
    }

    public MyTask(Plus plus, CountDownLatch countDownLatch) {
        this.plus = plus;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        synchronized (plus) {
            for (int i = 0; i < 100000000; i++) {
                plus.selfPlus();
            }
            countDownLatch.countDown(); //-1
        }
    }
}
import i_plus.PlusTask;

import java.util.concurrent.CountDownLatch;

public class Test1 {

    public static void main(String[] args) {

        Plus plus = new Plus();
        CountDownLatch countDownLatch = new CountDownLatch(4);
        MyTask task = new MyTask(plus, countDownLatch);

        for (int i = 0; i < 4; i++) {
            new Thread(task).start();
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("理论值 = 400000000");
        System.out.println("实际值 = " + plus.getCount());
        System.out.println("理论值和实际值相差 = " + (400000000 - plus.getCount()));
    }
}

八、线程并发协作(生产者/消费者模式)

在这里插入图片描述

8.1 角色介绍

  • 什么是生产者?
    生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
  • 什么是消费者?
    消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
  • 什么是缓冲区?
    消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

缓冲区是实现并发的核心,缓冲区的设置有两个好处:

  1. 实现线程的并发协作
    有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离,解除了生产者与消费者之间的耦合。
  2. 解决忙闲不均,提高效率
    生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据。

8.2 实现生产者与消费者模式

/**
 * 馒头类
 */
class ManTo{
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

/**
 * 定义一个缓冲区
 */
class ManToStack{

    private ManTo[] manTos = new ManTo[10];
    
    private int index;
    /**
     * 添加馒头的方法
     */
    public synchronized void push(ManTo manTo){
       while(this.manTos.length == index+1){
       //如果数组的长度等于现在的索引数加一 代表这个数组满了
           try {
               this.wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       this.notify();//唤醒等待的线程
       this.manTos[index++]=manTo;
    }

    /**
     * 取出馒头的方法
     */
    public synchronized ManTo pop(){
        while (this.index==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        this.index--;
        return  this.manTos[this.index];
    }

}

/**
 * 生产馒头的线程
 */
class Produce implements Runnable {

    private ManToStack manToStack;

    public Produce(ManToStack manToStack) {
        this.manToStack = manToStack;
    }

    @Override
    public void run() {
        for(int i=0;i<10;i++){
            ManTo mt = new ManTo();
            mt.setId(i+1);
            System.out.println("生产馒头:"+mt.getId());
            this.manToStack.push(mt);
        }
    }
}

/**
 * 消费馒头线程
 */
class Consume  implements Runnable{

    private ManToStack manToStack;

    public Consume(ManToStack manToStack) {
        this.manToStack = manToStack;
    }

    @Override
    public void run() {
        for(int i=0;i<10;i++){
            ManTo mt = this.manToStack.pop();
            System.out.println("消费馒头:"+mt.getId());
        }
    }
}

public class TestProduceThread1 {
    public static void main(String[] args) {
        ManToStack manToStack = new ManToStack();
        Thread t1 = new Thread(new Produce(manToStack));
        Thread t2 = new Thread(new Consume(manToStack));
        t1.start();
        t2.start();
    }
}

8.3 线程并发协作总结

线程并发协作(也叫线程通信)
生产者消费者模式:

  1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
  2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
  3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
  4. 在生产者消费者问题中,仅有synchronized是不够的。synchronized可阻止并发更新同一个共享资源,实现了同步但是synchronized不能用来实现不同线程之间的消息传递(通信)。
  5. 那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:
方法名作用
final void wait()表示线程一直等待,直到得到其它线程通知
void wait(long timeout)线程等待指定毫秒参数的时间
final void wait(longtimeout,int nanos)线程等待指定毫秒、微秒的时间
final void notify()唤醒一个处于等待状态的线程
final void notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先运行
  1. 以上方法均是java.lang.Object类的方法;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值