多线程

进程

每个应用程序在运行期间,操作系统为应用程序分配一个独立的内存空间,称为进程;多个进程之间的数据是相互隔离的; windows查看后台进程命令

tasklist

linux查看后台进程命令

ps -aux

线程

进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个程序可同一时间执行多个线程,就是支持多线程的; 充分利用CPU多核心的优势,进一步提高程序性能。

多线程的优点

1.可以更好的实现并行

2.恰当地使用线程时,可以降低开发和维护的开销,并且能够提高复杂应用的性能。

3.CPU在线程之间开关时的开销远比进程要少得多。因开关线程都在同一地址空间内,只需要修改线程控制表或队列,不涉及地址空间和其他工作。

4.创建和撤销线程的开销较之进程要少。

Java在语言级提供了对多线程程序设计的支持

多线程操作会增加程序的执行效率。各线程之间切换执行,时间比较短,看似是多线程同时运行,但对于执行者CPU来说,某一个时刻只有一个线程在运行 分时系统的特点:

实现多线程

java中,每个线程都是一个Thread类的对象;Thread类中常用的两个方法:run()start();

run():方法是实现单个线程的主体,我们在实现多线程时,需要重写run方法,并在方法体以内完成我们要做的事情;run()不是给我们来调用,而是让多个线程在JVM中运行时,由CPU来控制线程的切换,以及对run方法的调用;

start():使线程进入到可执行状态,等待cpu为线程分配执行时所需要的资源;

Thread类

构造方法

方法描述
Thread() 创建一个新的线程对象
Thread(Runnable target) 创建一个新的线程,线程的实现位于Runnable对象中
Thread(Runnable target,String name) 创建一个新的线程,通过Runnable对象实现,并为线程命名
Thread(String name) 创建一个线程,并为其指定名称

方法摘要

方法描述
getId():long 返回线程的唯一标识符
getName():String 返回线程的名称
getPriority():int 返回线程的优先级;默认优先级为5,最低1,最高为10
getState():Thread.State 返回线程的状态
join():void 等待当前线程结束
run():void 实现线程的主体方法,不需要我们手动调用
setDaemon(boolean) 是否将线程设置为守护线程
setPriority(int) 为线程设置优先级;默认优先级为5,最低1,最高为10
static sleep(long) 使线程休眠指定的毫秒数,不会失去线程的锁
start():void 使线程进入到可执行状态
static yield():void 使线程放弃对CPU的使用权,从新进入到可执行状态
static currentThread():Thread 返回当前正在运行的线程

实现线程的方法

创建自己的线程有两种方式;

  1. 用一个类继承Thread,不推荐;java是单继承的特性

  2. 用一个类实现Runnable接口,推荐;java是多实现的特性

public class Thread1 extends Thread{
    /*
    * 继承Thread类,必须重写父类中的run方法
    * 程序在运行期间,jvm负责调用run方法中的代码实现多线程的切换
    * */
    @Override
    public void run() {
        Common.test1();
    }
}
/*实现Runnable接口*/
public class Thread2 implements Runnable {
    @Override
    public void run() {
        Common.test2();
    }
}

线程的调度策略

通过线程模拟铁路售票;开启3个线程销售100张票;

public class Ticket implements Runnable{
    private int ticket = 100; //模拟总共有100张票
    @Override
    public void run() {
        /*
        * 只要余票大于0,就要继续卖
        * */
        while (ticket > 0) {
            //得到当前正在运行的线程的名称
            String name = Thread.currentThread().getName();
            System.out.println( name + "卖出了票号" + ticket + "的票");
            this.ticket--;
        }
    }
}
/*测试类*/
public class Client {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket,"A窗口");
        Thread t2 = new Thread(ticket,"B窗口");
        Thread t3 = new Thread(ticket,"C窗口");
​
        t1.start();
        t2.start();
        t3.start();
    }
}
A窗口卖出了票号100的票
C窗口卖出了票号100的票
B窗口卖出了票号100的票
...
C窗口卖出了票号1的票
A窗口卖出了票号99的票
B窗口卖出了票号36的票

存在的问题:

  1. 票号有重复的

  2. 总共100张票被卖了102次

为了理解为什么出现如上的问题,我们需要理解线程的调度策略;

