Java高级编程十二:多线程及线程池的使用

135 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

第十二章 多线程

1.基本概念:程序、进程、线程

程序(Program)

为了完成特定的任务、用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象

进程(process)

是程序的一次执行过程,或是正在运行的程序,是一个动态的过程,有自身的生命周期

线程(thread)

进程可以进一步细分为线程,是一个程序内部的一条执行路径

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程最为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器(pc),线程切换的开销小
  • 一个进程的多个线程共享相同的内存单元/内存地址空间,他们从同一个堆空间中分配对象,可以访问相同的变量和对象,使得线程间的通信更简洁、高效,但是多个线程操作共享的系统资源时可能会带来安全隐患

2.线程的创建和使用

2.1 多线程的创建

  1. 方式一:继承Thread类

    ①创建一个类继承于Thread类

    ②重写Thread的***run()方法 -->将此线程要执行的操作声明在run()***中

    ③创建一个Thread类的子类对象

    ④通过此对象调用start()第一步启动当前线程,第二部调用当前线程的 run()

    public class MyThread extends Thread{
    	@Override
      	public void run(){
     		for(int i = 0; i < 100;i++){
                if(i % 2 == 0){
                   System.out.println(i);
                }
            }		     
      	}
    }
    public class ThreadTest{
        public static void main(String[] args){
            //创建Thread类的子类对象
        	MyThread t1 = new MyThread();
            //通过此线程调用start()方法
            t1.start();
        }
    }
    
  2. 方式二:实现Runnable接口

    ①创建一个实现了 Runnable接口的类

    ②实现 Runnable 中的抽象方法:run()

    ③创建实现类的对象,将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象

    ④通过 Thread 类的对象调用 start()

    /**
    创建三个窗口同时卖100张票
    */
    class MThread implements Runnable{
       private int ticket = 100;
    	@Override
       public void run(){
           //TODO
           while(true){
               synchronized(this){
                   if(ticket > 0){
                     System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
                       ticket--;
                   }else{
                       break;
                   }
               }
           }
       }
    }
    public class Test{
       public static void main(String[] args){
       	MThread m = new MThread();
           Thread t1 = new Thread(m);
           Thread t2 = new Thread(m);
           Thread t3 = new Thread(m);
           
           t1.setName("窗口1");
    	t2.setName("窗口2");
           t3.setName("窗口3");
           
           t1.start();
           t2.start();
           t3.start();
       }
    }
    
  3. 比较两种创建多线程的方式

    开发中: 优先选择 实现 Runnable 接口的方式。原因:① 实现的方式没有类的单继承性的局限性 ② 实现的方式更适合来处理多线程有共享数据的情况

    联系 : public class Thread implements Runnable

    ==相同点:== 两种方法都需要重写 run() 将线程要执行的操作声明在 run() 中。

    例题:创建两个线程一个打印100以内奇数,一个打印偶数

    public class ThreadTest {
        public static void main(String[] args){
     		new Thread(){
    			@Override
    			public void run(){
    				for(int i = 0; i < 100;i++){
    					if(i % 2 == 1){
    					   System.out.println(Thread.currentThread().getName() + ":" + i);
    					}
    				}
    			}
            }.start();
    
      		new Thread(){
    			@Override
    			public void run(){
                	for(int i = 0; i < 100;i++){
    					if(i % 2 == 0){
    					   System.out.println(Thread.currentThread().getName() + ":" + i);
    					}
           			}
    			}
            }.start();
    
        }
    }
    
    

2.2 Thread中的常用方法

  1. start()

    启动当前线程,调用当前线程的 run()

  2. run()

    通常需要重写Thread类中的方法,将创建的线程要执行的操作声明在此方法中

  3. currentThread()

    静态方法,返回执行当前代码的线程

  4. getName()

    获取当前线程的名字

    同样的也有setName()方法

  5. yield()

    释放当前CPU的执行权

  6. join()

    在主线程a中调用线程b的 join()。此时线程a进入阻塞状态,直到线程b完全执行完,a才会结束状态

  7. sleep()

    让当前线程休眠多少毫秒

2.3 线程的调度

  1. 线程的优先级

线程的最大优先级是10,最小为1,默认为5

  1. 获取和设置当前线程的优先级

    getPriority()h获取当前的优先级

    setPriority()设置当前线程的优先级

3.线程的生命周期

