多线程整理

1、线程概述:

1.1、进程

正在进行中的程序。其实进程就是一个应用程序运行时的内存分配空间。

1.2、线程

在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看作程序的一条条线索,被称为线程。当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。是进程中的一个执行路径,共享一个内存,线程间可以自由切换,并发执行,一个进程最少有一个线程。

2、线程的创建

在Java中,可以通过三种方式来实现多线程;第一种是继承Thread类,重写run()方法;第二种是实现Runnbale接口,重写run()方法;第三种是实现Callable接口,重写call()方法,并使用Future来获取call()方法的返回结果。

2.1、Thread类实现多线程

Thread类是java.lang包下的一个线程类,用来实现java多线程,步骤如下:
(1)、创建Thread子类;
(2)、重写Thread类的run()方法;
(3)、创建子类的实例对象;
(4)、通过实例对象调用start()方法来启动线程;

2.2、Runnable接口实现多线程

通过Thread类的方式创建多线程有一定的局限性,Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程,在这种情况下可以通过实现Runnable接口的方式实现多线程,步骤如下:
(1)、创建Runnable接口实现类,重写接口中的run(0方法;
(2)、创建Runnable接口的实现类对象;
(3)、使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类是实例对象多为参数传入;
(4)、调用线程实例的start()方法启动线程。

2.3、Callable接口实现多线程

通过Thread类和Runnable接口实现多线程时,需重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回值结果。从JDK5开始,java提供了一个Callable接口,来满足这种既能创建多线程又可以有返回值的需求。步骤如下:
(1)、创建一个Callable接口的实现类,同时重写Callable接口的call()方法;
(2)、创建Callable接口的实现类对象;
(3)、通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;
(4)、使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
(5)、调用线程实例的start()方法启动线程;

2.4、三种实现多线程方式的对比分析:

通过实现Runnable接口(或Callable几口)相对于继承Thread类实现多线程来说,有如下显著好处:
(1)、适合多个线程去处理同一个共享资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想;
(2)、可以避免Java单继承带来的局限性。由于一个类不能同时有两个父类,所以在当前类已经有一个父类的基础上,只能采用实现Runnable接口或者Callable接口的方法来实现多线程。

2.5、后台线程

对于Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了setDaemon(ture)语句,这个线程就变成了一个后台进程。

3、线程的生命周期及状态转换

  • NEW
    尚未启动的线程处于此状态。

  • RUNNABLE
    在Java虚拟机中执行的线程处于此状态。

  • BLOCKED
    被阻塞等待监视器锁定的线程处于此状态。

  • WAITING
    无限期等待另一个线程执行特定操作的线程处于此状态。

  • TIMED_WAITING
    正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。

  • TERMINATED
    已退出的线程处于此状态。
    在这里插入图片描述

4.线程调度

4.1、线程的优先级

线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
Thread有三个常量定义了线程的优先级范围

  • Thread.MIN_PRIORITY;最低1
  • Thread.NORM_PRIORITY;默认5
  • Thread.MAX_PRIORITY;最高10

4.2、线程休眠

优先级高的线程有更大的概率优先执行,而优先级低的线程可能会后执行,如果想人为的控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(ling millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待时间,这样其他的线程就可以得到执行的机会。slleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或声明抛出该异常。
需要注意的是,线程类Thread提供了两种线程休眠方法:sleep(long millis)和sleep(long millis,int nanos),这两种方法都带有休眠时间参数,当其他线程都会终止后并不代表当前休眠的线程会立即执行,而是必须当休眠时间结束后,线程才会转换到就绪状态。

4.3、线程让步

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。

5、多线程同步

5.1、同步代码块

当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个使用synchronized关键字修饰的代码块中,这段代码块被称作同步代码块。格式如下:

synchronized(lock){
    //操作共享资源代码块
        }

其中lock是一个锁对象,当线程执行同步代码块时,首先会先检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等到当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中代码。
示例:

package com.anli;
public class Demo3 {
    public static void main(String[] args) {
        //线程不安全解决办法一
        //同步代码块
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
    static class Ticket implements Runnable {
        //总票数
        private int count = 10;
        private Object o = new Object();

        @Override
        public void run() {

                while (true) {
                    synchronized (o) {
                    if (count > 0) {
                        System.out.println("正在准备卖票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                    }else{
                        break;
                    }
                }
            }
        }
    }
}

执行结果:在这里插入图片描述
注意:同步代码块中的锁对象是任意类型的对象,但多个线程共享的锁对象必须是相同的锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间便不能产生同步效果。

5.2、同步方法

在方法前使用synchronized关键字来修饰,被修饰的方法为同步方法,他能实现和同步代码块相同的功能。格式如下:

[修饰符] synchronized 返回值类型 方法名([参数]){}

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
示例:

package com.anli;
public class Demo4 {
    public static void main(String[] args) {
        Runnable run = new Demo4.Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
    static class Ticket implements Runnable{
        private int count = 10;
        @Override
        public void run() {
            while(true){
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }

            }
            public synchronized boolean sale(){
                if (count > 0) {
                    System.out.println("正在准备卖票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                    return true;
                }
                return false;
            }
        }
}

同步代码块和同步方法解决多线程问题有好处也有弊端,同步解决了多个线程同时访问共享资源时的线程安全问题,只要加上通过一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码块时每次都会判断锁的状态,非常消耗资源,效率较低

5.3、同步锁

从JDK5开始,Java增加了一个功能更强大的Lock锁,Lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外Lock锁在使用时也更加灵活
示例:

package com.anli;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo5{
    public static void main(String[] args) {
        Runnable run = new Demo5.Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        //显式锁
        private Lock l = new ReentrantLock();

        @Override
        public void run() {
            while(true) {
                l.lock();
                if (count > 0) {
                    System.out.println("正在准备卖票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);

                }else{
                    break;
                }
                l.unlock();
            }
        }
    }
        }

通过Lock接口的实现类ReentrantLock来创建一个Lock锁对象,并通过Lock锁对象的lock()方法和unlock()方法对核心代码块进行了上锁和解锁。需要注意的是ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了lock()方法和unlock()方法外,还提供了一些其他同步锁操作的方法。在使用Lock同步锁时,可根据需要在不同的代码位置灵活地上锁和解锁,为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常情况下会在finally()代码块中调用unlock()方法来解锁。

5.4、显式锁与隐式锁

5.4.1、定义

  • 隐式锁(Synchronized)是Java的关键字,当它用来修饰一个方法或一个代码块时,能够保证在同一时刻最多只有一个线程执行该代码。因为当调用Synchronized修饰的代码时,并不需要显示的加锁和解锁的过程,所以称之为隐式锁。
  • 显示锁(Lock)是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。

5.4.2、区别

1.出身不同

  • Synchronized:Java中的关键字,是由IVM来维护的,是JVM层面的锁。
    Synchronized的获取锁和释放锁是在JVM内部实现的,他就相当于给当前线程做了一个标记,由JVM内部去识别,并做出相应反应。

  • Lock:是JDK5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。
    lock是通过调用对应的API方法来获取锁和释放锁的。

2.使用方法不同

  • Synchronized是隐式锁。Lock是显示锁

  • 所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。

  • 我们大家都知道,在使用Synchronized关键字的时候,我们使用者根本不用写其他的代码,然后程序就能够获取锁和释放锁了。那是因为当Synchronized代码块执行完成之后,系统会自动的让程序释放占用的锁。Sync是由系统维护的,如果非逻辑问题的话,是不会出现死锁的。

  • 在使用lock的时候,我们使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:lock.lock()。释放锁:unlock方法。需要配合tyr/finaly语句块来完成。

//隐式锁
synchronized(锁对象){
    //任务代码
}

//显示锁
	Lock l = new ReentrantLock()
    @Override
    public void run(){
    	l.lock;
        //任务代码
        try{
        
        }finaly{
            l.unlock;
        }
    }

3.等待是否可中断

  • Synchronized是不可中断的。除非抛出异常或者正常运行完成
  • Lock可以中断的。中断方式:
    (1)、调用设置超时方法tryLock(long timeout ,timeUnit unit)
    (2)、调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断

4.加锁的时候是否公平

  • Synchronized是非公平锁
  • Lock既可以是公平锁也可以是非公平锁,它默认是非公平锁,在调用其构造方法时可以传入Boolean类型的参数,如果是true,则是公平锁;如果false,则是非公平锁。

5.能否精准唤醒

  • Synchronized: 不能精准唤醒,要么随机唤醒一个线程;要么是唤醒所有等待的线程。
  • Lock: 用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像Synchronized那样,不能精确唤醒线程。

6.性能比较

  • Synchronized是托管给JVM执行的,而Lock是Java写的控制锁的代码。
  • 在Java1.5中,Synchronized是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
  • 但是到了Java1.6版本,发生了变化。Synchronized在语义上很清晰,可以进行很多优化,实现了许多以前没有的操作,Synchronized的性能这个时候并不比Lock性能差,并且官方也表示,他们更支持Synchronized,在未来的版本中还有优化的空间。

5.5、死锁

当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

6、多线程通信

Java在Object类中提供了wait()、notify()、notifyAll()等方法用余解决线程间的通信问题,任何类的实例对象都可以直接使用这些方法。

方法声明功能描述
void wait()使当前线程放弃同步锁并进入等待状态,直到其他线程进入此同步锁,
并调用notify()或notifyAll()方法唤醒该线程为止
void notify()唤醒此同步锁上等待的第一个调用wait()方法的线程
void notifyAll()唤醒此同步锁上调用wait()方法的所有线程

示例:

package com.anli;
public class Demo6 {
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();

    }
    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndsetTaste("小米粥","甜");
                }else{
                    f.setNameAndsetTaste("煎饼果子","咸");
                }
            }
        }
    }//服务员
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();

            }
        }
    }
    //食物
    static class Food{
        private String name;;
        private String taste;
        boolean flag = true;

       public synchronized void get(){
           if(!flag){
           System.out.println("服务员端走的菜的名称是:"+name+"味道"+taste);
       }
           flag = true;
           this.notifyAll();
           try {
               this.wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }

       public synchronized void setNameAndsetTaste(String name,String taste) {
           if (flag) {
               this.name = name;
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               this.taste = taste;
           }
           flag = false;
           this.notifyAll();
           try {
               this.wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
}

7、线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处

  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。

Java中有四种线程池:

  • 缓存线程池
  • 定长线程池
  • 单线程线程池
  • 周期定长线程池
  • 7.1、缓存线程池

  • 缓存线程池:长度无限制
    执行流程:
    (1)、判断线程池是否存在空闲线程
    (2)、存在则使用
    (3)、不存在,则创建线程并放入线程池, 然后使用

示例:

package com.anli;

import javax.print.attribute.standard.RequestingUserName;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 缓存线程池
 */
public class Demo8 {
    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();
        //指挥线程池中执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });

    }
}

执行结果:pool-1-thread-2锄禾日当午 pool-1-thread-1锄禾日当午 pool-1-thread-3锄禾日当午 pool-1-thread-3锄禾日当午

从最后可以看到,重复利用了线程3.

7.2、定长线程池

指定长度的线程池。
示例:

package java03.com.app.core.section5;

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

/**
 * @Author: deemoHui
 * @Description: 定长线程池
 * @Date Created in 2020-08-10 18:00
 * @Modified By:
 */
public class FixedLengthRoutePoolDemo {
    /*定长线程池
   长度是指定的线程池
   加入任务后的执行流程
       1 判断线程池是否存在空闲线程
       2 存在则使用
       3 不存在空闲线程  且线程池未满的情况下  则创建线程  并放入线程池中  然后使用
       4 不存在空闲线程  且线程池已满的情况下  则等待线程池的空闲线程
   **/
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"锄禾日当午");
            }
        });
        service.shutdown();
    }
}