线程的调度策略

  1. 时间片策略

  2. 抢占式策略

同优先级线程组成先进先出队列(先到先服务),使用时间片策略

对高优先级,使用优先调度的抢占式策略

 

多个线程在执行期间,默认以抢占式策略来尝试获取到CPU的使用权;在多线程环境中,同一个时间点只有一个线程被执行,但是CPU为每个线程分配的使用时间非常的短暂,因此给我们感官上,多个线程同时在执行;

抢占式策略类似于在食堂排队吃饭;默认情况下按照抢占式策略,谁的动作快谁就先完成打饭;如果大家动作都一样,就看谁的块头大(优先级);

线程的优先级:默认为5,最高为10,最低为1;通过Thread类中的setPriority(int)设置优先级;

线程的生命周期

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件

运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态

死亡:线程完成了它的全部工作或线程被提前强制性地中止

 

线程的同步

是为了防止多个线程访问一个数据对象时,对数据造成的破坏

线程同步,其实就是排队,然后一个一个对共享资源进行

操作,而不是同时进行操作

只有共享资源的读写访问才需要同步

线程同步能够保证多线程中共享资源的数据安全问题(有的线程读,有的线程写);

默认情况下,线程是按照时间片策略来执行;但是使用了同步以后,线程的执行时间就不由时间片策略管理,而是交给线程本身;因此被同步的资源,前一个线程没有释放它的控制(锁),后一个线程就会一直等待;

线程同步的两种方式

  1. 同步代码块

    /*同步代码块存在于方法以内*/
    public void run(){
        synchronized(对象){//共享锁
             //需要同步的代码;
        }
    }
  2. 同步方法

    同步方法必须位于共享资源类中;不能定义到其他的地方;因为同步方法同样需要加锁,同步方法加锁的对象为共享资源对象本身;

    /*哪个线程得到对同步方法使用权限时,方法只要没结束,别的线程就无法访问同步方法*/
    public synchronized void test(){ //同步方法
        
    }

死锁

 

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 死锁出现时:程序出现假死现象;没有结果,但也不会结束。

解决方法: 专门的算法、原则 尽量减少同步资源的定义

 

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用,任务使用的资源中至少有一个是不能共享的。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:请求已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上面死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

如何防患?
  1)尽量避免使用多个锁(如果有可能的话)。

  2)规范的使用多个锁,并设计好锁的获取顺序。

  3)随用随放。即是,手里有锁,如果还要获得别的锁,必须释放全部资源才能各取所需。

  4)规范好循环等待条件。比如,使用超时循环等待,提高程序可控性。

在JAVA编程中,有3种典型的死锁类型:

  • 静态的锁顺序死锁  

    a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;

    另一个线程执行了b方法且已经获得了B锁,在等待A锁。

//可能发生静态锁顺序死锁的代码
class StaticLockOrderDeadLock {
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    public void a() {
        synchronized (lockA) {
            synchronized (lockB) {
                System.out.println("function a");
            }
        }
    }
    
    public void b() {
        synchronized (lockB) {
            synchronized (lockA) {
                System.out.println("function b");
            }
        }
    }
}

  解决办法: 所有需要多个锁的线程,都要以相同的顺序来获得锁。

//正确的代码
class StaticLockOrderDeadLock {
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    public void a() {
        synchronized (lockA) {
            synchronized (lockB) {
                System.out.println("function a");
            }
        }
    }
    
    public void b() {
        synchronized (lockA) {
            synchronized (lockB) {
                System.out.println("function b");
            }
        }
    }
}
  • 动态的锁顺序死锁

    动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。

    一个线程调用了transferMoney方法并传入参数accountA,accountB;
    另一个线程调用了transferMoney方法并传入参数accountB,accountA。
    此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁并等待accountA锁。

//可能发生动态锁顺序死锁的代码
class DynamicLockOrderDeadLock {
    public void transefMoney(Account fromAccount, Account toAccount, Double amount) {
        synchronized (fromAccount) {
            synchronized (toAccount) {
                //...
                fromAccount.minus(amount);
                toAccount.add(amount);
                //...
            }
        }
    }
}

  解决方案:使用System.identifyHashCode来定义锁的顺序。确保所有的线程都以相同的顺序获得锁。

