并发编程简介

Java 并发编程

一、 并发编程三要素

  1. 原子性:即一个不可被分割的操作。
    Java 中的原子性指:一个或多个操作要么全部执行成功,要么全部执行失败

  2. 有序性:程序执行的顺序是按照代码的先后顺序执行的。
    cpu 有可能会对指令进行重排序

  3. 可见性:当多个线程访问同一个共享变量时,如果其中一个线程对其进行了修改操作,其它线程能立即获取到最新修改的值。

二、线程的五大状态

对应cpu 的线程状态

  1. 新建状态:通过new 创建一个线程
  2. 就绪状态:调用start 方法,处于就绪状态的线程不一定立马就会执行run 方法,还需要等CPU的调度。
  3. 运行状态:cpu开始调度线程,开始执行run方法
  4. 阻塞状态:线程在执行过程中由于某些原因进入阻塞状态。如:sleep()、获取锁失败、时间片用完等
  5. 死亡状态:run方法执行完,或执行过程中遇到异常。

三、悲观锁与乐观锁

悲观锁:每次操作都会加锁,造成线程阻塞。如:synchronized、reentrantlock
乐观锁:每次操作都不会加锁,而是假定没有冲突而去完成某种操作。如果有冲突就重试,直到成功为止,不会造成线程的阻塞。虽然不会造成线程阻塞,但是频繁的重试会导致cpu的长时间被占用。

四、线程之间的协作

wait、notify、notifyAll等

五、synchronized 关键字

synchronized 是Java中的关键字,是一种同步锁,底层使用的是Monitor监视器。
它修饰的对象有一下几种:

  1. 修饰一个代码块 被修饰的代码块称为同步代码块,其作用范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
  2. 修饰一个方法 被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
  3. 修饰一个静态的方法 其作用范围是整个静态方法,作用的对象是这个类对象
  4. 修饰一个类 其作用的范围是synchronized后面括起来的部分,作用的对象是这个类对象。

对第4点解释:

public void test(){
	// 当前synchronized 作用的范围是 {} 的部分
	// 加锁的对象是 A.class 对象
	synchronized(A.class){

	}
}

六、CAS

CAS:Compare And Swap 比较并交换。

原理:

使用到的是cpu的一个原子操作,操作包含三个操作数,内存值、预期值、更新的值。如果内存值与预期值相同,那么就能将该值修改为更新的值。否则修改失败。

缺点:

  1. ABA 问题 可通过版本号或者时间戳解决
  2. 循环时间长对CPU的开销大 可以增加重试次数,超过重试次数则直接放弃
  3. 只能保证一个共享变量的原子操作。后续可以使用原子引用对象来解决该问题

七、Thread.State 类中定义的六种线程状态

  1. new:Thread对象已经创建,但是还没有执行。
  2. Runnable:Thread对象在java 虚拟机中运行。
  3. Block:Thread对象被阻塞。
  4. Waitting:Thread对象在等待另外一个线程的动作。
  5. Time_Waiting:Thread对象在等待另外一个线程的动作,但是有时间限制。
  6. Terminated:Thread已经完成了执行

八、Thread类的常用方法:

  • 获取和设置Thread对象信息的方法
    • getId(): 返回Thread 对象的标识符。该标识符是在线程创建时分配的一个正整数。在线程的整个生命周期中是唯一且无法改变的。
    • getName()/setName():获取、设置Thead 对象的名称。这个名称是一个String 类型。也可以在Thread的构造方法中指定。
    • getPriority()/setPriority():获取或者设置Thread对象的优先级。
    • isDaemon()/setDaemon():获取或者设置守护线程。
    • getState():返回Thread对象的状态。
  • interrupt():中断目标线程。给目标线程打上一个中断标记。
  • interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
  • isInterrupted():判断目标线程是否被中断,不会清除中断标记。
  • sleep(long ms):暂停当前线程ms时间。
  • join():暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另外一个线程结束。
  • currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。

