Java-多线程

141 阅读16分钟

一.多线程

1.概念

1.1并发:指两个或多个事件在同一时间段发生

并行:指两个或多个事件在同一时刻发生(同时发生)

1.2进程:加载进内存的应用程序,是OS资源分配的最小单元

线程:CPU调度的最小单元

线程调度:分时调度 抢占式调度(java)

2.创建线程的方式

2.1继承Thread

Thread thread1 = new Thread(() -> System.out.println("新线程01 = " + Thread.currentThread().getName()));
thread1.start();
System.out.println("主线程 = " + Thread.currentThread().getName());
----
主线程 = main
新线程01 = Thread-0

2.2实现Runnable

Runnable run = () -> System.out.println("新线程02 = "+Thread.currentThread().getName());
run.run();
System.out.println("主线程 = " + Thread.currentThread().getName());
----
新线程02 = main
主线程 = main    
Runnable run = () -> System.out.println("新线程02 = "+Thread.currentThread().getName());
Thread thread2 = new Thread(run);
thread2.start();
System.out.println("主线程 = " + Thread.currentThread().getName());
----
主线程 = main
新线程02 = Thread-0    

start():表示启动线程,使其成为一条单独的执行流,系统会分配线程相关资源,会有单独的程序计数器和 栈。(不能多次调用,否则发生llegalThreadStateException)

run():只是普通方法,不会生成新线程

2.3Callable配合FutureTask

继承关系:
1.public interface Future<V>
2.public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}
3.public class FutureTask<V> implements RunnableFuture<V>{}
​
所以FutureTask就是Runnable的子类
Callable<String> callable = () -> {
            System.out.println("新线程 =  "+Thread.currentThread().getName());
            return "我是新线程";
        };
​
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
System.out.println(task.get());
----
新线程 =  Thread-0
我是新线程

3.常用用方法

3.1 getState()获取线程状态

New Runnable Blocked Waiting Timed-waiting Terminated

3.2 interrupt方法可以用来请求终止线程

Thread.currentThread().isInterrupted() 获取中断状态

interrupted()是一个静态方法,检测当前的线程是否被中断,而且会清除该线程的中断状态,设置中断状态为false

isInterrupted()是一个实例方法,不会改变线程的中断状态

3.3 sleep方法 静态方法

会让当前线程状态从Runnable进入TimedWaiting状态

使用interrpt()方法打断正在sleep的线程,会抛出InterruptedException

3.4 yield方法

导致当前执行线程处于让步状态,具体的实现还是依赖任务调度器

3.5 join方法

等待指定线程执行结束(在哪个线程执行就需要哪个线程等待)

3.6 public final void setPriority(int newPriorit)

线程优先级从1到10,默认是5,

3.7 setDaemon()

标识线程为守护线程,唯一用途就是为其他线程提供服务【计时器的声音信号线程】

wait()和sleep()的区别:

1.wait是Object方法 sleep是Thread方法

2.sleep不需要synchronized配合使用

3.都会释放CPU使用权 ,但sleep不会释放锁、wait会

二. 线程安全

临界区:一段代码内如果存在对共享资源的多线程读写操作,称这段代码为临界区

原子性 - 有序性- 可见性

解决线程安全问题:阻塞式- Synchronized/显式锁

非阻塞式-原子变量

解决内存可见性:volatile/Sychronized/显式锁

public class CountAddDemo {
    static int counter = 0;
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter++;
            }
        }, "t1");
​
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter--;
            }
        }, "t2");
​
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(" counter = " + counter);
    }
}
----
counter = 14516
public class CountAddDemo {
    static int counter = 0;
​
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (CountAddDemo.class) {
                for (int i = 0; i < 50000; i++) {
                    counter++;
                }
            }
        }, "t1");
​
        Thread t2 = new Thread(() -> {
            synchronized (CountAddDemo.class) {
                for (int i = 0; i < 50000; i++) {
                    counter--;
                }
            }
        }, "t2");
​
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(" counter = " + counter);
    }
}
----
counter = 0

2.1Sychronized 悲观锁

2.1.1修饰实例方法

多个线程是可以同时访问同一个sychronized实例方法,只要他们访问的对象时不同即可。

sychronized实例方法保护的是当前实例对象this

2.1.2修饰静态方法

保护的时类对象XX.class(每个对象都有一个锁和一个等待队列)

2.1.3代码块

可以使用单独对象作为锁的对象

2.1.4可重入性

Synchronized是可重入的,对同一个线程获得锁之后,在调用其他需要同样锁的代码时,可以直接调用。

可重入是通过记录锁的持有线程和持有数量来实现的。

