多线程基础
提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!
多线程基础
1. 多线程创建
1. 1 多线程创建方式
- 继承Thread类,重写run方法实现多线程
class Task1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Task1输出:" + i);
}
}
}
class Task2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(" Task2输出:" + i);
}
}
}
/**
* @Description: 继承Thread,重写run方法,实现多线程
* @Author noneplus
* @Date 2020/8/3 17:34
*/
public class ExtendThread {
public static void main(String[] args) {
Task1 task1 = new Task1();
task1.start();
Task2 task2 = new Task2();
task2.start();
for (int i = 0; i < 10; i++) {
System.out.println(" mainTask输出:" + i);
}
//3个线程的执行顺序由CPU的线程调度决定
}
}
- 实现Runnable接口
class Task3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Task1输出:" + i);
}
}
}
class Task4 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(" Task2输出:" + i);
}
}
}
/**
* @Description: 实现Runnable接口,实现多线程,弥补单继承的问题
* @Author noneplus
* @Date 2020/8/3 17:34
*/
public class ImplementRunnable {
public static void main(String[] args) {
Task3 runnable3 = new Task3();
Task4 runnable4 = new Task4();
Thread task3 = new Thread(runnable3);
Thread task4 = new Thread(runnable4);
task3.start();
task4.start();
for (int i = 0; i < 10; i++) {
System.out.println("mainTask输出:" + i);
}
//3个线程的执行顺序由CPU的线程调度决定
}
}
- 实现Callable接口:Callable支持返回值(但用多线程加返回值有点奇怪)
class Task5 implements Callable {
@Override
public Integer call() throws Exception {
Integer i = 0;
for (; i < 10; i++) {
System.out.println("Task5输出:" + i);
}
return i;
}
}
class Task6 implements Callable {
@Override
public Integer call() throws Exception {
Integer i = 0;
for (; i < 10; i++) {
System.out.println(" Task6输出:" + i);
}
return i;
}
}
/**
* @Description: 实现Callable接口,可以定义返回值
* @Author noneplus
* @Date 2020/8/3 17:53
*/
public class ImplementCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println("mainTask输出:" + i);
}
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Task5());
FutureTask<Integer> futureTask1 = new FutureTask<Integer>(new Task6());
Thread thread = new Thread(futureTask);
Thread thread1 = new Thread(futureTask1);
thread.start();
thread1.start();
System.out.println("任务返回结果:"+futureTask.get());
System.out.println("任务返回结果:"+futureTask1.get());
}
}
- 使用线程池ThreadPoolExecutor
class Task7 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Task7输出:" + i + "当前线程" + Thread.currentThread().getName());
}
}
}
class Task8 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Task8输出:" + i + "当前线程" + Thread.currentThread().getName());
}
}
}
/**
* @Description: 使用线程池创建线程池,实现线程复用和管理
* @Author noneplus
* @Date 2020/8/5 15:38
*/
public class ThreadPool {
public static void main(String[] args) {
/**
* 阿里推荐:ThreadPoolExecutor
* ```
* public ThreadPoolExecutor(int corePoolSize,
* int maximumPoolSize,
* long keepAliveTime,
* TimeUnit unit,
* BlockingQueue<Runnable> workQueue)
* ```
* 1、corePoolSize 核心线程数大小,当线程数 < corePoolSize ,会创建线程执行 runnable
* 2、maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把 runnable 放入 workQueue中
* 3、keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
* 4、unit 时间单位
* 5、workQueue 保存任务的阻塞队列
* 6、threadFactory 创建线程的工厂
* 7、handler 拒绝策略
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3));
for (int i=0;i<10;i++){
threadPoolExecutor.execute(new Task7());
threadPoolExecutor.execute(new Task8());
}
}
}
1. 1 多线程创建方式比较
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦(耦合:类与类之间关系)操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable接口线程,不能直接放入继承Thread的类。
2. 线程状态
- NEW(新建)
线程刚被创建即new出来的时候,但是并未启动。还没调用start方法。- Runnable(可运行)
线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器cpu。- Blocked(锁阻塞)
当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有时,该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable可运行状态。- Waiting(无限等待)
一个线程在等待另一个线程执行一个(唤醒)动作时,调用wait()方法,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify随机唤醒一个或者notifyAll方法才能够唤醒。- Timed Waiting(计时等待)
同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep (时间参数)、Object.wait(时间参数5000毫秒自动唤醒)。- Teminated(被终止)
执行完run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
3. 线程相关API
Object类相关api(相关的方法一定是当前线程在获取了对应的锁对象才能调用,否则会抛出异常)
o.wait() :锁对象调用该方法使当前线程进入等待状态,并立刻释放锁对象,直到被其他线程唤醒进入等锁池。
o.wait(long) :锁对象调用该方法使当前线程进入等待状态,同时释放锁对象。但是超过等待的时间后线程会自动唤醒,或者被其他线程唤醒,并进入等锁池中。
o.wait(long,int) :和o.wait(long)方法一样,如果int参数大于0则前面的long数字加1
o.notify():随机唤醒一个处于等待中的线程(同一个等待阻塞池中)
o.notifyAll():唤醒所有等待中的线程(同一个等待阻塞池中)
Thread类的相关api
Thread.currentThread():返回对当前线程对象的引用
Thread.interrupted():检测当前线程是否已经中断(调用该方法后后就将该线程的中断标志位设置位false,所以连续两次调用该方法第二次肯定时false)
Thread.sleep(long millis):使当前线程睡眠(不会释放锁对象,可以让其他线程有执行的机会)
Thread.yield():使当前线程放弃cpu的执行权(有可能立刻又被重新选中继续执行,只可能给优先级更高的线程机会)
t.getId():返回该线程的id
t.getName():返回该线程的名字
t.getPriority():返回该线程的优先级
t.setPriority(int i):设置线程的优先级
t.getState():返回该线程的状态
t.getThreadGroup():返回该线程所属的线程组
t.interrupt():将该线程中断(实际并不会中断,只是将中断标志设置为true),如果线程正处在sleep(),join(),wait()方法中时(也就是正在阻塞中)调用该方法,该方法会抛出异常。
t.interrupted():检测该线程是否已经中断(对中断标志位不作处理)
t.isAlive():检测该线程是否还活着
t.isDaemon():检测该线程是否为守护线程
t.isInterrupted():检测该线程是否已经中断
t.join():在a线程中调用b.join(),则a线程阻塞,直到b线程执行完
t.join(long millis):和上面的方法一样,不过a线程阻塞的时间根据long的大小有关,如果达到设定的阻塞时间,就算b线程没有执行完,a线程也会被唤醒。
4. 线程同步
线程安全问题:
多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题
同步概念:
同步就可以让cpu在某段时间内只让一个线程进来做事情,其他线程不能进来,等你做完了才能一个一个进来
同步机制:
- 同步代码块(锁对象设置为公共资源的对象)。
- 同步方法(独占锁,默认锁对象是this或当前类Class对象)。
- 锁机制(可重入锁:ReentrantLock,它拥有和synchronized相同的并发性和内存语义)。
同步缺点:
- 一个线程持有锁会导致其他线程线程挂起
- 多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
lock与synchronized的区别:
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定。
但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;- Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。举个例子:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的
死锁:
线程死锁是指两个或两个以上的线程互相持有对方所需要的资源,由于synchronized的特性,一个线程持有一个资源,或者说获得一个锁,在该线程释放这个锁之前,其它线程是获取不到这个锁的,而且会一直死等下去,因此这便造成了死锁。
死锁产生的条件:
- 互斥条件:一个资源,或者说一个锁只能被一个线程所占用,当一个线程首先获取到这个锁之后,在该线程释放这个锁之前,其它线程均是无法获取到这个锁的。
- 占有且等待:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。
- 不可剥夺条件:任何一个线程都无法强制获取别的线程已经占有的锁
- 循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。
如何避免死锁
- 加锁顺序:线程按照相同的顺序加锁。
- 加锁时限,线程获取锁的过程中限制一定的时间,如果给定时间内获取不到,就算了,别勉强自己。这需要用到Lock的一些API。
public class DeadLock {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void method1() throws InterruptedException {
synchronized(lock1){
System.out.println(Thread.currentThread().getName() + "获取到lock1,请求获取lock2....");
Thread.sleep(1000);
synchronized (lock2){
System.out.println("获取到lock2....");
}
}
}
public void method2() throws InterruptedException {
synchronized(lock2){
System.out.println(Thread.currentThread().getName() + "获取到lock2,请求获取lock1....");
Thread.sleep(1000);
synchronized (lock1){
System.out.println("获取到lock1....");
}
}
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(()-> {
try {
deadLock.method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()-> {
try {
deadLock.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
5. 线程通信(等待唤醒机制)
为什么要处理线程间的通信?
多个线程并发执行时, 在默认情况下,CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多个线程之间就需要一些协调和通信,帮我们达到多个线程间,共同操作同一份数据!!!
什么是等待唤醒机制?
在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用( notifyAll())来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
等待唤醒中的方法
- wait:线程不再活动,不再参与调度,进入 wait set(等待集合) 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;即随机唤醒其中一个等待的线程
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节(把握一个原则,用锁等待,用同一个锁唤醒)
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数方法中使用。因为:必须要通过锁对象调用这2个方法。
5.1 生产消费者模型
- 生产消费者模型-管程法(利用缓存区解决)
package com.yuan.thread;
/**
* @description: 测试线程通讯-生产者和消费者1-管程法(缓冲区)
* @author: ybl
* @create: 2021-02-22 11:22
**/
//测试生产者和消费者
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生产者
class Productor extends Thread {
SynContainer synContainer;
public Productor(SynContainer synContainer) {
this.synContainer = synContainer;
}
//生产
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
synContainer.push(new BaoZi(i));
System.out.println("生产了第" + i + "个包子");
}
}
}
//消费者
class Consumer extends Thread {
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
//消费
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("消费了第" + synContainer.pop().id + "个包子");
}
}
}
//产品(包子)
class BaoZi {
//产品编号
int id;
public BaoZi(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer {
//容器
BaoZi[] baoZis = new BaoZi[10];
//容器计数器
int count = 0;
//生产者生产产品
public synchronized void push(BaoZi baoZi) {
//如果容器满了,等待消费者消费,生产者等待
if (count == baoZis.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果容器没有满,生产产品
baoZis[count] = baoZi;
count++;
//通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized BaoZi pop() {
//判断能否消费
if (count == 0) {
//等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count--;
BaoZi baoZi = baoZis[count];
//吃完了通知生产者生产
this.notifyAll();
return baoZi;
}
}
- 生产消费者模型-信号灯法(利用标志位解决)
package com.yuan.thread;
/**
* @description: 测试线程通讯-生产者和消费者2-信号灯法(利用标志位解决)
* @author: ybl
* @create: 2021-02-22 11:22
**/
//测试生产者和消费者
public class TestPC {
public static void main(String[] args) {
BaoZi bz = new BaoZi();//创建包子对象名,传入吃货线程和生产线程使用来判断状态,设置属性等
ChiHuo ch = new ChiHuo("吃货",bz);
BaoZiPu bzp = new BaoZiPu("包子铺",bz);
ch.start();
bzp.start();
}
}
class BaoZi {
String pier ;//皮,包子属性,成员变量表示事物的属性
String xianer ;//馅
boolean flag = false ;//包子状态,false表示没有包子
}
class ChiHuo extends Thread{
private BaoZi bz;
public ChiHuo(String name,BaoZi bz){//构造传入线程名字和包子对象,才能判断包子的状态等
super(name);
this.bz = bz;
}
@Override
public void run() {
while(true){//死循环不断的吃包子显示包子属性,每次吃完包子,包子没有了标记为false,通知生产线程
synchronized (bz){//等待唤醒方法要被锁对象调用,所以这里写在同步代码块里面
if(bz.flag == false){//如果没包子,吃货线程等待
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
bz.flag = false;
bz.notify();
}
}
}
}
class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
int count = 0;//标记控制
while(true){//死循环不断生产包子,设置包子属性,每生产完一个包子,包子有了标记为true,通知吃货线程
synchronized (bz){//等待唤醒方法要被锁对象调用,所以这里写在同步代码块里面
if(bz.flag == true){//如果有包子,生产线程,等待,不生产
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("包子铺开始做包子");
if(count%2 == 0){//奇偶标记控制,设置包子不同属性
bz.pier = "冰皮";
bz.xianer = "五仁";
}else{
bz.pier = "薄皮";
bz.xianer = "牛肉大葱";
}
count++;
bz.flag=true;
System.out.println("包子造好了:"+bz.pier+bz.xianer);
System.out.println("吃货来吃吧");
//唤醒等待线程 (吃货)
bz.notify();
}
}
}
}