Java多线程

Java多线程

一、线程的概念

1、程序是静止的,只有运行起来的程序才是进程

2、线程是程序运行时最小的调度单位,而进程是程序运行时分配资源的最小单位

3、进程就像是一个任务,而线程是执行这个任务的工人,一个任务可以由多个工人一起来来完成;假定在执行某个任务时,调度某几个工人去完成某个任务

4、当一个进程的所有线程都执行完毕后,该进程结束

5、宏观上来看是多进程轮转执行,微观上来看是多线程竞争时间片抢占式执行

多线程宏观并行,微观串行
ps:这就如同电灯的频闪一样,当频闪的频率足够快时,肉眼是识别不出的,用户只会觉得电灯一直在亮

二、线程的组成

1、CPU时间片

顾名思义,是一个时间片段,操作系统会为每个线程分配一段执行时间,当一个线程抢到时间片时就可以执行该线程的逻辑代码;当时间片到期时,线程就不可再执行,直到下一次再抢到时间片

2、运行数据

  • 堆空间:存储线程运行时需要使用的对象,多个线程共享堆中的对象
  • 栈空间:存储线程运行时需要使用的局部变量,每个线程都拥有独立的栈

堆空间共享,栈空间独立

3、线程的逻辑代码

每个线程需要执行的任务

三、线程的创建方式

1、继承Thread类

在面向对象的世界中,万物皆对象,所以线程也是一个对象。因此继承Thread类,其子类也就具有线程的特征

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("First Thread:"+i);
        }
    }
}
public class TestExtendsThread {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        ThreadTwo threadTwo=new ThreadTwo();
        myThread.start();
        threadTwo.start();
        for (int i=0;i<100;i++){
            System.out.println("main:"+i);
        }
    }
}

通过继承Thread类并重写run方法创建一个线程

通过start方法启动一个线程

2、实现Runnable接口

接口代表能力,重写Runnable接口便具有了线程的能力

public class MyThread1 implements Runnable{
    @Override
    public void run() {
        for (int i=1;i<=50;i++){
            System.out.println("thread1=="+i);
        }
    }
}
public class TestImplementRunable {
    public static void main(String[] args) {
        MyThread1 task1=new MyThread1();
        MyThread2 task2=new MyThread2();
        Thread thread=new Thread(task1);
        Thread thread1=new Thread(task2);
        thread1.start();
        thread.start();
    }
}

通过实现Runnable接口,并重写run方法创建一个线程

此时创建的严格意义上来说不是一个线程,而是一个线程需要完成的任务task,因此不具备启动线程的能力,需要一个线程去完成这个任务

通过Thread的构造方法,将task赋给target,从而可以通过start方法启动线程并完成任务

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

3、两种线程创建方式的比较

  • 继承Thread类创建线程,由于java只支持单继承,因此无法再去继承其他类,且继承时继承了Thread类的所有方法,因此此方式不是很灵活
  • 实现Runnable接口,java支持多继承,因此任意一个类只要实现了Runnable接口就都具有了线程启动的能力,且只需要重写run方法,因此此方式比较灵活
  • 从程序设计上来看,继承Thread的子类也是一个线程类,因此不利于程序的扩展
  • 任意一个类实现了Runnable接口就都具有了线程启动的能力,因此具有较强的扩展性

线程通过thread.start()方法启动,此方法只是通知一下此线程可以启动了,主线程继续执行

四、线程等待

1、sleep(long millis)

该方法是Thread类的一个静态方法,其表示线程休眠,其参数是休眠时间;该方法在哪个线程的代码块中,就在哪个线程生效。当线程休眠时,线程进入有限期等待状态,期限为sleep方法的参数

2、yield()

该方法是Thread类的一个静态方法,其表示处于运行态的线程放弃当前时间片,并进入无限期等待状态,但马上退出等待状态并进入就绪态,重新竞争时间片

3、join()

该方法为Thread类的一个实例方法,该方法在哪个线程的代码块中,就在哪个线程生效,其表示另一个线程加入到当前线程中。当该方法生效时,原线程由运行态进入无限期等待状态,加入的线程由就绪态进入运行态,只有当加入的线程执行完毕进入终止态时,原线程才退出无限期等待的状态并进入就绪态,继续竞争时间片。