2.1.5原理

synchronized(obj)

obj 指向操作系统的Monitor对象

[1.WaitSet等待队列-获得锁但是条件不满足 Waiting - 调用notify/notifyAll时唤醒

  1. EntryList阻塞队列-Blocked-在线程释放锁时唤醒
  2. Owner所有者]

实现原理[jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。

是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。]

使用synchronized关键字获取锁的过程不响应中断请求(局限性)

2.2线程协作机制 wait/notify

除了锁的等待队列,每个对象还有一个条件等待队列,用于线程间的协作。

调用wait()会把当前线程放到条件队列上并阻塞,需要一个条件才能继续执行,需要其他线程改变,当其他线程改变了条件之后,应该调用notify/notifyAll方法,notify方法就是从条件队列中选一个线程将其从队列中移除并唤醒

wait()-notify()/notifyAll()

-都是Object类的final方法

-只能在同步方法/代码块中调用

-如果调用方法前当前线程没有持有锁,会抛出 java.lang.IllegalMontorStateException

虚假唤醒(while(条件变量))

2.3LockSupport:

park():[使当前的线程放弃CPU并进入等待状态]

unpart(Thread thread):[使参数使用的线程恢复可运行状态]

注:暂停恢复具体线程 与执行顺序无关

三.原子变量/显示锁/显示条件

3.1原子变量与CAS

所有以原子方式实现组合操作的方法都依赖compareAndSet(int expect, int update)

--如果当前的值为expect,则更新为update,否则不更新,如果更新成功返回true,否则返回false

Unsafe

一般的计算机系统都在硬件层次上支持CAS指令,而java的实现会利用这些特殊指令

CAS更新存在ABA问题,可以用AtomicStampedReference解决,附带时间戳

3.2显式锁( 依赖于CAS和LockSupport )

Lock->ReentrantLock

ReadWriteLock->ReentrantReadWriteLock

相比sychronized,都支持可重入( 可重入性-当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象的锁的。 )

{ 显示锁:

a.支持以非阻塞的方式获取锁

b.可以响应中断(防止无线等待) rt.lockInterruptibly()

c.可以限时

d.可以设置公平锁

e.可以支持多个条件变量

Synchronized不用用户手动释放锁,Lock必须手动释放

}

Lock底层实现基于AQS(Abstract Queued Sychchronizer)(用于实现依赖先进先出FIFO等待队列阻塞锁和相关同步器)

lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)

lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。

lock释放锁的过程:修改状态值,调整等待链表。

可以看到在整个实现过程中,lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。

因此在非必要的情况下,建议使用synchronized做同步操作。

3.3显示条件

条件变量 Condition

newCondition()返回一个与锁相关的条件对象

await() 将该线程放到条件的等待集中

signalAll()解除该条件的等待集中的所有线程的阻塞状态

signal()从该条件的等待集合随机选择一个线程解除阻塞状态

3.4公平锁与非公平锁

synchronized内部锁是非公平的。

ReentrantLock提供构造方法可以设置为公平锁(内部维护一个有序队列)

-非公平锁系统倾向于让一个线程再次获得已经持有的锁,这种分配高效非公平

-公平锁多个线程不会发生同一个线程连续多次获得锁的可能,保证公平性

四.并发容器

4.1写时复制容器 CopyOnWriteArrayList CopyOnWriteArraySet

4.1.1 CopyOnWriteArrayList

-线程安全,可以被多个线程并发访问

-它的迭代器不支持修改操作,也不会抛出ConcurrentModificationException

-它以原子方式支持一些复合操作

读不需要锁,可以并行,读写可以并行,不能多个线程同时写。

不适合数组较大且修改操作频繁的操作。(缓存)

保证线程安全的思路:锁(sychronized和ReentrantLock) 循环CAS,都是控制对同一资源的访问冲突

写时复制通过复制资源减少冲突。

4.1.2 CopyOnWriteArraySet 基于CopyOnWriteArrayList实现

4.2 CurrentHashMap

-并发安全

-直接支持一些原子复合操作

-支持高并发,读写完全并行,写操作支持一定程度并行

(java7 分段锁 读不需要锁 数组+链表

java8 数组+红黑树 提高查找效率 细化锁粒度)

-与同步容器Collections.synchronizedMap相比,迭代不用加锁,不会抛出Concurre ntModificationException

-弱一致性(ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历元素,在遍历过程中如果内部元素发生变化,变化如果在已遍历部分迭代器不回反映出来,如果变化在未遍历部分则会反映处理)

java无并发版的hashset。

