zoukankan      html  css  js  c++  java
  • java笔记十五——多线程

    一、线程的基本概念

    1.1 进程

    任何的软件存储在磁盘中,运行软件的时候,OS(操作系统)使用IO技术,将磁盘中的软件的文件加载到内存,程序才能运行。

    进程的概念 : 应用程序(如typerpa、IDEA)运行的时候进入到内存,程序在内存中占用的内存空间就是进程。

    1.2 线程

    线程(Thread) : 在内存和CPU之间,建立一条连接通路,CPU可以到内存中取出数据进行计算,这个连接的通路,就是线程。

    一个内存资源 : 一个独立的进程,进程中可以开启多个线程 (多条通路)。

    并发: 同一个时刻多个线程同时操作了同一个数据

    并行: 同一个时刻多个线程同时执行不同的程序

    1.3 进程与线程的区别

    (1)进程是操作系统资源分配和调度的一个独立单元,而线程是CPU调度的基本单元。

    (2)同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程。

    二、多线程的实现方式一:继承Thread类

    一切都是对象,线程也是对象,Thread类是线程对象的描述类。

    2.1 步骤

    1、 定义类继承Thread类

    2、子类重写run()方法

    3、创建子类对象

    4、调用子类对象的方法start()开启线程

    代码:

    //- 定义类继承Thread
    //- 子类重写方法run
    public class SubThread extends Thread {
        public void run(){
            for(int x = 0 ; x < 50 ;x++)
                System.out.println("run..."+x);
        }
    }
    
    public static void main(String[] args) {
        //创建线程程序
        SubThread subThread = new SubThread();
        //调用子类对象的方法start()启动线程
        //启动线程,JVM调用方法run
        subThread.start();
        for(int x = 0 ; x < 50 ;x++)
        	System.out.println("main..."+x);
    }
    

    2.2 线程的内存图

    2.3 Thread类的方法

    • Thread类的方法 String getName()返回线程的名字
    public class ThreadName extends Thread {
        public void run (){
            System.out.println("线程名字:: "+ super.getName());
        }
    }
    
       public static void main(String[] args) {
            ThreadName threadName = new ThreadName();
            //threadName.setName("旺财");
            threadName.start();
    
            ThreadName threadName1 = new ThreadName();
            //threadName1.setName("小强");
            threadName1.start();
        }
    
    • Thread类静态方法 : static Thread currentThread()
      • 静态调用,作用是放回当前的线程对象
      • "当前" , 当今皇上. 本地主机
    //获取当前线程对象,拿到运行main方法的线程对象
    Thread thread =  Thread.currentThread();
    System.out.println("name::"+thread.getName());
    
    • Thread类的方法 join()
      • 解释:执行join()方法的线程,他不结束,其它线程运行不了
        public static void main(String[] args) throws InterruptedException {
            JoinThread t0 = new JoinThread();
            JoinThread t1 = new JoinThread();
    
            t0.start();
            t0.join();
            t1.start();
        }
    
    • Thread类的方法 static yield()
      • 线程让步,线程把执行权让出
        public void run() {
            for(int x = 0 ; x < 50 ;x++){
                Thread.yield();
                System.out.println(Thread.currentThread().getName()+"x.."+x);
            }
        }
    

    三、多线程的实现方式二:实现Runnable接口

    3.1 步骤

    1、定义类实现Runnable接口

    2、重写run()方法

    3、创建Thread类对象

    • Thread类的构造方法中,传递Runnable接口的实现类对象

    4、调用Thread类的start()方法开启线程

    示例代码:

    //- 定义类实现接口
    // - 重写接口的抽象方法run()
    public class SubRunnable implements Runnable{
        @Override
        public void run() {
            for(int x = 0 ; x < 50 ;x++){
                System.out.println(Thread.currentThread().getName()+"x.."+x);
            }
        }
    }
    
    public static void main(String[] args) {
            //创建接口实现类对象
            Runnable r = new SubRunnable();
            //创建Thread对象,构造方法传递接口实现类
            Thread t0 = new Thread(r);
            t0.start();
    
            for(int x = 0 ; x < 50 ;x++){
                System.out.println(Thread.currentThread().getName()+"x.."+x);
            }
        }
    

    3.2 实现接口的好处

    接口实现好处是设计上的分离效果 : 线程要执行的任务和线程对象本身是分离的。

    继承Thread重写方法run() : Thread是线程对象,run()是线程要执行的任务

    实现Runnable接口 : 方法run()在实现类,和线程无关。创建Thread类传递接口的实现类对象,线程的任务和Thread没有联系,,实现解耦操作。

    四、线程安全问题

    4.1 线程安全如何发生的

    当满足以下两个条件时,会出现线程安全问题

    ① 存在多个线程共享同一个资源(成员变量)

    ② 有多句代码操作共享数据

    线程执行调用方法run,同一个资源是堆内存的。

    4.2 售票案例

    以下代码是一个线程不安全的示例

    /**
     * 票源对象,需要多个线程同时操作
     */
    public class Ticket implements Runnable {
    
        //定义票源
        private int tickets = 100;
    
        @Override
        public void run() {
            while (true) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);//线程休眠,暂停执行
                    }catch (Exception ex){}
                    System.out.println(Thread.currentThread().getName()+" 出售第" + tickets + "张");
                    tickets--;
                }else
                    break;;
            }
        }
    }
    
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //创建3个窗口,3个线程
        Thread t0 = new Thread(ticket);
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
    
        t0.start();
        t1.start();
        t2.start();
    }
    

    解决线程的安全问题 : 当一个线程没有完成全部操作的时候,其它线程不能操作

    4.3 同步代码块

    同步代码块可以解决线程安全问题 : 格式 synchronized关键字

    synchronized(任意对象){
        //线程操作的共享资源
    }
    

    任意对象 : 在同步中这个对象称为对象锁,简称锁。官方的文档称为 对象监视器

    4.4 同步代码块的执行原理

    同步代码块的执行原理 : 关键点就是对象锁

    • 线程执行到同步代码块,判断锁是否存在
      • 如果锁存在,获取到锁,进入到同步代码块中执行
      • 执行完毕,线程出去同步代码块,将锁对象归还
    • 线程执行到同步代码块,判断锁所否存在
      • 如果锁不存在,线程只能在同步代码块这里等待锁的到来

    使用同步 : 线程要先判断锁,然后获取锁,出去同步要释放锁, 增加了许多步骤,因此线程安全运行速度慢。 牺牲性能,不能牺牲数据安全。

    4.5 同步方法

    当一个方法中所有代码都是线程操作的共享内容时,可以在方法的定义上添加同步的关键字 synchronized , 这就是同步方法,或者称为同步的函数。

    ​ 静态同步方法的对象锁:类名.class

     @Override
        public void run() {
            while (true)
              sale();
        }
    
    private static synchronized void sale(){
        //  synchronized (Ticket.class) {
        if (tickets > 0) {
        try {
            Thread.sleep(20);//线程休眠,暂停执行
            } catch (Exception ex) {
        }
        System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张");
        tickets--;
        }
    //  }
    }
    

    五、死锁

    死锁程序 : 多个线程同时争夺同一个锁资源,出现的程序假死现象.

    面试点 : 考察开发人员是否充分理解同步代码的执行原理

    同步代码块 : 线程判断锁,获取锁,释放锁,不出代码,锁不释放。

    完成死锁的案例 : 同步代码块的嵌套

    死锁代码:

    /**
     * 实现死锁程序
     */
    public class ThreadDeadLock implements Runnable{
    
        private boolean flag ;
    
        public ThreadDeadLock(boolean flag){
            this.flag = flag;
        }
    
        @Override
        public void run() {
            while (true){
                //同步代码块的嵌套
                if (flag){
                    //先进入A锁同步
                    synchronized (LockA.lockA){
                        System.out.println("线程获取A锁");
                        //在进入另一个同步B锁
                        synchronized (LockB.lockB){
                            System.out.println("线程获取B锁");
                        }
                    }
                }else {
                    //先进入B锁同步
                    synchronized (LockB.lockB){
                        System.out.println("线程获取B锁");
                        //再进入另一个同步锁A锁
                        synchronized (LockA.lockA){
                            System.out.println("线程获取A锁");
                        }
                    }
                }
            }
        }
    }
    
    public class LockA {
        public static LockA lockA = new LockA();
    }
    
    public class LockB {
        public static LockB lockB = new LockB();
    }
    
        public static void main(String[] args) {
            ThreadDeadLock threadDeadLock = new ThreadDeadLock(true);
            ThreadDeadLock threadDeadLock2 = new ThreadDeadLock(false);
    
            new Thread(threadDeadLock).start();
            new Thread(threadDeadLock2).start();
        }
    

    六、JDK5新特性Lock锁

    JDK5新的特性 : java.util.concurrent.locks包。定义了接口Lock。

    Lock接口替代了synchronized,可以更加灵活

    • Lock接口的方法
      • void lock() 获取锁
      • void unlock()释放锁
    • Lock接口的实现类ReentrantLock
    /**
     *  优化为juc包的接口Lock
     */
    public class Ticket implements Runnable {
    
        //定义票源
        private  int tickets = 100;
        //获取Lock接口的实现类对象
        private Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true)
              sale();
        }
    
        private void sale(){
            //获取锁
            lock.lock();
            if (tickets > 0) {
                try {
                    Thread.sleep(20);//线程休眠,暂停执行
                } catch (Exception ex) {
                }
                System.out.println(Thread.currentThread().getName() + " 出售第" + tickets + "张");
                tickets--;
            }
            //释放锁
            lock.unlock();
        }
    }
    

    七 、线程通信

    ​ 线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

    7.1 生产者消费者问题

    创建2个线程,一个线程表示生产者,另一个线程表示消费者。

    案例

    /**
     * 定义资源对象
     *   成员 : 产生商品的计数器
     *          标志位
     */
    public class Resource {
        int count ;
        boolean flag ;
    }
    
    
    /**
     * 生产者线程
     *   资源对象中的变量++
     */
    public class Produce implements Runnable{
    
        private Resource r ;
    
        public Produce(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true){
                synchronized (r) {
                    //判断标志位,是否允许生产
                    //flag是true,生产完成,等待消费
                    if (r.flag )
                        //无限等待
                       try{ r.wait();
                       }catch (Exception ex){}
                    r.count++;
                    System.out.println("生产第" + r.count + "个");
                    //修改标志位,已经生产了,需要消费
                    r.flag = true;
                    //唤醒消费者线程
                    r.notify();
                }
            }
        }
    }
    
    /**
     * 消费者线程
     *   资源对象中的变量输出打印
     */
    public class Customer implements Runnable{
        private Resource r ;
    
        public Customer(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true){
                synchronized (r) {
                    //是否要消费,判断标志位 ,允许消费才能执行
                    if (!r.flag )
                        //消费完成,不能再次消费,等待生产
                        try{r.wait();}catch (Exception ex){}
                    System.out.println("消费第" + r.count);
                        //消费完成后,修改标志位,变成已经消费
                    r.flag = false;
                    //唤醒生产线程
                    r.notify();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Resource r = new Resource();
        //接口实现类,生产的,消费的
        Produce produce = new Produce(r);
        Customer customer = new Customer(r);
        //创建线程
        new Thread(produce).start();
        new Thread(customer).start();
    }
    

    7.2 wait/notify机制

    线程通信的方法 wait() notify()

    • 方法的调用必须写在同步中
    • 调用者必须是作为锁的对象
    • wait(),notify()为什么要定义在Object类
      • 同步中的锁,是任意对象,任何类都继承Object

    案例实现线程同步:

    /**
     * 定义资源对象
     *   成员 : 产生商品的计数器
     *          标志位
     */
    public class Resource {
       private int count ;
       private boolean flag ;
    
       //消费者调用
       public synchronized void getCount() {
             //flag是false,消费完成,等待生产
             if (!flag)
                //无限等待
                try{this.wait();}catch (Exception ex){}
             System.out.println("消费第"+count);
                //修改标志位,为消费完成
             flag = false;
             //唤醒对方线程
             this.notify();
       }
       //生产者调用
       public synchronized void setCount() {
             //flag是true,生产完成,等待消费
             if (flag)
                //无限等待
                try{this.wait();}catch (Exception ex){}
             count++;
             System.out.println("生产第"+count+"个");
             //修改标志位,为生产完成
             flag = true;
             //唤醒对方线程
             this.notify();
       }
    }
    
    /**
     * 消费者线程
     *   资源对象中的变量输出打印
     */
    public class Customer implements Runnable{
        private Resource r ;
    
        public Customer(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true) {
                r.getCount();
            }
        }
    }
    
    /**
     * 生产者线程
     *   资源对象中的变量++
     */
    public class Produce implements Runnable{
    
        private Resource r ;
    
        public Produce(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true) {
                r.setCount();
            }
        }
    }
    

    7.3 多生产者多消费者问题

    7.3.1 安全问题的产生

    当上述案例变为多生产者、多消费者时,又会出现新的问题

    • 线程的notify(),只唤醒第一个等待的线程,有可能重复唤醒同一种类型的线程(只唤醒生产者的某个线程,或只唤醒消费者的某个线程)
      • 解决办法 : 全部唤醒 notifyAll()
    • 被唤醒线程,已经进行过if判断,一旦醒来继续执行
      • 线程被唤醒后,不能立刻就执行,再次判断标志位,利用循环
      • while(标志位) 标志位是true,永远也出不去(以生产者为例)

    代码改造:

    /**
     * 定义资源对象
     *   成员 : 产生商品的计数器
     *          标志位
     */
    public class Resource {
       private int count ;
       private boolean flag ;
    
       //消费者调用
       public synchronized void getCount() {
             //flag是false,消费完成,等待生产
             while (!flag)
                //无限等待
                try{this.wait();}catch (Exception ex){}
             System.out.println("消费第"+count);
                //修改标志位,为消费完成
             flag = false;
             //唤醒对方线程
             this.notifyAll();
       }
       //生产者调用
       public synchronized void setCount() {
             //flag是true,生产完成,等待消费
           while (flag)
                //无限等待
                try{this.wait();}catch (Exception ex){}
             count++;
             System.out.println("生产第"+count+"个");
             //修改标志位,为生产完成
             flag = true;
             //唤醒对方线程
             this.notifyAll();
       }
    }
    
    
    /**
     * 生产者线程
     *   资源对象中的变量++
     */
    public class Produce implements Runnable{
    
        private Resource r ;
    
        public Produce(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true) {
                r.setCount();
            }
        }
    }
    
    /**
     * 消费者线程
     *   资源对象中的变量输出打印
     */
    public class Customer implements Runnable{
        private Resource r ;
    
        public Customer(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true) {
                r.getCount();
            }
        }
    }
    
        public static void main(String[] args) {
            Resource r = new Resource();
            //接口实现类,生产的,消费的
            Produce produce = new Produce(r);
            Customer customer = new Customer(r);
            //创建线程
            new Thread(produce).start();
            new Thread(produce).start();
            new Thread(produce).start();
            new Thread(produce).start();
            new Thread(produce).start();
            new Thread(produce).start();
            new Thread(customer).start();
            new Thread(customer).start();
            new Thread(customer).start();
            new Thread(customer).start();
            new Thread(customer).start();
            new Thread(customer).start();
        }
    

    7.4 线程方法sleep和wait的区别

    • sleep在休眠的过程中,同步锁不会丢失 ,不释放
    • wait()等待的时候,发布监视器的所属权, 释放锁。唤醒后要重新获取锁,才能执行。

    Thread.sleep(time);

    锁对象.wait()

    7.5 生产者和消费者案例性能问题

    wait()方法和notify()方法,本地方法调用OS的功能,和操作系统交互,JVM找OS,把线程停止。频繁等待与唤醒,导致JVM和OS交互的次数过多。

    notifyAll()唤醒全部的线程,也浪费线程资源,为了一个线程,不得以唤醒了全部的线程。

    7.6 Lock接口改进

    Lock接口替换了同步synchronized,提供了更加灵活,性能更好的锁定操作

    • Lock接口中方法 : newCondition() 方法的返回值是接口 : Condition

    7.6.1 生产者消费者改进为Lock接口

    • Condition接口 (线程的阻塞队列)
      • 进入队列的线程,释放锁
      • 出去队列的线程,再次的获取锁
      • 接口的方法 : await() 线程释放锁,进入队列
      • 接口的方法 : signal() 线程出去队列,再次获取锁

    线程的阻塞队列,依赖Lock接口创建

    /**
     *  改进为高性能的Lock接口和线程的阻塞队列
     */
    public class Resource {
       private int count ;
       private boolean flag ;
       private Lock lock = new ReentrantLock();//Lock接口实现类对象
    
        //Lock接口锁,创建出2个线程的阻塞队列
        private Condition prod = lock.newCondition();//生产者线程阻塞队列
        private Condition cust = lock.newCondition();//消费者线程阻塞队列
    
       //消费者调用
       public  void getCount() {
           lock.lock();//获取锁
             //flag是false,消费完成,等待生产
             while (!flag)
                //无限等待,消费线程等待,执行到这里的线程,释放锁,进入到消费者的阻塞队列
                 try{cust.await();}catch (Exception ex){}
    
             System.out.println("消费第"+count);
                //修改标志位,为消费完成
             flag = false;
             //唤醒生产线程队列中的一个
             prod.signal();
             lock.unlock();//释放锁
       }
       //生产者调用
       public  void setCount() {
           lock.lock();//获取锁
             //flag是true,生产完成,等待消费
           while (flag)
                //无限等待,释放锁,进入到生产线程队列
                try{prod.await();}catch (Exception ex){}
             count++;
             System.out.println("生产第"+count+"个");
             //修改标志位,为生产完成
             flag = true;
             //唤醒消费者线程阻塞队列中年的一个
             cust.signal();
           lock.unlock();//释放锁
       }
    }
    
    

    7.6.2 Lock锁的实现原理

    使用技术不开源,技术的名称叫做轻量级锁

    使用的是CAS锁 (Compare And Swap) 自旋锁

    JDK限制 : 当竞争的线程大于等于10,或者单个线程自旋超过10次的时候,JDK强制CAS锁取消。升级为重量级锁 (OS锁定CPU和内存的通信总线)。

    八、单例设计模式

    要求 : 保证一个类的对象在内存中的唯一性

    8.1 饿汉式

    实现步骤

    • 私有修饰构造方法
    • 自己创建自己的对象
    • 方法get,返回本类对象
    /**
     * - 私有修饰构造方法
     * - 自己创建自己的对象
     * - 方法get,返回本类对象
     */
    public class Single {
        private Single(){}
    	//饿汉式
        private static final Single s = new Single(); // 自己创建自己的对象
    
    //    方法get,返回本类对象
        public static Single getInstance(){
            return s;
        }
    }
    

    8.2 懒汉式

    实现步骤

    • 私有修饰构造方法
    • 创建本类的成员变量, 不new对象
    • 方法get,返回本类对象
    /**
     * - 私有修饰构造方法
     * - 创建本类的成员变量, 不new对象
     * - 方法get,返回本类对象
     */
    public class Single {
        private Single(){}
    	//懒汉,对象的延迟加载
        private static Single s = null;
    
        public static Single getInstance(){
            //判断变量s,是null就创建
            if (s == null) {
                s = new Single();
            }
            return s;
        }
    }
    

    8.2.2 懒汉式的安全问题

    一个线程判断完变量 s=null,还没有执行new对象,被另一个线程抢到CPU资源,同时有2个线程都进行判断变量,对象创建多次。因此需要加上同步代码块:
    
        public static Single getInstance(){
            synchronized (Single.class) {
                //判断变量s,是null就创建
                if (s == null) {
                    s = new Single();
                }
            }
            return s;
        }
    

    ​ 性能问题 : 第一个线程获取锁,创建对象,返回对象。 第二个线程调用方法的时候,变量s已经有对象了,根本就不需要再进同步,不要再判断空,直接return才是最高效的。双重的if判断,提高效率 Double Check Lock。

    最终的懒汉式代码如下:

    /**
     * - 私有修饰构造方法
     * - 创建本类的成员变量, 不new对象
     * - 方法get,返回本类对象
     */
    public class Single {
        private Single(){}
    private static volatile Single s = null; 
    public static Single getInstance(){
            //再次判断变量,提高效率
            if(s == null) {
                synchronized (Single.class) {
                    //判断变量s,是null就创建
                    if (s == null) {
                        s = new Single();
                    }
                }
            }
            return s;
        }
    }
    

    8.3 关键字volatile

    成员变量修饰符,不能修饰其它内容

    关键字作用 :

    • 保证被修饰的变量,在线程中的可见性
    • 防止指令重排序
      • 单例模式,使用了该关键字,不使用关键字,可能线程会拿到一个尚未初始化完成的对象(半初始化)。

    九、线程池ThreadPool

    线程的缓冲池,目的就是提高效率。 new Thread().start(),线程是内存中一个独立的方法栈区,JVM没有能力开辟内存空间,需要和OS交互。

    JDK5开始内置线程池

    9.1 Executors类

    • 静态方法static newFixedThreadPool(int 线程的个数)

      • 方法的返回值是ExecutorService,Executors接口的实现类,管理池子里面的线程
    • ExecutorService接口的方法

      • submit (Runnable r)提交线程执行的任务

    9.2 Callable接口

    实现多线程的程序 : 接口特点是有返回值,可以抛出异常 (Runnable没有)

    抽象的方法只有一个 call()

    启动线程,线程调用重写方法call()

    • ExecutorService接口的方法
      • submit (Callable c)提交线程执行的任务
      • Future submit(Callable c)方法提交线程任务后,方法有个返回值 Future接口类型
      • Future接口的get()方法,获取线程执行后的返回值结果
    public class MyCall implements Callable<String> {
        public String call() throws Exception{
            return "返回字符串";
        }
    }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建线程池,线程的个数是2个
           ExecutorService  es =  Executors.newFixedThreadPool(2);
           //线程池管理对象service,调用方法啊submit提交线程的任务
            MyRunnable my = new MyRunnable();
            //提交线程任务,使用Callable接口实现类
            Future<String> future = es.submit(new MyCall());//返回接口类型 Future
            //接口的方法get,获取线程的返回值
            String str = future.get();
            System.out.println("str = " + str);
    
    
        }
    

    9.3 ConcurrentHashMap

    ConcurrentHashMap类本质上是Map集合,键值对的集合。使用方式和HashMap没有区别。

    凡是对于此Map集合的操作,不去修改里面的元素,不会锁定。

  • 相关阅读:
    NEFU84——五指山(Exgcd)
    Scoi2010——传送带(三分套三分=九分)
    POJ3352Road construction(边双联通分量)
    CEOI2005——关键网线(割边)
    ZJOI2004——嗅探器
    POJ1845s——Sumdiv()
    Web 服务器安全
    渗透测试神器Cobalt Strike的使用
    Windows中的用户和组以及用户密码处理
    漏洞及渗透练习平台 【大全】
  • 原文地址:https://www.cnblogs.com/tianwenxin/p/14746690.html
Copyright © 2011-2022 走看看