Java多线程基础知识总结

Java多线程基础知识总结

在Java中关键字synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或某个代码块,且synchronized可保证一个线程的变化被其它线程所看到(保证可见性,完全可以替代volatile)

线程的五种状态

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 终止(TERMINATED):表示该线程已经执行完毕。

线程状态图
Java多线程基础知识总结
悲观锁与乐观锁
悲观锁在一个线程进行加锁操作后,变为该线程的独有对象,其他线程都会被阻拦无法操作
缺陷
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的线程先执行

锁的状态对比
Java多线程基础知识总结

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

上一篇:Java中wait()方法为什么要放在同步块中


下一篇:面试题 | 一个公家单位的面试