深度分析Java多线程线程安全并发包

54次阅读

共计 3907 个字符,预计需要花费 10 分钟才能阅读完成。

1:synchronized(保证原子性和可见性)

1. 同步锁。多线程同时访问时,同一时刻只能有一个线程能够访问使 synchronized 修饰的代码块或方法。它修饰的对象有以下几种:

修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号 {} 括起来的代码,作用的对象是调用这个代码块的对象
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
修饰一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象

2:volatile 修饰的变量(只保证可见性,不保证原子性,且不阻塞线程,执行效率高)

线程在每次使用的时候都会读取变量修改后的最新的值。所以只有状态真正独立于程序内其他内容时,变量才能使用 volatile 修饰。
java 内存模型中的 happen-before 原则,两个线程同时修改访问 volatile 变量时,写操作比读操作优先级高。
实现原理:通过加入内存屏障和禁止指令重排序来实现。对 volatile 变量读之前会加一个 load 屏障指令,写之后会加一个 store 指令。

3:保证线程安全的思路

对非安全的代码进行加锁控制
使用线程安全的类
多线程并发情况下,线程共享的变量改为方法级的局部变量

4:synchronized 和 lock 比较

synchronized 是在 JVM 层面上实现的,可以通过一些监控工具监控 synchronized 的锁定,在代码执行时出现异常时 JVM 也会自动释放锁定。
Lock 是通过代码实现的,必须在 finally{}中调用 unLock()释放锁。
synchronized 是读写,读读,写写操作全互斥(性能不高…),使用 lock 可以提升性能,如:使用读锁锁住读的代码块,写锁锁住写的代码块。
synchronized 取得的锁都是对象,而不是把一段代码或函数当作锁。
每个对象只有一个锁(lock)和之相关联。
避免无谓的同步控制(实现同步需要很大的系统开销且使用不当会造成死锁)。

5:死锁

两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的原因:

因为系统资源不足
进程运行推进的顺序不合适
资源分配不当
产生死锁的四个必要条件:

互斥条件:所谓互斥就是进程在某一时间内独占资源。

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件: 进程已获得资源,在末使用完之前,不能强行剥夺。
循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。
打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。

6:线程类 & 池

Thread.sleep()是 Thread 的静态类方法,谁调用的谁去睡觉,即使在 a 线程里调用 b 的 sleep 方法,实际上还是 a 去睡觉,要让 b 线程睡觉要在 b 的代码中调用 sleep。
Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次 CPU 竞争”。
Thread.start()方法只是让线程处于就绪状态,线程真正抢到 CPU 时间片后才会执行线程,即此时执行 run()方法中的代码。
两个线程互为对方的参数,可以在一个线程的构造器中 new 一个对方线程的对象并 start(),则只启动一个线程即可启动两个线程。
创建线程方法:

继承 Thread 类,重写 run 方法
实现 Runnable 接口,重写 run 方法,实现 Runnable 接口的实现类的实例对象作为 Thread 构造函数的 target
通过 Callable 和 FutureTask 创建线程
通过线程池创建线程
说明:

前两种无返回值,通过重写 run 方法,run 方式的返回值是 void。访问当前线程,直接使用 this 即可获得当前线程。
后两种有返回值,通过 Callable 接口就要实现 call 方法,返回值是 Object。访问当前线程必须使用 Thread.currentThread()方法。
Callable 和 Runable 接口方法基本相同,只不过 Callable 接口定义的方法可以有返回值,而且可以声明抛出异常而已。Future 是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。
Future 接口定义如下:

public interface Future {boolean cancel(boolean mayInterruptIfRunning);// 中断任务 
    boolean isCancelled(); boolean isDone();// 判断任务是否完成 
    V get() throws InterruptedException,ExecutionException;// 获取任务执行结果 
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

cancel 方法用来取消任务,取消成功返回 true,取消失败返回 false。参数 mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务,如果设置 true,则表示可以取消正在执行过程中的任务。
如果任务已经完成,则无论 mayInterruptIfRunning 为 true 还是 false,此方法肯定返回 false,即如果取消已经完成的任务会返回 false;
如果任务还没有执行,则无论 mayInterruptIfRunning 为 true 还是 false,肯定返回 true
如果任务正在执行,若 mayInterruptIfRunning 设置为 true,则返回 true,若 mayInterruptIfRunning 设置为 false,则返回 false。
isCancelled 方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
isDone 方法表示任务是否已经完成,若任务完成,则返回 true;
get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回 null。

同步 & 异步,阻塞 & 非阻塞

同步和异步着重点在于多个任务的执行过程中,一个任务的执行是否会导致整个流程的暂时等待;
而阻塞和非阻塞着重点在于发出一个请求操作时,如果进行操作的条件不满足是否会返会一个标志信息告知条件不满足。

CountDownLatch

是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
CountDownLatch 是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减 1。当计数器值到达 0 后,表示所有的线程都已经完成任务,然后在闭锁上等待的线程就可以恢复执行任务。
用法:实现最大的并行性,开始执行前等待 n 个线程完成各自任务,死锁检测。
CountDownLatch 一组线程等待另一组线程都执行完了再继续执行。CyclicBarrier 使一组线程在一个时间点上达到同步,可以是一起开始执行全部任务或者一部分任务,同时它是可以循环使用的。
Semaphore 只允许一定数量的线程同时执行一段任务。
场景:在一个主线程中,要求有大量 (很多很多) 子线程执行完之后,主线程才执行完成。多种方式,考虑效率。
1. 自定义一个 ImportThread 类继承自 java.lang.Thread,重载 run()方法:用一个 List 属性保存所有产生的线程。这样只要判断这个 List 是否为空就知道还有没有子线程没有执行完了。
问题:如果线程 1 开始并且结束了,而其他线程还没有开始,此时 runningThreads 的 size 也为 0 主线程会以为所有线程都执行完了。
方法:用一个非简单类型的计数器来取代 List 型的 runningThreads,并且在线程创建之前就应该设定好计数器的值。
2. 使用 CountDownLatch 代替 MyCountDown, 用 await()方法代替 while(true){…}

正文完
 0