执行结果:pool-1-thread-2锄禾日当午 pool-1-thread-1锄禾日当午 pool-1-thread-2锄禾日当午
可以看到只有两个线程,最后重复使用了线程2。

7.3、单线程线程池

长度为1的定长线程池。

7.4、周期定长线程池

按照一定时间间隔执行的线程池。
示例:

package com.anli;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 周期定长线程池
 */
public class Demo9 {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
         * 定时执行一次
         * 参数1.定时执行的任务
         * 参数2.时长数字
         * 参数3.时长数字的时间单位,TimeUnit的常量指定

        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("锄禾日当午");
            }
        },5, TimeUnit.SECONDS);*/
        /**
         * 周期执行的任务
         * 参数1.任务
         * 参数2.延迟时长数字(第一次执行在多少秒以后)
         * 参数3.周期时长数字(每隔多久执行一次)
         * 参数4.时长数字的单位
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("汗滴禾下土");

            }
        },5,1,TimeUnit.SECONDS);

    }
}

8、Lambda表达式

简化代码的一种方法。
格式:-> + 函数的主体
示例:

package com.anli;

public class Demo10 {
    public static void main(String[] args) {
        /**
         * lambda表达式
         * 函数式编程思想
         * 面向对象:创建对象调用方法,解决问题
         * 函数式编程:不关注过程,只注重结果
         */
        //冗余的Runnable代码
        /*Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("锄禾日当午");
            }
        });
        t.start();*/
        Thread t = new Thread(() -> System.out.println("锄禾日当午"));
        t.start();

    }


}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值