7 多线程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了7 多线程相关的知识,希望对你有一定的参考价值。

多线程


1.相比于多进程,多线程的优势有:

(1)进程之间不能共享数据,线程可以;

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

2.创建线程和启动(3种)

(1)继承Thread类,重写run()方法(用匿名类)

      Thread thread = new Thread(){
     public void  run(){
     };
  }
    t.start();

 (2) 实现Runnable接口,重写run方法
         两种写法:

    匿名:
             Runnable task = new Runnable(){
                     public void run()
                    {
                     }
                 };
              Thread t = new Thread( task );
               t.start();

        Lambda表达式

            Runnable task = () -> {
                System.out.println("HelloWorld");
            };
            Thread t = new Thread( task );

(3)通过Callable和Future创建线程

     Callable的特点:
         1.可以有返回值
         2.接口的方法抛出Exception,如果在任务主体里面有异常,可以不处理,系统自动处理

 使用Callable的步骤:
    1.创建Callable的实例
        Callable<String> call = () -> { return "xxx"; };
    2.包装成一个FutureTask(实现了Future和Runable接口)
        // FutureTask的泛型参数,必须和Callable的泛型参数一样,要求相同类型、兼容类型
        FutureTask<String> task = new FutureTask<>( call );

    3.把FutureTask作为任务,传递给Thread的构造器
        Thread t = new Thread( task );
    4.调用线程的start方法
        t.start();

3.线程的生命周期
(1) 、新建状态

用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

(2)、就绪状态

处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一会儿,转去执行子线程。

(3)、运行状态

处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

注: 当发生如下情况时,线程会从运行状态变为阻塞状态:

①、线程调用sleep方法主动放弃所占用的系统资源

 ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

 ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有

 ④、线程在等待某个通知(notify)

 ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

(4)、阻塞状态

 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:

(5)、死亡状态

当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
**4.线程管理**

(1)线程睡眠--sleep
        Thread.sleep(1000);

 (2)线程让步--yield
        Thread.yield();
            设置优先级:thread.setPriority(1);
注:关于sleep()方法和yield()方的区别如下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

      ②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

      ③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

     (3)线程合并 --join  (thread.join() )

           将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时

             有三个重载方法:

                  void join()   当前线程等该加入该线程后面,等待该线程终止。    

                  void join(long millis)     当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度  

                   void join(long millis,int nanos)     等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度  

     (4)设置线程的优先级(thread.setPriority(1) )
         优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
     每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。

       注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:
                 MAX_PRIORITY   =10

                 MIN_PRIORITY   =1

                 NORM_PRIORITY   =5

     class MyThread extends Thread {  
                public MyThread(String name,int pro) {  
                        super(name);//设置线程的名称  
                        setPriority(pro);//设置线程的优先级  
                        }  
                  @Override  
                 public void run() {  
                         for (int i = 0; i < 100; i++) {  
                     System.out.println(this.getName() + "线程第" + i + "次执行!");  
                        }  
                 }  
            }  

     public class Test1 {  
                   public static void main(String[] args) throws InterruptedException {  
                new MyThread("高级", 10).start();  
                new MyThread("低级", 1).start();  
             }  
           }  

     (5)后台(守护)进程 --thread.setDaemon(true);

                把线程对象设置为后台线程,此方法必须在start()之前调用。
        后台线程主要用于维护、监控任务。

        所有的非后台线程结束后,表示程序要结束。此时如果还有后台线程正在执行,那么所有的后台线程直接结束、中断。

 (6)正确结束线程
         废弃方法 Thread.stop(); Thread.suspend(); Thread.resume(); 

      ①正常执行完run方法,然后结束掉;

              ②控制循环条件和判断条件的标识符来结束掉线程。

5.线程同步(同步锁)

