前言:
以下是看马老师的视频以及自己阅读《Java多线程编程实战指南》所总结的基础内容,只是个人理解,如有不对还请大家指正。
1.线程的概念:
来自于百度百科:线程是独立调度和分派的基本单位。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
2.线程相关概念:
2.1 串行,并行,并发
计算机的任务/事情所耗费总时间是包括:实际做事时间+等待时间,因为有一些任务是需要等待的,如IO任务。假设A总15(实际做事时间5,等待10),B总10(做事2,等待8),C总10(做事10,等待0)。
如下图,串行容易理解,先做A,再做B,再做C,所以15+10+10
并发好处是上个任务等待时间可以进行下个任务开始做事,所以是5+2+10=17
并行就是极致的并发,理想型并发,总时长取决于最长时间,所以是15。
从硬件角度,在一个处理一次只能运行一个线程情况下,处理器是利用时间片分配技术来实现同一个时间段运行多个线程,因此一个处理器可以实现并发,并发这种方式更多是在多核上同一时间运行一个线程来实现。
还有一点,并发不一定会比串行来的效率更高或者效率提高那么明显。
2.2 竞态:
多线程编程中,有时候相同的输入,程序输出有时候正确,有时候错误,计算结果的正确性与时间有关的现象称为竞态。由于竞态常常伴随这脏读,简单的例子就是 多个线程都对某个共享变量进行递增操作,一个线程在写变量,但是这个递增动作还没完成,而另个线程读取的值是之前的值,导致了它递增的动作还是再原来值增加,覆盖了前一个线程的值,导致两次递增的结果是相同的。
竞态最常见的典型实例是在:read-modify-write(读-改-写)和check-then-act(检测后行动)
2.3 安全性
一个类在单线程环境下能够正常运行,并且在多线程环境下,使用方不必为其改变的情况下也能运行正常,那么我们就称为线程安全,对应的这个类是具有线程安全的。反之,单线程运行正常,多线程则无法正常运行,这个类就是非线程安全的。
2.4 原子性
原子字面意思是不可分割,对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该才做就是原子操作,相应的我们称该操作具有原子性。
这边不可分割,是指访问(读、写)某个共享变量的操作从其执行线程以外的任意线程来看,该操作要么已经执行要么尚未发生,其他线程不会“看到”该操作执行了部分中间结果。两种方式来实现原子性,一种就是使用锁,锁具有排他性,通常是软件层面实现,一种是处理器专门CAS(compare-and-Swap)指令,可以认为是一种“硬件锁”。
2.5 可见性
多线程环境下,一个线程对某个共享变量进行更新后,后续访问该变量的线程可能无法立刻读取到这个更新结果,甚至永远也无法读取到这个更新结果。
2.6 有序性和重排序
有序性是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另个处理器上运行的其他线程看起来乱序的。两个操作在代码上是有先后关系的,但是编译器可能改变两个操作的先后顺序,处理器可能不是按照程序的代码指定顺序执行指令,这种现象就是重排序,它是一种内存访问(读和写)的一种优化,不影响单线程正确性提高程序性能。
2.7 锁
这里面涉及了很多概念,原子性,可见性,重排序,那么一个好的多线程编程肯定是要保障原子性+可见性+有序性,很容易联想到就是,一个共享数据一次只能被一个线程访问,锁也是这种机制保障了线程安全的同步机制。
线程可以获得或者释放对应的锁,在这段时间内执行的代码成为“临界区”。锁有内部锁(synchronized关键字)和显式锁(java.concurrent.locks.lock接口实现类)。
锁具有排他性,也就是互斥,所以它满足了代码只能被一个线程执行,自然而然具有不可分割的特性,所以具备了原子性。同时锁的机制,在锁释放时候回冲刷处理器缓存动作,保证锁改变的共享变量能够被其他线程同步,所以保证了可见性。最后,锁能够保证有序性,就是在其他线程看来,临界区里面共享变量是同一时刻更新的,但是有一点需要注意,临界区内的任意两个操作依然可以再临界区之内被重新排序(不会排序到临界区外),这里结合后面可见性例子可以参考一下。
锁还有一个概念,可重入性,一个线程持有一个锁时候,还能继续成功申请该锁,就是可重入的锁,反之就是非可重入的锁。
线程同步机制底层是通过“内存屏障”来实现的,其作用的是保证持有锁线程能够读取到最新数据,并且持有线程对共享数据所做的更改,对后续线程可见,这里原文有很多,就不展开讲。
2.8 volatile关键字
书本上写着volatile关键字的作用是,保障可见性,保障了有序性,保障了long/double型变量读写操作的原子性。马老师课上说的这样,volatile保证了可见性,防止代码重排序。
3.线程启动方式:
java线程启动方式其实是有三种,一种是类继承Thread,一种是类实现Runnable接口,在new并将相应类对象输入Thread对象中,代码是第三种其实是第二种的变形。其实还有一种方式,是线程池的方式,Executors.newCacheThread方式。
public class Thread_base {
private static class T1 extends Thread{
@Override
public void run() {
System.out.println("T1");
}
}
private static class T2 implements Runnable{
@Override
public void run() {
System.out.println("T2");
}
}
public static void main(String[] args) {
new T1().start(); //第一种
new Thread(new T2()).start(); //第二种
new Thread(()->{
System.out.println("hello new");
}).start(); //这种其实是第二种变形
}
}
3.1线程状态的转换:
NEW,一个已创建而为启动线程,由于一个线程实例只能被启动一次,因此一个线程只可能有一个该状态。
RUNNABLE,一个复合状态包括两个子状态,READY和RUNNING。前者表示处于该状态线程可以被线程调度器进行调度而使之处于RUNNING状态,后者表示处于该状态正在运行,即对象的run方法对应指令正在处理器执行。
BLOCKED,一个线程发起一个阻塞式IO操作,或者申请一个由其他线程持有独占资源(锁),处于该状态,BLOCKED状态的线程并不会占用处理器资源。
WAITING,一个线程执行了某个特别方法就会处于这种等待其他线程执行另外一些特定操作的状态。包括Object.wait(),Thread.join()和lockSupport.park(Object),而能从WAITING变更为RUNNABLE的相应方法包括:Object.notify()/notifyALL()和LockSupport.unpart(Object)
TIMED_WAITING:该状态和WAITING类似,差别在于处于该状态的线程并非无限制等待其他线程执行特定操作,而是处于带有时间限制的等待状态。当其他线程没有在指定时间执行线程所期望的特定操作时,该线程的状态自动转换为RUNNABLE.
TERMINATED:已经执行结束的线程处于该状态.Thread.run()正常返回或者由于抛出异常而提前终止都会导致对应线程处于该状态。
一个线程在其整个生命周期中,只可能有一次处于NEW状态和TERMINATED状态。
3.2线程常用方法:
待续
参考文献
《Java多线程编程实战指南-核心篇》
马士兵多线程课程