1. Java线程的实现与创建方法
- 继承Thread类
- 实现Runnable接口
1.1 继承Thread类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法调用的start0()是一个 native 方法,它将启动一个新线程,并执行 run()方法。
public class ThreadTest extends Thread{
public void run(){
System.out.println("ThreadTest run");
}
public static void main(String[] args){
ThreadTest test=new ThreadTest();
test.run();
}
}
1.2 实现Runnable接口
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。
public class ThreadTest extends Object implements Runnable{
public void run(){
System.out.println("ThreadTest run");
}
public static void main(String[] args){
ThreadTest test=new ThreadTest();
Thread thread =new Thread(test);
thread.start();
}
}
Thread类内部
- start()方法会调用native的start0()方法
带有Runnable类参数的构造器
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
init方法对target成员赋值
this.target = target;
start与run的区别:
调用start()方法会创建一个新的子线程并启动
run()方法只是Thread的一个普通方法调用,依然在本线程进行,并未创建新的线程
1.3 有返回值的线程
由于有些程序的执行子任务的返回值来执行,当将子任务交由子线程处理,需要获取到它们的返回值。实现处理线程的返回值有以下3种方法:
public class MyRunnable implements Runnable {
private String name;
public void run() throws InterruptedException{
try{
Thread.sleep(5000);
}catch(InterruptException e){
e.printStackTrace();
}
name = "we hav data now";
}
public void setName(String name) {
this.name = name;
}
}
1.3.1 主线程等待法
让主线程进行等待,当子线程执行完获得返回之后再进行执行
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
// 获取子线程的返回值:主线程等待法
while (myRunnable.getName() == null){
Thread.sleep(500);
}
System.out.println(myRunnable.getName());
}
1.3.2 join()阻塞当前进程
使用join()方法进行线程的阻塞,实现更简单,但细度不够细
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
// 获取子线程的返回值:Thread的join方法来阻塞主线程,直到子线程返回
thread.join();
System.out.println(myRunnable.getName());
}
1.3.3(Callable 、ExecutorService、FutureTask)
有返回值的任务实现 ,可以通过Callable 接口与FutureTask或者ExecutorService共同使用来达成
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String>{
public String call() throws Exception {
String value="test";
System.out.println("Ready to work");
Thread.currentThread();
Thread.sleep(5000);
System.out.println("task done");
return value;
}
}
- 通过FutureTask
FutureTask实现接口RunnableFuture,而RunnableFuture继承了Runnable与Future
FutureTask的构造方法可以接收Callable实例,其中isDone()方法通过Callable中的call()的执行确定是否执行完成。同时FutureTask类中get()方法用来阻塞调用它的线程直到Callable实例中的call()方法执行完毕能取到返回值为止
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class FutureTaskDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<String> task=new FutureTask<String>(new MyCallable());
new Thread(task).start();
if(!task.isDone()){
System.out.println("task has not finished,please wait!");
}
System.out.println("task return :"+task.get());
}
}
- 通过ExecutorService
使用线程池的好处是我们可以提交多个实现Callable方法去让线程池并发地去处理结果,方便对这些Callable类的执行方式做统一的管理
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
Future<String> future = newCachedThreadPool.submit(new MyCallable());
if (!future.isDone()) {
System.out.println("task has not finished,please wait!");
}
try {
System.out.println("task return :" + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
finally{
newCachedThreadPool.shutdown();
}
}
}
1.4 给run()方法传参
1.4.1 构造函数传参
1.4.2 成员函数传参
2.4.3 回调函数传参
2. 线程的状态
Java线程有六个状态:
- 新建(new):线程创建后尚未启动的线程状态
- 运行(Runnable):包含Running与Ready,此状态的线程有可能正在执行,也有可能正在等待CPU分配资源
- 无限期等待(Waiting):不会被分配CPU执行时间,需要显式被唤醒
造成原因:
- 调用了没有设置TimeOut参数的Object.wait()方法
- 调用了没有设置TimeOut参数的Thread.wait()方法
- 调用了LockSupport.park()方法
- 限期等待(Timed Waiting):不会被分配CPU执行时间,在一定时间后系统会自动唤醒
造成原因:
- 调用了设置TimeOut参数的Object.wait()方法
- 调用了设置TimeOut参数的Thread.wait()方法
- 调用了LockSupport.parkNanos()方法
- 调用了LockSupport.parkUntil()方法
- 调用了Thread.sleep()方法
- 阻塞(Blocked):等待获取排他锁
- 结束状态(Terminated):已终止线程的状态,线程已经结束执行
3. 线程状态间的转换
3.1 sleep()与wait()
- sleep()是Thread类方法;wait()是Object类中的方法
- sleep()可以在任何地方使用;wait()方法只能再synchronized方法或synchronized块中使用
- Thread.sleep只会让出CPU,不会导致锁行为的改变;Object.wait不仅会让出CPU,还会释放已经占有的同步进程锁
public class WaitSleepDemo {
public static void main(String[] args) {
final Object lock = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread A is waiting to get lock");
//对A上lock锁
synchronized (lock){
try {
System.out.println("thread A get lock");
Thread.sleep(20);//模拟运行
System.out.println("thread A do wait method");
lock.wait(1000);
System.out.println("thread A is done");
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
try{
Thread.sleep(10);//使得A进程先开始
} catch (InterruptedException e){
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread B is waiting to get lock");
//对b上lock锁
synchronized (lock){
try {
System.out.println("thread B get lock");
System.out.println("thread B is sleeping 10 ms");
Thread.sleep(10);
System.out.println("thread B is done");
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}
}
用上述代码证明第三条区别
按推理:
- 线程A先执行输出"thread A is waiting to get lock"并且对线程A上锁并执行输出"thread A get lock"
- 由于接下来A sleep 20ms ,并且A,B启动只差10ms,所以接下来B线程输出"thread B is waiting to get lock"。但此时A并未释放锁,因此线程B并未获得锁
- A sleep 结束,继续执行,输出"thread A do wait method",并且进行wait,释放锁与CPU
- B执行整个锁块中的内容,最后A重新获得锁与CPU,输出最后一句
3.2 notify()与notifyAll()
- 锁池(EntryList):假设线程A已经拥有了某个对象(不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池(WaitSet):假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。
- notifyAll会让所有处于等待池中的线程全部进入到锁池去竞争获取锁的机会
- notify只会随机选取一个位于等待池中的线程进入到锁池去竞争获取锁的机会
3.3 yield
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但线程调度器可能会忽略这个暗示
yield() 不会使线程让出当前锁
3.4 interrupt
通知线程应该中断了,需要被调用的线程配合中断
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出异常
- 如果线程处于正常活动状态,那么该线程的中断标志设置为True。但被设置线程将继续正常运行,不受影响
4. 线程安全问题
4.1线程安全主要诱因
- 存在共享数据(临界资源)
- 存在多条线程共同操作这些共享数据
4.1 解决问题的根本方法
同一时刻有且仅有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
4.2互斥锁
4.2.1互斥锁的特性
- 互斥性(原子性):即同一时间只允许一个线程持有对象锁,通过这种特性来实现多线程的协调机制,在同一时间只有一个线程可以对代码块进行操作
- 可见性:必须保证在锁释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的,否则另一个线程可能是在本地缓存的副本上进行继续操作,从而引起不一致
4.2.2 synchronized
4.2.2.1 使用与基础
锁的不是代码,是对象
获取对象锁的两种方法:
- 同步代码块(synchronized(类实例对象))
- 同步非静态方法,锁适当前对象的实例对象
获得类锁的两种方法:
- 同步代码块(synchronized(类.class))
- 同步静态方法,锁是当前对象的类对象
对象锁和类锁的总结
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象非同步代码
- 若锁住同一个对象,访问同步代码块时,另一个线程访问同步代码块会被阻塞
- 若锁住同一个对象,访问同步方法时,另一个线程访问同步方法会被阻塞
- 若锁住同一个对象,访问同步方法时,另一个线程访问同步代码块会被阻塞,反之亦然
- 类锁是一种特殊的对象锁
- 类锁和对象锁互不干扰
4.2.2.2 synchronized 和 ReentrantLock
ReentrantLock(再入锁):
- 位于java.util.concurrent.locks包
- 和CountDownLatch,FutureTask,Semaphore一样基于AQS实现
- 能够实现比synchronized更细粒度的控制,例如fairness
- lock()后必须调用unlock()释放锁
- 性能未必比synchronized高,同样可重入
ReentrantLock公平性设置
ReentrantLock fairLock=new ReentrantLock(true);
参数为true,倾向于将锁赋予等待时间最久的线程
公平锁:获取锁的顺序按先后调用lock的方法的顺序
非公平锁:抢占的顺序不一定,看运气,synchronized为非公平锁
4.2.2.3 synchronized底层实现原理
实现synchronized的基础
Java对象头
对象在内存中的布局:
对象头
实例填充:
对其填充:
Monitor:每个对象天生自带了一把锁
5. Java内存模型JMM
Java内存模型本身是一种抽象概念,并不是真实存在,它描述的是一组规范或规则,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM与Java内存区域划分
JMM描述的是一组规则,围绕原子性,有序性,可见性展开
相似点:存在共享区域和私有区域
5.1 JMM中的主内存
- 存储Java实例对象
- 包括成员变量,类信息,常量,静态变量等
- 属于数据共享区域,多线程并发操作会造成线程安全问题
5.2 JMM中的工作内存
- 存储当前方法的所有本地信息变量,本地变量对其他线程不可见
- 字节码行号指示器,Native方法信息
- 属于线程私有区域,不存在线程安全问题
5.3 主内存与工作内存的数据存储类型以及操作方式归纳
- 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构
- 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
- 成员变量,static变量,类信息均会存储在主内存中
- 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新会主内存
5.4 指令重排序
指令重排序需要满足条件:
- 在单线程环境下不能改变程序运行结果
- 存在数据依赖关系的不允许重排序
即无法用happens-before 原则推导出来的,才能进行指令重排序
A操作的结果需要对B操作可见,则A与B存在happens-before的关系
5.5 happens-before
八大原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;(此处后面指时间的先后)
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;(此处后面指时间的先后)
- 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
5.6 volatile
JVM提供的轻量级的同步机制
- 保证被volatile 修饰的共享变量对所有线程是可见的
- 禁止指令的重排序优化
volatile变量为何立即可见?
当写一个volatile变量时,JMM会把会把该线程的工作内存中的共享变量值刷新到主内存中
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效
volatile如何禁止重排优化
内存屏障(Memory Barrier):
1.保证特定操作的执行顺序
2. 保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行局重排序优化
强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
6. Java线程池
使用线程池的原因:
- 降低资源消耗
- 提高线程可管理性
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
6.1 线程复用
每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。
6.2 线程池组成
一般的线程池主要分为以下 4 个组成部分:
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
ThreadPoolExecutor 的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize:指定了线程池中的线程数量。
- maximumPoolSize:指定了线程池中的最大线程数量。
- keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多 次时间内会被销毁。
- unit:keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务
6.3 拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
- AbortPolicy : 直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
- DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
- DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
6.4 Java 线程池工作过程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。