一.线程的状态
NEW:Thread对象有了,内核中的线程(PCB)还没有.任务布置了,但是还没有开始执行. RUNNABLE:就绪状态,当前线程正在CPU上执行或者已经准备好随时到CPU中,有一个专门的就绪队列来维护. BLOCK(等待锁),wait,sleep:阻塞状态,当前线程暂时停下来,不会继续到CPU上执行,等到时间成熟才会有机会执行. TERMINATED:内核中的线程已经结束了(PCB没了),但是代码中的Thread对象还在(这个对象要等垃圾回收机制来回收) isAlive:线程存活,除了NEW和TERMINATED之外,其他状态都表示线程存活.(PCB是否存活)
二.线程安全问题
线程安全:多线程并发执行某个代码,没有逻辑上的错误,就是"线程安全". 线程不安全:多线程并发执行某个代码,产生逻辑上的错误,就是"线程不安全".
1.线程不安全的原因
1.线程是抢占式执行的(线程不安全的万恶之源)线程之间的调度完全由内核负责,用户代码中感知不到,也无法控制. 线程之间谁先执行,谁后执行,谁执行到哪里,从CPU上下来,这样的过程都是用户无法控制也无法感知的
2.自增操作不是原子的 原则性的意思是即使执行的过程中被调度器换走单它会继续执行直到结束,虽然线程里的自增不是原子的,但它有上下文能记录一次切换走的位置继续执行,当CPU执行到上面三个布置的任何一个步骤的时候都可能被调度器调走,让给其他线程来执行
3.三个线程尝试修改同一变量 a.如果一个线程修车一个变量,线程安全. b.如果多个线程读取同一个变量,线程安全. 但是如果多个线程,一个读取变量一个修改变量,最后线程还是不安全的 c.如果多个线程修改不同的变量,线程安全.
4.内存可见性 当两个线程同时操作一个内存,例如一个读一个写,但是当写操作的线程进行修改的时候,读线程可能读到修改前的数据,也可能读到修改后的数据,这是不确定的. 内存可见性的本质是编译器优化导致了线程1修改的数据没有及时到写入到内存中,线程2就读取不到.
t1这个线程在循环读这个变量,内存读取操作相比于读取寄存器是一个非常低效的操作,因此在t1中频繁的读取到这里的内存值就会非常低效,而且如果t2线程迟迟不修改t1,线程读到的值又始终是一样的值,所以,ti就有一个大胆的想法.就不会再从内存中读取数据,而且直接从寄存器里读,不执行(load),一旦t2修改了coun值,t1就不能感知到了.
5.指令重排序 与线程不安全,编译器优化直接相关。为了提高程序的执行效率,调整了执行的顺序(调整的目的是为了提高效率,不改变逻辑)编译器也会自动对顺序进行调整。单线程调整不会出问题,多线程会出很大问题
如何解决线程安全问题
synchronized的基本用法:
1.把synchronized加到普通方法上 如果直接修饰普通方法,也就是把锁对象指定为this(当前类的对象) 注意:代码中只需要将类被调用的方法 前面加上修饰的关键字synchronized 使用条件:如果操作共享数据的代码完整声明在一个方法上,可以使用此方法,将方法声明为同步的
2.synchronize加到代码块上
这里的this指的是锁对象,如果要针对某个代码加锁,就需要手动指定,锁对象(针对锁对象进行加锁)
可重入
直观来说,同一个线程针对同一个锁,连续加锁两次,如果出现死锁,就是不可重入,如果不会出现死锁就是可重入 可重入的意义:降低了程序员的负担(提高了开发效率)但是也带来了代价,程序中需要更高的开销(降低了运行效率)
什么是死锁
这里可以看出,外层加了一次锁,里层又对同一个对象再加一次锁
外层锁:进入方法则开始加锁,这次能够加锁成功,当前锁是没有人占用的 里层锁:进入代码块开始加锁,这次加锁不能成功,因为锁被外层占用着,得等待外层锁释放后,里面的锁才能释放,但想要执行完整个方法,就得让里层加锁成功继续往下走。就成了死锁~~
死锁的四个必要条件
1.互斥使用:一个锁被一个线程占用了之后,其他线程占用不了 2.不可抢占:一个锁被一个线程占用以后,其他的线程不能把这个锁给抢走 3.请求和保持:当一个线程被占据了多把锁之后,除非释放锁否则这些锁始终被该线程持有的 4.环路等待:等待关系成环了。
volatile关键字
作用:禁止编译器优化,保证内存的可见性.被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象 注意:volatile只是保证可见性,不保证原子性,只处理一个线程读一个线程写的情况,而synchronized都能处理
wait和notify
wait和notif都是Object对象的方法,调用wait方法线程就会陷入阻塞,阻塞到其他线程通过notify来通知.
wait内部会做三件事: 1.先释放锁 2.等待其他线程的通知 3.收到通知之后,重新获得锁,并进行往下执行
注意:wait那个对象就需要对那个对象进行加锁
a先执行一通知就执行e,e执行完一通知就执行b.........
wait和notify都是针对同一个对象来操作的
例如现在有一个对象0和10个对象,都调用了0,wait,此时10个线程都是阻塞状态,如果调用了0.notify就会把10个其中的一个唤醒(唤醒那个不确定)
针对notiAll,就会把所有的10个线程的一个给唤醒,唤醒之后,就会重新尝试获取到锁(这个过程发生看竞争)相对来说还是用notif.