多线程与并发编程

多线程与并发编程

并发与并行

**并发:**多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是
同时执行。
**并行:**单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
**串行:**有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。

做一个形象的比喻:
并发 = 俩个人用一台电脑。
并行 = 俩个人分配了俩台电脑。
串行 = 俩个人排队使用一台电脑。

为什么使用并发编程
  • 提升多核cpu的利用: 现在的电脑基本都支持多线程,这样如果还是一个单线程应用的话,那2核cpu就会浪费50%,4核就会浪费75%
  • 方便进行业务拆分,提升应用性能
应用场景

数据库连接池、分批发送短信等;

并发编程缺点

使用多线程也会遇到很多问题,比如内存泄露、上下文的切换、线程安全、死锁、资源的共享等;

多线程缺点
  • **浪费资源:**线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
  • 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
  • 资源竞争: 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
三要素:原子性、可见性、有序性
  • **原子性:**原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • **可见性:**一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
  • **有序性:**程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
  • 对此,出现线程安全问题的原因一般都是这三个原因:
    • 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock);
    • 缓存导致可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题;
    • 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题;
死锁条件
  • 互斥条件: 在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。
  • 占有且等待条件: 进程已经保持了一个资源,但有***需要新的资源***,而该资源却被其他进程占用,此时该进程就会阻塞,等待其资源的释放,并且又不释放自己的资源。
  • 不可抢占条件: 进程占用了某个资源,其他应用不能应为自己需要该资源而去抢占资源。
  • 循环等待条件: 多个进程之间形成一种头尾相接的循环等待资源的关系。(比如一个进程集合,A在
    等B,B在等C,C在等A)
  • 对此,解决死锁的方式:
    • 避免一个线程同时获得多个锁
    1. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    2. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
线程的创建方式
  • 继承 Thread 类;

    public class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("....");
        }
        public static void main(String[] args){
    		MyThread myThread = new MyThread();
            myThread.start();
        }
    }
    
  • 实现 Runnable 接口;

    public class MyThread implements Runnable{
        @Override
        public void run(){
            System.out.println("....");
        }
        public static void main(String[] args){
            Thread thread = new Thread(new MyThread());
            thread.start();
        }
    }
    
  • 实现 Callable 接口;

    public class MyThread implements Callable<Integer>{
        @Override
        public Integer call(){
            System.out.println("...");
            return 1;
        }
        public static void main(String[] args){
            FuterTask futureTask = new FutureTask(new MyThread());
            Thread thread = new Thread(futureTask);
            thread.start();
        }
    }
    
  • 使用匿名内部类方式

    public class MyThread{
        public static void main(String[] srgs){
    		Thread thread = new Thread(new Runnale(){
                @Override
                public void run(){
                    System.out.println("...");
                }
            });
            thread.start();
        }
    }
    
runnable与callable有什么区别

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

主要区别:

  • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  • Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法
  • new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程工作。
  • 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

Callable 和 Future
  • Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
  • Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果Future 用于获取结果
FutureTask

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调
用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

callable返回值–>future获取值–>futuretask对值进行操作

在这里插入图片描述

  • 新建(new):新创建了一个线程对象。

  • **就绪(可运行状态)(runnable)**线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。

  • **运行(running):**可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处
    于就绪状态中;

  • **阻塞(block):**处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
    阻塞的情况分三种:

    • (一). **等待阻塞:**运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waittingqueue)中,使本线程进入到等待阻塞状态;
    • (二). **同步阻塞:**线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    • (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • **死亡(dead)(结束):**线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束
    生命周期。死亡的线程不可再次复生。

Java 中用到的线程调度算法是什么
  • 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用
    权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
    (Java是由JVM中的线程计数器来实现线程调度)
  • 有两种调度模型:分时调度模型和抢占式调度模型。
    • 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解。
    • Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

一个平均,一个按优先级。

  • 线程体中调用了 yield 方法让出了对 cpu 的占用权利
  • 线程体中调用了 sleep 方法使线程进入睡眠状态
  • 线程由于 IO 操作受到阻塞
  • 另外一个更高优先级线程出现
  • 在支持时间片的系统中,该线程的时间片用完
sleep() 和 wait() 有什么区别
  • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
在 Java 程序中怎么保证多线程的运行安全
  • 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger

  • 方法二:使用自动锁 synchronized。

  • 方法三:使用手动锁 Lock。

  • 手动锁 Java 示例代码如下:

    Lock lock = new ReentrantLock();
    lock.lock;
    try {
        System.out.println("获得锁");
    } catch (Exception e){
        // TODO: handle exception
    } finally{
        System.out.println("释放锁");
        lock.unlock();
    }
    

注:线程类中的构造方法和静态块是被new这个类所在的线程调用,只有run()方法里的代码才是被线程自身所调用的

多线程的常用方法
方法名描述
sleep()强迫一个进程睡眠n秒
isAlive()判断一个进程是否存活
join()等待线程终止
activeCount()程序中活跃的线程数
enumerate()枚举程序中的线程
currentThread()得到当前线程
isDaemon()一个线程是否为守护线程
setDaemon()设置一个线程为守护线程
wait()强迫一个线程等待
setName()为线程设置一个名称
notify通知一个线程继续运行
setPriority()设置一个线程的优先级
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值