因此,原线程退出无限期等待状态的条件就是加入的线程执行完毕

join方法可以控制线程的执行顺序

五、线程安全与不安全

1、线程不安全

当多线程并发访问临界资源时,如果破坏了原子操作,造成有效数据的凭空消失或不一致称为线程不安全

临界资源:多个线程共享的同一个对象

原子操作:不可分割的多部操作,被视为一个整体,不可中断且不可改变其顺序

2、解决方法

将可能会造成线程不安全的多部操作封装为一个原子操作,一般的解决办法为同步代码块或上锁,将多线程并发执行转化为单线程串行

也可将临界资源作为线程本地变量存储(ThreadLocal)

六、同步代码块synchronized(互斥锁标记)

1、互斥锁标记

只有持有互斥锁标记的线程才能进入同步代码块,可将同步代码块中的代码视为为一个原子操作,synchronized()方法中的参数可以为多线程共享的临界资源,但一定要保证其的唯一性。因此,只有一个线程才能竞争到互斥锁标记,同时只有一个线程才能进入到同步代码块。没有竞争到锁的线程会进入到阻塞态,只有持有互斥锁标记的线程退出同步代码块才会释放锁标记,处于阻塞态的线程会继续竞争互斥锁标记,竞争到的线程会重新进入就绪态并竞争时间片

每个对象都有一个互斥锁标记,用于分配给线程的

持有互斥锁标记的线程时间片到期后并不会释放所标记,而是继续带着所标记进入就绪态竞争新的时间片

synchronized就像是一把锁,互斥锁标记就是一个钥匙;同时只存在一把锁和一把钥匙,因此只有一个线程能够竞争到这把钥匙,只有一个线程才能开锁。

2、春节抢票

一个经典的线程安全的例子,一辆火车共有100张票,由5个人同时来抢,模拟出抢票的过程

定义Ticket类

public class Ticket {
    Integer num=100;
}

定义User类

public class User implements Runnable {
    private Ticket ticket;

    public User(Ticket ticket){
        this.ticket=ticket;
    }

    @Override
    public void run() {
        while (ticket.num>0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ticket.num<=0){
                System.out.println("不好意思,票已售罄");
                break;
            }
            System.out.println(Thread.currentThread().getName() + "抢到了第" + (100 - ticket.num--) + "张票");
        }
    }
}

启动类

public class BuyTicket {
    public static void main(String[] args) {
        Ticket ticket=new Ticket();
        Thread user1=new Thread(new User(ticket),"小明");
        Thread user2=new Thread(new User(ticket),"小张");
        Thread user3=new Thread(new User(ticket),"小红");
        Thread user4=new Thread(new User(ticket),"小刚");
        Thread user5=new Thread(new User(ticket),"小赵");

        user1.start();
        user2.start();
        user3.start();
        user4.start();
        user5.start();
    }
}

此时临界资源为Ticket类,由于没有设置同步代码块,有可能会造成线程不安全,其结果如下
请添加图片描述

如图,有可能会造成一张票重复卖或超卖,解决方法是加同步代码块

public class User implements Runnable {
    private Ticket ticket;

    public User(Ticket ticket){
        this.ticket=ticket;
    }

    @Override
    public void run() {
        while (ticket.num>0) {
            synchronized (ticket) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (ticket.num<=0){
                    System.out.println("不好意思"+Thread.currentThread().getName()+",票已售罄");
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第" + (100 - ticket.num--) + "张票");
            }
        }
    }
}

请添加图片描述

由于ticket为临界资源,且为独一份,因此可将ticket设置为互斥锁标记

代码分析

所有人线程启动后进入就绪态,假设小明最先抢到时间片,持有互斥锁标记并进入运行态,运行到sleep方法,持有互斥锁进入有限期等待状态,等待0.1秒,进入就绪态,重新竞争时间片;假设第二轮小刚抢到时间片,准备进入运行态,但小明没有释放互斥锁,因此无法持有互斥锁,进入阻塞态;直到小明退出同步代码块,释放互斥锁,小刚抢到互斥锁,进入就绪态继续竞争互斥锁标记。

