本文由 Oo鲁毅oO 首发于 掘金,下方为原文链接
多线程|深入浅出线程核心知识
1.多线程简介
1.1 什么是线程
线程是操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程。你可以通过使用多线程对运算密集的任务提速。比如,如果一个线程完成一个任务需要100毫秒,那么用十个线程完成此任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,他是一个很好的卖点。
1.2 进程与线程的区别
- 进程是资源分配的基本单位,线程是调度和执行的基本单位
- 它们的关系是包含的,一个进程中包含多个线程
- 当操作系统分配资源时会为进程分配资源不会为线程分配资源,线程使用的资源是所在进程的资源
- 线程间切换的资源开销比较小,而进程之间切换资源开销比较大。
1.3 为什么要使用多线程
当一个网站遇到并发量很大的问题时,普通的系统很快就会达到性能瓶颈,而使用多线程可以轻松的解决性能问题。
2.线程的实现方式有几种?
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started. For example, a thread that computes primes larger than a stated value could be written as follows
The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started. The same example in this other style looks like the following
上面的两段引用出自Oracle官方文档,将这两段英文翻译过来意思就是说实现线程的方式有两个种,第一种是继承Thread
类的方式,另一种就是实现Runnable
接口。
2.1 实现多线程
继承Thread类实现多线程
/**
* 用Thread方式实现线程
*/
public class ThreadStyle extends Thread {
public static void main(String[] args) {
ThreadStyle thread = new ThreadStyle();
thread.start();
}
@Override
public void run() {
System.out.println("用Thread类实现线程");
}
}
实现Runnable接口实现多线程
/**
* Runnable方式创建线程
*/
public class RunnableStyle implements Runnable {
public static void main(String[] args) {
Thread thread = new Thread(new RunnableStyle());
thread.start();
}
@Override
public void run() {
System.out.println("用Runnable方式实现线程");
}
}
2.2 多线程实现方式对比
- 实现
Runnable
接口方式可以降低代码耦合度 - 继承
Thread
类后就无法再继承别的类,降低了类的扩展性 - 如果使用
Thread
类需要每实现一个线程类就进行一次创建,造成了较大的资源开销。
总结:综上所述,实现线程采用Runnable
接口的方式比较好。
2.3 同时用两种方式会是怎样
/**
* 同时使用Runnable和Thread两种方式实现多线程
*/
public class BothRunnableThread {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我来自Runnable");
}
}) {
@Override
public void run() {
System.out.println("我来自Thread");
}
}.start();
}
}
运行结果
分析:
实现Runnable接口实现多线程的方式
@Override
public void run() {
if (target != null) {
target.run();
}
}
实现Runnable
接口方式会要求重写run()
方法,所以会执行其中的三行代码,其中target
是一个private Runnable target;
,因为后面又覆盖了一次Thread类的run
方法,所以if
判断也就消失了就会直接执行自己在run
方法中的打印语句。
总结:创建线程的方式只有构造Thread
类一种方法,但是实现Thread
类中的run
方法有两种方式。
3.线程的启动
3.1 启动线程的正确方式
/**
* 对比start和run这两种启动线程的方式
*/
public class StartAndRunMethod {
public static void main(String[] args) {
Runnable runnable = () ->{
System.out.println(Thread.currentThread().getName());
};
runnable.run();
new Thread(runnable).start();
}
}
运行结果
总结:启动线程需要使用start()
方法
3.2 start()方法原理解读
- start()方法含义
调用
start()
方法意味着向JVM发起通知,如果有空可以来我这里执行一下么,本质也就是通过调用start()
方法请求JVM运行此线程,但是调用该方法之后不一定就会立即运行,而是需要等到JVM有空执行时才会执行。
- start()方法源码解析
public synchronized void start() {
//进行线程状态的检查,默认值是0
if (threadStatus != 0)
throw new IllegalThreadStateException();
//加入线程组
group.add(this);
boolean started = false;
try {
//执行线程的方法,是一个native方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
总结:执行start()
要经历的步骤
- 检查线程状态
- 加入线程组
- 执行
start0()
方法
3.3 run()方法原理解读
@Override
public void run() {
//判断传入的Runnable是否为空
if (target != null) {
//不为空则启动
target.run();
}
}
从这里可以看出上面直接调用run()
方法为什么会在主线程中执行,这是因为target
是空的所以不会启动,这样就和调用一个普通的方法没有区别了。
4.线程的停止
4.1 线程停止的原则
在Java中停止线程的最好方式是使用interrupt,但是这样仅仅会对需要停止的线程进行通知而不是直接停掉,线程是否的停止的权利属于需要被停止的线程(何时停止以及是否停止),这就需要请求停止方和被停止方都遵循一种编码规范。
4.2 线程通常会在什么情况下停止
run()
方法中的代码运行完毕(最常见)- 有异常出现但是没有进行捕获
4.3 正确的停止方法——使用interrupt
- 在普通情况下停止
/**
* run方法内没有sleep或wait方法时,停止线程
*/
public class RightWayStopThreadWithoutSleep implements Runnable {
@Override
public void run() {
int num = 0;
//没有收到通知时进行循环操作
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2){
if (num % 10000 == 0){
System.out.println(num + "是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(2000);
//发起通知
thread.interrupt();
}
}
- 如果线程阻塞如何停止
/**
* 带有sleep的中断线程的停止方法
*/
public class RightWayStopThreadWhthSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () ->{
int num = 0;
try {
while (num <= 300 && !Thread.currentThread().isInterrupted()){
if (num % 100 == 0){
System.out.println(num + "是100的倍数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
- 线程每次迭代后都阻塞如何停止
/**
* 如果在每次循环中都会sleep或wait,需要如何停止线程
*/
public class RightWayStopThreadWithSleepEveryLoop implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithSleepEveryLoop());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
@Override
public void run() {
try {
int num = 0;
while (num <= 30){
if (num % 10 == 0){
System.out.println(num + "是10的倍数");
}
num++;
Thread.sleep(50);
}
System.out.println("任务完成了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意:如果每次迭代都有阻塞状态,这样就不需要判断是否收到中断请求,因为在sleep
过程中会对中断进行响应
while
内try/catch
的问题
/**
* 如果while里面放try/catch,会导致中断失效
*/
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () ->{
int num = 0;
while (num <= 10000){
if (num % 100 == 0 && !Thread.currentThread().isInterrupted()){
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
注意:在while
循环中加入try/catch
无法停止线程,因为在try/catch
中异常被捕获后还是不满足跳出循环的条件,interrupt
标记位被清除也就无法检查到被中断的迹象,所以会继续执行线程
4.4 实际开发中停止的两种最佳实践
原则:
- 优先选择:传递中断
- 不想或无法传递:恢复中断
- 不应屏蔽中断
抛出式
/**
* 最佳实践:catch住InterruptedException后优先选择在方法签名中抛出异常,
* 那么在run()方法中就会强制try/catch
*/
public class RightWayStopThreadInProd implements Runnable {
@Override
public void run() {
try {
while (true){
System.out.println("go");
throwInMethod();
}
} catch (InterruptedException e) {
//保存日志
//停止程序
System.out.println("保存日志、停止程序");
e.printStackTrace();
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
注意:在方法中遇到异常应该首先选择抛出,异常由run
方法进行处理,这样可以增加代码的健壮性
恢复中断式
/**
* 最佳实践2:在catch语句中调用Thread.currentThread.interrupt()
* 来恢复中断状态,以便于在后续的执行中依然能够检查到刚才发生了中断
*/
public class RightWayStopThreadInProd2 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()){
System.out.println("Interrupted,程序运行结束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
注意:如果在调用的方法中不抛出异常的话也可以在catch块再次调用Thread.currentThread().interrupt();
,这样可以重新设置中断表示已经有中断发生,从而让run
方法感知
4.5 响应中断的方法列表总结
- Object.wait()/wait(long)/wait(long,int)
- Thread.sleep(long)/sleep(long,int)
- Thread.join()/join(long)/join(long,int)
- java.util.concurrent.BlockingQueue.take()/put(E)
- java.util.concurrent.locks.Lock.lockInterruptibly()
- java.util.concurrent.CountDownLatch.await()
- java.util.concurrent.CyclicBarrier.await()
- java.util.concurrent.Exchanger.exchange(V)
- java.nio.channels.InterruptibleChannel相关方法
- java.nio.channels.Selector的相关方法
4.6 错误的停止线程方法
- 使用
stop
方法停止
/**
* 错误的停止方法:用stop停止线程,会导致线程运行一半突然停止,这样没有办法完成一个基本单位(一个连队)的操作,
* 会造成脏数据(有的连队多领取或少领取装备)
*/
public class StopThread implements Runnable {
@Override
public void run() {
/**
* 模拟指挥军队:一个5个连队,每个连队10人,以连队为单位发放弹药,叫到号
* 的士兵去领取
*/
for (int i = 0; i < 5; i++) {
System.out.println("连队" + i + "开始领取装备");
for (int j = 0; j < 10; j++) {
System.out.println(j);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队" + i + "领取完毕");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
//1s之后战争爆发需要奔赴战场
Thread.sleep(1000);
//停止领取
thread.stop();
}
}
volatile
设置boolean
标记位
/**
* 演示用volatile的局限part2 陷入阻塞时volatile无法停止
* 此例中生产者的生产速度很快,但是消费者的消费速度很慢,所以阻塞队列满了以后,
* 生产者会阻塞,生产者会等待消费者进一步消费
*/
public class WrongWayVolatileCantStop {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> storage = new ArrayBlockingQueue<>(10);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()){
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
//一旦消费者不需要更多数据了,我们应当让消费者也停下来,但是实际情况。。。
producer.canceled = true;
System.out.println(producer.canceled);
}
}
class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue<Integer> storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
//是100的倍数时,将num放入阻塞队列
storage.put(num);
System.out.println(num + "是100的倍数,被放入阻塞队列");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者停止运行");
}
}
}
class Consumer {
BlockingQueue<Integer> storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
注意:如果线程长时间阻塞,这种方法就会失效
对上面方式的修复
/**
* 用中断修复刚才一直等待的问题
*/
public class WrongWayVolatileFixed {
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixed body = new WrongWayVolatileFixed();
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = body.new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producerThread.interrupt();
}
class Producer implements Runnable {
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
//如果num是100的倍数,就将他添加到阻塞队列
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放入阻塞队列");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者线程阻塞");
}
}
}
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
}
4.7 无法响应中断时如何停止线程
答:需要根据情况的不同而采取不同的方法
如果线程阻塞是因为调用了sleep()、wait()或join()
导致的,可以通过抛出InterruptException
异常来唤醒,但是不能响应InterruptException
异常则无法通过这种方法进行唤醒。
但是我们可以利用其他可以响应中断的方法,比如:
ReentrantLock.lockInterruptibly()
,关闭套接字使线程立即返回等方法来达到目的。
所以如何处理不可中断的阻塞要视情况而定。
5.线程的生命周期
5.1 线程的6种状态及转化路径
- New(新生):已创建但是还没有启动的新线程,例如:线程创建出来还没有执行
start()
方法 - Runnable(可运行):一旦从New调用了
start()
方法就会进入Runnable状态。 - Blocked(阻塞):当线程进入到被
synchronized
修饰的代码后,并且该锁已经被其它线程拿走了就会进入阻塞状态。 - Waiting(等待):没有设置
timeout
参数的wait()
方法,需要等待唤醒否则不会醒来 - Timed Waiting(计时等待):为等待设置了时间,如果在时间结束前进行了唤醒,线程可以苏醒,如果不进行唤醒,等到时间结束也会自动苏醒。
- Terminated(死亡):线程执行完成或者
run()
方法被意外终止。
注意:一般而言把Blocked(被阻塞)、Waiting(等待)、Timed_Waiting(计时等待)
都称为阻塞,而不仅仅是Blocked
。
6.Thread类与Object类线程相关的方法
6.1 wait()、notify()、notifyAll()方法的使用
作用:wait()
方法会让线程进入等待状态,如果想让线程继续执行必须满足一下四种方式中的一种
- 调用
notify()
方法,本线程正好被唤醒 - 调用
notifyAll()
方法,所有线程都会被唤醒 - 设置的
wait(long timout)
达到了参数的时间,如果传入0会进入永久等待 - 调用
interrupt()
方法进行唤醒
/**
* 展示wait和notify的基本用法
* 1.研究代码执行顺序
* 2.证明wait是释放锁的
*/
public class Wait {
public static Object object = new Object();
static class Thread1 extends Thread{
@Override
public void run() {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"开始执行");
try {
object.wait(); //等待期间如果遇到中断会抛出InterruptedException
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"获取到了锁");
}
}
}
static class Thread2 extends Thread{
@Override
public void run() {
synchronized (object){
object.notify();
System.out.println("线程"+Thread.currentThread().getName()+"调用了notify()");
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread1().start();
Thread.sleep(200);
new Thread2().start();
}
}
总结:wait()方法会释放对象锁,只有释放了对象锁其他线程才可以进入synchronized
代码块
/**
* 3个线程,线程1和线程2首先被阻塞,线程3去唤醒线程1和线程2
* start先执行不代表线程先启动
*/
public class WaitNotifyAll implements Runnable {
private static final Object resourceA = new Object();
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+"得到对象锁");
try {
System.out.println(Thread.currentThread().getName()+"等待下一次开始");
resourceA.wait();
System.out.println(Thread.currentThread().getName()+"马上运行结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
WaitNotifyAll waitNotifyAll = new WaitNotifyAll();
Thread threadA = new Thread(waitNotifyAll);
Thread threadB = new Thread(waitNotifyAll);
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
resourceA.notifyAll();
System.out.println("线程C已经成功notify了");
}
}
});
threadA.start();
threadB.start();
Thread.sleep(200);
threadC.start();
}
}
总结:notify
只会唤醒等待线程中的一个而notifyAll
则会唤醒所有等待线程,在线程启动时一定要等到线程进入等待状态之后再进行唤醒
/**
* 证明wait只释放当前那把锁
*/
public class WaitNotifyReleaseOwnMonitor {
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("ThreadA got resourceA lock.");
synchronized (resourceB) {
System.out.println("ThreadA got resourceB lock.");
try {
System.out.println("ThreadA releases resourceA lock.");
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA){
System.out.println("ThreadB got resourceA lock.");
System.out.println("ThreadB tries to ResourceB lock.");
synchronized (resourceB){
System.out.println("ThreadB got resourceB lock.");
}
}
}
});
thread1.start();
thread2.start();
}
}
总结:wait()
只释放当前monitor
6.2 wait方法的原理
最初线程都会在入口集中,当线程进入到synchronized
时就会获取到对象锁,如果执行了wait
方法后就会进入到等待集,在等待集中如果对某个线程执行了notify
操作它就会再次回到入口集,如果使用的是notifyAll
那么等待集中的全部线程都会进入到入口集。
6.3 使用wait方法实现生产者消费者模式
/**
* 用wait和notify来实现
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
Thread thread1 = new Thread(producer);
Thread thread2 = new Thread(consumer);
thread1.start();
thread2.start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("生产出了一个商品,仓库中有:" + storage.size() + "个商品");
notify();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("取走了:" + storage.poll() + ",还剩下" + storage.size() + "个商品");
notify();
}
}
总结:生产者生产到10的时候就会进入wait
状态,否则就会进行生产,消费者将队列中的商品消费到0时就会进入wait
状态,就这就会消费,当生产者生产出了商品就会notify
消费者,将其唤醒。反之消费者就会notify
唤醒生产者
6.4 交替打印100以内的奇偶数
- synchronized方式
/**
* 两个线程交替打印0到100奇偶数
*/
public class WaitNotifyPrintOddEvenSyn {
private static int count;
private static final Object lock = new Object();
//新建2个线程
//1个只处理偶数,第2个处理奇数(用位运算)
//用synchronized进行通信
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100){
synchronized (lock){
if ((count & 1) == 0){
System.out.println(Thread.currentThread().getName() + ":" + count);
count++;
}
}
}
}
},"偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100){
synchronized (lock){
if ((count & 1) != 0){
System.out.println(Thread.currentThread().getName() + ":" + count);
count++;
}
}
}
}
},"奇数").start();
}
}
总结:这样实现有可能造成同一个线程总是拿到对象锁,但是if
判断只会进一次,这样就影响了效率,可以使用wait/notify方式解决
- wait/notify方法
/**
* 两个线程交替打印0到100的奇偶数,使用wait/notify
*/
public class WaitNotifyPrintOddEvenWait {
public static void main(String[] args) throws InterruptedException {
new Thread(new TurningRunner(), "偶数").start();
Thread.sleep(100);
new Thread(new TurningRunner(), "奇数").start();
}
//1.一旦拿到锁就打印
//2.打印完,唤醒其他线程,然后再休眠
static class TurningRunner implements Runnable{
private static int count;
private static Object lock = new Object();
@Override
public void run() {
while (count < 100){
synchronized (lock){
//拿到锁就打印
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count < 100){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
总结:拿到锁就打印,打印完就唤醒如果满足条件就等待,这样可以提高程序的效率
6.5 sleep方法详解
作用:让线程进入阻塞状态,睡眠时不会占用cpu资源。
注意:sleep
方法不会释放synchronized
和lock
锁
- 使用synchronized演示
/**
* 展示线程sleep的时候不释放synchronized的monitor,
* 等sleep的时间到了以后,正常结束后才会释放锁
*/
public class SleepDontReleaseMonitor implements Runnable {
public static void main(String[] args) {
SleepDontReleaseMonitor sleepDontReleaseMonitor = new SleepDontReleaseMonitor();
new Thread(sleepDontReleaseMonitor).start();
new Thread(sleepDontReleaseMonitor).start();
}
@Override
public void run() {
syn();
}
private synchronized void syn() {
System.out.println("线程" + Thread.currentThread().getName() + "获取到了monitor.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "退出了同步代码块.");
}
}
- 使用ReentrantLock
/**
* 演示sleep不释放lock(lock本身也需要手动释放)
*/
public class SleepDontReleaseLock implements Runnable {
private static final Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
System.out.println("线程" +Thread.currentThread().getName()+ "获取到了锁");
try {
Thread.sleep(5000);
System.out.println("线程" +Thread.currentThread().getName()+ "睡眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
new Thread(sleepDontReleaseLock).start();
new Thread(sleepDontReleaseLock).start();
}
}
6.6 对比wait和sleep方法的异同
相同:
- wait和sleep都会让线程进入阻塞状态
- wait和sleep都可以响应中断
不同:
- 使用位置不同,wait只能在synchronized代码块中使用,sleep不需要
- 所属的类不同,wait属于Object类,sleep属于Thread类
- wait可以释放对象锁,sleep不会释放对象锁
- 阻塞时间长短有差异,wait如果不传入参数就会永久等待直到对其进行唤醒,sleep只需要等到参数时间到了就会苏醒
6.7 join方法学习
作用:因为新的线程加入了我们,所以我们要等他执行完再出发
用法:main等待thread1执行完毕,主线程等待子线程。
- 普通用法
/**
* 演示join用法,注意语句输出顺序是否会变化
*/
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "已经执行完毕");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "已经执行完毕");
}
});
thread1.start();
thread2.start();
System.out.println("开始等待子线程运行完毕");
// thread1.join();
// thread2.join();
System.out.println("所有子线程执行完毕");
}
}
- 在join期间遇到中断
/**
* 演示join期间被中断的效果
*/
public class JoinInterrupt {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
mainThread.interrupt();
Thread.sleep(5000);
System.out.println("Thread1 finished.");
} catch (InterruptedException e) {
System.out.println("子线程中断");
}
}
});
thread1.start();
System.out.println("等待子线程运行完毕");
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "主线程被中断了");
thread1.interrupt();
}
System.out.println("子线程已经运行完毕");
}
}
- join原理分析
join源码
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
当调动join
方法默认传入的millis
是0,此时会进入永久等待,但是由于JVM的原因在Thread
类中每个方法结束都会有一个notify
操作,所以join
才不需要手动进行唤醒。
因为join
方法的底层是wait
所以使用如下代码可以与join
等价
synchronized (thread1){
thread1.wait();
}
为Thread
类的对象加锁,在结束时会自动进行释放,所以可以达到join
的效果。
6.8 yield方法详解
作用:释放我的CPU时间片,但不会释放锁也不会进入阻塞。
定位:JVM不保证遵循yield
- yield和sleep的区别
sleep期间线程进入阻塞状态所以不会再被调度。而yield只是暂时作出让步但还可以处于竞争状态。
7.线程各种属性
7.1 线程id
线程id是不可修改的,主线程的id从1开始
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
线程的id是先++后返回,所以主线程的id为1
查看子线程的id
/**
* Id从1开始,JVM运行起来之后,我们自己创建的线程Id早已不是0
*/
public class Id {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println("主线程的ID:" + Thread.currentThread().getId());
System.out.println("子线程的ID:" + thread.getId());
}
}
会发现子线程的id为11,这是为什么呢?
通过debug的方式可以发现,main线程启动后会创建很多线程,所以执行到子线程时id已经不会是2了
7.2 线程名字
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
通过Thread
类的构造方法可以发现,如果不传入线程的名字就会默认在Thread-
后面添加一个从0开始的数字,因为有synchronized
所以不会出现线程重名的情况。
7.3 守护线程
作用:给用户线程提供服务(一共有用户线程和守护线程两大类)
如果程序中有用户线程,JVM不会停止工作,但是如果程序中只有守护线程,那么守护线程也就没有了守护的对象,所以只有守护线程的情况下JVM会停止工作。例如:垃圾处理器就是守护线程。
守护线程的3个特性
- 线程类型默认继承自父线程
- 被谁启动,当程序启动时main线程是用户线程,其他的都是守护线程
- 不影响JVM退出,如果只有守护线程JVM会结束
守护线程和用户线程的区别
因为都是线程所以整体没什么区别
- 主要就是对JVM的影响,在有用户线程的情况下JVM不会退出,如果只有守护线程存在JVM会退出
- 用户线程用来执行任务,守护线程服务于用户线程
应该把用户线程设置为守护线程吗?
不应该。如果把用户线程设置为守护线程了,那么在执行任务时JVM发现此时没有用户线程,这样就会停止虚拟机,从而导致数据不一致的情况。
7.4 线程优先级
可以通过设置优先级来增加某个线程的运行次数,优先级最高可以设置为10,默认是5,最低是1。
注意:程序设计不应该依赖于优先级
- 因为不同操作系统不一样
- 优先级会被操作系统改变,例如:Windows中有一个优先级推进器,当它发现某个线程执行很积极,那么他就会越过优先级多为这个线程分配执行时间。另一种情况就是如果将线程的优先级设置的过低,那么操作系统可能就不为这个线程分配优先级,这样线程就有可能被“饿死”。
8.线程异常处理
8.1 为什么要处理线程异常
在程序的运行中有很多异常是不可预料的,如果在返回之前不被拦截而是直接返回给用户的话这样可能会引发安全性的问题。
8.2 怎么处理线程异常
使用UncaughtExceptionHandler
8.3 使用 UncaughtExceptionHandler的3个理由
- 主线程可以轻松发现异常,子线程却不行
/**
* 单线程,抛出,处理,有异常堆栈
* 多线程情况下子线程发生异常,会有什么不同?
*/
public class ExceptionInChildThread implements Runnable {
public static void main(String[] args) {
new Thread(new ExceptionInChildThread()).start();
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
当子线程抛出异常时,不会对主线程的运行产生影响。在真实的生产环境中因为有大量日志的产生,可能会忽略子线程中出现的问题。
- 子线程中的异常无法用传统的方法捕获
- 不加try-catch时抛出4个异常
/**
* 1.不加try catch时抛出4个异常,都带线程名
* 2.如果加了try catch,希望可以捕获到第一个线程的异常并处理,线程234不应该再运行,
* 希望看到打印出的Caught Exception
* 3.执行时发现,根本没有Caught Exception,线程234依然运行,并且还抛出异常
*
* 说明线程的异常不能用传统方法捕获
*/
public class CantCatchDirectly implements Runnable {
public static void main(String[] args) throws InterruptedException {
new Thread(new CantCatchDirectly(), "MyThread-1").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(), "MyThread-2").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(), "MyThread-3").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(), "MyThread-4").start();
}
@Override
public void run() {
throw new RuntimeException();
}
}
- 添加try-catch依然抛出异常
/**
* 1.不加try catch时抛出4个异常,都带线程名
* 2.如果加了try catch,希望可以捕获到第一个线程的异常并处理,线程234不应该再运行,
* 希望看到打印出的Caught Exception
* 3.执行时发现,根本没有Caught Exception,线程234依然运行,并且还抛出异常
*
* 说明线程的异常不能用传统方法捕获
*/
public class CantCatchDirectly implements Runnable {
public static void main(String[] args) throws InterruptedException {
try {
new Thread(new CantCatchDirectly(), "MyThread-1").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(), "MyThread-2").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(), "MyThread-3").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(), "MyThread-4").start();
} catch (RuntimeException e) {
System.out.println("Caught Exception");
e.printStackTrace();
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
注意:因为try-catch语句块添加在了主线程中,但是抛出异常的位置位于子线程,所以无法捕获
- 如果不能捕获异常子线程会停止
8.4 解决子线程抛出异常问题
- 方案1(不推荐):手动在每个
run()
方法中进行try-catch
@Override
public void run() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println("Caught Exception");
}
}
- 方案2(推荐):利用UncaughtExceptionHandler
8.5 自己实现并处理异常
实现方案
- 给程序统一设置
- 给每个线程单独设置
- 给线程池设置
创建MyUncaughtExceptionHandler
/**
* 实现自己的UncaughtExceptionHandler
*/
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name) {
this.name = name;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING, "线程异常,终止啦 : " + t.getName());
System.out.println(name + " 捕获了" + t.getName() + "的"+ e +"异常");
}
}
使用MyUncaughtExceptionHandler
/**
* 使用自己创建的UncaughtExceptionHandler
*/
public class UseOwnUncaughtExceptionHandler implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler(new
MyUncaughtExceptionHandler("捕获器1"));
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-4").start();
}
@Override
public void run() {
throw new RuntimeException();
}
}
9.多线程的缺点
线程是一把双刃剑,他在提高程序执行效率的同时也会存在一些弊端,比如线程安全问题,这会导致数据发生错乱,还有就是性能问题,比如服务响应慢、吞吐率低、资源开销大。使用线程的目的就是为了让程序更好的运行,如果这些问题不解决就本末倒置了,在这里学习一下如何解决吧!
9.1 什么是线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方在进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
9.2 线程安全问题如何避免
线程安全问题主要分为如下两种情况
- 数据征用:两个数据同时去写,这就有可能导致有一方的数据要么被丢弃要么写入错误
- 竞争条件:竞争条件主要体现在顺序上,比如一个线程对文件进行读取,读取发生在写入之前,这就会引起顺序上的错误。
9.3 线程安全——运行结果问题(i++加的次数会莫名消失)
/**
* 第一种情况:运行结果出错
* 演示计数不准确(减少),找出具体出错的位置
*/
public class MultiThreadError implements Runnable {
private int index = 0;
static MultiThreadError instance = new MultiThreadError();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(instance.index);
}
}
其实每次执行i++操作需要三步,如果线程1执行了i+1
的操作后线程进行了切换,线程2不知道线程1已经执行了+1
操作,依然执行+1,加完之后又切换到线程1,此时线程1的加操作已经完成了,i变成了2,线程2再次执行后,i的值也变成了2,这就是导致i出现少加的情况的原因。
9.4 i++错误的解决
/**
* 第一种情况:运行结果出错
* 演示计数不准确(减少),找出具体出错的位置
*/
public class MultiThreadError implements Runnable {
private int index = 0;
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
static MultiThreadError instance = new MultiThreadError();
final boolean[] marked = new boolean[10000000];
@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 10000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realIndex.incrementAndGet();
synchronized (instance){
if (marked[index] && marked[index-1]){
System.out.println("发生了错误" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上的结果是:" + instance.index);
System.out.println("真正运行的次数是:" + realIndex.get());
System.out.println("错误的次数是:" + wrongCount.get());
}
}
9.5 线程安全——活跃性问题(死锁、活锁、饥饿)
死锁的实现
/**
* 第二种线程安全问题,演示死锁
*/
public class MultiThreadError2 implements Runnable {
int flag;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MultiThreadError2 r1 = new MultiThreadError2();
MultiThreadError2 r2 = new MultiThreadError2();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
System.out.println("flag : " + flag);
if (flag == 1){
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){ //想拿到o2却始终拿不到
System.out.println("1");
}
}
}
if (flag == 0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){ //想拿到o1却始终拿不到
System.out.println("0");
}
}
}
}
}
9.6 线程安全——对象发布和初始化时的安全问题
什么是发布
对象可以让超出范围之内的类进行使用,比如在方法结束后return 一个对象,从而让外部可以使用这个对象,这就是对象的发布。
什么是逸出
逸出是指将对象发布到了不该发布的地方,比如:
- 方法返回一个
private
的对象(private正常只能在本类中使用) - 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
构造函数中未初始化完毕就this
赋值,隐式逸出——注册监听事件,构造函数中运行线程
return私有对象导致的逸出
/**
* 发布逸出
*/
public class MultiThreadError3 {
private Map<String, String> states;
public MultiThreadError3(){
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
public Map<String, String> getStates(){
return states;
}
public static void main(String[] args) {
MultiThreadError3 multiThreadError3 = new MultiThreadError3();
Map<String, String> states = multiThreadError3.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0HrOF3R2-1582528143185)(https://user-gold-cdn.xitu.io/2019/9/13/16d292bb017cd2cb?w=504&h=168&f=png&s=14177)]
private
的本意就是不希望外部访问到,可以通过return
之后可以从外部对数据进行修改,这就很危险了!
构造方法未初始化完成就赋值导致逸出
/**
* 初始化未完毕就this赋值
*/
public class MultiThreadError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
Thread.sleep(10);
if (point != null){
System.out.println(point);
}
}
}
class Point{
private final int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
MultiThreadError4.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return x + "," + y;
}
}
class PointMaker extends Thread{
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果将主线程中sleep
的时间增加到100ms以上便不会发生逸出
注册监听器导致逸出
/**
* 观察者模式
*/
public class MultiThreadError5 {
int count;
public MultiThreadError5(MySource source){
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
System.out.println("\n我得到的数字是:" + count);
}
});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e){
if (listener != null){
listener.onEvent(e);
}else{
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
MultiThreadError5 multiThreadError5 = new MultiThreadError5(mySource);
}
}
在构造方法中使用线程导致逸出
/**
* 构造函数中新建线程
*/
public class MultiThreadError6 {
private Map<String, String> states;
public MultiThreadError6() {
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
}).start();
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) {
MultiThreadError6 multiThreadError6 = new MultiThreadError6();
Map<String, String> states = multiThreadError6.getStates();
System.out.println(states.get("1"));
}
}
9.7 逸出的解决——用“副本”替代“真身”(针对返回私有对象)
/**
* 发布逸出
*/
public class MultiThreadError3 {
private Map<String, String> states;
public MultiThreadError3(){
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
public Map<String, String> getStates(){
return states;
}
public Map<String, String> getStatesImproved(){
return new HashMap<>(states); //创建一个states副本
}
public static void main(String[] args) {
MultiThreadError3 multiThreadError3 = new MultiThreadError3();
Map<String, String> states = multiThreadError3.getStates();
// System.out.println(states.get("1"));
// states.remove("1");
// System.out.println(states.get("1"));
System.out.println(multiThreadError3.getStatesImproved().get("1"));
multiThreadError3.getStatesImproved().remove("1");
System.out.println(multiThreadError3.getStatesImproved().get("1"));
}
}
9.8 逸出的解决——巧用工厂模式(可以修复监听器注册的问题)
/**
* 用工厂模式解决监听器注册问题
*/
public class MultiThreadError7 {
int count;
private EventListener listener;
private MultiThreadError7(MySource source){
listener = new EventListener() {
@Override
public void onEvent(MultiThreadError7.Event e) {
System.out.println("\n我得到的数字是:" + count);
}
};
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static MultiThreadError7 getInstance(MySource source){
MultiThreadError7 safeListener = new MultiThreadError7(source);
source.registerListener(safeListener.listener);
return safeListener;
}
static class MySource {
private MultiThreadError7.EventListener listener;
void registerListener(MultiThreadError7.EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(MultiThreadError7.Event e){
if (listener != null){
listener.onEvent(e);
}else{
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(MultiThreadError7.Event e);
}
interface Event {
}
public static void main(String[] args) {
MultiThreadError7.MySource mySource = new MultiThreadError7.MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new MultiThreadError7.Event() {
});
}
}).start();
MultiThreadError7 multiThreadError7 = new MultiThreadError7(mySource);
}
}
10.什么时候会有线程安全的问题
- 并发访问共享的资源时会产生线程安全问题,比如对象属性、静态变量、数据库、共享缓存等
- 所有依赖时序的操作,即使每步操作都是线程安全的,也会存在并发问题,其中包括:先读取后修改(这样读取到的内容有可能会失效)、先检查再执行
- 不同的数据之间存在绑定关系的时候,例如对外发布服务端口号和ip地址必须对应,即保证原子性。
- 在使用其他类时,不是线程安全的(比例
HashMap
在并发中可能会出现问题)
11.为什么多线程会有性能问题
-
调度:上下文切换
-
上下文切换可以认为是内核在CPU上进行的以下活动:(1)挂起一个进程将进程中的状态(上下文)存储在内存中的某处。(2)在内存中检索下一个进程的状态将它在CPU寄存器中恢复。(3)跳转到程序计数器所指向的位置以恢复该线程
-
缓存开销:CPU重新缓存
-
何时会导致密集的上下文切换:频繁竞争锁或者因为IO频繁导致阻塞
-
-
协作:内存同步
- 为了数据的正确性,同步手段往往会使用禁止编译器优化、使CPU的缓存失效