java中关于线程的概念

2021/5/6 20:25:20

本文主要是介绍java中关于线程的概念,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

10.线程

10.1.背景知识
  • 进程
    • 官方点的理解:计算机程序在某个数据集合上的运行活动,进程是操作系统进程资源分配与调度的基本单位。即:正在运行的程序(软件)。
    • 每个进程都被操作系统分配了自己独立的内存空间与系统资源,进程之间互不干扰。
    • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程与线程调度
    • 线程依赖于进程而存在,一个进程有多个子任务,而线程就是进程中的一个子任务,一个执行单元,一个顺序控制流。线程是CPU进行资源分配与调度的进本单位。JVM是多线程的。
    • 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,线程之间共享进程的数据。这个应用程序也可以称之为多线程程序。
    • 线程调度:
      • 协同式线程调度(Cooperative Thread-Scheduling):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
        • 使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,通知系统切换到另外一个线程上去。
        • 其最大的好处是实现简单,坏处是线程执行时间不可控
      • 抢占式线程调度(Preemptive Thread-Scheduling):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
        • 使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。
        • 其最大的好处是,线程的执行时间是可控的
        • 设置线程的优先级: 我们在java语言中设置的线程优先级,它仅仅只能被看做是一种"建议"(对操作系统的建议),实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级),线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点
          • 获取线程优先级:public final int getPriority()
          • 设置线程优先级:public final void setPriority(int newPriority)
          • 优先级的范围:1-10,默认是5
            • MIN_PRIORITY = 1
            • NORM_PRIORITY = 5
            • MAX_PRIORITY = 10
        • 抢占式调度详解:大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。这些程序是在同时运行,实际上CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。Java语言采用的是抢占式调度。
  • 串行,并行,并发。
    • 串行:一个任务接一个任务的执行
    • 并发:同一时间段内,多个任务同时执行
    • 并行:同一时刻,多个任务同时执行
      • 并行是一种理想状态下的并发。
10.2.Thread类
  • java.lang.Thread 类,API中该类中定义了有关线程的一些方法
  • 构造方法:
    • public Thread() :分配一个新的线程对象。
    • public Thread(String name) :分配一个指定名字的新的线程对象。
    • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
    • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
  • 常用方法:
    • public String getName() :获取当前线程名称。

    • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

    • public void run() :此线程要执行的任务在此处定义代码。

    • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

    • public static Thread currentThread() :返回对当前正在执行的线程对象的引用

      • Thread.sleep():参数是毫秒
      • 等效的方法 TimeUnit.SECONDS.sleep(10):休眠10秒,单位更加具体
    • public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出

    • public static void yield():暂停当前正在执行的线程对象,并执行其他线程

    • public void interrupt():方法只是改变中断状态而已,它不会中断一个正在运行的线程

    • public final void join(): 等待该线程终止,Join这行代码在哪个线程上运行,就是哪个线程在等待,等待的是使用join方法的这个线程对象执行完。

      实际上,join方法使得等待的线程阻塞,阻塞的是join这行代码在执行的那个线程上的线程。

