Java多线程基础知识总结
在Java中关键字synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或某个代码块,且synchronized可保证一个线程的变化被其它线程所看到(保证可见性,完全可以替代volatile)
线程的五种状态
线程状态图
悲观锁与乐观锁
悲观锁在一个线程进行加锁操作后,变为该线程的独有对象,其他线程都会被阻拦无法操作
缺陷
1.一个线程获得悲观锁后其他线程必须阻塞
2.线程不停切换释放和获取锁,开销大
3.可能会导致优先级倒置,synchronized是典型的悲观锁
乐观锁
乐观锁认为对一个对象操作不会引发冲突,所以每次操作都不进行加锁,只在最后提交更改验证是否发生冲突,若冲突则再试一遍直到成功,这个过程称为自旋
CAS(Compare And Swap) 比较交换
jdk1.5之前,java中的锁都是重量级的悲观锁,在1.5中引入了java.util.concurrent包提供了乐观锁的使用,整个JUC包实现的基石就是CAS操作
执行函数: CAS(V,E,N)
-V表示要更新的变量
-E表实预期值
-N表示新值
如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
且CAS操作必须时原子性的,获取原值时要保证原值对本线程可见。
多线程锁的升级原理
锁的四种状态: 无锁状态->偏向锁->轻量级锁->重量级锁(锁的竞争,只能升级,不能降级)
无锁
没有对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重复直到修改成功
偏向锁
指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态,如果还活着,就升级成轻量锁。
轻量级锁
当锁是偏向锁的时候,被第二个线程b访问,此时偏向锁就会升级成轻量级锁,线程b会通过自旋尝试获取锁,线程不会阻塞,从而提升性能。
当只有一个等待线程,则该线程将通过自旋进行等待,当自旋超过一定次数时,升级为重量级锁。
**自旋:**当一个线程已经获取锁了,另一个线程需要获取锁,就会不断循环等待直到获取锁然后退出循环
重量级锁
当一个线程获取锁后,其余所有等待该锁的线程都会阻塞。
重量级锁通过对象内部的监听器( monitor)实现,monitor的本质就是依赖底层操作系统的Mutex Lock实现的,操作系统实现线程之间的切换需要从用户切到内核,资源消耗巨大
synchronized的三种应用
1.修饰普通方法,作用与当前方法加锁,进入同步代码前要获得当前实例的锁
2.修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
3.修饰静态代码块,指定加锁对象,对给定对象加锁,进入同步代码前要获得给定对象的锁
start()、run()、join()方法区别
start(): 线程不会立即启动,相当于在就绪队列里
run(): 启动线程
join(): 让其他线程等待,调用join的线程先执行
锁的状态对比
synchronized 等待与唤醒机制
synchronized等待唤醒机制就是调用notify、notifyAll、wait方法,使用这三个方法是,必须处于synchronized中。
wait方法调用完线程被暂停,会释放锁,调用notify/notifyAll方法后才继续执行,sleep只让线程休眠,不释放锁。
notify/notifyAll方法调用后,不会马上释放锁,而是在相应的synchronized方法中结束后释放锁。
notify()和notifyAll区别
等待池:一个线程调用wait方法后,线程会释放该对象的锁,进入该对象的等待池。
锁池:只有获得了对象的锁,线程才会执行synchronized中的代码,对象的锁只有一个对象能获得,其他线程只能等待。
notify()方法随机唤醒对象等待池中的一个对象,进入锁池
notifyAll() 唤醒对象的等待池中的所有线程,进入锁池
public static void main(String[] args) { new Thread(new T1()).start(); new Thread(new T2()).start(); } static class T1 implements Runnable{ @Override public void run() { synchronized (Test7.class){ try { System.out.println("进入线程1"); Thread.sleep(5000); Test7.class.wait();//让出锁,线程暂停,进入等待池,其它线程可获得同步锁执行 } catch (InterruptedException e) { System.out.println(e.getMessage()); e.printStackTrace(); } System.out.println("线程1结束等待,继续执行"); System.out.println("线程1执行结束"); } } } static class T2 implements Runnable{ @Override public void run() { synchronized(Test7.class){ System.out.println("进入线程2"); System.out.println("线程2唤醒其他线程"); Test7.class.notify();//唤醒其他线程,但不让出锁,告诉T1可以开始获取对象锁了,直到执行结束T1继续执行 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程2继续执行"); System.out.println("线程2执行结束"); } } }
execute()和sunbmit()区别
execute(Runnable x) 没有返回值,可执行任务却无法判断是否成功,实现Runnable接口
sunbmint(Runnable x) 返回一个future,用来判断任务是否成功,实现Callable接口
sleep()和wait()区别
1.sleep()是thread类的静态方法,wait是Object类的
2.sleep()可以在任何地方使用,wait只能在同步代码块中使用
3.sleep()休眠当前线程,释放CPU资源,不会释放锁,休眠时间结束自动执行,wait放弃持有的对象
锁,进入等待池,调用notify方法后才有机会竞争对象锁
4.均需要捕获interruptedException异常
synchronized 和 volatile区别
1.synchronized作用与变量、方法;volatile只作用于变量
2.synchronized保证线程有序性、可见性、一致性;volatile保证线程有序性、可见性
3.synchronized线程阻塞,volatile线程不阻塞
4.synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其它线程;volatile表示变量在CPU寄存器中是不确定的,必须从内存中读取,保证多线程环境下变量的可见性和有序性
ReentrantReadWriteLock读写锁
表示两个锁。一个是读相关的锁,称为共享锁,多个线程能同时执行,另一个是写操作相关的锁,称为排他锁,只允许一个线程执行
死锁与活锁
死锁:表示两个或两个以上的进程在执行时,因资源争夺出现相互等待的过程
产生死锁的必要条件
1.互斥条件:进程在某一时间内独占资源
2.请求与保持条件:一个进程因请求资源而阻塞时,对以获得的资源保持不放
3.不剥夺条件:进程以获得资源,在未用完之前,不能强行剥夺
4.循环条件:若干进程之间形成一种头尾相接的循环等待关系
活锁
任务或执行者没有被阻塞,我认识因为一些资源不满足,导致一直重复尝试、失败
活锁死锁的区别
活锁可能会自己解,死锁不能,处于活锁的实体一直在不断的改变状态,而死锁一直处于等待状态
饥饿。
一个或多个线程因为某些原因无法获得需要的资源,导致以值无法执行的状态
1.优先级高的线程吞噬优先级低的cpu时间
2.线程被永久堵塞或永久等待(wait方法)
Executor框架
Executor是jdk1.5引入的一系列并发库中与Executor相关的功能类
1.用new Thread(…).start()方法处理多线程开销是很大的,线程也缺乏管理,没有线程池来限制线程的数量,当并发量高的时候会非常消耗资源。
2.使用线程池创建可实现线程复用,也能有效的控制并发数量,提供操作线程的功能了方便管理
Executor的重要接口和类
Callable
Callable位于java.util.concurrent包下,是一个接口,只声明了call()方法
它类似Runnable,但call比run方法更强大,call方法有返回值,且可声明抛出异常
Future
Future位于java.util.concurrent包下,是jdk1.5引入的接口
主要作用是对具体的Runnable或Callable任务的执行结果进行取消,查询是否完成、获取结果
Executor
Executor是一个接口,他将任务的提交与任务的执行分离,定义了一个接收Runnable对象的方法
ExecutorService
ExecutorService继承了Executor,是一个比Executor使用更广泛的接口,提供了终止任务、提交任务、跟踪任务返回结果等方法,它是可以关闭的,关闭之后不能接收任何任务,当没有使用时,应shutdown