3、同步方法

同步方法是在方法上加synchronized,这样整个方法都是同步代码块,其等价于synchronized(this)

public synchronized void get(){
    //内容
}
//等价于
public void get(){
    synchronized(this){
        //内容
    }
}

从灵活性上来说,同步代码块更灵活一些

若同步方法为静态方法,其互斥锁为本类的类对象,不影响使用

4、总结

并不是每一个方法都需要同步代码块,若一个方法涉及到临界资源但不会改变临界资源的值,就不需要同步代码块,若加了同步代码块反而会造成性能的降低;只有可会改变临界资源从而造成线程不安全的方法时,才需要同步代码块进行约束。

例如查询操作,虽然也涉及到临界资源,但不会改变临界资源的值,因此不需要加同步代码块,如果加了同步代码块,会将并行转成穿行,降低性能;而增删改操作会改变临界资源,因此必须要加同步代码块。

加不加同步代码块,取决于该方法是否可能会造成线程不安全来决定

七、常见集合的线程安全

1、ArrayList与Vector

ArrayList线程不安全,但效率高
请添加图片描述
Vector线程安全,但效率低
请添加图片描述

2、HashMap与HashTable

HashMap线程不安全,效率高
请添加图片描述

HashTble线程安全,效率低

请添加图片描述

3、StringBuffer与StringBuilder

StringBuilder线程不安全,效率高

请添加图片描述

StringBuffer线程安全,效率低

请添加图片描述

4、ConcurrentHashMap

线程安全,但其使用的并不是同步方法,而是同步代码块分段加锁,因此线程安全且效率上来说比Hashtable要高

八、线程的状态

1、线程状态

一个线程可以有以下6种状态:

  • NEW
    线程尚未开始在运行。
  • RUNNABLE
    java虚拟机运行线程。
  • BLOCKED
    线程阻塞等待并监控这个状态。
  • WAITING
    处于这种状态的线程被无限期地等待另一个线程来执行特定的动作。
  • TIMED_WAITING
    处于这种状态的线程正在等待另一个线程上执行一个动作指定的等待时间。
  • TERMINATED
    处于这种状态的线程结束运行。

2、线程状态图

请添加图片描述

jdk1.5之后,就绪态与运行态是一种状态RUNNABLE

九、线程通信

1、死锁

若线程一持有A锁,并等待B锁;同时线程二持有B锁,并等待A锁;此时线程进入死锁

一个线程可以持有多把锁,当一个线程进入阻塞时并不会释放已持有的锁,可能会造成死锁

解决死锁就需要通信,即任意一方释放掉自身持有的锁

2、线程通信

等待

thread.wait();

必须在同步代码块中使用,调用这会释放掉自身所持有的所有所以及时间片进入到无限期等待状态

thread.wait(long timeout)

调用该方法进入期等待的线程具有自救能力,即释放掉自身持有的锁和时间片后进入限期等待状态,待时间到期后进入就绪态重新竞争时间片和锁

唤醒

thread.notify();

thread.notifyAll();

同样必须在同步代码块中使用,会唤醒一个或所有因释放锁而进入无限期等待的线程;被唤醒的线程会从无限期等待状态进入到就绪态中,重新竞争时间片以及互斥锁标记

且线程唤醒的是同锁内最先进入阻塞状态的线程

若没有唤醒线程,则处于无限期等待的线程将永久沉睡下去

3、生产者与消费者

无数个生产者生产产品,无数个消费者消费产品,为了能时生产者与消费者并发执行,在生产者与消费者之间设置一个队列,即生产者将生产的产品放入到队列中,消费者从队列中取走产品。为了能够使生产者与消费者同步进行,不允许生产者向已满的队列中再放入产品,也不允许消费者从一个空的队列中取走产品。

为了实现以上效果,就需要线程间的通讯。即生产者生产了产品时通知消费者来消费,若队列已满则进入等待;当消费者收到生产者的消息时会去队列种消费产品,当队列为空时消费者进入等待