//1.获取线程名称public class Demo {    public static void main(String[] args) {       // 获取main线程名        Thread thread = Thread.currentThread();        System.out.println(thread.getName());        // 创建对象        MyThread t1 = new MyThread();        //Thread(String name)  利用构造函数给线程设置名字        // 分配新的 Thread 对象。        // 给线程设置名字        t1.setName("吴彦祖");        // 通过getName方法得到的默认线程名  Thread-0        String name = t1.getName();        MyThread t2 = new MyThread("彭于晏");        String name1 = t2.getName();        System.out.println(name1);        //System.out.println(name1);        //System.out.println(name);        //启动线程        t1.start();        //t2.start();    }}// 继承Threadclass MyThread extends Thread {    public MyThread(String name) {        super(name);    }    public MyThread() {    }    @Override    public void run() {        // 在run方法中也能获取到线程名        System.out.println(Thread.currentThread().getName() + ":hello thread");    }}//2.守护线程public class ThreadDaemonDemo {    public static void main(String[] args) throws InterruptedException {        // 创建对象        ThreadDaemon threadDaemon = new ThreadDaemon();        threadDaemon.setName("PDD");        // 标记成守护线程        threadDaemon.setDaemon(true);        // 启动线程        threadDaemon.start();        // 休眠3s        Thread.sleep(3000);        //System.out.println("123");    }}class ThreadDaemon extends Thread {    // 重写run方法    @Override    public void run() {        for (int i = 0; i < 100; i++) {            System.out.println(getName() + "-----" + i);            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}//3.线程合并public class ThreadJoinDemo {    public static void main(String[] args) throws InterruptedException {        System.out.println("main start");        // 创建对象        ThreadJoin threadJoin = new ThreadJoin();        threadJoin.setName("正方形打野");        // 开启线程        threadJoin.start();        threadJoin.join();        System.out.println("main end");    }}class ThreadJoin extends Thread {    // 重写run方法    @Override    public void run() {        for (int i = 0; i < 10; i++) {            System.out.println(getName() + "------" + i);        }    }}//4.线程休眠public class ThreadSleepDemo {    public static void main(String[] args) {        // 创建子类对象        ThreadSleep t = new ThreadSleep();        // 启动线程        t.start();    }}class ThreadSleep extends Thread {    @Override    public void run() {        System.out.println("sleep before");        try {            // 当休眠时间到,就会自己醒来。            Thread.sleep(2000);            // TimeUnit.MILLISECONDS.sleep() 可以按毫秒,秒,分钟,小时,天            // TimeUnit.SECONDS.sleep(2);            // TimeUnit.HOURS.sleep(1);            // TimeUnit.DAYS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("sleep after");    }}//5.线程终止public class ThreadStop {    public static void main(String[] args) throws InterruptedException {        // 创建对象        ThreadInterrupt threadInterrupt = new ThreadInterrupt();        // 启动线程        threadInterrupt.start();        Thread.sleep(3000);        // 泼了一盆冷水,叫醒        // 实际上是根据java当中的异常处理机制        //threadInterrupt.interrupt();        threadInterrupt.stop();    }}class ThreadInterrupt extends Thread {    // 重写run方法    @Override    public void run() {        System.out.println("run start");        try {            Thread.sleep(10000);        } catch (InterruptedException e) {            e.printStackTrace();        }        // stop方法具有固有的不安全性        // 这里就不会再执行了        System.out.println("run end");    }}//6.线程礼让public class ThreadYieldDemo {    public static void main(String[] args) {        // 创建对象        ThreadYield threadYield = new ThreadYield();        ThreadYield threadYield2 = new ThreadYield();        threadYield.setName("55开");        threadYield2.setName("卢本伟");        // 启动2个线程        threadYield.start();        threadYield2.start();    }}class ThreadYield extends Thread {    // 重写run方法    @Override    public void run() {        for (int i = 0; i < 10; i++) {            System.out.println(getName()+"------"+i);            yield();            // 暂停当前正在执行的线程对象,            // 并执行其他线程。            // 虽然放弃了CPU的执行权,但是我们java当中采用的是抢占式的调度方式            // 所以下一次不一定是谁抢到        }    }}
10.3.实现多线程
  • 创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式
10.3.1.方法一
  • Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象
    3. 调用线程对象的start()方法来启动该线程