九、Callable

  • 接口。有简单类型参数,与call() 方法的返回类型相对应
  • 声明了call()。执行器运行任务时,该方法会被执行器执行。必须返回声明中指定类型的对象。
  • call() 方法可以抛出任何一种校验异常。可以实现自己的执行器并重载afterExecute() 方法来处理这些异常`
package com.yj.juc;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(500);
        return "call 的返回值";
    }
}


package com.yj.juc;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        // 启动线程执行任务
        new Thread(futureTask).start();
        // 需要同步阻塞获取到任务的结果
        System.out.println(futureTask.get());
        System.out.println("main1 方法执行结束");
    }
}

package com.yj.juc;

import java.util.concurrent.*;

public class Main2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5,
                5,
                1000,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10)
        ){
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
//                super.afterExecute(r, t);
                System.out.println("call 执行结束"+t);
            }
        };

        Future<String> future = threadPoolExecutor.submit(new MyCallable());
        String result = future.get();
        System.out.println(result);
        threadPoolExecutor.shutdown();
    }
}

// Main2 的打印结果:
call 的返回值
call 执行结束null

十、生产者与消费者模型

生产者、消费者是一个常见的多线程模型。
在这里插入图片描述
解释:

多个生产者线程往内存队列中存放数据;多个消费者线程从内存队列中取数据。

前提条件:

  1. 加锁,内存队列本身要加锁,才能实现队列的安全。
  2. 阻塞,当内存队列满了,生产者放不进去数据,就会阻塞。当队列为空时,消费者取不到数据,就会被阻塞。
  3. 唤醒通知,消费者消费之后,要通知生产者生产新的数据。反之生产者生产消息之后,要通知消费者消费

如何阻塞?
办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait和notify
办法2:用一个阻塞队列,当娶不到或者放不进去数据的时候,入队、出队操作阻塞、

如何唤醒、通知?
办法1:使用wait和notify。必须在Synchronized中使用
办法2:Condition机制。Lock 锁

案例:

任务队列:

package com.yj.juc.demo01;

/**
 * 任务队列
 */
public class MyQueue {
    // 存放任务的队列
    private String [] queue;
    // 添加时元素的下标
    private Integer putIndex = 0;
    // 获取时元素的下标
    private Integer getIndex = 0;
    // 元素的个数
    private Integer size = 0;

    // 锁对象
    private static final Object lock = new Object();


    public MyQueue(Integer size){
        this.queue = new String[size];
    }

    /**
     * 当前方法存在的问题:
     * 1、在单线程的情况下没有问题
     * 2、多线程的情况可能会出现多个线程同时阻塞,然后同时被唤醒。那么添加的元素就有可能超过队列的长度。
     * 解决方案:
     * 1、唤醒之后重新获取锁,获取成功则添加元素
     * @param element
     */
    public synchronized void put(String element){
        // 如果队列中的元素已经存满,则阻塞
        if (size == queue.length){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        queue[putIndex++] = element;
        ++size;
        putIndex = putIndex % queue.length;
        notify();
    }

    public synchronized String get(){
        if (size == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String result = queue[getIndex++];
        --size;
        getIndex = getIndex % queue.length;
        notify();
        return result;
    }
}

消费者:

package com.yj.juc.demo01;

import java.util.Random;

public class MyConsumer implements Runnable {

    private MyQueue myQueue;

    public MyConsumer(MyQueue myQueue){
        this.myQueue = myQueue;
    }


    @Override
    public void run() {
        while (true){
            try {
                String result = myQueue.get();
                System.out.println("消费者消费消息:" + result);
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

生产者:

package com.yj.juc.demo01;

import java.util.Random;

public class MyProvider implements Runnable {

    private MyQueue myQueue;

    public MyProvider(MyQueue myQueue){
        this.myQueue = myQueue;
    }


    @Override
    public void run() {
        int index = 0;
        while (true){
            try {
                String temp = "生产者生产消息"+index++;
                myQueue.put(temp);
                System.out.println(temp);
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

当前存在的问题:在多线程情况下有可能会通知多个线程。
解决方案:
在通知唤醒之后,尝试获取锁,获取成功才能执行对应的操作。

package com.yj.juc.demo01;

public class MyQueue2 extends MyQueue {

    // 存放任务的队列
    private String [] queue;
    // 添加时元素的下标
    private Integer putIndex = 0;
    // 获取时元素的下标
    private Integer getIndex = 0;
    // 元素的个数
    private Integer size = 0;

    // 锁对象
    private static final Object lock = new Object();


    public MyQueue2(Integer size){
        super(size);
    }

    /**
     * 当前方法存在的问题:
     * 1、在单线程的情况下没有问题
     * 2、多线程的情况可能会出现多个线程同时阻塞,然后同时被唤醒。那么添加的元素就有可能超过队列的长度。
     * 解决方案:
     * 1、唤醒之后重新获取锁,获取成功则添加元素
     * @param element
     */
    public synchronized void put(String element){
        // 如果队列中的元素已经存满,则阻塞
        if (size == queue.length){
            try {
                wait();
                put(element);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            put1(element);
            notify();
        }

    }

    private void put1(String element) {
        queue[putIndex++] = element;
        ++size;
        putIndex = putIndex % queue.length;
    }

    public synchronized String get(){
        if (size == 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return get();
        }else{
            String result = get1();
            notify();
            return result;
        }
    }

    private String get1() {
        String result = queue[getIndex++];
        --size;
        getIndex = getIndex % queue.length;
        return result;
    }
}

当前代码还存在的问题:
notify()通知唤醒是随机唤醒的。

十一、interrupt() 和 interruptedException()

interrupt():用来打断正在睡眠的线程,如wait()、sleep()
interruptedException():只有在方法上声明了interruptException 异常,被打断的话就会抛出异常。

十二、轻量级阻塞和重量级阻塞

轻量级阻塞:能够被中断的阻塞称为轻量级阻塞。对应的线程状态为WAITING;
重量级阻塞:synchronized 不能被中断的阻塞称为重量级阻塞,对应的状态为Block;

在这里插入图片描述

十三、线程的interrupt、interrupted 、isInterrupted 区别?

总结:
1、interrupt:置线程的中断状态
2、isInterrupt:线程是否中断
3、interrupted:返回线程的上次的中断状态,并清除中断状态

十四、线程如何优雅关闭

14.1、stop、destory方法

这两个方法会强制杀死线程,比如线程运行到了一半被强制杀死?
问题:
这些方法不建议使用,因为强制杀死线程,则线程中所使用的资源,例如文件资源、网络连接等都无法关闭。
因此,一个线程一旦运行中,则应该尽量让他运行完,合理的释放资源,不要强行关闭。
如果是一个不断循环运行的线程,就需要用到线程通信机制,让主线程通知其退出。

14.2、使用守护线程

因为当用户线程执行完之后,守护线程也会结束。

14.3、设置关闭的标志位

通过标志位的方式,停止正在循环运行的线程

public class MyThread extends Thread{

private boolean flag = true;

@Override
public void run(){
	while(flag){
		// do ...
	}
}

public void stop(){
	this.flag = false;
}

}

但是当前代码存在一个问题:
如果Mythread 在while 循环里被阻塞了,例如里面调用了Object.wait(),则永远都无法执行到while(false),也就会一直无法退出。此时就可以用到interrupt()或interruptException()。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值