创建队列

public class MyQueue {
    public Object[] queue=new Object[5];

    public Integer size=0;

    public Integer max=5;

    public synchronized void offer(Object obj) throws InterruptedException {
        this.notifyAll();
        while (size==max) {
            System.out.println(Thread.currentThread().getName()+"进入等待");
            this.wait();
        }
        queue[++size]=obj;
        System.out.println(Thread.currentThread().getName()+"放入了第"+size+"个"+obj);
    }

    public synchronized Object poll() throws InterruptedException {
        this.notifyAll();
        while (size==0){
            System.out.println(Thread.currentThread().getName()+"进入等待");
            this.wait();
        }

        Object obj=queue[0];

        for (int i=1;i<size;i++){
            queue[i-1]=queue[i];
        }
        queue[--size]=null;
        System.out.println(Thread.currentThread().getName()+"取走了"+obj);

        return obj;
    }
}

创建消费者

public class Consumer implements Runnable {
    private MyQueue mq;

    public Consumer(MyQueue mq) {
        this.mq = mq;
    }

    @Override
    public void run() {
        try {
            for (int i=0;i<5;i++){
                Object obj = mq.poll();
                System.out.println(obj);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建生产者

public class Producer implements Runnable {
    private MyQueue mq;

    public Producer(MyQueue mq){
        this.mq=mq;
    }

    @Override
    public void run() {
        String[] food=new String[]{"汉堡","薯条","可乐","大鸡腿","鸡排"};
        for (String f : food) {
            try {
                mq.offer(f);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

创建启动类

public class ThreadRun {
    public static void main(String[] args) {
        MyQueue mq=new MyQueue();

        Thread consumer1=new Thread(new Consumer(mq),"小明");
        Thread consumer2=new Thread(new Consumer(mq),"小刚");
        Thread consumer3=new Thread(new Consumer(mq),"小红");

        Thread producer1=new Thread(new Producer(mq),"粤菜大厨");
        Thread producer2=new Thread(new Producer(mq),"陕菜大厨");
        Thread producer3=new Thread(new Producer(mq),"川菜大厨");

        producer1.start();
        producer2.start();
        producer3.start();
        consumer1.start();
        consumer2.start();
        consumer3.start();
    }
}

运行结果如下:

请添加图片描述

在生产者与消费者使用while而不用if是因为,当队列为空时生产者进入等待,当另外的线程将消费者唤醒后生产者还为生产,而消费者继续向下执行,从队列中取到空值。因此加入while持续判断,当消费者醒来后再看一眼队列,若队列仍为空则继续等待;生产者同理

十、线程池

1、概念

线程是计算机中的稀缺资源,一个线程大概占1Mb左右的内存,过多的创建线程可能会造成内存溢出;而频繁的创建与销毁线程会造成JVM的运行压力,降低程序性能。

Thread类将线程与任务绑定,即任务执行完毕线程也运行完毕,而线程池可以将线程与任务分开,从而实现线程的复用,即一个线程可以执行多个任务,从而不会频繁的创建销毁线程,提高程序运行效率;线程池也可以预设线程的数量,分配线程数量的上限。

线程池也是一个池,线程池中有若干个线程,当有需要执行的任务时,线程池会从池中选择一个线程来执行该任务,在任务执行完毕后会将该线程放回线程池中;若线程池中的线程不够用时,会将任务先放入阻塞队列中,排队等候空闲线程来执行该任务。

2、Executor

Executor是线程池的顶级接口,该接口执行已提交的Runnable任务的对象,提供能够将任务与线程分离开来执行的方法execute(Runnable r)方法。该接口从宏观上定义了线程池具有将线程与任务分开的能力

3、ExecutorService

ExecutorService是Executor的子接口,其中还提供了更多的方法去描述线程池,从微观的角度等具体的定义了线程池的功能。其中重载了多个sumbit()方法,使线程池不仅可以使用Runnable接口还可以使用Callable接口

请添加图片描述

4、Callable与Future

Callable也可以定义一个线程的任务,但功能比Runnable接口强大。Runnable接口不能抛异常也没有返回值,而Callable接口可以抛异常也可以有返回值。Callable可以定义一个泛型,该泛型为其返回值的类型

Thread类可以通过构造方法传入Runnable接口从而执行其定义的任务,但Thread类没有任何一个方法可以执行Callable的任务,但ExecutorService接口的submit方法支持Callable接口,因此可以使用线程池去执行Callable接口定义的任务

线程池不是直接返回Callable的返回值,而是定义了一个Futrue接口封装了Callable的返回值,可以通过Future的get方法获取其返回值

Future的get方法在线程启动时进入无限期等待状态,直到线程运行结束,从无限等待状态中退出并返回线程运行的结果

因此ExecutorService、Callable、Future构成了一个整体,线程池执行Callable定义的命令,Future接收其运行的结果

5、ThreadPoolExecutor

Java中通过调用ThreadPoolExecutor的构造方法来创建一个线程池

public ThreadPoolExecutor(int corePoolSize,  
                             int maximumPoolSize,  
                             long keepAliveTime,  
                             TimeUnit unit,  
                             BlockingQueue<Runnable> workQueue,  
                             ThreadFactory threadFactory,  
                             RejectedExecutionHandler handler);

参数说明:

corePoolSize:核心线程数,线程池中的线程分为核心线程与非核心线程

maximumPoolSize:最大线程数量,即该线程池所能容纳的最大线程的数量

keepAliveTime:线程最长保留时间,线程池中的核心线程即使在没有任务时也不会被销毁,而该参数可以设置非核心线程的最大保留时间,即非核心线程在空闲一定时间后会被销毁

unit:时间单位,即每unit个时间单位后执行一个任务,可做定时任务的线程池

workQueue:阻塞队列,若线程池中没有空闲的核心线程时,会将任务加入到阻塞队列中,若阻塞队列也满了,需要判断线程是否已满(判断线程数是否超过maximumPoolSize),若线程池没有满则需要创建非核心线程来执行任务,否则执行拒绝策略。该队列的模式类似于9.3的生产车与消费者模式

threadFactory:线程工厂,即创建线程的线程的方法,是一个接口,其中只有一个抽象方法

Thread newThread(Runnable r);

Java默认的实现类为:

static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
		//构造方法
        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }
		//创建一个新线程
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

handler:拒绝策略,在线程池满了时需要执行的策略,一般有四种策略

  • AbortPolicy:不执行新任务,直接抛异常,并提示线程池已满
  • DisCardPolicy:不执行新任务,也不抛异常,没有任何提示
  • DisCardOldSetPolicy:将线程池中的第一个任务替换为当前任务
  • CallerRunsPolicy:直接执行当前任务

6、阻塞队列

阻塞队列为生产者与消费者模式,生产者为调用者,消费者为线程池,产品为需要执行的任务;在线程池中核心线程已满时,会将任务

阻塞队列中常用的操作如下:

// 将任务添加到队列尾部,若队列已满则直接抛出异常
boolean add(E e)
 
// 将任务添加到队列尾部,若成功则返回true,若队列已满则返回false
boolean offer(E e)
 
// 将任务添加到队列的尾部,若队列已满,则线程进入无限期等待,等待队列中的任务被消费
void put(E e)
     
// 移除队列中的某个任务,若队列为空,抛出异常。
boolean remove(Object o)
 
// 查看队列中的第一个元素,若为空,则返回 null。
E peek()
 
// 查看并移除队列中的第一个元素,若为空,返回null。
E poll()
 
// 查看并移除队列中的第一个元素,若队列为空,则线程进入无限期等待,等待生产者向队列中添加元素
E take()
 
//移除此队列中所有任务,并将其拷贝到给定的collection集合中。           
int drainTo(Collection<? super E> c) 
 
//移除此队列中给定数量的任务,并将其添加加到给定的collection集合中。       
int drainTo(Collection<? super E> c, int maxElements)

Java中的阻塞队列共有7种,其实现类的实现类如下:

1.ArrayBlockingQueue

数组有界阻塞队列,按照阻塞的先后顺序访问队列,默认情况下不保证线程公平的访问队列,如果要保证公平性,会降低一定的吞吐量

2.LinkedBlockingQueue

链表有界阻塞队列,默认最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

3.PriorityBlockingQueue

优先级队列,可以自定义排序方法,但是对于同级元素不能保证顺序

4.DelayQueue

延迟获取元素队列,指定时间后获取,为无界阻塞队列。

5.SynchronousQueue

不存储元素的阻塞队列。每一个put操作必须订单tabke 操作,否则不能继续添加元素。

6.LinkedTransfetQueue

无界阻塞队列,多了tryTransfer 和transfet方法

7.LinkedBlockingQueue

链表结构组成的双向阻塞队列。可以从队列的两端插入和移除元素。

7、线程池的运行流程

请添加图片描述

8、Java中用的线程池

Executors是线程池的工具类,可以通过Executors创建处不同的线程池。

常见的线程池主要有两种:FixedThreadPool与CachedThreadPool,他们都是通过调用ThreadPoolExecutor类不同的构造方法创建

  • FixedThreadPool是一个固定线程的线程池

    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>(),
                                          threadFactory);
        }
    

    FixedThreadPool线程池提供了两套方法,可以传入自定义的线程工厂;FixedThreadPool中的核心线程数等于最大线程数,也就是说当阻塞队列满了时不会创建非核心线程

    由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。

  • CachedThreadPool是一个动态线程数量的线程池,当线程数量不够时会主动去创建线程,当一个线程的空闲时间为60s时会将这个线程销毁

    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>(),
                                          threadFactory);
        }
    

    CachedThreadPool 也可以传入自定义的线程工厂,其中核心线程数为0,最大线程数可以视为无限;因此CachedThreadPool 中创建的每一个线程都为非核心线程,当一个线程的空闲时间为60s时会将这个线程销毁

    CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

  • SingleThreadPool是一个单例线程池,即线程池中只有一个线程,任务会进入阻塞队列中顺序等候执行,一般可用于控制线程的执行顺序,因为任务在阻塞队列中按照先进先出的顺序排序

    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>(),
                                        threadFactory));
        }
    

    SingleThreadPool最大线程数与核心线程数都为1,因此该线程,只会有一个工作线程

  • ScheduledThreadPool是一个定时线程池,可以给该线程池中的线程设置时间,定时执行任务

    public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
        }
    
    public ScheduledThreadPoolExecutor(int corePoolSize,
                                           ThreadFactory threadFactory) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue(), threadFactory);
        }
    

    ScheduledThreadPool可以自定义核心线程数,而最大线程数可以视为无限

    ScheduledThreadPool 和 SingleThreadScheduledExecutor这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

