Java 实现多线程的几种方式
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 实现Callable接口通过FutureTask包装器来创建Thread线程
- 使用ExecutorService、Callable、Future实现有返回结果的线程(线程池方式)
Thread类本质上也是实现了Runnable接口的一个实例,代表一个线程的实例。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
继承Thread类创建线程
public class Demo1 extends Thread{
public Demo1(String name){
this.setName(name);
}
@Override
public void run() {
System.out.println("当前线程名"+Thread.currentThread().getName());
}
public static void main(String[] args) {
new Demo1("MyThread1").start();
new Demo1("MyThread2").start();
}
}
实现Runnable接口创建线程
public class Demo2 implements Runnable{
@Override
public void run() {
System.out.println("当前线程名"+Thread.currentThread().getName());
}
public static void main(String[] args) {
Demo2 runnable = new Demo2();
new Thread(runnable,"MyThread1").start();
new Thread(runnable,"MyThread2").start();
}
}
实现Callable接口通过FutureTask包装器来创建Thread线程
public class Demo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = () -> {
System.out.println("线程任务开始执行了...");
Thread.sleep(1000);
return 0;
};
FutureTask<Integer> task = new FutureTask<>(callable);
new Thread(task).start();
System.out.println("线程启动之后,线程结果返回之前...");
Integer result = task.get();
System.out.println("主线程中拿到异步任务执行的结果为:" + result);
}
}
一个线程的生命周期有哪几种状态?它们之间如何流转的?
NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。
RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态。
BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行。
WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过 join()方法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线程就进入了 RUNNABLE 状态继续运行。
TIMED_WAITING:表示线程进入了一个有时限的等待,如 sleep(3000),等待 3 秒后线程重新进行 RUNNABLE 状态继续运行。
TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到 RUNNABLE 状态
sleep()方法和wait()方法的不同点
- wait()、notify()方法必须写在同步方法中,是为了防止死锁和永久等待,使线程更安全,而sleep()方法不需要有这个限制
- .wait()方法调用后会释放锁sleep()方法调用后不会释放锁。(趣记:抱着锁睡觉)
- sleep()方法必须要指定时间参数;wait()方法可以指定时间参数。
- 两个方法所属类不同,sleep()方法属于Thread类;wait()属于Object类中,放在Object类中是因为Java中每个类都可以是一把锁。
为什么必须写在同步代码块内
// 线程A 的代码
while(!condition){ // 不能使用 if , 因为存在一些特殊情况, 使得线程没有收到 notify 时也能退出等待状态
wait();
}
// do something
// 线程 B 的代码
if(!condition){
// do something ...
condition = true;
notify();
}
现在考虑, 如果wait() 和 notify() 的操作没有相应的同步机制, 则会发生如下情况
- 线程A】 进入了 while 循环后(通过了 !condition 判断条件, 但尚未执行 wait 方法), CPU 时间片耗尽, CPU 开始执行线程B的代码
- 线程B】 执行完毕了 condition = true; notify(); 的操作, 此时【线程A】的 wait() 操作尚未被执行, notify() 操作没有产生任何效果
- 线程A】执行wait() 操作, 进入等待状态,如果没有额外的 notify() 操作, 该线程将持续在 condition = true 的情形下, 持续处于等待状态得不到执行。
由于错误的条件下进行了wait,那么就有可能永远不会被notify到,所以我们需要强制wait/notify在synchronized中
synchronized 中的 4 个优化
锁膨胀
指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级
锁消除
JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
锁粗化
锁粗化是指,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
自适应自旋锁
自旋锁是指通过自身循环,尝试获取锁的一种方式.如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。
synchronized 与ReentrantLock
1.层面:
底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成,对象只有在同步块或同步方法中才能调用wait/notify方法.
ReentrantLock 提供的API层面的锁。
2.实现:
synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁;
ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
3.是否可手动释放:
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;
ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
4.是否可中断
synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;
ReentrantLock则可以中断,可通过设置超时方法或者调用interrupt方法进行中断。
5.是否公平锁
synchronized为非公平锁
ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
6.锁是否可绑定条件Condition
synchronized不能绑定;
ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒.
7.锁的对象
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
为什么不建议使用stop中止线程
1.调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常
2.调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
建议使用:1.使用 interrupt() 中断线程 2.使用 interrupt() 中断线程
线程池的启动流程
- 创建线程池,设置各个参数, 此时没有一个线程
- execute或submit传入一个任务,同时会唤醒休眠的线程。
- 先判断线程数达到最大线程数没有, 如果没有达到就新建一个线程来运行任务
- 如果达到最大线程数,就开始会把任务加入队列中
- 任务队列也加满了,就会执行拒绝策略,
- 当某个线程运行完任务后, 会再次从队列中获取新的任务运行。
- 如果队列中没有任务,线程会休眠,休眠时间是传入的时间
- 某个线程休眠结束后,会再次从任务队列中获取任务,如果任务队列是空的, 则判断当前存活线程数是否大于核心线程数, 如果大于则这个线程就会死亡。
- 如果小于或者等于最小核心线程, 就会继续休眠。
为什么不推荐使用 Executors 来创建线程
4种线程池参数配置不合理 :
- 线程池大小(可缓存线程池INT的最大值)
- 任务队列无界
- 定时任务的线程池(定时任务执行过长,来不及释放就会创建线程,如果前后两个任务存在锁,可能会出现死锁问题)。