Java多线程

本文详细介绍了Java多线程的概念,包括线程状态、线程属性、线程创建、线程同步、可见性、原子性、阻塞队列以及线程安全的集合。重点讨论了线程状态的转换,如New、Runnable、Blocked、Waiting、Timed waiting和Terminated。还涵盖了线程同步的多种方式,如synchronized、ReentrantLock和Condition,以及线程安全的集合如CopyOnWriteArrayList、CopyOnWriteArraySet和ConcurrentHashMap。此外,文章还提到了线程池的创建和使用,包括ThreadPoolExecutor和相关工厂方法。

线程状态

线程有6种状态,一个线程实例可以通过调用getState()方法来获取线程当前的状态

New:

新创建线程——当用new操作符创建一个线程时,线程还没有开始运行,当前线程的状态为New

Runnable

Java线程中将将就绪(Rready)和运行中(Running)两种状态统称为Runnable

一旦调用start()方法线程就处于Runnable状态,一个Runnable的线程可能正在运行也可能还没有运行,这取决于CPU的调度

Ready

就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态

  • 调用线程的start()方法,此线程进入就绪状态。
  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
  • 锁池里的线程拿到对象锁后,进入就绪状态

Running

线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这是线程进入运行状态的唯一一种方式。

Blocked

当线程处于被阻塞或等待状态时,它不运行任何代码且消耗最少的资源。当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态,当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程变成非阻塞状态。

Waiting

处于等待状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。在调用wait()的或join()方法,或是等待Lock或Condition时,当前线程进入等待状态。而notify()notifyAll()的作用,则是唤醒当前对象上的等待线程。

Timed waiting

调用带有超时参数的方法,如sleep,wait,wait,await,tryLock方法。调用它们导致线程进入计时等待状态。这一状态将一直保持到计时期满或接收到通知。

Terminated

线程因如下两个原因之一被终止:

  • run方法的正常退出而自然死亡
  • 因为一个没有捕获的异常终止了run方法而意外死亡

一般希望线程自然结束run方法而终止,不建议调用被摒弃的stop方法

一旦线程进入终止状态,就不能再调用start方法了。


线程状态转换

在这里插入图片描述

线程属性

线程优先级

  • 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
  • Java 线程的优先级是一个整数,其取值范围是 1(Thread.MIN_PRIORITY )- 10 (Thread.MAX_PRIORITY )
  • 默认线程优先级为5(NORM_PRIORITY)
  • 优先级高调度的可能性大,但不代表高优先级一定在低优先级之前调度

守护线程

可以通过调用

t.setDaemon(true);  

将线程转换为守护线程。

  • 守护线程唯一的作用是为其它线程提供服务

  • 当只剩下守护线程时,虚拟机就退出了

  • 守护线程不应该去访问固有资源,如文件,数据库,因为随时可能发生中断

线程创建

Thread

可以通过继承Thread类,重写run方法,该方法是新线程的入口点

run()可以调用其他方法,使用其他类,并声明变量,就像主线程一样

最后创建一个该类的实例,调用start()开始执行

下面是Tread类中的一些常用方法:

public void start();             //使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
public final void setName(String name);   //设置线程名称。
public final void setPriority(int priority);    //设置线程的优先级。
public final void setDaemon(boolean on);      //将该线程标记为守护线程或用户线程。
public final void join(long millisec);      //等待这个线程终止再继续执行,时间最长为 millis 毫秒。
public final boolean isAlive();             //测试线程是否处于活动状态。

//静态方法
public static void yield();      //将该线程由Running转换为Ready,和其他线程竞争
public static void sleep(long millisec);      //让当前正在执行的线程休眠
public static boolean holdsLock(Object x);    //当前线程在指定的对象上保持监视器锁时,返回 true。
public static Thread currentThread();         //返回对当前正在执行的线程对象的引用。

Runnable接口

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类,实现其中的run方法。

new一个Thread对象,将接口作为参数传递进去

Runnable是函数式接口,最好使用lamda表达式,降低耦合性

Thread(Runnable target,String threadName);,调用start()开始执行

Callable&Future

CallableRunnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个call方法:

public interface Callable<V> {
	V call() throws Exception;
}