并发包中可排序的版本不是基于树,而是基于Skip List 跳表

4.3ConcurrentSkipListMap ConcurrentSkipListSet

4.3.1ConcurrentSkipListMap

-没有使用锁,所有操作都是无阻塞的,所有操作可以并行

-与ConcurrentHashMap类似,迭代器不会抛出ConcurrentModificationException,是弱一致的

-与ConcurrentHashMap类似,同样实现了ConcurrentMap接口,支持一些原子复合操作

-与TreeMap一样,可排序,默认按键的自然顺序,也可以传递比较器自定义排序,实现了SortedMap和NavigableMap接口

跳表是基于链表的,在链表的基础上家乐多层索引结构

4.4并发队列

无锁非阻塞并发队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque

--基于链表,大小无限制且无界(内部使用循环CAS实现)

普通阻塞队列:基于数组的ArrayBlockingQueue,基于链表的LinkedBlockingQueue和LinkedBlockingDeque

--ArrayBlockingQueue基于数组实现,有界需要指定大小

--LinkedBlockingQueue/LinkedBlockingDeque 基于链表实现(内部使用显式锁和显式条件实现)

优先级阻塞队列:PriorityBlockingQueue

延迟阻塞队列:DelayQueue

其他阻塞队列:SynchronousQueue和LinkedTransferQueue

--SynchronousQueue,无存储元素空间。适用于两个线程间直接传递消息。

五.同步协作工具

5.1读写锁 ReentrantReadWriteLock

synchonized内部锁和ReentrantLock都是独占锁(排它锁),同一时间只允许一个线程执行同步代码,可以保证线程安全性,但是执行效率低

ReentrantReadWriteLock读写锁是一种改进的排它锁,也称为共享锁。允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。

读锁可以同时被多个线程持有。线程在修改共享数据前必须持有写锁,写锁时排他的,

读写锁允许读读共享,读写互斥,写写互斥

readLock和writeLock 方法返回的锁对象时同一个锁的两个不同角色,不是两个不同的锁

5.2信号量Semaphore 可以限制对资源的并发访问 基于AQS实现

一般锁只能由持有锁的线程释放,而Semaphore表示的只是一个许可数,任意线程都可以调用其release方法。

一般锁实现类是可重入的,而Semaphore不是,每一次acquire都会消耗一个许可。

5.3倒计时门栓CountDownLatch

希望通过该门的线程都需要等待,倒计时变为0时,门栓打开,等待所有线程通过

--是一次性的,打开后无法再关闭。

同时开始,主线程依赖子线程结果

5.4循环栅栏CyclicBarrier

所有线程在到达该栅栏都需要等待其他线程,等所有线程到达后再一起通过,是循环的可做重复同步。

--适用于并行迭代计算。

CountDownLatch参与线程是不同角色,用于不用角色线程的同步。

CyclicBarrier参与线程角色是一样的。

CountDownLatch一次性的

CyclicBarrier可重复利用

5.5Exchanger 用于两个线程间交换信息。

仅可用作两个线程的信息交换。当超过两个线程调用同一个exchanger对象时,得到的结果是随机的,而剩下的未得到配对的

5.6ThreadLocal 线程本地变量

调用initialValue获取初始值

-日期处理

-随机数

每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键值读写当前线程的Map

六.异步任务

6.1解释

Runnable/Callable:表示要执行的异步任务。

Executor/ExecutorService 表示执行服务

Furure:表示异步任务的结果

Runnable没有返回结果,而Callable有,Runnable不会抛出异常,而Callable会

6.2线程池

--重用线程,避免线程创建的开销

-任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成。

ThreadPoolExecutor->AbstractExecutorService->ExecutorService

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

scheduleAtFixedRate -- 按照特定频率执行任务,如果任务时间大于间隔时间,则在任务执行完毕后执行下次任务

scheduleWithFixedDelay -- 在任务执行完毕间隔x后再次执行

6.2.1底层实现

ThreadPoolExecutor

public ThreadPoolExecutor(

int corePoolSize, -- 核心线程数量

int maximumPoolSize, -- 最大线程数量

long keepAliveTime, -- 当线程池数量超过corePoolSize时,多余的空闲线程的存活时长

TimeUnit unit, -- keepAliveTime时长单位

BlockingQueue workQueue, --任务队列,把任务提交到该队列等待执行

ThreadFactory threadFactory, -- 线程工厂,用于创建线程

RejectedExecutionHandler handler) --拒绝策略,当任务太多来不及处理时如何拒绝

6.2.2BlockingQueue队列功能分类

