多线程与并发编程
并发与并行
**并发:**多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是
同时执行。
**并行:**单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
**串行:**有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻:
并发 = 俩个人用一台电脑。
并行 = 俩个人分配了俩台电脑。
串行 = 俩个人排队使用一台电脑。
为什么使用并发编程
- 提升多核cpu的利用: 现在的电脑基本都支持多线程,这样如果还是一个单线程应用的话,那2核cpu就会浪费50%,4核就会浪费75%
- 方便进行业务拆分,提升应用性能
应用场景
数据库连接池、分批发送短信等;
并发编程缺点
使用多线程也会遇到很多问题,比如内存泄露、上下文的切换、线程安全、死锁、资源的共享等;
多线程缺点
- **浪费资源:**线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
- 资源竞争: 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
三要素:原子性、可见性、有序性
- **原子性:**原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- **可见性:**一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
- **有序性:**程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
- 对此,出现线程安全问题的原因一般都是这三个原因:
- 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock);
- 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题;
- 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题;
死锁条件
- 互斥条件: 在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。
- 占有且等待条件: 进程已经保持了一个资源,但有***需要新的资源***,而该资源却被其他进程占用,此时该进程就会阻塞,等待其资源的释放,并且又不释放自己的资源。
- 不可抢占条件: 进程占用了某个资源,其他应用不能应为自己需要该资源而去抢占资源。
- 循环等待条件: 多个进程之间形成一种头尾相接的循环等待资源的关系。(比如一个进程集合,A在
等B,B在等C,C在等A) - 对此,解决死锁的方式:
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
线程的创建方式
-
继承 Thread 类;
public class MyThread extends Thread{ @Override public void run(){ System.out.println("...."); } public static void main(String[] args){ MyThread myThread = new MyThread(); myThread.start(); } }
-
实现 Runnable 接口;
public class MyThread implements Runnable{ @Override public void run(){ System.out.println("...."); } public static void main(String[] args){ Thread thread = new Thread(new MyThread()); thread.start(); } }
-
实现 Callable 接口;
public class MyThread implements Callable<Integer>{ @Override public Integer call(){ System.out.println("..."); return 1; } public static void main(String[] args){ FuterTask futureTask = new FutureTask(new MyThread()); Thread thread = new Thread(futureTask); thread.start(); } }
-
使用匿名内部类方式
public class MyThread{ public static void main(String[] srgs){ Thread thread = new Thread(new Runnale(){ @Override public void run(){ System.out.println("..."); } }); thread.start(); } }
runnable与callable有什么区别
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
主要区别:
- Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法
- new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程工作。
- 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
Callable 和 Future
- Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
- Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。
FutureTask
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调
用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
callable返回值–>future获取值–>futuretask对值进行操作
-
新建(new):新创建了一个线程对象。
-
**就绪(可运行状态)(runnable)**线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
-
**运行(running):**可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处
于就绪状态中; -
**阻塞(block):**处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:- (一). **等待阻塞:**运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waittingqueue)中,使本线程进入到等待阻塞状态;
- (二). **同步阻塞:**线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
- (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
-
**死亡(dead)(结束):**线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束
生命周期。死亡的线程不可再次复生。
Java 中用到的线程调度算法是什么
- 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用
权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
(Java是由JVM中的线程计数器来实现线程调度) - 有两种调度模型:分时调度模型和抢占式调度模型。
- 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解。
- Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
一个平均,一个按优先级。
- 线程体中调用了 yield 方法让出了对 cpu 的占用权利
- 线程体中调用了 sleep 方法使线程进入睡眠状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中,该线程的时间片用完
sleep() 和 wait() 有什么区别
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
在 Java 程序中怎么保证多线程的运行安全
-
方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
-
方法二:使用自动锁 synchronized。
-
方法三:使用手动锁 Lock。
-
手动锁 Java 示例代码如下:
Lock lock = new ReentrantLock(); lock.lock; try { System.out.println("获得锁"); } catch (Exception e){ // TODO: handle exception } finally{ System.out.println("释放锁"); lock.unlock(); }
注:线程类中的构造方法和静态块是被new这个类所在的线程调用,只有run()方法里的代码才是被线程自身所调用的
多线程的常用方法
方法名 | 描述 |
---|---|
sleep() | 强迫一个进程睡眠n秒 |
isAlive() | 判断一个进程是否存活 |
join() | 等待线程终止 |
activeCount() | 程序中活跃的线程数 |
enumerate() | 枚举程序中的线程 |
currentThread() | 得到当前线程 |
isDaemon() | 一个线程是否为守护线程 |
setDaemon() | 设置一个线程为守护线程 |
wait() | 强迫一个线程等待 |
setName() | 为线程设置一个名称 |
notify | 通知一个线程继续运行 |
setPriority() | 设置一个线程的优先级 |