多线程
JAVA多线程
多线程基础
线程
定义:
线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位
在java中堆内存是唯一的,栈内存不是唯一的,是与线程相关的,一个线程对应一个栈,因此栈也可以叫做线程栈
简单理解: 线程就是软件中互相独立的,可以同时运行的功能
进程: 进程是程序的基本执行实体(一个软件运行之后就是一个进程)
并发:
在同一时刻,有多个指令在单个CPU上交替执行
并行:
在同一时刻,有多个指令在多个CPU上同时执行
创建多线程
java虚拟机允许应用程序并发的运行多个线程
java中有3中方式可实现多线程
1.继承Thread类:
自己定义一个类继承Thread类,重写run方法,把需要多线程执行的方法写入run方法内,在外部创建该类对象,调用start方法启动
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i); }}}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
//void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
my1.start(); }}
2,实现Runnable接口:
定义一个类实现Runnable接口,并重写run方法,在外部创建一个Thread对象和该类对象,在构造参数里传入该类对象
使用Thread对象调用run方法启动
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++) {
//这里的Thread.currentThread()方法可获得当前线程的对象
System.out.println(Thread.currentThread().getName()+":"+i); }}}
public class MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
Thread t1 = new Thread(my,"坦克");
//启动线程
t1.start(); }}
3.利用Callable接口和Future接口:
定义一个类实现Callable接口,并定义泛型(该泛型就是返回值类型),重写call方法,将需要多线程执行的语句放入call方法
在外部创建该类对象,FutureTask对象(用于管理多线程运行的结果),以及Thread对象
在FutureTask构造方法内传入该类对象,并定义泛型类型为返回值类型
在Thread构造方法内传入FutureTask对象,调用Thread对象start方法启动线程,调用FutureTask对象get方法获取该线程返回值
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("i);
}
//返回值就表示线程运行完毕之后的结果
return "结果";}}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
//作为参数传递给Thread对象,获取线程执行完毕之后的结果.
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
//开启线程
t1.start();
String s = ft.get();
System.out.println(s); }}
线程常见方法
线程优先级:
线程调度:
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型
优先级设置:
可以使用setPriority()方法设置该线程的优先级
优先级有 1到10,默认优先级为5,优先级越高,抢到线程的概率越高
守护线程:
当其他非守护进程执行完毕后,守护进程会陆续结束
setDaemon()方法:将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
插入线程:
在一个线程运行方法内部创建一个新的线程,调用该线程的join()方法
该方法可将调用join方法的这个线程插入到当前线程之前,调用join方法线程执行完毕后,再执行当前线程
中断线程:
在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行
Tip:
如果目标线程处于等待状态,调用 interrupt()方法 join()方法会立即抛出异常
线程的生命周期:
New:新创建的线程,尚未执行 //new方法
Runnable:运行中的线程,正在执行run()方法的Java代码 //start方法
Blocked:运行中的线程,因为某些操作被阻塞而挂起 //无法获得锁对象
Waiting:运行中的线程,因为某些操作在等待中 //wait方法
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待 //sleep方法
Terminated:线程已终止,因为run()方法执行完毕 //全部代码执行完毕
过程:
创建对象 => 调用start方法开始抢占执行权 => 抢占到执行权 => 线程代码运行完毕,变成垃圾被回收
在java中只有6种状态,没有运行状态
线程同步
同步代码块:
利用synchronized关键字声明一个代码块,在该关键字后面声明一个锁对象(该锁对象可以是任何对象)
在线程执行到该代码块时,若锁对象相同,会检测该代码块是否被别的线程使用,
若使用,则等待占用线程运行完毕该代码再执行同步代码块代码
Tip:
若需要改代码块只能被一个线程执行则设置锁对象为唯一对象
一般把锁对象设置为当且类的字节码文件对象, 类名.class
//声明
synchronized(任意对象) {
多条语句操作共享数据的代码
}
同步方法:
将synchronized关键字写到方法上
同步方法的锁是java指定的
非静态方法:锁对象为this
静态方法: 锁对象为当前类的字节码文件
//声明
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//注:若使用实现Runnable接口线程,则调用该对象形成的线程时,都是调用同一个对象,因此使用非静态同步方法也可以保证锁对象唯一
// 并且内部的成员变量也是共享的
//Tip: StringBuilder与StringBuffer的不同就在于 StringBuffer内部的方法都是被 synchronized 修饰的同步方法
Lock锁(JDK5新增):
与 synchronized关键字加锁不同,Lock是自己调用方法上锁以及自己调用方法开锁
使用:
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
创建锁对象(用static修饰保证锁对象唯一)
lock() | 获得锁
unlock() | 释放锁
lock相当于synchronized的前{ unlock相当于 }
Tip:
与synchronized修饰不同, Lock锁必须执行unlock方法后才能开锁,因此可以用try..catch..finally方法来保证不会发生阻塞
死锁:
死锁是一种错误
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
避免: 避免出现2个锁嵌套起来
等待唤醒机制:
方法:
| Object类的等待和唤醒方法 | 说明 |
|---|---|
| void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
| void notify() | 唤醒正在等待对象监视器的单个线程 |
| void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
使用:
在synchronized内部可以调用wait()使线程进入等待状态;
必须在已获得的锁对象上调用wait()方法;
必须在已获得的锁对象上调用notify()或notifyAll()方法;
在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程
已唤醒的线程还需要重新获得锁后才能继续执行。
一次只有一个线程可以拥有对象的监视器
Tip:
wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()
调用wait()方法后,线程进入等待状态,wait()方法不会返回,直到将来某个时刻,
线程从等待状态被其他线程唤醒后,wait()方法才会返回,然后,继续执行下一条语句
!当一个线程执行 锁对象.wait()方法后,其会立即释放当前的锁
调用sleep()不会释放锁
阻塞队列:
BlockingQueue的核心方法:
put(anObject): 将参数放入队列,如果放不进去会阻塞
take(): 取出第一个数据,取不到会阻塞
阻塞队列内部的方法会自定上锁,尽量避免在外部在加锁造成锁的嵌套
ArrayBlockingQueue在创建对象时可以传入一个int参数,代表其可以放入几个数据
线程中断:
Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为 true(默认是 flase)
Thread.isInterrupted():测试当前线程是否被中断
Thread.interrupted():检测当前线程是否被中断,与 isInterrupted() 方法不同的是,这个方法如果发现当前线程被中断,会清除线程的中断状态。
在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为 true,
但是具体被要求中断的线程要怎么处理,完全由被中断线程自己决定,可以在合适的时机中断请求,也可以完全不处理继续执行下去。
线程池
线程池的核心原理:
首先会创建一个池子,池子是空的
当提交线程任务时,线程池会创建新的线程对象,任务执行完毕,线程会回到线程池,下次再提交任务时,就不用穿件新的线程对象,
复用原来的线程对象即可。
线程池可以自定义大小,当线程池满时,再次提交任务,该任务会排队等待其他线程执行完毕,回到线程池再继续执行
代码实现:
可以使用Executors中所提供的静态方法来创建线程池
| 方法名 | 说明 |
|---|---|
| static ExecutorService newCachedThreadPool() | 创建默认线程池(没有上限) |
| static ExecutorService newFixedThreadPool(int nThreads) | 创建有上限的线程池 |
public static void main(String[] args) throws InterruptedException {
//1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
ExecutorService executorService = Executors.newCachedThreadPool();
//Executors --- 可以帮助我们创建线程池对象
//ExecutorService --- 可以帮助我们控制线程池
executorService.submit("Runnbale实现类或Callbale实现类"); //在调用submit方法后,传入的线程对象就会开始运行(若获得线程的话)
//销毁线程池
executorService.shutdown(); }
自定义线程池:
对象创建:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,
空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
运行细节:
当提交的任务未超过核心线程数量时,直接分配线程
当超过核心线程数量时,会把超过的部分放入任务队列中排队等候
当超过核心线程数列且也超过任务队列大小时,会创建临时线程,没有进入任务队列的任务会被分配临时线程
当超过最大线程数量和任务队列之和时,会把超出部分的任务根据任务拒绝策略进行处理
代码实现:
//使用列子
// 参数一:核心线程数量
// 参数二:最大线程数
// 参数三:空闲线程最大存活时间
// 参数四:时间单位 用TimeUnit指定
// 参数五:任务队列
// 参数六:创建线程工厂
// 参数七:任务的拒绝策略
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
pool.submit(new MyRunnable());
pool.shutdown(); }
//创建对象
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
// corePoolSize: 核心线程的最大值,不能小于0
// maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
// keepAliveTime: 空闲线程最大存活时间,不能小于0
// unit: 时间单位
// workQueue: 任务队列,不能为null
// threadFactory: 创建线程工厂,不能为null
// handler: 任务的拒绝策略,不能为null
//拒绝策略
// RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。
// ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
// ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
// ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
// ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
线程池大小:
CPU密集型计算:
最大并行数 + 1
I/O密集型计算:
最大并行数 * 期望CPU利用率 * ( 总时间(CPU计算时间 + 等待时间) / CPU计算时间)
最大并行数:
当前java虚拟机可使用的线程数量
调用 Runtime.getRuntime().avaliableProcessors() 获取
原子性
JMM模型:
JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
特点:
1.所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。
不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
2.每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本
3.线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成
简单解释:
每个线程都有一个工作内存,每个工作内存都保留了一份主内存的副本,副本内记录者共享变量的值
每次线程操作变量时,都是操作自己的工作内存,然后再把工作内存写入主内存中达到同步
volatile关键字:
为了保证多线程操作共享变量的可见性,可用volatile修饰变量
当一个共享变量被 volatile 修饰时,它会保证修改的值立即更新到主存当中,这样的话,当有其他线程需要读取时,就会从内存中读到新值。
普通的共享变量不能保证可见性,因为变量被修改后什么时候刷回到主存是不确定的,因此另外一个线程读到的可能就是旧值。
Java 的锁机制如 synchronized 和 lock 也是可以保证可见性的
作用:
当写一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去;
volatile会禁止指令重排
当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障
(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点
写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存
读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取
也就是说执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见
注:
volatile只保证单个变量的原子性,以及其读和写的内存可见性
但不保证一整个过程的内存可见性,
如 线程1获取了 被volatile修饰的变量a,b也获取了a,此时2者获取的a值相同,若2者都对其修改,则不能实现预测的结果
JUC
JMM模型
概述
JMM: JAVA 内存模型
在Java中,使用的是共享内存并发模型。
重排序
概述:
编译器为了优化代码的执行,会对代码的执行顺序进行重排,但最后的结果和java代码顺序结果相同
但重排序对代码顺序的保证仅限于 单个线程, 多个线程的重排不进行保证
happens-before
概述:
为了解决对多线程重排的乱序,java提供了happens-before关系,只要遵从happens-before,重排也对多线程保证
在Java中,有以下天然的happens-before关系:
- 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作、
- join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

浙公网安备 33010602011771号