class DynamicLockOrderDeadLock {
    private final Object myLock = new Object();
    public void transefMoney(final Account fromAccount, final Account toAccount, final Double amount) {
        class Helper {
            public void transfer() {
            //...
            fromAccount.minus(amount);
            toAccount.add(amount);
            //...
            }
        }
        int fromHash = System.identityHashCode(fromAccount);
        int toHash = System.identityHashCode(toAccount);
        if (fromHash < toHash) {
            synchronized (fromAccount) {
                synchronized (toAccount) {
                    new Helper().transfer();
                 }
            }
        } else if (fromHash > toHash) {
            synchronized (toAccount) {
                synchronized (fromAccount) {
                    new Helper().transfer();
                }
            }
        } else {
            synchronized (myLock) {
                synchronized (fromAccount) {
                    synchronized (toAccount) {
                        new Helper().transfer();
                    }
                }
            }
        }
        
    }
}        
  • 协作对象之间发生的死锁。

    有时,死锁并不会那么明显,比如两个相互协作的类之间的死锁

    一个线程调用了Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getImage方法。

    此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Dispatcher对象锁并等待Taxi对象锁

//可能发生死锁
class Taxi {
    private Point location, destination;
    private final Dispatcher dispatcher;
    public Taxi(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }
    
    public synchronized Point getLocation() {
        return location;
    }
    public synchronized void setLocation(Point location) {
        this.location = location;
        if (location.equals(destination))
            dispatcher.notifyAvailable(this);//外部调用方法,可能等待Dispatcher对象锁
    }
}

class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;
    public Dispatcher() {
        taxis = new HashSet<Taxi>();
        availableTaxis = new HashSet<Taxi>();
    }
    
    public synchronized void notifyAvailable(Taxi taxi) {
        availableTaxis.add(taxi);
    }
    public synchronized Image getImage() {
        Image image = new Image();
        for (Taxi t : taxis)
            image.drawMarker(t.getLocation());//外部调用方法,可能等待Taxi对象锁
        return image;
    }
}

我们在持有锁的情况下调用了外部的方法,这是非常危险的(可能发生死锁)。为了避免这种危险的情况发生, 我们使用开放调用。如果调用某个外部方法时不需要持有锁,我们称之为开放调用。

  解决方案:需要使用开放调用,即避免在持有锁的情况下调用外部的方法。

//正确的代码
class Taxi {
    private Point location, destination;
    private final Dispatcher dispatcher;
    public Taxi(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }
    public synchronized Point getLocation() {
        return location;
    }
    public void setLocation(Point location) {
        boolean flag = false;
        synchronized (this) {
            this.location = location;
            flag = location.equals(destination);
        }
        if (flag)
            dispatcher.notifyAvailable(this);//使用开放调用
    }
}

class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;
    public Dispatcher() {
        taxis = new HashSet<Taxi>();
        availableTaxis = new HashSet<Taxi>();
    }
    public synchronized void notifyAvailable(Taxi taxi) {
        availableTaxis.add(taxi);
    }
    public Image getImage() {
        Set<Taxi> copy;
        synchronized (this) {
            copy = new HashSet<Taxi>(taxis);
        }
        Image image = new Image();
        for (Taxi t : copy)
            image.drawMarker(t.getLocation());//使用开放调用
        return image;
    }
}

 

wait()和sleep()有什么区别

这两个方法都能使线程进入阻塞状态; sleep():是Thread类中的静态方法。 在不会失去共享资源锁的情况下阻塞指定时间;当时间到了以后,继续执行; wait() : 是Object类中的实例方法;当前线程失去共享资源锁的情况下进入阻塞状态;当被别的线程调用notify()或者notifyAll()时唤醒,进入到就绪状态;

线程之间的通信

线程进行协同工作时,通过wait(),notify(),notifyAll()这几个方法进行。 wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问。线程进入阻塞状态。(失去共享资源的锁) notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待,进入就绪状态。 notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

 

可重入锁

从java1.5开始出现可重入锁java.util.concurrent.locks.ReentrantLock

synchronized的区别:

  • synchronized是jvm从语法层面支持的写法

  • ReentrantLock是一个java类,通过CAS和AQS实现线程的同步。内部维护的是一个双端线程队列。