将从1到3000的累加的任务分配给三个线程执行,再汇总执行结果,得出最终的结果

package com.qf.threadpool;

import java.util.concurrent.*;

public class ThreadPool {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService service= Executors.newFixedThreadPool(3);

        FirstThread firstThread=new FirstThread();
        SecondThread secondThread=new SecondThread();
        ThirdThread thirdThread=new ThirdThread();

        Future<Integer> first = service.submit(firstThread);
        Future<Integer> second = service.submit(secondThread);
        Future<Integer> third = service.submit(thirdThread);

        Integer res1 = first.get();
        Integer res2 = second.get();
        Integer res3 = third.get();
        System.out.println(res1+res2+res3);
    }
}
//第一个任务
class FirstThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        Integer sum=0;
        for (int i=1;i<=1000;i++){
            sum+=i;
        }
        return sum;
    }
}
//第二个任务
class SecondThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        Integer sum=0;
        for (int i=1001;i<=2000;i++){
            sum+=i;
        }
        return sum;
    }
}
//第三个任务
class ThirdThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        Integer sum=0;
        for (int i=2001;i<=3000;i++){
            sum+=i;
        }
        return sum;
    }
}

十一、锁

1、概念

锁与Synchronized一样,也可以将多线程并发执行的代码改为串行,但与Synchronized相比结构更灵活,启动定义了多个方法,功能更强大

