文章已参与[新人创作礼]活动,一起开启掘金创作之路.
微信公众号:秀基宝。如有问题,请后台留言,反正我也不会听。
前言
众多周知啊,在我们java基础中,有一个基础是我们面试必不可少的话题,但是实际工作中,又很少使用,乃至根本不怎么用。那么这个就是多线程,但也有朋友说根据场景来使用的,也不是用在哪里都好,只有结合时间复杂度以及性能、异步、线程的调度才能完美演绎。那么今天就来讲下多线程好处以及实际工作用到的场景
一、概念
1、学习思路
为什么学习线程?为了解决CPU利用率问题,提高CPU利用率。 =》 什么是进程?什么是线程? =》 怎么创建线程?有哪几种方式?有什么特点? =》 分别怎么启动线程? =》 多线程带来了数据安全问题,该怎么解决? =》 怎么使用synchronized(同步)决解? =》使用同步可能会产生死锁,该怎么决解? =》 线程之间是如何通信的? =》 线程有返回值吗?该如何拿到? =》 怎么才能一次性启动几百上千个的线程?
2、为什么学多线程?
平时面试我们面试都会这么说,提高利用cpu利用率
3、进程和线程区别?
例如打开QQ、打开微信、秀基宝小程序,这种操作系统中执行不同程序就是进程 例如我们QQ中,发送了一个消息,那么后台接受请求,开辟一个主线程,这样就是线程,你也知道电脑不可能只有一个线程,那么多核服务器中,多个线程同时工作,此时就是多线程的场景。
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。
4、其他问题
– Java的多线程
• Java 中的多线程是通过java.lang.Thread类来实现的.
• 一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
• 使用多线程的优点。
– 背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短(因为单线程的可以减少cup的调度消耗的时间),为何仍需多线程呢?
– 多线程程序的优点:
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。同时做多个事情。比如:一边听歌、一边写代码。
2.提高计算机系统CPU的利用率。
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
• 何时需要多线程
– 程序需要同时执行两个或多个任务。
– 需要一些后台运行的程序时。比如:Java后台运行的GC功能。
主线程
– 概念
• 即使Java程序没有显示的来声明一个线程,Java也会有一个线程存在该线程叫做主线程
• 可以调用Thread.currentThread()来获得当前线程
二、线程创建方法
- 继承Thread类
- 覆盖run方法
- 实现Runnable接口
- Runnable有一个run方法,定义逻辑
- 实现 Callable
其实还有其他方法,总共四种。
三、启动和终止
启动
- 调用Thread的start方法
终止
当run方法返回,线程终止,一旦线程终止后不能再次启动。
- 1、设置一个标记来控制线程的终止。
public volatile boolean exit = false;
public void run()
{
while (!exit);
}
public static void main(String[] args) throws Exception
{
ThreadFlag thread = new ThreadFlag();
thread.start();
sleep(5000); // 主线程延迟5秒
thread.exit = true; // 终止线程thread
thread.join();
System.out.println("线程退出!");
}
在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值
-
2、stop终止 终止线程、暴力终止,可能导致工作没有完全完成,哈可能导致对锁定的内容进行解锁,从而造成数据不同步
-
3、调用线程的interrupt方法 interrup只是终止睡眠吧,并不会终止线程
public class MyThread4 extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
System.out.println( "线程已经结束,我要退出" );
break;
}
System.out.println( "i=" + (i + 1) );
}
System.out.println( "我是for下面的语句,我被执行说明线程没有真正结束" );
}
}
public static void main(String[] args) {
try {
MyThread4 myThread4 = new MyThread4();
myThread4.start();
Thread.sleep( 20);
myThread4.interrupt();
} catch (InterruptedException e) {
System.out.println( "main catch" );
e.printStackTrace();
}
}
根据打印结果发现for后面的内容依旧会执行,为了解决这种问题,可以采用抛异常的方式,或return的方式终止线程。一般推荐抛异常的方式,这样才能使得线程停止得以扩散。
public class MyThread4 extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
System.out.println( "线程已经结束,我要退出" );
// return;
throw new InterruptedException();
}
System.out.println( "i=" + (i + 1) );
}
System.out.println( "我是for下面的语句,我被执行说明线程没有真正结束" );
} catch (InterruptedException e) {
System.out.println( "进入MyThread.java类中run方法的catch异常了" );
e.printStackTrace();
}
}
}
另一种就是睡眠中停止,具体方式就是先sleep,然后interrupt
try {
for (int i = 0; i < 10000; i++) {
System.out.println( "i=" +(i + 1) );
}
System.out.println( "run begin" );
Thread.sleep( 200 );
System.out.println( "run end" );
} catch (InterruptedException e) {
System.out.println( "先停止,后sleep" );
e.printStackTrace();
}
public static void main(String[] args) {
MyThread5 thread5 = new MyThread5();
thread5.start();
thread5.interrupt();
}
四、线程基本方法
4.1、有关方法
- start():启动线程并执行对象的run()方法
- run() 线程被调度执行
- String getName():返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread():返回当前线程。
4.2、基本方法
- isAlive 判断线程是否存活
- getPriority 获得线程优先级
- setPriority 写入优先级
- sleep 睡眠
- join 加入线程队列,接着执行
- yield 让出CPU
- wait 等待,让其他线程先执行
- notify/notifyAll 通知/通知所有
五、线程优先级
- 线程优先级越高、占用CPU时间越高
- 最高10级,最低1级,默认5级
六、线程状态
- 创建
- 就绪
- 运行
- 阻塞
- 死亡
七、线程同步
在我们工作中,有时间同一资源可能会被多个线程抢占使用,会造成脏数据,这个时候我们肯定就需要用到同步,常见问题就是银行转账的问题
7.1、线程安全
当多个线程访问同一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方式代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
- 同一时间
- 多个线程
- 同一资源
7.2、解决
为了解决线程安全,我们常用的就是synchronized和lock
synchronized 是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
1、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
2、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
4、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
7.3、如何避免
- 数据单线程内可见。
- 例如:ThreadLocal
- 只读对象
- 使用 final 关键字修饰类,避免被继承;没有任何更新方法;返回值不能为可变对象。
- 线程安全类
- 它采用 synchronized 关键字来修饰相关方法,例如StringBuffer
- 同步与锁机制
- 自定义实现相关的安全同步机制
7.4、要不只读,要不加锁
核心理念就是“要不只读,要不加锁”。JDK 提供的并发包,主要分成以下几个类族:
- 线程同步类
这些类使线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用 Object 的 wait() 和 notify() 进行同步的方式。主要代表有 CountDownLatch、Semaphore、CyclicBarrier 等。
- 并发集合类
集合并发操作的要求是执行速度快,提取数据准。最典型的莫过于 ConcurrentHashMap,经过不断的优化,有刚开始的分段式锁到后来的 CAS ,不断的提高并发性能。
- 线程管理类
虽然 Thread 和 ThreadLocal 在 JDK1.0 就已经引入,但是真正把 Thread 的作用发挥到极致的是线程池。根据实际场景的需要,提供了多种创建线程池的快捷方式,如使用 Executors 静态工厂或者使用 ThreadPoolExecutors 等。另外,通过 ScheduledExecutorService 来执行定时任务。
- 锁相关类
锁以 Lock 接口为核心,派生出一些实际场景中进行互斥操作的锁相关类。最有名的是 ReentrantLock 。
八、死锁
上门讲了锁可以给我们带来多线程场景下会导致数据不一致,脏数据等问题,所以我们给他们加上锁,那么也有一个坑,那就是当多个线程都对一个资源进行不释放,都在等待对方释放需要同步的资源,这样会导致线程的死锁。
概念 多个并发进程因争夺系统资源而产生相互等待的现象。
8.1、危害
这个时候程序也不会抛异常,最终会造成程序阻塞、无法继续进行
8.2、四个必要条件
- 互斥:一个资源每次只能被一个进程使用;
- 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺;
- 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系;
8.3、解决
- 避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源
- 减少需要同步的资源定义
- 尽量避免嵌套同步
- 死锁检测与解除 ----- 在检测到运行系统进入死锁,进行恢复。
如果利用死锁检测算法检测出系统已经出现了死锁 ,那么,此时就需要对系统采取相应的措施。常用的解除死锁的方法:
1、抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。
2、终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。
九、线程的通信
概念
在我们线程执行中,我们线程难免会造成阻塞、或者同步、等待各种情况,他们之间其实可以互相通讯的,会被提高优先级、改变线程的状态,这个时候就会用到通讯
9.1、方式
- Object.wait 与 Object.notify/notifyAll
- LockSupport.park 与 LockSupport.unpark
- ReentrantLock + Condition
- 共享内存方式:volatile 关键字、辅助类(CountDownLatch、CyclicBarrier、Semaphore)
// LockSupport.park 与 LockSupport.unpark
public class ThreadDemo {
public static void main(String[] args) {
final Thread threadA = new Thread(() -> {
System.out.println("开始阻塞线程");
LockSupport.park();
// park之后线程已被阻塞,只有被唤醒之后下面的句子才能输出
System.out.println("阻塞唤醒完毕");
});
threadA.start();
System.out.println("开始唤醒线程");
LockSupport.unpark(threadA);
}
}
// 输出:
// 开始阻塞线程
// 开始唤醒线程
// 阻塞唤醒完毕
9.2、方法
- wait 进入阻塞状态,并释放同步监视器。
- notify 唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的。
- notifyAll 醒所有被wait的线程。
9.3、前提
- 大多数情况下,线程间的通信机制指的是线程间的交互,即线程的唤醒和阻塞。并不是字面意义上的多个线程之间互相共享和交换数据的“通信”。
- 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器否则,会出现IllegaLMonitorStateException异常.
public class ProducerConsumer {
public static void main(String[] args) {
BaoziStack baoziStack = new BaoziStack();
Producer p1 = new Producer(baoziStack);
Consumer c1 = new Consumer(baoziStack);
p1.start();
c1.start();
}
}
// 包子类
class Baozi {
int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "包子 : " + id;
}
}
// 包子筐
class BaoziStack {
Baozi[] bz = new Baozi[10];
int index = 0;
// 装包子
public synchronized void pushBZ(Baozi baozi) {
if (index >= bz.length) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
bz[index] = baozi;
index++;
notify();
}
// 取包子
public synchronized Baozi popBZ() {
if (index <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
index--;
Baozi baozi = bz[index];
notify();
return baozi;
}
}
// 生产者:生产包子,放到包子筐里
class Producer extends Thread {
private BaoziStack baoziStack;
public Producer(BaoziStack baoziStack) {
this.baoziStack = baoziStack;
}
@Override
public void run() {
// 生产包子(一天生产100个包子)
for (int i = 1; i <= 100; i++) {
Baozi baozi = new Baozi(i);
System.out.println("生产者生产了一个包子ID为: " + i);
baoziStack.pushBZ(baozi);
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer extends Thread {
private BaoziStack baoziStack;
public Consumer(BaoziStack baoziStack) {
this.baoziStack = baoziStack;
}
@Override
public void run() {
// 一天的消费量为100个包子
for (int i = 1; i <= 100; i++) {
Baozi baozi = baoziStack.popBZ();
System.out.println("消费者消费了一个包子ID为:" + baozi.id + "的包子");
try {
sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
十、拓展
1、Runnable和Callable的区别
- Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能有返回值。
- call方法可以抛出异常,run方法不可以。
2、使用线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
下面一篇会好好讲解这篇的
JDK5.0起提供了线程池相关API: ExecutorService 和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务1命令,没有返回值,-般用来执行Runnable
< T > Future< T > submit(Callable< T > task):执行任务, 有返回值,一般来执行Callable
void shutdown():关闭连接池
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor(): 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
好处:
1.提高响应速度(减少创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolsize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
3、多线程传参
由于多线程是由继承 Thread 或实现 Runnable 并重写 run() 方法,通过 thread.start() 进行运行的,而本身重写的 run() 方法是不具备传参能力的,
class ThreadA extends Thread{
private String age;
public ThreadA(String age){
this.age = age;
}
@Override
public void run() {
System.out.println("age=" + age);
}
}
public static void main(String[] args) {
String age = new String("12");
ThreadA a = new ThreadA(age);
a.start();
}
结论: 无论 extends Thread 还是 implements Runnable ,传参都需要使用线程初始化的有参构造形式,从而达到多线程传参的目的。也可以做到重载有参构造,传入各式对象。
网址:[www.idearyou.cn]
谷歌插件搜:秀基宝
小程序:秀基宝
复制代码
后语
如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。 另外,关注免费学习。