ReentrantLock能够实现公平锁和非公平锁。

使用synchronized同步时:

public synchronized void test(){
    //同步方法的开始
    
    //同步方法的结束
}

使用ReentrantLock同步时:

ReentrantLock lock = new ReentrantLock();
lock.lock(); //手动加锁
    //需要同步的代码
lock.unlock(); //手动的解锁

内建锁隐式支持重入锁,Synchronized通过获取自增,释放自减的方式实现重入

1.重入锁实现原理

重入锁的特点:

1)线程获取锁时,如果已经获取锁的线程是当期线程直接再次获取;

2)由于锁会被获取N次,因此锁只有被释放N次之后才算真正释放成功

2.公平锁与非公平锁

公平锁:锁的获取顺序一定满足时间上的绝对顺序,等待时间最长的线程一定最先获取到锁

 

Reentrantlock默认使用非公平锁 对比:公平锁保证每次获取锁均为同步队列的第一个节点,保证了请求资源时间上的绝对顺序,但是效率

较低,需要频繁的进行上下文切换。

非公平锁会降低性能开销,降低一定得上下文切换,但是可能导致其他线程永远无法获取到锁,造成线程“饥饿”现象。

通常来讲,没有特定公平性要求,尽量选择非公平锁

线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

通过Executors工具类中的方法创建线程池

方法描述
newFixedThreadPool(int nThreads) 创建一个具有默认大小的线程池
newSingleThreadExecutor() 创建只有1个线程的线程池
newCachedThreadPool() 创建一个具有缓存的线程池
newScheduledThreadPool(int corePoolSize) 创建一个延时的线程池
//创建了一个具有4个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
//创建了一个具有1个线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
//创建具有缓存的线程池,适合短时间内,多个执行时间比较短的任务
//线程池初始化大小为0,当任务到达时,从线程池中尝试获取一个线程使用
//如果线程池中没有可用线程,则创建一个线程并放入线程池
//当线程池中的线程空闲时间达到60秒钟,则这线程将从线程池中删除。
ExecutorService pool = Executors.newCachedThreadPool();

线程池具有的方法描述

  • execute(Runnable): 执行线程任务,不返回结果

  • submit(Runnable): 执行线程任务,返回特定的结果

在Java中可以通过线程池来达到这样的效果。

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

 下面解释下一下构造器中各个参数的含义:

  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

  • maximumPoolSize:线程池中的最大线程数。表示线程池中最多可以创建多少个线程,很多人以为它的作用是这样的:”当线程池中的任务数超过 corePoolSize 后,线程池会继续创建线程,直到线程池中的线程数小于maximumPoolSize“,其实这种理解是完全错误的。它真正的作用是:当线程池中的线程数等于 corePoolSize 并且 workQueue 已满,这时就要看当前线程数是否大于 maximumPoolSize,如果小于maximumPoolSize 定义的值,则会继续创建线程去执行任务, 否则将会调用去相应的任务拒绝策略来拒绝这个任务。另外超过 corePoolSize的线程被称做"Idle Thread", 这部分线程会有一个最大空闲存活时间(keepAliveTime),如果超过这个空闲存活时间还没有任务被分配,则会将这部分线程进行回收。

  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性

  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响

  • threadFactory:线程工厂,主要用来创建线程

  • handler:表示当拒绝处理任务时的策略;当线程池中的线程数已经达到了最大允许的线程数,并且队列已满,则新的任务会导致拒绝策略的执行。默认抛出java.util.concurrent.RejectedExecutionException异常

Executor和ExecutorService接口

这两个接口定义了线程池具备的方法。

 

 

 

ThreadPoolExecutor是一个实现类,实现了ExecutorService接口。ExecutorService接口继承了Executor接口。

Executor接口中的方法

public interface Executor {

    /*让线程池执行一个不返回结果的任务*/
    void execute(Runnable command);
}

 

ExecutorService接口中的方法

public interface ExecutorService {
    /*同样也是将任务提交给线程池执行,但是需要返回一个结果*/
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
}

 

 
posted @ 2022-09-22 14:39  岁月记忆  阅读(102)  评论(0)    收藏  举报