2、Lock

Lock是java.util.concurrent.locks包下的接口,其中定义了锁所具有的能力

public interface Lock {
    //获取锁
    void lock();
    
    //获取一个可中断阻塞的锁
    void lockInterruptibly() throws InterruptedException;
    
    //尝试获取锁,若获取不到则返回false
    boolean tryLock();
    
    //尝试获取锁,并定义等待时间,若在该时间段内未获取到锁则返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    //释放锁
    void unlock();
    
    //Condition定义了一系列线程通讯的方法,例如await、signal等
    Condition newCondition();
}

lockInterruptibly方法可以获取到一个可中断等待的锁,例如线程A与线程B同时获取到lockInterruptibly锁,当线程B进入阻塞状态时,可以调用线程B的B.interrupt()方法,从而中断掉线程B

tryLock是指线程尝试获取锁,若成功获取锁则返回true,否则返回false;因此可以将获取锁的顺序执行变为异步执行,即若获取不到锁可以先执行其他语句,随后再去尝试获取锁

3、ReentrantLock

ReentrantLock为可重入锁,用法与Synchronized一样,但功能更强大。其可重入体现在,若一个上锁的代码递归调用自身,不需要再获取锁,而是重新分配一把锁;但当代码执行完毕后释放锁时,要根据分配锁的顺序逆序释放锁,若有任一个锁没有释放掉,则该线程一直占有该锁