public class MyThread extends Thread{    /*    * 利用继承中的特点    * 将线程名称传递 进行设置    */    public MyThread(String name){        super(name);    }    /*    * 重写run方法    * 定义线程要执行的代码    */    public void run(){        for (int i = 0; i < 20; i++) {            //getName()方法 来自父亲            System.out.println(getName()+i);        }    }}public class Demo {    public static void main(String[] args) {        System.out.println("这里是main线程");        MyThread mt = new MyThread("小强");        mt.start();//开启了一个新的线程        for (int i = 0; i < 20; i++) {            System.out.println("旺财:"+i);        }    }}//对于这种方式实现的多线程,如果存在变量不一致的问题,可以采用static关键字来解决。
  • 注意事项:
    1. 一个Thread子类对象代表一个线程
    2. 只有Thread run()方法中的代码,才会执行在子线程中,保证运行其中的是我们想要在子线程中运行的代码(run方法封装的是被线程执行的任务)
    3. 一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中
    4. 启动线程,必须使用start()方法来启动,才能是Thread中的run方法运行在子线程中。调用run方法执行Thread的run方法代码,这仅仅只是普通的方法调用
    5. 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次,如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象
10.3.2.方式二
  • 实现 java.lang.Runnable 也是非常常见的一种,只需要重写run方法即可。
    1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    3. 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{    @Override    public void run() {        for (int i = 0; i < 20; i++) {            System.out.println(Thread.currentThread().getName()+" "+i);        }    }}public class Demo {    public static void main(String[] args) {        //创建自定义类对象 线程任务对象        MyRunnable mr = new MyRunnable();        //创建线程对象        Thread t = new Thread(mr, "小强");        t.start();        for (int i = 0; i < 20; i++) {            System.out.println("旺财 " + i);        }    }}
  1. Thread类实际上也是实现了Runnable接口的类。
  2. 在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
  3. 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
10.3.4.匿名内部类创建线程
public class NoNameInnerClassThread {    public static void main(String[] args) {    //new Runnable(){    //    public void run(){    //        for (int i = 0; i < 20; i++) {    //            System.out.println("张宇:"+i);    //        }    //    }    //}; //‐‐‐这个整体 相当于new MyRunnable()            Runnable r = new Runnable(){            public void run(){                for (int i = 0; i < 20; i++) {                    System.out.println("张宇:"+i);                }            }        };        new Thread(r).start();            for (int i = 0; i < 20; i++) {            System.out.println("费玉清:"+i);        }    }}
10.4.Thread和Runnable的区别
  • 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
  • 实现Runnable接口比继承Thread类所具有的优势:
    1. 适合多个相同的程序代码的线程去共享同一个资源。
    2. 可以避免Java中的单继承的局限性。
    3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    4. 线程池只能放入实现Runable或Callable的类的线程,不能直接放入继承Thread的类。

    注:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程

10.5.线程安全
10.5.1.线程同步
  • 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
  • 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了线程同步机制(synchronized)来解决。
  • 三种方法:
    1. 同步代码块。
    2. 同步方法。
    3. 锁机制。
10.5.2.同步代码块
  • synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){    //需要同步操作的代码}public class Ticket implements Runnable{    private int ticket = 100;    Object lock = new Object();    /*    * 执行卖票操作    */    @Override    public void run() {        //每个窗口卖票的操作        //窗口 永远开启        while(true){            synchronized (lock) {                if(ticket>0){//有票 可以卖                    //出票操作                    //使用sleep模拟一下出票时间                    try {                        Thread.sleep(50);                    } catch (InterruptedException e) {                        // TODO Auto‐generated catch block                        e.printStackTrace();                    }                    //获取当前线程对象的名字                    String name = Thread.currentThread().getName();                    System.out.println(name+"正在卖:"+ticket‐‐);                }            }        }    }}public class Demo {    public static void main(String[] args) {        //创建线程任务对象        Ticket ticket = new Ticket();        //创建三个窗口对象        Thread t1 = new Thread(ticket, "窗口1");        Thread t2 = new Thread(ticket, "窗口2");        Thread t3 = new Thread(ticket, "窗口3");        //同时卖票        t1.start();        t2.start();        t3.start();    }}

同步锁:

  1. 锁对象可以是任意的Java对象。
  2. 对于非static方法,同步锁对象就是this。
  3. 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
10.5.3.同步方法
  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){    //可能会产生线程安全问题的代码}public class Ticket implements Runnable{    private int ticket = 100;    /*    * 执行卖票操作    */    @Override    public void run() {        //每个窗口卖票的操作        //窗口 永远开启        while(true){            sellTicket();        }    }        /*    * 锁对象:是谁调用这个方法,就是谁    * 隐含锁对象就是 this    */    public synchronized void sellTicket(){        if(ticket>0){//有票 可以卖            //出票操作            //使用sleep模拟一下出票时间            try {                Thread.sleep(100);            } catch (InterruptedException e) {                // TODO Auto‐generated catch block                e.printStackTrace();            }            //获取当前线程对象的名字            String name = Thread.currentThread().getName();            System.out.println(name+"正在卖:"+ticket‐‐);        }    }}
10.5.4.Lock锁
  • java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
  • Lock锁也称同步锁,加锁与释放锁方法化了:
    • public void lock() :加同步锁。
    • public void unlock() :释放同步锁。
public class Ticket implements Runnable{    private int ticket = 100;    Lock lock = new ReentrantLock();    /*    * 执行卖票操作    */    @Override    public void run() {        //每个窗口卖票的操作        //窗口 永远开启        while(true){            lock.lock();            if(ticket>0){//有票 可以卖                //出票操作                //使用sleep模拟一下出票时间                try {                    Thread.sleep(50);                } catch (InterruptedException e) {                    // TODO Auto‐generated catch block                    e.printStackTrace();                }                //获取当前线程对象的名字                String name = Thread.currentThread().getName();                System.out.println(name+"正在卖:"+ticket--);            }            lock.unlock();        }    }}
10.6.线程状态
  • java.lang.Thread.State 这个枚举中给出了六种线程状态
线程状态导致状态发生条件
New(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
10.6.1.Timed Waiting计时等待
  • 一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
  • 当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待)。
public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print(" 线程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
    new MyThread().start();
    }
}
  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
  4. sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始立刻执行。
10.6.2.BLOCKED(锁阻塞)
  • 一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
10.6.3.Waiting(无限等待)
  • 一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
public class WaitingTest {
    public static Object obj = new Object();
    
    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 象,调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait(); //无限等待
                            //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 从waiting状态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                // while (true){ //每隔3秒 唤醒一次
                try {
                    System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj){
                    System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对象,调用notify方法,释放锁对象");
                    obj.notify();
                }    
            }
        // }
        },"唤醒线程").start();
    }
}
  • 通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
  • 其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
  • 当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

注:

​ 我们在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。

10.7.死锁
  • 死锁:线程一占用A资源,等待B资源,线程二占用B资源,等待A资源,这种互锁现象称为死锁

    个人理解:死锁如果用通俗易懂的话来解释,其实就像我们生活中,和迎面而来的人相遇,给对面的人让路,对面的人也给我们让路,然而两个人让路的方向是同一边,这样就是死锁。

  • 解决方法:
    1. 更改加锁顺序
    2. 新加一把锁,把这个操作变成原子操作
public class Demo {    public static void main(String[] args) {        DieLock dieLock1 = new DieLock(true);        DieLock dieLock2 = new DieLock(false);        new Thread(dieLock1).start();        new Thread(dieLock2).start();    }}// 描述锁对象class MyLock {    public static final Object objA = new Object();    public static final Object objB = new Object();    public static final Object objAB = new Object();}class DieLock implements Runnable {    boolean flag;    public DieLock(boolean flag) {        this.flag = flag;    }    @Override    public void run() {        if (flag) {                synchronized (MyLock.objA) {                    System.out.println("if A");                    // 发生了线程切换                    synchronized (MyLock.objB) {                        System.out.println("if B");                    }                }            }        }        else {            synchronized (MyLock.objB) {                System.out.println("else B");                // 打印了else B                synchronized (MyLock.objA) {                    System.out.println("else A");                }            }        }            //解决方法:        //1.新加一把锁        if (flag) {            synchronized (MyLock.objAB) {                synchronized (MyLock.objA) {                System.out.println("if A");                // 发生了线程切换                synchronized (MyLock.objB) {                    System.out.println("if B");                }            }        }else {            synchronized (MyLock.objAB) {                synchronized (MyLock.objB) {                    System.out.println("else B");                    // 打印了else B                    synchronized (MyLock.objA) {                        System.out.println("else A");                    }                }            }        }                //2.更改加锁顺序,就是把原来程序的其中一个分支调换顺序      }}
10.8.线程通信
  • 以生产者,消费者,包子铺为例子,说明线程通信
public class Box {    // 包子    Food food;    // 生产包子  只有生产者线程调用setFood方法    public synchronized void setFood(Food newFood) throws InterruptedException {        //先判断蒸笼是否为空        //如果蒸笼为空,没有包子        if (food == null) {            //做包子并放入蒸笼,            food = newFood;            System.out.println(Thread.currentThread().getName() + "做了:" + food);            Thread.sleep(3000);            // 通知消费者来吃            notify();            //notifyAll();        } else {            //如果蒸笼非空            //说明蒸笼里有包子,            // 阻止自己,不在生产            try {                wait();            } catch (InterruptedException e) {                e.printStackTrace();            }            //System.out.println("生产者 wait 后面的代码");        }    }    // 吃包子 只有消费者线程调用eatFood方法    public synchronized void eatFood() throws InterruptedException {        // 先判断蒸笼状态        if (food == null) {            //如果蒸笼为空,没有包子            //阻止自己            try {                wait();            } catch (InterruptedException e) {                e.printStackTrace();            }            //System.out.println("消费者 wait 后面的代码");        } else {            //如果蒸笼非空 有包子            //吃包子            System.out.println(Thread.currentThread().getName() + "吃了:" + food);            Thread.sleep(3000);            food = null;            //通知生产者去再生产包子            notify();            //notifyAll();        }    }}// 该类描述包子class Food {    String name;    int price;    public Food(String name, int price) {        this.name = name;        this.price = price;    }    @Override    public String toString() {        return "Food{" +                "name='" + name + '\'' +                ", price=" + price +                '}';    }}public class ConsumerTask implements Runnable {    // 蒸笼    Box box;    public ConsumerTask(Box box) {        this.box = box;    }    @Override    public void run() {        while (true) {            try {                box.eatFood();            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class ProducerTask implements Runnable {    Box box;    Food[] foods = {new Food("庆丰包子", 8),            new Food("上海生煎包", 2), new Food("广式叉烧包", 10)};    Random random;    public ProducerTask(Box box) {        this.box = box;        random = new Random();    }    @Override    public void run() {        while (true) {            // 只生产包子            int index = random.nextInt(foods.length);            try {                box.setFood(foods[index]);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class Demo {    public static void main(String[] args) throws InterruptedException {        Box box = new Box();        ProducerTask producerTask = new ProducerTask(box);        ConsumerTask consumerTask = new ConsumerTask(box);        //创建2个线程并启动        // 生产者线程        Thread t1 = new Thread(producerTask);        // 消费者线程        Thread t2 = new Thread(consumerTask);        Thread t3 = new Thread(producerTask);        // 消费者线程        Thread t4 = new Thread(consumerTask);        t1.setName("生产者1");        t2.setName("消费者1");        t3.setName("生产者2");        t4.setName("消费者2");        t1.start();        t2.start();        t3.start();        t4.start();    }}
  • 解释:
    1. 对于notify方法,两个生产者,两个消费者,当生产者生产完毕后,通知的却不一定是消费者,notify方法是随机的,如果另一个生产者抢到了执行权,进入if判断语句,发现已经有包子了,那么程序就会执行else语句,进入等待。
    2. 对于notifyAll方法,当生产者生产完毕后,唤醒所有线程,三个线程需要争夺执行权限,此时如果另一个生产者抢到了执行权,会进入阻塞状态,剩余两个消费者再次争夺,当某个消费者消费完成后,执行notifyAll方法,唤醒其余线程,进入阻塞状态的生产者会立即执行完wait后面的代码,并立即加入本轮争夺,也就是说,此次消费者消费完成后,依旧是有两个生产者争夺执行权。
    3. 某个线程想要执行,需要两个条件同时满足,第一,获取到锁对象,第二,抢到了cpu执行权,即其他线程调用了notify方法或notifyAll方法,本线程争夺到了cpu执行权。但是需要注意的是,两个条件必须同时满足,假设这样一种情形,某个程序只有两个线程,一个线程正在运行时,调用了notify方法或notifyAll方法,但是自身却没有执行wait方法释放锁对象,那么另一个线程就会进入阻塞状态,直到正在运行的线程释放了锁对象。
10.9.线程池
10.9.1.概念
  • 一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
  • 优点:
    1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
10.9.1.线程池的创建
  • Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象
    • 不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:
      Executors.newCachedThreadPool();    //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUEExecutors.newSingleThreadExecutor();  //创建容量为1的缓冲池Executors.newFixedThreadPool(int n);  //创建固定容量大小为n的缓冲池
      
    • 一般需要根据任务的类型来配置线程池大小:
      1. 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 N(CPU) + 1
      2. 如果是IO密集型任务,参考值可以设置为2*N(CPU)
  • 线程池的创建:JDK5提供了Executors来产生线程池,有如下方法
    • public ExecutorService newCachedThreadPool()
      1. 会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程
      2. 线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务)
    • public ExecutorService newFixedThreadPool(int nThreads)
      1. 线程数量固定
      2. 维护一个无界队列(暂存已提交的来不及执行的任务)
      3. 按照任务的提交顺序,将任务执行完毕
    • public ExecutorService newSingleThreadExecutor()
      1. 单个线程
      2. 维护了一个无界队列(暂存已提交的来不及执行的任务)
      3. 按照任务的提交顺序,将任务执行完毕
10.9.2.线程池的使用
  • 定义了一个使用线程池对象的方法如下:
    • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • 使用线程池中线程对象的步骤:
    1. 创建线程池对象。
    2. 创建Runnable接口子类对象。(task)
    3. 提交Runnable接口子类对象。(take task)
    4. 关闭线程池(一般不做)。
  • ExecutorService(接口):Future submit(Callable task)
    • 代表有返回值的异步任务
    • Future对象代表异步任务的计算结果 可通过get方法获取
    • public Future<?> submit(Runnable task):代表没有返回值的异步任务
  • 关闭线程池:
    • public shutdown():启动一次顺序关闭,执行以前提交的任务,但不接收新任务
    • public shutdownNow():立刻关闭
public class ThreadPoolDemo {    public static void main(String[] args) {        // 创建线程池对象        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象        // 创建Runnable实例对象        MyRunnable r = new MyRunnable();        //自己创建线程对象的方式        // Thread t = new Thread(r);        // t.start(); ‐‐‐> 调用MyRunnable中的run()        // 从线程池中获取线程对象,然后调用MyRunnable中的run()        service.submit(r);        // 再获取个线程对象,调用MyRunnable中的run()        service.submit(r);        //线程池只有两个线程对象,此任务会进入队列,等待下一轮执行        service.submit(r);        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。        // 将使用完的线程又归还到了线程池中        // 关闭线程池,执行完全部任务后关闭        //service.shutdown();        //执行两个任务就会关闭        //service.shutdownNow();    }}class MyRunnable implements Runnable {    @Override    public void run() {        System.out.println("我要一个教练");        try {            //这里的sleep函数可以清楚地看到执行顺序            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("教练来了: " + Thread.currentThread().getName());        System.out.println("教我游泳,教完后,教练回到了游泳池");    }}
10.10.定时器
  • 定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式来执行。
    • 定时器用来管理时间
    • Timer触发执行的任务就是定时任务,即TimerTask
    • 用来完成调度定时任务的功能
//创建定时器Timer timer = new Timer()//调度定时任务//在指定的时间点,调度定时任务执行schedule(TimerTask task, Date time)//在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务schedule(TimerTask task, long delay, long period)//在firstTime时间点,首次执行,之后每period毫秒执行一次schedule(TimerTask task, Date firstTime, long period)//在delay毫秒的延时后,首次调度task执行,之后每period毫秒执行一次scheduleAtFixedRate(TimerTask task, long delay, long period)    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");
  • TimerTask(抽象类):表示待执行的定时任务
  • 定义一个定时任务
    • 继承TimerTask
    • 重写run方法
  • 终止定时器
    • timer 当中的的cancle方法:定时器中所有任务都会被终止
public class Demo {    public static void main(String[] args) throws ParseException {        // /在指定的时间点,调度定时任务执行        //schedule(TimerTask task, Date time)        在delay毫秒的延时之后,首次调度task执行,之后每period毫秒执行一次定时任务        //schedule(TimerTask task, long delay, long period)        // 创建定时器        Timer timer = new Timer();        // 进行任务调度        String s = "2021-01-23 11:43:40";        // SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        //Date firstTime = simpleDateFormat.parse("2021-01-23 07:38:59");        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        Date date = simpleDateFormat.parse(s);        //timer.schedule(new MyTask(),date);        timer.schedule(new MyTask(),3000,5000);        timer.cancel();    }}/***如何定义一个定时任务**- 继承TimerTask- 重写run方法* */class MyTask extends TimerTask {    @Override    public void run() {        // 定时任务要做的事情写到run方法里        System.out.println("boom! 爆炸了!");    }}
10.11.Callable
public class Demo {    public static void main(String[] args) throws ExecutionException, InterruptedException {        // 创建线程池        ExecutorService pool = Executors.newFixedThreadPool(2);        // 提交任务        Future future = pool.submit(new MyCallable());        //get() 获取计算结果        //如有必要,等待计算完成,然后获取其结果。        System.out.println("get before");        String s = (String) future.get();        System.out.println(s);    }}/*可以理解为多线程的实现方式3 ,但是必须要依赖线程池*/class MyCallable implements Callable {    @Override    public Object call() throws Exception {        for (int i = 0; i < 10; i++) {            System.out.println(Thread.currentThread().getName() + "------" +i);        }        Thread.sleep(3000);        return "hello";    }}
  • Runnable和Callable的区别:
    1. Callable规定的被重写的方法是call(),Runnable规定的被重写的方法是run()
    2. Callable的任务执行后可以有返回值,而Runnable的任务没有返回值
    3. call方法可以抛出异常,run方法不可以
    4. 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
10.12.volatile
  • volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
  • 特性:
    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
    • 禁止进行指令重排序。(实现有序性)
    • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

      关于volatile原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

  • volatile写:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
  • volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
  • volatile指令重排:volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:
    第一个操作/第二个操作普通读写volatile读volatile写
    普通读写不允许
    volatile读不允许不允许不允许
    volatile写不允许不允许


这篇关于java中关于线程的概念的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程