• Java多线程梳理


    这几天学的Java多线程,多线程涉及的挺多,但我只是学的基础部分,写一篇博客,也算给自己整理一下吧。以后有空也可以多回顾一下。好吧,go!

    一.进程与线程

    学计算机的同学都学过操作系统这门课,可惜我不是计算机专业的。但是进程这个概念不一定要计算机系的同学才能了解,平时用操作系统的任务管理器都可以了解。

    给进程下个定义,那就是指操作系统进行资源分配和调度的基本单位,如打开qq,qq在后台就是一个进程,或者打开浏览器,浏览器可能会有多个进程。

    所谓线程:是程序使用cpu的基本单位。多个线程就是多条执行路径。

    Java程序是运行在Java虚拟机上的,这个大家都知道。java程序启动也会启动JVM,也相当于启动了一个进程。而且,一个Java程序会至少会启动一个进程。

    jvm是多线程,如何理解?因为还有个GC

    线程有两个调度模型,一个是CPU分时调度,还有一个是抢占式。java的多线程就是属于抢占式,如何理解呢,就是调度哪个,什么时候调度是由CPU决定的。多线程中,假如A线程执行到一半,可能会中途被CPU挂起,然后CPU去执行线程B,同样在执行线程B中,也可能挂起,然后去执行线程C。


    二、线程的实现

    两种方式

    1、继承Thread类,重写run方法,新建实例,调用start方法

    2、类A实现Runnable接口,重写run方法,A类实例化,新建一个Thread实例,同时将A类对象传入,Thread子类调用start方法。

    看代码吧

    第一种方式

    public class ThreadDemo {
        
        public static void main(String[] args) {
    //        3、实例化一个对象
            MyThread Luna=new MyThread();
            MyThread Mars=new MyThread();
            Luna.setName("Luna-----");
            Mars.setName("Mars-----");
    //        4、调用对象的run()方法
            Luna.start();
            Mars.start();
        }
    }
    //1、继承Thread类
    class MyThread extends Thread{
    //    2、重写父类的run()方法
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println(getName()+"-----"+i);
            }
        }
    }

    第二种方式,实现runnable接口

    public class RunThread {
        public static void main(String[] args) {
            
            MyRunThread runThread=new MyRunThread();
            Thread thread1=new Thread(runThread, "Luna");
            Thread thread2=new Thread(runThread, "Mars");
            thread1.start();
            thread2.start();
        }
    }//1.实现runnable接口
    class MyRunThread implements Runnable{
        //2.重写run方法
        @Override
        public void run() {
            for (int i = 0; i < 30; i++) {
                System.out.println(Thread.currentThread().getName()+"-----"+i);
            }
        }
    }

    这里要说明的一点是,其实Thread类也是实现了Runnable接口的。


    三、线程控制及优先级

    Java提供了挺多的方法,简单说几个常用的

    线程睡眠:sleep(long millis),睡眠多少毫秒.会抛出异常InterruptedException.这个异常会在interrupt( )方法后抛出

    线程加入:public final void join(),指的是将线程合并到另一个线程,如合并到main线程,这个也会造成线程阻塞。

    线程礼让:public static void yield()指的是让另一个线程先执行,至于是哪个线程,就看CPU决定

    线程优先级,由1-10,默认是5,可以通过setPriority()方法设置优先级.但是这个优先级只是在java程序中有优先级,CPU并不一定会是优先执行.


     四、线程安全

    线程安全
    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

    上面是从百度百科里面参考的,我觉得表达的比较清楚。总结一下发生线程不安全的原因

    1.前提肯定是要有多线程

    2.有数据共享

    3.多线程对共享数据的操作不是原子操作。

    理解一个概念,什么是原子操作?原子在物理学中,看作是不可再分的,所谓原子操作就是要么执行完毕,要么不执行。为何呢,前面说了,多线程时候,CPU可能执行到中途,会把当前线程挂起,然后去执行其他线程。所以在执行共享数据代码时候执行到某个时刻,停了一下,这时候其他线程又来执行。所以出现了线程不安全的问题。

    理解了出现线程不安全的原因,那么就容易解决了。既然不是原子操作,那就将共享数据代码块编程原子操作,用synchronized关键字,加大括号可以将代码锁定

    在使用synchronized关键字时候,要传入一个对象,这个对象作为一个锁,如果要锁定,那就注意这个锁一定要是唯一的,不能有多个锁。

    好吧,贴一段代码。

    import java.util.Scanner;
    /*
         请实现一个类,继承自Thread,来实现模拟迅雷多线程下载程序。
         要求:迅雷每个线程可以下载1M的资源。对一个文件大小为x M的资源,进行下载,
        动态调整下载线程的个数(命令行输入下载的资源 和 希望启动的下载线程个数)。当下载完成时,提示用户下载任务完成。(可以再命令行输出提示)
        PS:完成过程中可以分别使用同步代码块 和同步方法来解决程序中可能出现的线程安全问题。*/
    
    public class ThunderDemo {    
        static Object object=new Object();
        public static void main(String[] args) {
            Scanner sc=new Scanner(System.in);
            System.out.println("请输入下载资源:_ _ _");
            int recSize=sc.nextInt();
            DownLoad.size=recSize;
            System.out.println("请输入线程个数:_ _ _");
            int threadCount=sc.nextInt();
            sc.close();
            for (int i = 0; i < threadCount; i++) {
                DownLoad downLoad4 = new DownLoad();
                downLoad4.start();
            }        
        }
    }
    
    class DownLoad extends Thread {
        static int size;
        static int downed = 0;
        boolean flag = true;
        @Override
        public void run() {
            while (size - downed >= 0) {
                //!!!!!!!严重注意,这里synchronized如果传了this,则调用new thread的时候不是同一把锁,而是多把!!!!!!
                synchronized (ThunderDemo.object) {
                    if (size - downed >= 0) {
    
                        float percent = (float) ((downed * 1.0 / size) * 100.0);
                        System.out.println(
                                getName() + "已下载---" + downed + "M,---剩余---" + (size - downed) + "----" + percent + "%");
                        downed++;
                        if (size - downed < 0) {
                            System.out.println("DownLoad Complete!!------100%");
                        }
                        try {
                            sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                    }
                }
            }
    
        }
    }

    使用线程同步固然可以消除线程不安全问题,但同时也有一些缺陷,就是消耗资源(当线程比较多的时候,每次都要去判断同步锁),影响效率;还有一点是如果出现嵌套锁,就会容易出现死锁。

    还有就是synchronized关键字可以放在方法声明中,这是表示的就是锁定当前方法了.


    五、死锁问题

    好吧,又是操作系统这门课中有提到的死锁,但我们这里说的线程死锁。并非操作系统中的死锁。想当初看中山大学考研复试经验中提的比较多的就是死锁问题,看来还挺重要的。

    所谓死锁(线程的),就是指两个线程锁,互相抢占了对方的资源,导致线程无法继续运行。如A拿到了资源a,再拿到资源b,就可以执行完,而B拿到了资源b,再拿到资源a就可以执行完。但是双方的原则都是要先执行完自己的线程才能释放资源,这就造成了线程死锁

    死锁是指两个以上的线程在执行过程中,因为争夺资源而产生的一种相互等待的现象
    死锁问题无法解决,只能修改代码。并且写程序时注意逻辑

    照例贴一段代码吧

    public class MyThread extends Thread {
    
         @Override
        public void run() {
        
            if (getName().equals("中国人")) {
                //中国人进来应该执行这段代码,拿到锁A
                synchronized (MyLock.LockA) {
                    System.out.println(getName() + "MyThread.run() 我得到了A锁,要继续执行的话需要B锁" );            
                    synchronized (MyLock.LockB) {    
                        System.out.println(getName() + "MyThread.run() 我得到了B锁" );
                        //xxdxd
                        System.out.println(getName()+"MyThread.run() 两把锁都得到,开始吃饭");    
                    }    
                 }
            }else {
                //英国人进来,需要执行这段代码
                synchronized (MyLock.LockB) {
                    System.out.println(getName() + "MyThread.run() 我得到了B锁,要继续执行的话需要A锁" );            
                    synchronized (MyLock.LockA) {    
                        System.out.println(getName() + "MyThread.run() 我得到了A锁" );
                        //xxdxd
                        System.out.println(getName()+"MyThread.run() 两把锁都得到,开始吃饭");    
                    }    
                 }
            }        
        }
         //穿入一个字符串,会使用该字符串去设置名字
        public MyThread(String name,Object obj){    
            super(name); //setName(name)    
            //this.obj=obj;
        } 
    }
    public class MyLock {    
      static    Object LockA = new Object();
      static    Object LockB = new Object();
    }
    public class Main {
    
        public static void main(String[] args) {
            Object object = new Object();        
            MyThread t1 = new MyThread("中国人",object);        
            MyThread t2 = new MyThread("英国人",object);        
            t1.start();
            t2.start();
        }
    
    }

    六、线程同步机制(生产者消费者问题)

    首先,当线程在继续执行前需要等待一个条件方可继续执行时,仅有 synchronized 关键字是不够的。因为虽然synchronized关键字可以阻止并发更新同一个共享资源,实现了同步,但是它不能用来实现线程间的消息传递,也就是所谓的通信。

    而在处理此类问题的时候又必须遵循一种原则,即:对于生产者,在生产者没有生产之前,要通知消费者等待;在生产者生产之后,马上又通知消费者消费;对于消费者,在消费者消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。

    实,Java提供了3个非常重要的方法来巧妙地解决线程间的通信问题。这3个方法分别是:wait()、notify()和notifyAll()。它们都是Object类的最终方法,因此每一个类都默认拥有它们。
    调用wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行态退出,进入等待队列,直到被再次唤醒。
    调用notify()方法可以唤醒等待队列中第一个等待同一共享资源的线程,并使该线程退出等待队列,进入可运行态。
    调用notifyAll()方法可以使所有正在等待队列中等待同一共享资源的线程从等待状态退出,进入可运行状态,此时,优先级最高的那个线程最先执行。
    显然,利用这些方法就不必再循环检测共享资源的状态,而是在需要的时候直接唤醒等待队列中的线程就可以了。这样不但节省了宝贵的CPU资源,也提高了程序的效率。
    好吧,上面这一段也是我从其他博客摘抄的。我觉得他们写的比我的好,表达的更加清楚。所以..我还是应增强一点写博客的能力.
    PS:一些细节
    wait()方法,等待(放弃执行权,让其他线程执行,wait的同时会释放锁)
    notify()方法,通知等待的线程,但是不会释放锁!!
    wait被notify之后,并且重新拿到锁,从wait()后面开始继续执行
    notify 和 wait机制需要放到同步代码块里使用

    七、线程的完整生命周期

    线程生命周期(主要)

    新建:就是创建了线程对象

    就绪:当线程对象调用了start方法

    运行:获得CPU执行权,开始执行,阻塞:没有执行资格,不执行

    死亡:执行完毕,等待垃圾回收

    完整版见下图吧
     
     
     
     
    总的来说就这么多吧,还有一个线程池的知识。
    为什么会有线程池?
    启动一个新线程的成本是比较高的,因为它涉及到与操作系统进行交互。这种情况下使用线程池可以更好的提高性能,尤其在当前程序需要创建大量的生存周期很短的线程时,更应该考虑使用线程池。
    原理就是跟数据库连接池差不多,就是
    线程池里每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一次被使用。

    嗯,差不多就这么多,想起来重要的知识点会继续补充。

    写博客真的挺耗时间的,而且要写得好的话需要更多时间更多能力。 

  • 相关阅读:
    在同时满足if 和 else 条件的情况下,输出所需的内容。
    可查找部分书籍的有效网址
    SecureCRT连接开发板 串口传输、tftp传输
    链接错误:multiple definition of 'xxx' 问题解决及其原理
    一个变量 赋值问题
    C代码通过编译器编译成可执行文件, 需经历 预处理、编译、汇编、链接 四个阶段
    SSM最基础项目搭建
    构建vue项目,vue init webpack无法使用的解决办法及vue-cli 4.0版本的创建方法
    VueCLI 通过process.env配置环境变量
    vue Element Admin 登录、验证流程
  • 原文地址:https://www.cnblogs.com/linchaohao/p/5154265.html
Copyright © 2020-2023  润新知