线程.png

4.多线程常用方法

==控制线程相关方法==

  1. setPriorty(inyt)[不建议使用的]

    setPriority(int) 可以设置现成的优先级别,可选范围是 1 - 10 ,默认级别为5,线程的优先级别越高,抢到时间片的**概率越高,并不代表一定抢到时间片**

  2. static sleep(long)

    使当前线程休眠多少毫秒,会从运行态变成阻塞态

  3. static yield()

    当前线程放弃已经持有的时间片,允许其他线程执行

  4. join()

    当前线程邀请其他线程先执行,谁在运行中,join就在谁的体内

    注意:
    1. 线程章节所有的静态方法,不要关注是谁调用的方法,而要关注谁的线程体内;
    2. 这个章节所有主动进入阻塞状态的方法,都出要进行异常处理。因为他们都有throws InterruputedException的声明,这是一个非运行时异常,必须出来

    ==线程的其他方法==

  5. setName() / getName()

    设置和得到线程的名字

  6. static activeCount()

    得到程序中所有活跃线程的总数[就绪 + 运行 + 阻塞]

  7. setDaemon()

    设置线程成为守护线程;守护线程是为了给其他线程提供服务的,当程序中只有守护进程的时候,守护进程会自动结束
    守护线程要设置成while(true);必须按在调用 start() 方法之前就设置成守护线程

  8. interrupt()

    中断线程的阻塞状态

  9. static currentThread()

    • 在主方法中使用相当于得到了主线程的线程对象
    • 在 run() 调用的其他方法中,用来获取当前的线程是谁
    • 不应该直接出现在 run() 方法中,因为返回的线程相当与this

5. 线程的同步

  • 在java中通过同步机制,来解决线程安全问题
  • 操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率较低
  • 当锁标记没被释放的时候,其他线程会进入锁池进行等待
  1. 方式一:同步代码块

    synchronized(同步监视器){
        //需要被同步的代码
    }
    

    ==说明== ①操作共享数据的代码,即为需要被同步的代码 ②:horse:共享数据: 多个线程共同操作的变量,如ticket ③同步监视器,俗称锁。任何一个类的对象都可以充当锁, 要求:多个线程必须共用一把锁

    ④在实现 Runnable 接口的创建多线程的方式中,可以考虑使用 this 充当同步监视器,在继承Thread的方式中,慎用

  2. 方式二:同步方法

    ==如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明成同步的==

==修改懒汉式==