1.直接提交队列 -- SynchronousQueue,无容量,提交的线程池任务不会真是保存,总是将新的任务提交给线程池,如果线程达到MAXNUM则执行拒绝策略(newCachedThreadPool)

2.有界任务队列 -- ArrayBlockingQueue,可以指定一个容量。

如果线程池中线程数小于corePoolSize 创建新线程

如果大于corePoolSize ,则加入等待队列

如果队列已满且小于maximumPoolSize 继续创建

如果队列已满且大于maximumPoolSize 则执行拒绝策略

3.无界任务队列 -- LinkBlockingQueue 不存在任务入队失败的情况,除非系统资源耗尽(newFixedThreadPool newSingleThreadExecutor)

4.优先任务队列 -- PriorityBlockingQueue 带有任务优先级的队列,是一个特殊的无界队列

6.2.3

newCachedThreadPool() -- 每次提交都会创建线程执行。适合耗时较短,提交频繁的任务

newFixedThreadPool(int nThreads) --

newSingleThreadExecutor() --

newScheduledThreadPool()

6.2.4拒绝策略 - 当提交给线程池的任务量超过实际承载能力时,通过拒绝策略处理

四种策略:

(1)CallerRunsPolicy implements RejectedExecutionHandler

--只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务

(2)AbortPolicy implements RejectedExecutionHandler (默认策略)

---抛出异常

(3)DiscardPolicy implements RejectedExecutionHandler

--直接丢弃无法处理的任务

(4)DiscardOldestPolicy implements RejectedExecutionHandler

--将队列当中最老的任务丢弃,尝试再次提交新任务

6.2.5

a.ThreadFactory 创建线程的方式

b.监控线程池

beforeExecure/afterExecure/terminated

c.线程池适合提交相互独立的任务,而不是彼此依赖的任务。

对于彼此依赖的任务可以考虑分别提交给不同的线程池来执行

d.提交任务的方法

submit() execute() --对比

submit提交任务如果抛出异常无提示直接吃掉异常

6.3 ForkjoinPool

(和ThreadPoolExecutor一样,继承AbstractExecutorService)

ForkJoinPool和ExcutorService相比一个典型特性就是Work stealing.

clipboard3 (1).png

submit()

ForkJoinTask

子类:RecursiveAction(无返回值 ) RecursiveTask(有返回值)

-普通线程池共享一个工作队列,有空闲线程时工作队列中的任务才得到执行

-ForkJoinPool中每个线程都有自己的工作队列,每个工作线程运行中产生的新的任务,放在队尾

-某个工作线程会尝试窃取别个工作线程队列中的任务,从队列头窃取

-遇到join时,如果join任务尚未完成,则可先处理其他任务

6.4CompletionService

ExecutorService启动多个Callable时,每个Callable返回一个Future,而当我们执行Future的get方法获取结果时,可能拿到的Future并不是第一个执行完成的Callable的Future,就会进行阻塞,从而不能获取到第一个完成的Callable结果,那么这样就造成了很严重的性能损耗问题

CompletionService可以按照任务顺序获取结果

public interface CompletionService {

Future submit(Callable task);

Future submit(Runnable task, V result);

Future take() throws InterruptedException;

Future poll();

Future poll(long timeout, TimeUnit unit) throws InterruptedException;

}

submit方法和ExecutorService一样

take() poll()获取下一个完成任务的结果

take会阻塞等待 poll立即返回,如果没有已完成的任务,返回null

实现类是ExecutorCompletionService

CompletionService会根据线程池中Task的执行结果按执行完成的先后顺序排序,任务先完成的可优先获取到

6.5CompletableFuture

ExecutorService可以方便地提交单个独立的异步任务,可以方便地在需要的时候通过Future接口获取异步任务的结果,但是Future是通过阻塞或者轮询的方式得到任务的结果。

--CompletableFuture,它是一个具体的类,实现了两个接口,一个是Future,另一个是CompletionStage,Future表示异步任务的结果,而CompletionStage字面意思是完成阶段,多个CompletionStage可以以流水线的方式组合起来,对于其中一个CompletionStage,它有一个计算任务,但可能需要等待其他一个或多个阶段完成才能开始,它完成后,可能会触发其他阶段开始运行

--使用CompletableFuture可以实现类似功能,不过,它不支持使用Callable表示异步任务,而支持Runnable和Supplier,Supplier替代Callable表示有返回结果的异步任务,与Callale的区别是,它不能抛出受检异常,如果会发生异常,可以抛出运行时异常

使用Future,我们只能通过get获取结果,而get()可能会需要阻塞等待,而通过CompletionStage,可以注册回调函数,当任务完成或异常结束时自动触发执行