类型参数是返回值类型,例如Callable<Integer>,表示该线程最终会返回一个Integer对象。

Callable一般情况下是配合ExecutorService来使用的,在ExecutorService接口中声明了若干个 submit方法的重载版本:

Future submit(Callable task);               
Future submit(Runnable task);
Future submit(Runnable task, T result);

Future 类位于 java.util.concurrent 包下,它是一个接口:

public interface Future {
    boolean cancel(boolean mayInterruptIfRunning);           //取消任务
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)            //指定时间内未获取到结果就返回null
        throws InterruptedException, ExecutionException, TimeoutException;
}

也就是说 Future 提供了三种功能:

  • 判断任务是否完成
  • 能够中断任务
  • 能够获取任务执行结果

如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。

FutureTask

因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,FutureTask 包装器是一种非常便利的机制,它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。 它同时实现了二者的接口。例如:

Callable<Integer> myComputation = ...;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
Thread t= new Thread(task);      //it's a Runnable 
t.start;
...
Integer result = task.get();     //it's a Future

线程同步

Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

synchronized

由于Java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。例如:

public synchronized void save(){}

synchronized关键字也可以修饰语句块:

synchronized(object){
    ....
}

被该关键字修饰的语句块会自动被加上内置锁,从而实现同步,再一个线程操纵object时,其他线程想要操作会被阻塞。

注意:将静态方法声明为synchronized也是合法的,但当该方法被调用时,整个类对象会被锁住,因此没有其他线程可以调用这个类的

任何同步静态方法。

ReentrantLock

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,保护代码块的基本结构如下:

private Lock lock = new ReentrantLock();
lock.lock();             //获得锁
try{
    ...
}
finally{ 
    lock.unlock();       //释放锁
}

Condition

通常,线程获得锁后,却发现某一条件满足才能执行(不能先判断满足条件再获得锁,因为由于阻塞可能获得锁的时候条件已经不满足了)。例如一个模拟银行取钱的示例:

public void transfer(int from , int to ,int amount){
    bankLock.lock();
    try{
        while(account[from] < amount){
            //wait
            ...
        }
        ...     //transfer funds
    } finally{
        bankLock.unlock();
    }
}

当账户中余额不足的时候,应该等待直到另一个线程向账户注入资金,才可以继续完成转账操作,可是这个线程获得这个锁没有释放,别的线程没有机会进行操作,这时就需要一个条件对象。

一个锁可以有多个相关的条件对象,通过newCondition()获得锁的一个条件对象,通常命名为可以表示这个条件的名字。例如:

private Condition sufficientFunds;    //实现声明一个资金充足的条件
sufficientFunds = bankLock.newCondition();      //获得一个条件
...
public void transfer(int from , int to ,int amount){
    bankLock.lock();
    try{
        while(account[from] < amount){
            sufficientFunds.await();    //wait
            ...
        }
        ...     //transfer funds
    } finally{
        bankLock.unlock();
    }
}

当调用await方法后,该线程进入该条件的等待集,直到另一个线程调用同一条件上的signalAll方法为止。当一个线程完成转账,就执行

sufficientFunds.signalAll();

这一调用重新激活因为这个条件而等待的所有线程,让它们再次成为可运行的,等待调度器调度。

synchronized关键字中使用的内部对象锁只有一个相关条件,wait方法添加一个线程到等待集中,notifyAll/notify方法是唤醒等待这个对象的线程并允许它们去获得对象锁

可见性

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronizedLock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

volatile特点:

  • 轻量级

  • 保证可见性

  • 不保证原子性

  • 禁止指令重排

在读操作>>写操作时,使用volatile关键字,提高效率

原子性

原子性操作:不可分割,不可被中断的,要么执行,要么不执行。

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

Java类库中提供了原子类来保证原子性。

阻塞队列

对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的形式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。当试图向线程添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞。

BlockingQueue接口的方法分为以下四类:

//抛出异常型
boolean add(E e);     //添加元素,如果队列满,则抛出异常
E element();          //返回队列头元素,如果此队列为空,它将抛出异常
E remove();           //移除并返回头元素,如果此队列为空,它将抛出异常

