一、多线程的三大问题:可见性、原子性,有序性。
1.可见性:因CPU缓存导致,因为多线程的本质就是利用上多核CPU的性能,由于每个现代CPU都有一级或者二级CPU cache用来提升性能,所以如果使用多线程对堆数据进行修改操作,势必会先读取到CPU缓存中,然后在缓存中修改后写回到堆内存。如果多个线程同时操作,就会造成A、B线程同时读取一条数据,然后A、B都在各自CPU cache中进行了修改,写回堆内存时一个结果覆盖了另一个结果。 这就是可见性问题,即多个线程对数据的操作,互相不可见。
2.原子性:假设a是堆内存的数据,比如a++这样的操作,就不满足原子性的要求。因为这个属于多个操作,CPU先从内存中读取a,然后在缓存中对a的值进行+1,再写回到堆内存当中。对于这样的操作就必须满足原子性的要求。
3.有序性:由于JVM或者更底层的汇编代码优化,有时候会将我们写的代码的汇编顺序进行调换,就产生了有序性问题。
二、解决办法
可见性问题可以通过禁用缓存来解决,有序性问题可以通过禁用编译优化解决,但是缓存和编译优化就是为了提高CPU性能而设计的,如果完全禁用会导致CPU性能太差。为了保证功能、性能两不误,所以设计出一些机制让程序员按需禁用。比如volatile、synchronized、final关键字等。
原子性问题,可以使用互斥锁来解决。使用synchronized关键字,synchronized使用的时候需要给一个对象锁或者类锁,如果一个线程想要访问临界区的代码必须要获取到相应的锁才能够继续运行,否则只能阻塞等待。 加锁本质就是在锁对象的对象头中写入当前线程id。
三、编译器排序优化的六个Happens-Before规则:
1.同一个线程中,前面的操作happens-before后面的操作。
2.对一个volatile变量的写操作,happens-before后续对这个变量的读操作。
3.传递性:A happens-before B,B happens-before C,那么A happens-before C。
4.对一个锁的解锁 happens-before 后续对一个锁的加锁。
5.start原则:主线程A启动子线程B后,子线程B能看到A在启动B之前的操作。
6.线程join原则:在线程A中调用线程B的join方法,线程B的所有操作happens-before线程A的后续操作。
四、synchronized实现等待-通知机制
synchronized中使用wait()、notify()、notifyAll()这三个方法,能够实现等待-通知。
wait():在synchronized块中使用该方法,能够让当前线程释放所持有的锁,同时让当前线程进入相应锁的等待队列。
notify():在synchronized块中使用该方法,能够让当前线程释放所持有的锁,同时通知相应锁的等待队列,从等待队列中随机选取一个线程获得锁。
notifyAll():在synchronized块中使用该方法,能够让当前线程释放锁,同时通知相应锁的所有线程,让他们进行争抢。
这三个方法只能在synchronized块当中使用,同时只能被当前锁使用,假如用的是target当作锁,那么是target.wait()。
五、安全性、活跃性、以及性能
1.安全性:
多个线程对共享的数据进行操作的时候,就会发生数据竞争。 竞态条件:程序的执行结果依赖线程执行的顺序。
2.活跃性:
活锁:两个线程同时争抢对方需要的资源,又同时更换目标,导致完美错开。
饥饿:线程因无法访问所需资源而无法执行下去的情况。
3.性能:
在满足业务需求的前提下,尽量保证多个线程同时操作。
六、MESA模型
管程的模型。
七、线程的状态转换:通用多线程的生命周期分为5个状态,分别是新建状态、可运行状态、运行状态、阻塞状态、终止状态。
但是Java多线程的生命周期分为6个状态,分别是初始化状态、可运行状态/运行状态、阻塞状态、无时限等待、有时限等待、终止状态。
- Runnable与Blocked状态的转换:只有synchronized场景下会触发这种状态,一段代码同时只能被一个线程访问,而获得synchronized锁的线程又从Blocked状态转到Runnable状态。
- Runnable与Waiting状态的转换:(1).在加了synchronized锁的代码中执行wait()方法。(2).调用无参数的A.join()方法,在B中执行A.joing()方法,会让B进入Waiting状态等待线程A执行完。执行完后B回到Runnable状态。(3).调用LockSupport.park()方法,当前线程会进入Waiting状态;调用LockSupport.unpark(Thread thread)会唤醒thread线程。
- Runnable到Timed_waiting状态的转换:(1).调用Thread.sleep(long millis) 方法;(2).获得synchronized锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;(3).调用Thread.join(long millis) 方法;(4).调用LockSupport.parkNanos(Object blocker, long deadline) 方法;(5).调用LockSupport.parkUntil(long deadline) 方法。
- New到Runnable状态的转换:(1).继承Thread类,重写run方法。(2).实现Runnable接口,重写run方法。然后创建线程对象,调用thread.start()方法将线程由New转换到Runnable状态。
- Runnable到Terminated状态:线程正常结束或者异常结束。 (1).调用stop()和interrupt()方法,可以终止线程。但stop()方法过时了,因为如果线程持有ReentrantLock锁,被stop的线程不会自动调用ReentrantLock的unlock()方法去解锁,而是会直接杀死线程,这会导致其他线程再也无法获得锁了。(2).调用该线程的interrupt()方法时,如果线程处于waiting/timed_wating状态,就会抛出异常throws InterruptedException。当线程 A 处于 RUNNABLE 状态时,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;而阻塞在 java.nio.channels.Selector 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 的 java.nio.channels.Selector 会立即返回。(3).如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,这时就得依赖线程 A 主动检测中断状态了。如果其他线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。
八、多线程状态分析
可以通过 jstack 命令或者Java VisualVM这个可视化工具将 JVM 所有的线程栈信息导出来,完整的线程栈信息不仅包括线程的当前状态、调用栈,还包括了锁的信息。