class Bank{
    private static Bank instance = null;
    public static Bank getInstance(){
        if(instance == null){
            synchronized(Bank.class){
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}
  1. 方式三:Lock(锁) ---- JDK5.0新增

    Lock构造方法是可以传参指定是否创建公平锁

    new ReentrantLock( true ) 按照先来后到 默认false

     class Window implements Runnable{
         private int ticket = 100;
         //1.实例化ReentrantLock对象
         private ReentrantLock lock= new ReentrantLock();
             
         @Override
         public void run(){
             while(true){
                 try{
                     //2.调用锁定方法lock()
                     lock.lock();
                     if(ticket > 0){
                         try{
                             Thread.sleep(100);
                         }catch(InterruptedException e){
                             e.printStackTrace();
                         }
                         System.out.println("卖票" + ticket);
                         ticket--;
                     }
                 }finally{
                     //3.调用解锁方法unlock()
                     lock.unlock();
                 }
          }
         }
    

}


**<font color=blue>synchronized和Lock区别</font>**

> ==相同点:==
> 两者都可以解决线程安全问题
>
> ==不同点:==synchronized机制在执行完相应的同步带吗后,自动的释放同步监视器,Lock需要手动的启动同步用完释放
>
> 建议顺序  Lock--同步代码块--同步方法
> 

6. 线程的通信

==涉及到的三个方法== wait() : 使当前线程进入阻塞状态,并释放同步锁 notify() : 唤醒被 wait() 的线程,如果有多个线程同时阻塞,就优先唤醒优先级高的,一样时随机唤醒。 notifyAll() : 唤醒所有阻塞的线程

①这三个方法只能出现在同步代码块或者同步方法中;
②且调用者必须是同步代码块或同步方法中的同步监视器
③这三个方法是定义在Object类中的

例子:使用两个线程交替打印1-100

class Number implements Runnable{
    private int num = 1;
    @Override
    public void run(){
        while(true){
            synchronized(this){
                notify();
                if(num <= 100){
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                    try{
                        wait();
                    }catch(InterruptedException){
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}
public class Test{
    public static void main(String[] args){
    	Number num = new Number();       
        Thread t1 = new Thread(num);
        Thread t2 = new Thread(num);
        
        t1.setName("线程1");
        t2.setName("线程2");
        
        t1.start();
        t2.start();        
    }
}

面试题:sleep() 和wait() 的异同

①相同点: 一旦执行方法,都可以使当前的线程进入阻塞状态

②不同点:

  1. 两个方法的声明位置不同:Thread类中声明sleep(),Object类中声明wait()
  2. 调用要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或者同步方法中
  3. 是否释放同步监视器:如果两个方法都是用在同步代码块或者同步方法中,sleep()不会释放同步监视器,wait()则会。

生产者和消费者问题

生产者(Productor) 将产品交给店员(Clerk)。而消费者从店员处取走产品,店员一次只能持有固定数量的产品(20),如果生产者使徒生产更多的产品,店员会让生产者停一下,如果店里有空位置放了,店员会通知生产者继续生产,如果没有产品了。会告诉消费者等一下,有了会通知缴费者取走商品

public class ProductTest{
   public static void main(String[] args){
       Clerk clerk = new Clerk();
       Producer p1 = new Producer(clerk);
       Customer c1 = new Customer(clerk);
       
       p1.setName("生产者");
       c1.setName("消费者");
       
       p1.start();
       c1.start();
   }  
}
class Clerk{
    //产品数量
    private int num = 0;
    //生产产品
    public synchronized void produceProduct(){
    	if(num < 20){
          num++;  System.out.println(Thread.currentThread().getName() + "开始生产第" + num + "个产品");
          notify();
        }else{
          	try{
                wait();
            }catch(InterriptedExceptione){
                e.printStackTrace();
            }  
        } 
    }
    //消费产品
    public synchronized void sonsumeProduct(){
        if(num > 0){
    		 System.out.println(Thread.currentThread().getName() + "开始消费第" + num + "个产品");
            num--;
            notify();
        }else{
            try{
                wait();
            }catch(InterriptedExceptione){
                e.printStackTrace();
            }
        }
    }
}
class Producer implements Runnable{
   //生产者
    private Clerk clerk;
    
    public Producer(Clerk clerk){
        this.clerk = clerk;
    }
    
    @Override
    public void run(){
       System.out.println("生产者开始生产产品...");
        while(true){
            clerk.produceProduct();
        }
    }
}
class Customer implements Runnable{
    //消费者
    private Clerk clerk;
    
    public Customer(Clerk clerk){
        this.clerk = clerk;
    }
    
    @Override
    public void run(){
      	System.out.println("消费者开始消费产品...");
      while(true){
            clerk.consumeProduct();
        }  
    }
}

7.JDK5.0新增的线程创建方式

1. 实现Callable接口

与使用 Runnable相比,使用 Callable功能更强大

  • 相比 run() 方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回类型
  • 需要借助 FutureTask 类,比如获取返回结果
class NumThread implements Callable{
    @Override
    public Object call() throws Exception{
        int sum = 0;
        for(int i = 1;i <= 100;i++){
            if(i % 2 == 0){
            	sum += i; 
            }
        }
        return sum;
    }
}
public class Test {
    public static void main(String[] args){
    	NumThread num = new NumThread();
        FutureTask ft = new FutureTesk(num);
        new Thread(ft).start();
        System.out.println(ft.get());
    }
}
  1. 创建一个实现Callable的实现类

  2. 实现 call() 方法,将次线程要执行的操作声明在call() 方法中

  3. 创建Callable实现类的对象

  4. 将此对象传递到FutureTask构造器中,创建FutureTask对象

  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象并调用start()

2.使用线程池

提前创造号多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁的创建销毁、实现重复利用。

==好处==

  1. 提高了响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中的线程,不需要每次创建)
  3. 便于线程管理
public class ThreadPool{
    public static void main(String[] args){
    	//提供指定数量的线程
        ExecutorService es = Executors.newFixedThreadPool(10);
        //适合适用于Runnable
        es.execute(new NumThread);
        //适合使用于Callable
        //es.submit();
        //关闭线程池
        es.shutdown();
    }
}