【摘要】 八、并发编程
8.1 基本概念
进程:代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,每个进程都有一个独立的内存空间。
线程:CPU分配的基本单位,线程是进程中的一个执行路径。一个进程中可以有多个线程,它们共享进程的堆和方法区资源,但每个线程都有自己的程序计数器和栈区域。线程并发执行,可自由切换。一个进程至少有一个线程
并发:同一个时间段内多…
八、并发编程
8.1 基本概念
进程:代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,每个进程都有一个独立的内存空间。
线程:CPU分配的基本单位,线程是进程中的一个执行路径。一个进程中可以有多个线程,它们共享进程的堆和方法区资源,但每个线程都有自己的程序计数器和栈区域。线程并发执行,可自由切换。一个进程至少有一个线程
并发:同一个时间段内多个任务同时都在执行,
并行:单位时间内多个任务同时在执行
同步:排队执行,效率低但安全
异步:同时执行,效率高但不安全
synchronized
synchronized是java提供的一种原子性内置锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者同步块内调用了该内置锁的wait方法时释放该内置锁。synchronized是隐式锁
使用方法:
synchronized(锁对象){
//同步代码块区域
//锁对象必须是类对象,不能是基本数据类型,也不能是包装类
}
synchronized int sum(){
//同步方法区域
}
volatile
volatile确保对一个变量的更新对其他线程立马可见,但不保证操作的原子性。
使用volatile 的情形:
1.写入变量值不依赖变量的当前值时。依赖当前值需要获取-计算-写入三步操作,这三步操作不是原子性的,volatile不保证原子性
2.读写变量值时没有加锁。加锁本身已经保证了内存可见性,不需要把变量声明为volatile
显式锁
synchronized采用的是隐式锁的机制,加锁的方式是隐藏的。如果想显式的添加的一个锁,可以使用Lock类,创建一个ReentrantLock对象,用lock()方法加锁,unlock()方法解锁。如果给ReentrantLock构造方法传递一个true参数,则该锁是公平锁,锁解开后先执行的线程先获得这个锁。
8.2线程的创建与运行
1.继承Thread类
public static class MyThread extends Thread{
@Override
public void run(){
//这里重写run方法
}
}
调用:
MyThread thread =new MyThread();//创建线程
thread.start();//启动线程。调用start()后线程处于就绪状态
(或者使用匿名对象 new MyThread().start())
优点:在run方法内获取当前线程直接使用this,无需使用Thread.currentThread(),可以在子类添加成员变量进行传参。
缺点:java不支持多继承,无法再继承其他类;任务没有与代码分离;线程池不接收Thread类型的任务
2.实现Runnable接口的run方法
public static class MyRunnable implements Runnable{
@Override
public void run(){
//这里重写run方法
}
}
调用:
MyRunnable task =new MyRunnable();创建线程
new Thread(task,”我的第一个线程”).start();//启动线程。调用start()后线程处于就绪状态
优点:1.避免单继承带来的局限性。
2.任务与线程是分离的,提高了程序的健壮性
3.线程池接收Runnable类型的任务,不接收Thread类型的任务
缺点:只能使用主线程里被声明为final 的变量,
3.使用Callable方式
public static class MyThread implements Callable{
@Override
public String call(){
//这里重写call方法
}
}
调用:FutureTask task= new FutureTask<>(new MyThread());//创建异步任务
new Thread(task).start();
try{
String result=task.get();
}catch(ExecutionException e){e.printStackTrace();}
前两种方法都没有返回值,而使用Callable可以
8.3 wait()
线程的等待:当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起
直到以下情况发生:(1)其他线程调用了该共享对象的notify()或notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
防止虚假唤醒:
synchronized(obj){
while(条件不满足){
obj.wait();
}
}
当前线程调用共享变量的wait()后只会释放当前变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。
8.4 interrupt()
void interrupt():向线程发送中断请求。线程的中断状态被设置为true。如果当前该线程被一个sleep调用阻塞,,则抛出一个InterruptedException异常
static boolean interrupted(): 测试当前线程是否被中断。将当前线程的中断状态重置为false
boolean isInterrupted():测试线程是否被中断,不改变线程的中断状态
8.5 notify()
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程等待,具体唤醒哪个线程是随机的。被唤醒的线程只有在竞争到共享变量的监视器锁后才可以继续执行。
notifyAll() 唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程
8.6 join()
用于等待线程加载完毕再进行后续处理,由Thread类直接提供。wait()是Object类的方法
8.7 sleep()
调用sleep的线程暂时让出指定时间的执行权,在这期间不参与CPU的调度,但是该线程所拥有的监视器资源是不让出的。睡眠时间到后正常返回,线程处于就绪状态,参与CPU调度,获取到CPU资源后继续运行。sleep的参数单位为毫秒
8.8 yield()
当一个线程调用yield()时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然亦可能恰好是让出CPU使用权的线程。
yield和sleep的区别:当线程调用sleep时会被阻塞挂起指定的时间,在这期间线程调度器不会区调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,处于就绪状态,线程调度器下一次调度时就可能调度到当前线程执行
8.9死锁
死锁:两个或两个以上线程在执行过程中,因抢夺资源而造成的相互等待的现象。
死锁产生的条件:
1.互斥:线程对已经获得的资源进行排他性使用,即该资源只能由一个线程占用。如果有其他线程请求获取该资源,则请求者只能等待,直到占有该资源的线程释放该资源。
2.请求并持有条件:一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时不释放自己已经获取的资源。
3.不可剥夺条件:线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
4.环路等待:发生死锁时,必然存在一个线程一资源的环形链
8.10 守护线程与用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。main函数所在的线程就是一个用户线程,垃圾回收线程是一个守护线程。
区别:只有当最后一个用户线程结束后,JVM会正常退出。而守护线程是否结束不影响JVM的退出,当最后一个用户进程结束后,所有守护线程自动死亡。
设置守护线程方法:
MyThread thread =new MyThread();
thread.setDaemon(true)
8.11 CAS
CAS即compare and swap
8.12 乐观锁与悲观锁
悲观锁是指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前对数据进行加锁。
乐观锁认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁
8.13 API
Thread
currentThread() 获取当前线程
getName() 获取线程名称,如果没有设置线程名称,默认从0开始递增1
setName() 设置线程名称
文章来源: blog.csdn.net,作者:码来,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_44289205/article/details/116170804