多线程并发时,多个线程同时操作一个可共享的资源时,将会导致数据不准确。
      (1)同步方法
     既有synchronized关键字修饰的方法。由于java每一个对象都有一个内置锁,当用此关键字修饰方法时,
     内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则处于阻塞状态。
     注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
  (2)同步代码块
   既有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
  注:同步是一种高开销的操作,因此应尽量减少同步的内容。
  (3)使用重入锁(Lock)实现线程同步
       ReentrantLock类是可重入、互斥、实现了Lock接口的锁。
       ReentrantLock() : 创建一个ReentrantLock实例         
              lock() : 获得锁        
              unlock() : 释放锁

       class Bank {

        private int account = 100;
        //需要声明这个锁
        private Lock lock = new ReentrantLock();
        public int getAccount() {
            return account;
        }
        //这里不再需要synchronized 
        public void save(int money) {
            lock.lock();
            try{
                account += money;
            }finally{
                lock.unlock();
            }

        }
    }

6.线程通信(生产者和消费者)

(1)、借助于Object类的wait()、notify()和notifyAll()实现通信

线程执行wait()后,就放弃了运行资格,处于冻结状态;

线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
notifyall(), 唤醒线程池中所有线程。
注:   
① wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中;
② wait(),notify(),notifyall(),  在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

有一个字段flag来判断生产品的数量是否为空,消费品是否为空。true表示产品有,通知消费者消费;false就是没有商品
true:生产者等待消费,消费者通知,并设置为false
false:消费者等待生产,生产者通知,并设置为true

class Resource{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
while(flag) 
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag=true;
this.notifyAll();
}
public synchronized void out(){
while(!flag) 

try{wait();}catch(Exception e){}

System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
this.notifyAll();

}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(pro);
Thread t4=new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}

(2)、使用Condition控制线程通信

jdk1.5中,提供了多线程的升级解决方案为:

      ①将同步synchronized替换为显式的Lock操作;

      ②将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;

      ③一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。
class Resource{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock = new ReentrantLock();/Lock是一个接口,ReentrantLock是该接口的一个直接子类。/
private Condition condition_pro=lock.newCondition(); /创建代表生产者方面的Condition对象/
private Condition condition_con=lock.newCondition(); /使用同一个锁,创建代表消费者方面的Condition对象/

public void set(String name){  
        lock.lock();//锁住此语句与lock.unlock()之间的代码  
        try{  
            while(flag)  
                condition_pro.await(); //生产者线程在conndition_pro对象上等待  
            this.name=name+"---"+count++;  
            System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);  
            flag=true;  
             condition_con.signalAll();  
        }  
        finally{  
            lock.unlock(); //unlock()要放在finally块中。  
        }  
    }  
    public void out(){  
        lock.lock(); //锁住此语句与lock.unlock()之间的代码  
        try{  
            while(!flag)  
                condition_con.await(); //消费者线程在conndition_con对象上等待  
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);  
        flag=false;  
        condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/  
        }  
        finally{  
            lock.unlock();  
        }  
    }  
}  
    **(3)、使用阻塞队列(BlockingQueue)控制线程通信**

     BlockingQueue是一个接口,也是Queue的子接口。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

 BlockingQueue提供如下两个支持阻塞的方法:

                 ①put(E e):尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。

                 ②take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

 BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:

                 ①在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

                 ②在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

                 ③在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。