//不抛出异常
boolean offer(E e);     //添加一个元素,成功返回true,队列满返回false
E peek();               //返回队列头元素,如果此队列为空,返回null
E poll();               //移除并返回头元素,如果此队列为空,返回null

//阻塞型
void put(E e);         //添加一个元素,队列满则阻塞
E take();              //移除并返回头元素,队列空则阻塞

//offer poll 的变体 ,计时等待
boolean offer(E e, long timeout, TimeUnit unit);
E poll(long timeout, TimeUnit unit); 

几个实现类

  • LinkedBlockingQueue:容量无上界,但是也可以指定最大容量
  • LinkedBlockingDeque:双端版本
  • ArrayBlockingDeque:在构造时需要指定容量,可选参数来指定是否需要公平性
  • PriorityBlockingDeque:容量无上限,元素按照优先级顺序被移出

BlockingQueue是线程安全的,由于pollpeek方法返回空来指示失败,因此不能向队列中插入null值

线程安全的集合

下面先看一个并发操作ArrayList不安全的例子:

import java.util.ArrayList;

public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->list.add(Thread.currentThread().getName())).start();
        }
        TimeUnit.SECONDS.sleep(1);   //主线程等待所有线程执行完毕
        System.out.println(list.size());
    }
}

该示例循环1w次,每次new一个线程来执行元素add操作,希望能够添加1w个元素,但最后输出结果是达不到1w个的,这是因为在add的过程中,两个线程对同一个位置进行操作,导致覆盖,所以这样的集合是不安全的。

同步包装器

早期版本中,JDK使用了同步包装器变成线程安全的,提供Collections工具类获取:

List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());
Map<K,V> synchHashMap = Collections.synchronizedMap(new HashMap<K,V>());
...

这样的实现线程虽然安全了,但并不高效

高效的映射、集和队列

JUC包提供了许多线程安全的集合、Map的实现类。

这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化

CopyOnWriteArrayList

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。下面是add的源码:

public boolean add(E e) {
    //1、先加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //2、拷贝数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //3、将元素加入到新数组中
        newElements[len] = e;
        //4、将array引用指向到新数组
        setArray(newElements);
        return true;
    } finally {
       //5、解锁
        lock.unlock();
    }
}

由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:

  • 如果写操作未完成,那么直接读取原数组的数据;
  • 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
  • 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

缺点:由于写操作回拷贝一份原始数组,会消耗内存,这个时候很有可能造成频繁的Yong GC和Full GC 。如果是经常被修改的数组列表,同步的ArrayList可以胜过CopyOnWriteArrayList

CopyOnWriteArraySet

它的底层实现是利用数组,它的上层实现是 CopyOnWriteArrayList,因为CopyOnWriteArrayList是线程安全的,所以 CopyOnWriteArraySet 操作也是线程安全的。

ConcurrentHashMap

使用HashMap,在多个线程同时进行put操作时,容量不够rehash的过程中,可能会产生环形链表,导致get操作时,cpu空转,所以在并发中使用HashMap是非常危险的

ConcurrentHashMap锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发。

同步器

CyclicBarrier

考虑大量线程运行在一次计算的不同部分的情形。当一个线程完成了它的那部分任务后,我们让它运行到“栅栏”处,一旦所有的线程都到达了这个“栅栏” ,“栅栏”就撤销,线程可以继续运行。

首先,构造一个CyclicBarrier,参数给出参与的线程数和一个可选的动作,当所有线程集结完毕,就执行这个动作:

CyclicBarrier barrier = new CyclicBarrier(nthreads,runnable);

每个线程执行自己要完成的任务,完成后调用await:

public void run(){
    doSomething();
    barrier.await();     //--counts,当nthreads个线程调用了awiat,就可以解除这个barrier
}

CyclicBarrier可以重复使用,调用reset(),重置为初始状态。

CountDownLatch

一个倒计时门栓让一个线程等待直到计数变为0,这个实现是一次性的,一旦计数为0,就不能再复用了

构造函数:

public void CountDownLatch(int count){...}  //count 是闭锁需要等待的线程数量

等待线程调用await()阻塞等待其他线程完成任务,其他线程调用countDown(),每调用一次,count 的值就减 1,当最终减为0时,等待线程解除等待

