盼望一件事会发生的人祈祷;相信一件事将发生的人专注;让一件事能发生的人行动
Java进程和线程的关系
- Java对操作系统提供的功能进行封装,包括进程和线程
- 运行一个Java程序会产生一个进程,一个进程包含至少一个线程
- 每一个Java进程对应一个JVM实例,而每一个JVM实例会唯一对应一个堆。每个线程都有自己私有的栈
- Java采用单线程编程模型,如果程序不声明创建线程,程序会自动创建主线程——进程就好像投资者,线程才是真正干活的人。
- 当Java程序启动时,主线程立刻运行。主线程可以创建子线程,原则上要后于子线程完成执行。
因为主要干活的都是线程,所以后面分析都以线程为主,进程为辅。
Thread中的start和run方法的区别
用run()方法会沿用主线程执行方法,用start()会新创建一个子线程。
如下图所示:
总结来是:
- 调用start()方法会创建一个新的子线程并启动
- run()方法只是Thread的一个普通方法的调用
如果结合源码,会发现调用start的时候,会调用到thread_entry这个方法,会新创建内容。如下图所示:
Thread和Runnable的关系
- Thread是一个类,而Runnable是一个接口。具体来说,Thread是实现了Runnable接口的类,使得run支持多线程。
- 由于Java类的单一继承原则,推荐多使用Runnable接口
Thread是一个类:
Runnable是一个接口:
实际上Runnable里面并没有多线程的特性,而是依赖实现它的Thread去调用start()方法来创建新线程的,然后再在这个子线程里面调用Thread实现好的run方法来执行相应的业务逻辑。
如何实现处理线程的返回值
如何给run()传参
一般来说和线程相关的业务逻辑需要放在run()里面去执行。但是既然run()没有参数,那么如何给run()传参数呢?
主要有三种方法:
- 构造函数传参
- 成员变量传参——用setName之类的方法给成员变量赋值
- 回调函数传参
实现处理线程的返回值是痛点
这里考查的是有没有活用线程相关的知识。
实现方式也是有三种:
- 主线程等待法——让主线程循环等待,直到目标子线程返回值为止。缺点是需要自己手动实现等待的逻辑。
代码实例:
一开始按照原先的多线程的案例去尝试打印,发现只让线程sleep()一下,我们不能精准控制等到start()子任务执行完返回结果的时候才执行下一条语句。
如下图所示:
在这个例子中,主线程在执行完t.start()之后,没有等run()里面sleep()执行完,而是直接往下走去执行打印的语句了,所以最后打印出来的结果,cw.value是null。
那如何让其等到子线程的返回值再执行打印呢?——我们可以用主线程等待法。
如下图所示:
但是因为需要自己手动实现这个等待的逻辑,所以当需要等待的变量很多的时候,代码会变得臃肿。更致命的是,具体要等待多久比较难把握,比较难有精准的控制。
- 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
如下图所示:
只需要加一行t.join()
代码即可成功等待,并且也不需要让线程去等待了。
join()等待法可以做到比主线程等待法更精准的控制,实现起来也更简单。
但是缺点是粒度不够细。
- 通过Callable接口实现:通过FutureTask 或 线程池获取
在JDK5之前,线程是没有返回值的。
线程的状态
按照官方的说法,线程的状态一共有如下图所示的留个状态:
具体来说:
- 新建(New):创建后尚未启动的线程的状态。刚创建的线程,如果还没有调用start()方法,那么这个线程会处于New状态。
- 运行(Runnable):包含Running和Ready。处于此状态的进程可能正在执行,也可能正在等待CPU为它分配执行时间。
- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
让线程进入无限期等待有三种方法:
- 限期等待(Timed Waiting):在一定时间后会由系统自动唤醒。处于这种状态的进程也不会被CPU分配执行时间,但是不用显示唤醒,过一段时间后系统会自动唤醒它们。
让线程进入限期等待主要有六种方法:
- 阻塞(Blocked):等待获取排它锁。
阻塞状态和等待状态的区别是,阻塞状态在等待获取一个排它锁(这会在另一个线程放弃这个锁的时候发生)。而等待状态就是等时间或者被唤醒,不用等其他线程的锁。
比如说,当某个线程进入到了synchronized修饰的方法或者代码块,即获取锁去执行的时候,其他想进入此方法或者代码块的线程就只能等待,他们的状态就都是blocked
- 结束(Terminated):已终止线程的状态,线程已经结束执行。
当线程的run()或者主线程的main()完成时,我们就认为它终止了。这个线程对象虽然可能还是活的,但是它已经不是一个单独执行的线程了。
线程只要终止了,就不能再复生。在一个终止的线程上调用start()会抛出java.lang.IllegalThreadStateException
这个异常。
程序举例:
在关闭了线程之后再次启动,也无法执行。
sleep和wait的区别
基本的差别
- sleep是Thread类的方法,wait是Object类中定义的方法
- sleep()方法可以在任何地方使用
- wait()方法只能在synchronized方法或synchronized块中使用
最主要的本职区别
- Thread.sleep只会让出CPU,不会导致锁行为的改变
- Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
synchronized
线程安全问题的主要诱因
- 存在共享数据(也称临界资源)
- 存在多条线程共同操作这些共享数据
解决问题的根本方法
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
互斥锁的特性
互斥锁可以达到互斥访问的目的。简单来说就是某个共享数据如果在某时刻正在被一个线程访问,如果数据被当前访问的数据加了互斥锁,那么在同一时刻其他线程只能处于等待状态,直到当前线程处理完释放掉该锁。
互斥锁有两种特性:
- 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(符合操作)进行访问。互斥性也称为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
- synchronized锁的不是代码,而是对象。
之前已经知道了,JVM的堆存储空间是线程间共享的,所以恰当地、合理地给一个线程上锁,是解决线程安全问题的关键。
根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法:
- 同步代码块(synchronized(this), synchronized(类实例对象)),锁是小括号()中的实例对象。
- 同步非静态方法(synchronized method),锁是当前对象的实例对象
获取类锁的两种用法
- 同步代码块(synchronized (类.class)),锁是小括号()中的类对象(Class对象)。
- 同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)。
对象锁和类锁的总结
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
- 同一个类的不同对象的对象锁互不干扰;
- 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
- 类锁和对象锁互不干扰
synchronized底层实现原理
- Java对象头
- Monitor
synchronized和ReentrantLock的区别
Java5之前只有synchronized,Java5之后开始提供ReentrantLock(再入锁)。ReentrantLock的语义和synchronized基本相同
ReentrantLock位于java.util.concurrent.locks
包下,也就是业界著名的”JUC”。
ReentrantLock和CountDownLatch、FutureTask、Semaphore一样都是基于AQS实现的。