java多线程基础

作者: 吾乃零陵上将军邢道荣是也 | 来源:发表于2020-10-04 16:43 被阅读0次

    一、 基础知识

    并发与并行:

    • 并发:指两个或多个事件在同一时间间隔内发生。
    • 并行:指两个或多个事件在同一时刻发生。

    线程与进程:

    • 进程:进程是一个具有某个功能的程序在计算机中的一次动态执行过程,是操作系统进行资源分配和调度的一个独立单位。因为它是资源分配的一个独立单位,所以每个进程都有一个独立的内存空间
    • 线程:首先线程是进程中的一个执行单元。在早期的OS中并没有线程的概念,进程是拥有资源和独立运行的最小单位。后来,由于计算机的发展,对CPU的要求越来越高,而且进程之间的切换开销较大(因为每个进程都有一个独立的内存空间)。为了更好的满足需求,就发明了线程。让线程成为CPU调度的最小单位,而一个进程可以包括多个线程,多个线程共享一个内存空间,这样我们进行线程切换的开销要比进程的切换要小得多。

    总结:
    线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。

    举个例子:我们运行着的一个QQ就是一个进程,我们可以在QQ上进行视频聊天、发语音和文字聊天等功能,这里的三个功能就对应着进程中的线程。

    进程与线程的关系图.png

    二、多线程

    创建线程:
    Java使用 java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java中创建一个新的线程有两种方法。一个是通过继承Thread类来创建并启动多线程,具体步骤如下 :

    • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    • 创建Thread子类的实例,即创建了线程对象。
    • 调用线程对象的start()方法来启动该线程。

    代码如下:

    测试类:
    public class Demo01 { 
          public static void main(String[] args) { 
            //创建自定义线程对象 
            MyThread mt = new MyThread("新的线程!"); 
            //开启新线程 
            mt.start(); 
            //在主方法中执行for循环 
            for (int i = 0; i < 10; i++) { 
                System.out.println("main线程!"+i); 
            } 
        } 
    }
    
    自定义线程类:
    public class MyThread extends Thread { 
        //定义指定线程名称的构造方法 
        public MyThread(String name) { 
           //调用父类的String参数的构造方法,指定线程的名称  
           super(name); 
        }
         //重写run方法,完成该线程执行的逻辑 
         @Override 
         public void run() { 
            for (int i = 0; i < 10; i++) {
               System.out.println(getName()+":正在执行!"+i); 
            } 
         } 
    }
    

    多线程原理:程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的 start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

    Thread类:
    构造方法:

    • 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类方式,还有一种就是实现Runnable接口方式。

    实现Runnable接口来创建线程
    具体步骤如下:

    • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
    • 调用线程对象的start()方法来启动线程。

    代码示例如下:

    public class RunnableImpl implements Runnable{
        @Override
        public void run() {
            for (int i=0;i<20;i++){
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
    
    public class Demo03Runnable{
        public static void main(String[] args) {
            RunnableImpl run=new RunnableImpl();
            //创建线程对象
            Thread t=new Thread(run);
            t.start();
            for (int i=0;i<20;i++){
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
    

    实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现 Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

    实现Runnable接口比继承Thread类所具有的优势:

    • 适合多个相同的程序代码的线程去共享同一个资源。
    • 可以避免java中的单继承的局限性。
    • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
    • 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

    使用匿名内部类来实现线程的创建

    public class Demo04InnerClassThread {
        public static void main(String[] args) {
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName() + "-->" + "程序员");
                    }
                }
            };
    
            new Thread(r).start();
    
        }
    }
    

    三、线程安全

    所谓的线程安全,就是指在多个线程同时运行的情况下,程序仍能按照我们期望的那样运行下去。

    举个例子:

    public class GetCount implements Runnable{
    
        private  Integer count=0;
        
        @Override
        public void run() {
            while (true){
                count++;
                System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            //创建线程任务对象
            GetCount gc=new GetCount();
            //创建三个线程
            Thread t1=new Thread(gc,"线程一");
            Thread t2=new Thread(gc,"线程二");
            Thread t3=new Thread(gc,"线程三");
    
            //同时访问count变量
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    
    运行结果:
    线程一正在访问count,count为:2
    线程三正在访问count,count为:3
    线程二正在访问count,count为:2
    ......
    

    我们可以看到,这里出现了两个2,这种情况明显就是不符合我们的预期,也就是所谓的线程不安全。出现这种问题的原因有很多,最常见的就是,当线程一在进入方法后,拿到了count的值,刚把该值读取出来,但还没有进行count++操作,线程二就进来了,结果导致线程一和线程二拿到的count值是一样的。

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

    线程同步
    为了解决上面所讲的线程安全问题,java为我们提供了同步机制(synchronized)来解决。

    这里我们先讲一下异步和同步的关系。

    • 异步:由于OS中的进程是并发执行的,所以进程以不可预知的速度向前推进。内存中的每个进程何时执行,何时暂停,以怎样的速度向前推进,每道程序总共需要多少时间才能完成等,都是不可预知的。
    • 同步:同步是为了解决异步产生的问题,使得程序最终能够按照我们预期的那样执行。

    java中提供了三种方式来完成同步操作:

    • 同步代码块
    • 同步方法
    • 锁机制

    同步代码块

    • 同步代码块:synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

    格式:

    synchronized(同步锁){
        需要同步操作的代码
    }
    

    同步锁
    对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

    • 锁对象可以是任意类型。
    • 多个线程对象要使用同一把锁。

    注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。

    修改线程任务类的代码:

    public class GetCount implements Runnable{
    
        private  Integer count=0;
    
        Object lock=new Object();
    
        @Override
        public void run() {
            while (true){
                synchronized (lock){
                    count++;
                    System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    运行结果:
    线程一正在访问count,count为:1
    线程三正在访问count,count为:2
    线程二正在访问count,count为:3
    线程一正在访问count,count为:4
    线程二正在访问count,count为:5
    线程三正在访问count,count为:6
    线程一正在访问count,count为:7
    线程三正在访问count,count为:8
    线程二正在访问count,count为:9
    线程一正在访问count,count为:10
    线程二正在访问count,count为:11
    线程三正在访问count,count为:12
    线程一正在访问count,count为:13
    线程二正在访问count,count为:14
    线程三正在访问count,count为:15
    ......
    

    使用了同步代码块后,上述的线程安全问题就解决了。

    同步方法

    • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

    格式:

    public synchronized void method(){ 
        可能会产生线程安全问题的代码 
    }
    

    同步锁是谁?
    对于非static方法,同步锁就是this。
    对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

    修改线程任务类:

    public class GetCount implements Runnable{
    
        private  Integer count=0;
    
        public synchronized void getCount(){
            count++;
            System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
    
        }
    
        @Override
        public void run() {
            while (true){
                getCount();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    执行结果:
    线程一正在访问count,count为:1
    线程三正在访问count,count为:2
    线程二正在访问count,count为:3
    线程一正在访问count,count为:4
    线程三正在访问count,count为:5
    线程二正在访问count,count为:6
    线程三正在访问count,count为:7
    线程二正在访问count,count为:8
    ......
    

    同步方法同样解决了线程不安全的问题。

    Lock锁
    java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

    Lock锁也称同步锁,将加锁与释放锁方法化了,如下:

    • public void lock():加同步锁。
    • public void unlock():释放同步锁。

    使用如下:

    public class GetCount implements Runnable{
    
        private  Integer count=0;
    
        Lock lock=new ReentrantLock();
    
        @Override
        public void run() {
            while (true){
                lock.lock();
                count++;
                System.out.println(Thread.currentThread().getName() + "正在访问count,count为:" + count);
                lock.unlock();
    
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    执行结果:
    线程三正在访问count,count为:1
    线程二正在访问count,count为:2
    线程一正在访问count,count为:3
    线程二正在访问count,count为:4
    线程一正在访问count,count为:5
    线程三正在访问count,count为:6
    线程二正在访问count,count为:7
    线程一正在访问count,count为:8
    线程三正在访问count,count为:9
    ......
    

    四、线程状态和线程池

    线程状态概述
    在线程的生命周期中,拥有六种状态。在api中java.lang.Thread.State这个枚举中给出了六种线程状态。

    线程状态表 线程状态转换图

    我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态即可。

    等待和唤醒机制
    前面我们谈到的都是线程之间的竞争,比如去争夺锁。下面我们讲讲线程之间的协作机制,即等待唤醒机制。

    等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

    • wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中。
    • notify:则选取所通知对象的 wait set 中的一个线程释放;
    • notifyAll:则释放所通知对象的 wait set 上的全部线程。

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

    总结如下:
    如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
    否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态

    调用wait和notify方法需要注意的细节

    • wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
    • wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
    • wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

    举例:生产者与消费者问题

    包子资源类:
    public class BaoZi { 
        String pier ; 
        String xianer ; 
        boolean flag = false ;//包子资源是否存在 
    }
    
    吃货线程类:
    public class ChiHuo extends Thread{
    
        private BaoZi bz;
    
        public ChiHuo(BaoZi bz){
            this.bz=bz;
        }
    
        @Override
        public void run() {
            while (true){
                synchronized (bz){
                    if (bz.flag==false){
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //被唤醒之后
                    System.out.println("吃货正在吃:"+bz.pi+bz.xian+"包子");
    
                    bz.flag=false;
    
                    bz.notify();
    
                    System.out.println("吃货已经把:"+bz.pi+bz.xian+"包子吃完了,包子铺开始生产包子");
    
                    System.out.println("--------------------------");
                }
    
            }
        }
    }
    
    包子铺线程类:
    public class BaoZiPu extends Thread{
        private BaoZi bz;
    
        public BaoZiPu(BaoZi bz) {
            this.bz = bz;
        }
    
        @Override
        public void run() {
            int count=0;
            while (true){
                synchronized (bz){
                    if (bz.flag==true){
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    if (count%2==0){
                        //生产薄皮三鲜馅包子
                        bz.pi="薄皮";
                        bz.xian="三鲜馅";
                    }else {
                        //生产 冰皮 牛肉大葱馅
                        bz.pi="冰皮";
                        bz.xian="牛肉大葱馅";
                    }
                    count++;
                    System.out.println("包子铺正在生产:"+bz.pi+bz.xian+"包子");
    
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    bz.flag=true;
    
                    bz.notify();
    
                    System.out.println("包子铺已经生产好了:"+bz.pi+bz.xian+"包子,吃货可以开吃了");
    
                }
            }
        }
    }
    
    测试类:
    public class Demo {
        public static void main(String[] args) {
            BaoZi bz=new BaoZi();
            new BaoZiPu(bz).start();
            new ChiHuo(bz).start();
    
        }
    }
    
    执行结果:
    包子铺正在生产:薄皮三鲜馅包子
    包子铺已经生产好了:薄皮三鲜馅包子,吃货可以开吃了
    吃货正在吃:薄皮三鲜馅包子
    吃货已经把:薄皮三鲜馅包子吃完了,包子铺开始生产包子
    --------------------------
    包子铺正在生产:冰皮牛肉大葱馅包子
    包子铺已经生产好了:冰皮牛肉大葱馅包子,吃货可以开吃了
    吃货正在吃:冰皮牛肉大葱馅包子
    吃货已经把:冰皮牛肉大葱馅包子吃完了,包子铺开始生产包子
    --------------------------
    ......
    

    线程池
    前面我们每次使用线程的时候就去创建一个线程,这会导致一个问题。比如说系统中并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建和销毁线程就会大大降低系统的效率。

    因此java中提供了一种可以复用线程的方法(线程池),就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。

    • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。

    线程池的使用
    java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

    Executors类中有个创建线程池的方法如下:

    • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线 程池,也就是池中的线程个数可以指定最大数量)

    获取到了一个线程池 ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

    • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。

    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

    使用线程池中线程对象的步骤:

    1. 创建线程池对象。
    2. 创建Runnable接口子类对象。
    3. 提交Runnable接口子类对象。
    4. 关闭线程池(一般不用)。

    示例代码如下:

    public class RunnableImpl implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
        }
    }
    
    public class Demo01ThreadPool {
    
        public static void main(String[] args) {
            //创建线程池对象
            ExecutorService es = Executors.newFixedThreadPool(2);
    
            //从线程池中获取线程对象,然后调用其的run()方法
            es.submit(new RunnableImpl());
            es.submit(new RunnableImpl());
            es.submit(new RunnableImpl());
    
            //关闭线程池
            es.shutdown();
    
        }
    
    }
    

    相关文章

      网友评论

        本文标题:java多线程基础

        本文链接:https://www.haomeiwen.com/subject/fnewuktx.html