• 多线程


    一,什么是多线程?

    进程:一个正在执行的程序,比如说微信

    线程:进程中的一个独立控制单元,线程控制着进程的执行

    并行与并发:

    • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
    • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

    二,为什么使用多线程编程?

        ①、为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;

        ②、进程之间不能共享数据,线程可以;

        ③、系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;

        ④、Java语言内置了多线程功能支持,简化了java多线程编程。

    三,线程的生命周期?

    • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
    • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
    • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
    • 等待/阻塞/睡眠 :在一个线程执行了sleep(不会释放锁)(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
      • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态,释放锁,调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),使线程回到可运行状态(Runnable)

      • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

      • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

    • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。

     monitor

    java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

    wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

    当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

    用法:

    synchronized单独使用:

    • 代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容
    • public class Thread1 implements Runnable {
         Object lock;
         public void run() {  
             synchronized(lock){
               ..do something
             }
         }
      }
    • 直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。
    • public class Thread1 implements Runnable {
         public synchronized void run() {  
              ..do something
         }
      }
    • synchronized, wait, notify结合:典型场景生产者消费者问题
    • /**
         * 生产者生产出来的产品交给店员
         */
        public synchronized void produce()
        {
            if(this.product >= MAX_PRODUCT)
            {
                try
                {
                    wait();  
                    System.out.println("产品已满,请稍候再生产");
                }
                catch(InterruptedException e)
                {
                    e.printStackTrace();
                }
                return;
            }
      
            this.product++;
            System.out.println("生产者生产第" + this.product + "个产品.");
            notifyAll();   //通知等待区的消费者可以取出产品了
        }
      
        /**
         * 消费者从店员取产品
         */
        public synchronized void consume()
        {
            if(this.product <= MIN_PRODUCT)
            {
                try 
                {
                    wait(); 
                    System.out.println("缺货,稍候再取");
                } 
                catch (InterruptedException e) 
                {
                    e.printStackTrace();
                }
                return;
            }
      
            System.out.println("消费者取走了第" + this.product + "个产品.");
            this.product--;
            notifyAll();   //通知等待去的生产者可以生产产品了
        }

      volatile

      多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

    • 针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的或者网络延迟,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能

    四,创建线程

    1、继承Thread类:

        步骤:①、定义类继承Thread;

         ②、复写Thread类中的run方法;
        目的:将自定义代码存储在run方法,让线程运行
         ③、调用线程的start方法:
        该方法有两步:启动线程,调用run方法。
    public class ThreadDemo1 {
    
        public static void main(String[] args) {
            
            //创建两个线程
            ThreadDemo td = new ThreadDemo("zhangsan");
            ThreadDemo tt = new ThreadDemo("lisi");
            //执行多线程特有方法,如果使用td.run();也会执行,但会以单线程方式执行。
            td.start();
            tt.start();
            //主线程
            for (int i = 0; i < 5; i++) {
                System.out.println("main" + ":run" + i);
            }
        }
    }
    //继承Thread类
    class ThreadDemo extends Thread{
        
        //设置线程名称
        ThreadDemo(String name){
            super(name);
        }
        //重写run方法。
        public void run(){
            for(int i = 0; i < 5; i++){
            System.out.println(this.getName() + ":run" + i);  //currentThread()  获取当前线程对象(静态)。  getName() 获取线程名称。
            }
        }
    }

    2、实现Runnable接口: 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。

         实现步骤:  ①、定义类实现Runnable接口

              ②、覆盖Runnable接口中的run方法

                 将线程要运行的代码放在该run方法中。

              ③、通过Thread类建立线程对象。

              ④、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

                 自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象

              ⑤、调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

    public class RunnableDemo {
        public static void main(String[] args) {
            RunTest rt = new RunTest();
            //建立线程对象
            Thread t1 = new Thread(rt);
            Thread t2 = new Thread(rt);
            //开启线程并调用run方法。
            t1.start();
            t2.start();
        }
    }
    
    //定义类实现Runnable接口
    class RunTest implements Runnable{
        private int tick = 10;
        //覆盖Runnable接口中的run方法,并将线程要运行的代码放在该run方法中。
        public void run(){
            while (true) {
                if(tick > 0){
                    System.out.println(Thread.currentThread().getName() + "..." + tick--);
                }
            }
        }
    }

    两种方法对比:

    继承Thread:线程代码存放在Thread子类run方法中。

            优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。

            劣势:已经继承了Thread类,无法再继承其他类。

        实现Runnable:线程代码存放在接口的子类的run方法中。

            优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

            劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。

    ,常用API

    currentThread()     获取当前线程,如果在main方法中,则获取的是main,而不是被start()调用的方法

    isAlive()   判断当前线程是否处于活跃状态,线程处于正在运行状态,或者准备开始运行的状态,都认为是活跃状态

    sleep()   让当前正在执行的

     yield() 该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。

    interrupt() 虚拟机会在此线程上标记一个标志(这个中断标志只是一个布尔类型的变量),代表这个线程可能被中断,在后面的中断操作也是根据这个中断标志执行的,如果一个线程被标记了中断标识(调用                            interrupt方法),然后调用sleep,wait,jion,io操作等,进入阻塞状态时,会抛出InterruptedException异常,然后清除标识位的中断标记

    interrupted():测试当前线程是否中断。 该方法可以清除线程的中断状态 。

    isInterrupted():测试这个线程是否被中断。线程的中断状态不受此方法的影响。

    六,线程间通信

    wait()

    • wait( )     使当前线程执行代码的线程进行等待,wait()方法时object类的方法,在wait()所在代码行处停止运行,直到接到通知或中断为止。在调用wait()方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用此方法,在调用wait()方法后,线程释放锁。在wait()方法返回前,线程与其他线程竞争重新获取锁,如果调用wait()方法时,没有持有适当的锁,,则抛出IllegalMonitorStateException异常
    • 线程呈wait()状态时,调用线程对象的interrupt()方法会出现InturreputException异常
    • 当多个执行 相同任务的线程,条件判断时wait(),用while(),不能用if,  当线程wait后,又被唤醒时,是从wait后main开始执行,如果用if,就不会执行条件判断了

     notify()

    notify()也必须在同步方法或同步块中调用,即在调用前,线程必须获取该对象的级别锁,该方法用于唤醒处于wait状态的线程,此方法执行后,不会释放锁,只有在在notify所在方法块执行完才会释放锁

     

    join()

    B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。

     

     两个队列:

    就绪队列

    • 存储即将获得锁的线程
    • 被notify并且拿到锁的线程

     阻塞队列:

    • 被阻塞的线程
    • 调用wait的线程

    Threadlocal类:

    ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

    1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。

    2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。

    3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。

    4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

    七,Lock类

    相关API

    getHoldCount()    方法来获取当前线程的锁定个数,所谓锁定个数就是当前线程调用lock方法的次数。

    getQueueLength()   方法来得到等待锁释放的线程的个数。

    hasQueuedThread(Thread thread)  查询该Thread是否等待该lock对象的释放。

    hasQueuedThreads()  查询是否有线程等待获取此锁定

    isLocked()       Java提供了简单判断一个锁是不是被一个线程持有,

    isHeldByCurrentThread(),  判断当前线程是否有此锁定。

    lockInterruptibly()  也可以实现加锁,但是当线程被中断的时候,就会加锁失败,进行异常处理

    tryLock()方法来进行尝试加锁,只有该锁未被其他线程持有的基础上,才会成功加锁。

    isFair    查看这个锁是否公平

     

    lock: 在java.util.concurrent包内。共有三个实现:

    ReentrantLock
    ReentrantReadWriteLock.ReadLock
    ReentrantReadWriteLock.WriteLock

    主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。

    ReentrantLock的用法

    Lock类的用法也是这样,通过Lock对象lock,用lock.lock来加锁,用lock.unlock来释放锁。在两者中间放置需要同步处理的代码。

    public class MyConditionService {
    
        private Lock lock = new ReentrantLock();
        public void testMethod(){
            lock.lock();
            for (int i = 0 ;i < 5;i++){
                System.out.println("ThreadName = " + Thread.currentThread().getName() + (" " + (i + 1)));
            }
            lock.unlock();
        }
    }

    公平锁与非公平锁

    公平锁:按照线程加锁的顺序来获取锁,即先来先得
    非公平锁:随机竞争来得到锁

     Condition的用法:

    Condition是Java提供了来实现等待/通知的类,Condition类还提供比wait/notify更丰富的功能,Condition对象是由lock对象所创建的,但是同一个锁可以创建多个Condition的对象,即创建多个对象监视器。这样的好处就是可以指定唤醒线程。notify唤醒的线程是随机唤醒一个。

    下面,看一个例子,显示简单的等待/通知

    public class ConditionWaitNotifyService {
    
        private Lock lock = new ReentrantLock();//创建锁
        public Condition condition = lock.newCondition();//创建condition实例
    
    
        public void await(){
            try{
                lock.lock();//开启锁
                System.out.println("await的时间为 " + System.currentTimeMillis());
                condition.await();
                System.out.println("await结束的时间" + System.currentTimeMillis());
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();//释放锁
            }
        }
    
    
        public void signal(){
            try{
                lock.lock();
                System.out.println("sign的时间为" + System.currentTimeMillis());
                condition.signal();
            }finally {
                lock.unlock();
            }
        }
    }
    • condition对象通过lock.newCondition()来创建,用condition.await()来实现让线程等待,是线程进入阻塞。
    • condition.signal()来实现唤醒线程。唤醒的线程是用同一个conditon对象调用await()方法而进入阻塞。
    • await()和signal()也是在同步代码区内执行。

     读写锁ReentrantReadWriteLock

    读写锁分成两个锁,一个锁是读锁,一个锁是写锁。读锁与读锁之间是共享的,读锁与写锁之间是互斥的,写锁与写锁之间也是互斥的。

    看下面的读读共享的例子:

    public class ReadReadService {
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        public void read(){
            try{
                try{
                    lock.readLock().lock();
                    System.out.println("获得读锁" + Thread.currentThread().getName() +
                    " " + System.currentTimeMillis());
                    Thread.sleep(1000 * 10);
                }finally {
                    lock.readLock().unlock();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    } 

    测试的代码和结果如下:

    ReadReadService service = new ReadReadService();
            Thread a = new Thread(service::read);
            a.setName("A");
    
            Thread b = new Thread(service::read);
            b.setName("B");
    
            a.start();
            b.start();

    八,定时器

    Timer

    作用是设置计划任务,而封装任务内容的类是TimerTask类.此类是一个抽象类,继承需要实现一个run方法.

    package com.wang.reflect;
    
    import java.util.Timer;
    import java.util.TimerTask;
    
    class MyTask extends TimerTask{
    
        @Override
        public void run() {
            System.out.println("您该起床了!!!!");
        }
    }
    public class TimerDemo {
        public static void main(String[] args) {
            //创建定时器对象
            Timer t=new Timer();
            //在3秒后执行MyTask类中的run方法
            t.schedule(new MyTask(), 3000);
            
        }
    }

    创建了一个Timer就相当于启动了一个新线程,这个新线程并不是守护线程,所以会一直运行.

     

    在Time类和TimerTask类中都有一个cancel()方法.

          TimerTask类中的作用是:将自身从任务队列中清除,(一个Timer对象可以执行多个Timertask任务)

          Timer类中的作用是:将任务队列中的全部任务清空.

    我给出的Date类型的时间,早于当前的时间.则会立即执行task任务.

     

     

  • 相关阅读:
    《Linux Device Drivers》第十二章 PCI司机——note
    Swift开放StatsD后上传数据的出现,出现退换货503的Bug
    google login page
    Use GraceNote SDK in iOS(一)通过序列化GDO查询专辑封面
    【人在职场】能力与价值
    HDU 5067-Harry And Dig Machine(DFS)
    LeetCode:Merge Two Sorted Lists
    HTML5硕士学习笔记
    通过设置注册表隐藏桌面图标
    SharePoint 要一个多行文本类型字段为特殊类型的链接
  • 原文地址:https://www.cnblogs.com/zkkkk-/p/10535313.html
Copyright © 2020-2023  润新知