public class BlockingQueueTest{ public static void main(String[] args)throws Exception{ //创建一个容量为1的BlockingQueue

BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
    //启动3个生产者线程
    new Producer(b).start();
    new Producer(b).start();
    new Producer(b).start();
    //启动一个消费者线程
    new Consumer(b).start();

}
} class Producer extends Thread{ private BlockingQueue<String> b;

public Producer(BlockingQueue<String> b){
    this.b=b;

}
public synchronized void run(){
    String [] str=new String[]{
        "java",
        "struts",
        "Spring"
    };
    for(int i=0;i<9999999;i++){
        System.out.println(getName()+"生产者准备生产集合元素!");
        try{

            b.put(str[i%3]);
            sleep(1000);
            //尝试放入元素,如果队列已满,则线程被阻塞

        }catch(Exception e){System.out.println(e);}
        System.out.println(getName()+"生产完成:"+b);
    }

}
} class Consumer extends Thread{ private BlockingQueue<String> b; public Consumer(BlockingQueue<String> b){ this.b=b; } public synchronized void run(){

while(true){
        System.out.println(getName()+"消费者准备消费集合元素!");
        try{
            sleep(1000);
            //尝试取出元素,如果队列已空,则线程被阻塞
            b.take();
        }catch(Exception e){System.out.println(e);}
        System.out.println(getName()+"消费完:"+b);
    }

}

7.线程池

 线程池的核心: 
           ①.创建一堆的线程放到内存里面备用。每个线程的run方法都不会结束。 在没有任务的时候,wait状态。

      ②如果有计算任务到达,就从线程池里面获取一个线程对象出来,并且把任务设置给线程对象。
        设置完以后,发送notify通知线程要执行任务。

      ③任务执行完成以后,就会把线程放回线程池,并且进入wait状态。
合理利用线程池能够带来三个好处。

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
使用Executors工厂类产生线程池

Executor线程池框架的最大优点是把任务的提交和执行解耦。客户端将要执行的任务封装成Task,然后提交即可
  ExecutorService(实现类 ThreadPoolExecutor,ScheduledThreadPoolExecutor)继承了Executor接口(注意区分Executor接口和Executors工厂类),
使用Executors执行多线程任务的步骤如下:
? 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;

? 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;

? 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;

? 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

【重点】ThreadPoolExecutor

简单的线程池,就是创建线程备用的。

    创建3个Runnable对象,这个对象里面每次执行都需要3秒钟。
    把3个任务,提交给线程池,但是线程池的大小是1,意味着最多同时执行1个任务。

    ExecutorService pool = Executors.newFixedThreadPool( 大小 );

ScheduledThreadPoolExecutor

    可以调度的线程池,里面的任务可以按照一定的规则循环、重复执行。

    定时任务,一般会使用spring-timer来代替,支持更加复杂的任务调度方式。

    ScheduledExecutorService pool = Executors.newScheduledThreadPool( 大小 );

    定时重复调用的方法:
        scheduleAtFixedRate  : 以固定的频率执行任务,以任务的开始时间计算频率。
            假设间隔2秒,每次执行任务需要3秒。
            频率的间隔比任务所需要的时间要小。

            此时前面的任务完成以后,马上执行下一次任务。

            *间隔以开始时间计算

        scheduleWithFixedDelay : 以固定的间隔执行任务

8.死锁

产生死锁的四个必要条件如下。当下边的四个条件都满足时即产生死锁,即任意一个条件不满足既不会产生死锁。

(1)死锁的四个必要条件 互斥条件:资源不能被共享,只能被同一个进程使用 请求与保持条件:已经得到资源的进程可以申请新的资源 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源

举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
(2)处理死锁的方法

忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
检测死锁并恢复;
资源进行动态分配;
破除上面的四种死锁条件之一。

9.线程相关类

ThreadLocal

ThreadLocal它并不是一个线程,而是一个可以在每个线程中存储数据的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到该线程的数据。 即多个线程通过同一个ThreadLocal获取到的东西是不一样的,就算有的时候出现的结果是一样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不同的。使用这个工具类可以简化多线程编程时的并发访问,很简洁的隔离多线程程序的竞争资源。

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
若多个线程之间需要共享资源,以达到线程间的通信时,就使用同步机制;若仅仅需要隔离多线程之间的关系资源,则可以使用ThreadLocal。

以上是关于7 多线程的主要内容,如果未能解决你的问题,请参考以下文章

线程学习知识点总结

多个请求是多线程吗

python小白学习记录 多线程爬取ts片段

python3.7多线程代码不执行?

多线程编程

多线程编程