ReentrantLock需要显示定义,即需要new一个ReentrantLock对象,通过该对象的方法上锁;需要注意的是,同一个ReentrantLock对象视为同一把锁,即多个线程的ReentrantLock对象为同一个对象,锁才能生效

例如之前的买票问题,可以将User对象进行如下改造:

import java.util.concurrent.locks.ReentrantLock;

public class User implements Runnable {
    private Ticket ticket;

    private ReentrantLock lock;

    public User(Ticket ticket,ReentrantLock lock){
        this.ticket=ticket;
        this.lock=lock;
    }

    @Override
    public void run() {
        while (ticket.num>0) {
            lock.lock();
            try {
                Thread.sleep(100L);
                if (ticket.num<=0){
                    System.out.println("不好意思"+Thread.currentThread().getName()+",票已售罄");
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "抢到了第" + (100 - ticket.num--) + "张票");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}

在调用时需要传入同一把锁

import java.util.concurrent.locks.ReentrantLock;

public class BuyTicket {
    public static void main(String[] args) {

        ReentrantLock lock=new ReentrantLock();

        Ticket ticket=new Ticket();
        Thread user1=new Thread(new User(ticket,lock),"小明");
        Thread user2=new Thread(new User(ticket,lock),"小张");
        Thread user3=new Thread(new User(ticket,lock),"小红");
        Thread user4=new Thread(new User(ticket,lock),"小刚");
        Thread user5=new Thread(new User(ticket,lock),"小赵");
        user1.start();
        user2.start();
        user3.start();
        user4.start();
        user5.start();
    }
}

需要注意的是,最好在finally代码块中调用unlock方法,否则可能会出现死锁

ReentrantLock还可以通过构造方法设置公平策略。若为不公平锁,则时间片完全随机分配,即第一次由A线程抢到时间片,第二次A线程仍有可能抢到时间片;而公平锁则是多个线程轮转获取时间片

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

4、ReadWriteLock

可以支持读写分离的锁,可以分别分配读锁与写锁。可以分配多把读锁,使多个读操作可以并发执行。

回顾线程不安全的定义:多个线程访问同一个临界资源,如果破坏了原子性操作,造成临界资源数据的凭空消失或不一致,称为线程不安全

因此增删改操作可能会造成线程不安全,但读操作一定不会造成线程不安全。但如果给所有操作都加上锁,会使增删改查操作都互斥,降低系统的性能。因此将读写锁分开,进行读操作时可以并发执行可以在一定程度上提升系统的性能;而增删改操作可能会造成线程的不安全,即使概率再低也要保证系统的绝对安全,因此要保证串行。

读写锁的互斥规则:

写——写:互斥

读——写:互斥,都阻塞写、写阻塞读

读——读:不互斥

在读操作远高于写操作时,可保证系统安全,提高系统运行效率

ReadWriteLock也是一个接口,定义了读写锁的规则

public interface ReadWriteLock {
   
    //读锁
    Lock readLock();

	//写锁
    Lock writeLock();
}

5、ReentrantReadWriteLock

ReentrantReadWriteLock是ReadWriteLock的一个实现类,其也是一个可重入锁

ReentrantReadWriteLock也支持公平锁机制

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

其中,ReadLock与WriteLock是ReentrantReadWriteLock的静态内部类,使用时需要先new出ReentrantReadWrite类,再点出ReadLock与WriteLock

ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

锁的主要方法如上,还有一些其他方法与特性,可自行查看,本文不做详述!

十二、Volatile

1、概念

Volatile是Java中的一个关键字,主要用于保证线程的可见性与可排序性

2、JAVA内存模型

请添加图片描述

线程堆共享,而栈独立;因此在JVM中堆是所有线程所共享的一片空间,而每一个线程都有自己独立的工作内存,在多线程并发操作堆中的临界资源时,线程需要先将堆中的临界资源的值拉取到自己的工作内存中,对该临界资源操作完成后再将其更新回堆中

在硬件中,CPU有自己的缓存与寄存器,但线程的工作内存不一定就是CPU的缓存,是由内存与CPU缓存以及寄存器分布存储,因此只是逻辑上这样划分,物理上不一定这样存储

3、线程的特性

线程有三大特性

原子性:一组操作要么全部执行,要么全部不执行,且不可中断,不可改变其顺序

可见性:一个线程改变了一个临界资源的值,其他线程都应该直到这个临界资源被改变

有序性:在编译代码时,不一定会按照代码书写的顺序执行;为了执行的效率,会乱序执行相互没有依赖的代码,相互依赖的代码(下一条语句依赖上一条语句的执行结果)会按照书写顺序执行,但会保证最终代码运行结果的正确性

其中,有序性在单线程中没有问题,但在多线程并发执行时可能会有问题

4、Volatile关键字

  • 可见性

    加了Volatile关键字的临界资源,在线程本地内存中被操作并更新会主存中后,会发一个消息给其他线程,收到消息的线程会判定自己的临界资源过期,需要从主存中重新拉去临界资源,从而做到可见性

  • 有序性

    加了Volatile关键字的临界资源,在线程本地内存中会加入一个内存屏障,会保证在内存屏障之前的代码一定在内存屏障之前执行,在内存屏障之后的代码一定在内存屏障之后执行,但不保证内存屏障前后代码的绝对有序执行,从而做到有序性

  • Volatile无法解决原子性,因此Volatile无法代替Synchronized

十三、乐观锁与悲观锁

1、CAS算法与乐观锁

CAS算法是通过比对特定字段从而实现加锁的算法,这种非阻塞式加锁的方式就是乐观锁

  • CAS算法首先查询一个字段的状态,再以该字段为条件去操作该条数据。比如在付款操作时,首先查询该条记录是否付款,若查询结果为未付款,则以未付款为条件将该条记录的状态改为付款状态

    这种算法可能会存在ABA问题,比如A线程查询状态为未付款,此时该线程时间片到期,由运行态进入就绪态;B线程进行付款操作,将未付款状态改为付款状态;由于一些问题,用户付款后立即退款,C线程又将付款改为未付款;此时A线程又重新竞争到时间片,又将未付款改为付款,此时就出现了数据一致性的问题

  • 解决ABA问题的方法是基于Version的CAS算法,在原本的字段后额外增加一个自增的Version字段,每个线程进行操作后对Version进行加一操作,此时以查询的条件去操作数据就不存在ABA问题,但需要将操作数据和Version加一操作封装为一个原子操作

  • 自旋锁:在基于Version的CAS算法中,若查询Version的结果与操作数据时Version的数据不同时,该线程进入while循环,每次循环将Version加一,再去操作数据,直到操作成功为止

2、悲观锁

悲观锁就是正常的加锁操作,如同步代码块或加锁

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值