Semaphore

Semaphore 也叫信号量,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

构造方法,参数需要一个许可证的数量和一个可选的公平设置:

Semaphore(int permits, boolean fair) 
//创建一个 Semaphore与给定数量的许可证和给定的公平设置。 

核心方法:

void acquire();          //从该信号量获取许可证,若许可证被取完,线程阻塞
void release();          //释放许可证,通知其阻塞线程获取

可以发现Semaphore 与生产者消费者模式类似。

Exchanger

允许两个线程在要交换的对象准备好的时候交换对象

核心方法:

Exchanger();           //无参构造
V exchange(V x);      //等待另一个线程到达此交换点(调用此函数),然后将给定对象传输给它,接收其对象作为回报。
V exchange(V x, long timeout, TimeUnit unit)  //计时等待

SynchronousQueue

当一个线程调用同步队列的put方法时,它会阻塞直到另一个线程调用take方法为止。

与Exchanger不同,数据仅仅沿一个方向传递。

虽然实现了BlockingQueue,概念上讲,它不是一个队列,因为它没有任何元素,size方法总返回0

线程池

线程的创建和销毁是有一定代价的,创建大量的线程会大大降低性能甚至使虚拟机崩溃。

线程池的好处

  • 重复使用,降低资源的消耗
  • 限制并发数,提高响应速度
  • 方便管理

工厂方法

Executors类有许多静态工厂方法用来构建线程池:

方法描述
newSingleThreadExecutor()只有一个线程,该线程依次执行每个提交的任务
newFixedThreadPool()包含固定数量线程的池,空闲线程会被一直保留
newCachedThreadPool()可缓存线程池,必要时创建线程数量,空闲线程会被保留60秒
newScheduledThreadPool(nthreads)包含固定数量线程的池,支持定时及周期性任务执行

线程执行:

  1. 线程池调用excute方法,传入一个Runnable参数
  2. 调用submit方法,会返回一个Future对象
Future<?> submit(Runnable task);    //get方法返回null
Future<T> submit(Runnable task, T result);
Future<T> submit(Callable<T> task);

使用线程池的步骤:

  1. 调用Executors类中的静态方法创建一个线程池(后续推荐使用原生方法)
ExecutorService pool = Executors.newFixedThreadPool(10);
  1. 调用executesubmit提交Runnable或Callable对象
  2. 当不需要线程池时,调用shutdown关闭,置于finally中

ThreadPoolExecutor

我们先来看一下newSingleThreadExecutor,newSingleThreadExecutor和newCachedThreadPool构造方法的源码:

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

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

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

可以发现都是通过ThreadPoolExecutor来构建实例对象的,下面分析ThreadPoolExecutor的源码:

//该构造器有7个参数
public ThreadPoolExecutor(int corePoolSize,          //线程池最小线程数
                              int maximumPoolSize,       //最大线程数,当核心线程和阻塞队列都满了,触发 
                              long keepAliveTime,        //超时销毁
                              TimeUnit unit,			//时间单元
                              BlockingQueue<Runnable> workQueue) {//阻塞队列存放待执行的线程
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler;
}

这样就可以自定义一个线程池,线程池的最大容量为:最大线程数量+阻塞队列大小。当超出最大容量还有线程提交执行的时候,就会触发拒绝策略,最后一个参数,一共有四种拒绝策略:

ThreadPoolExecutor.AbortPolicy();    //触发抛出异常
ThreadPoolExecutor.DiscardPolicy();           //丢掉任务,不会抛出异常
ThreadPoolExecutor.DiscardOldestPolicy();    //尝试和最早的竞争,失败则丢掉任务
ThreadPoolExecutor.CallerRunsPolicy();    //让提交的线程去执行这个任务

至此,我们就可以自己创建一个合适的线程了,推荐使用这种原生的构造方法,这样的方式可以更加明确线程的运行规则,规避资源耗尽的风险。

使用Executors创建的弊端:

  • SingleThreadExecutorFixedThreadPool

    允许提交队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM

  • CachedThreadPoolScheduledThreadPool

    允许创建线程最大数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值