多线程

一,什么是多线程?

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

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

并行与并发:

  • 并行:多个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任务.

 

 

原文地址:https://www.cnblogs.com/zkkkk-/p/10535313.html