java并行程序基础

目录

基本概念

并发级别

线程的基本状态

线程的基本操作

volatile与Java内存模型(JVM)

线程组

守护线程(Daemon)

线程安全和synchronized关键字


基本概念

同步和异步:用来形容一次方法的调用

  • 同步:同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步:异步方法通常会在另一个线程中“真实”地执行。整个过程不会阻碍调用者的工作。

并发和并行

  • 并发:多个任务交替执行,而多个任务之间有可能还是串行的。
  • 并行:多个任务“同时执行”。注:真实地并行只可能出现在拥有多个CPU的系统中。

临界区

  • 临界区:共享数据,可以被多个线程使用,但是每次只能有一个线程使用。

阻塞和非阻塞:用来形容多线程间的互相影响

  • 阻塞:如果一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在临界区中等待。
  • 非阻塞:没有一个线程可以妨碍其它线程执行。

死锁、饥饿和活锁:多线程的活跃性问题

  • 死锁:指多个进程在运行过程中因争夺资源而造成相互等待。
  • 饥饿:一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
  • 活锁:线程主动将资源释放给他人使用,出现资源不断在两个线程中跳动,而没有一个线程可以拿到所有资源而正常执行。

并发级别

  • 阻塞:一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。
  • 无饥饿:对于非公平的锁来说,系统允许高优先级的线程插队,可能会导致低优先级线程产生饥饿。但如果锁是公平的,满足先来后到,那么饥饿就不会产生。
  • 无障碍:这是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么它们不会因为临界区的问题导致一方被挂起。如果发现数据异常,就会对之前的操作进行回滚,确保数据安全。当临界区资源存在严重冲突时,所有的线程可能都会不断回滚自己的操作,而没有一个线程可以走出临界区。可行的无障碍策略:“一致性标记”。操作之前,线程先读取并保持这个标记,操作完成之后,再次读取看释放别更改过,如果两者一致,说明资源访问没有冲突,如果不一致,说明资源可能在操作过程中与其他写线程冲突,需要回滚重试。并且,任何对资源有修改操作的线程,都需要在修改数据之前,更新这个一致性标记,表示数据不再安全。
  • 无锁:无锁就是无障碍,同时保证了必然有一个线程能够在有限步内完成操作离开临界区。在无锁的调用中,可能会包含一个无限循环。循环中,线程会不断尝试修改共享变量。而对于竞争失败的线程,可能会出现饥饿的情况。
  • 无等待:无等待在无锁的基础上进一步扩展,它要求所有线程都必须在有限步内完成,这样就不会引起饥饿问题。RCU(Read-Copy-Update)是一种典型的无等待结构:所有的读线程都是无等待,但在写数据时,先取得原始数据的副本,接着只修改副本数据,修改完成后,在合适的时机回写数据。

线程的基本状态

  • 新建( new ):新创建了一个线程对象。 
  • 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。 
  • 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
  • 阻塞( blocked):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种: 等待阻塞,运行( running )的线程执行 o.wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。 其他阻塞: 运行( running )的线程执行 Thread.sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。 当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。 
  • 死亡( terminated):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程的基本操作

实现线程:继承Thread类或实现Runnable接口

终止线程:Thread.stop(),这是一个已经被被标注为废弃的方法,用stop()方法强行把执行的线程终止,可能会引起一些数据不一致的问题。它在结束线程时,会直接终止线程,并且会释放这个线程所持有的锁。

public class MyThread implements Runnable{
    volatile boolean stop = false;
    public void stopMe(){//增加一个方法来自行决定线程何时退出
        stop = true;
    }
    @Override
    public void run() {
        while(true){
            if(stop)
                break;
            synchronized (u){
            ...
            }
        }
    }
}

线程中断:线程中断并不会使线程立即退出,而是设置中断标志位,至于目标线程如何处理,则完全由目标线程自行决定。

public void Thread.interrupt()  //中断线程
public boolean Thread.isInterrupted()  //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

注意:如果Thread.sleep()方法由于中断而抛出异常,此时它会清除中断标记,应该在异常捕获中再次设置中断标志,以免无法捕获这个中断。

等待(wait)和通知(notify):object.wait()和object.notify()都需要首先获得目标对象的一个监视器

  • 如果一个线程调用了object.wait(),那么它会进入object对象的等待队列
  • 当object.notify()被调用时,它会从这个等待队列中随机唤醒一个线程(这个随机是不公平的,并不是先等待的会先选择)
  • 当object.notifyAll()被调用时,它会唤醒这个等待队列中的所有等待的线程

 object.wait()和Thread.sleep()的区别

  • object.wait():可以被唤醒,并且会释放目标对象的锁
  • Thread.sleep():不会释放任何资源

挂起(suspend)和继续执行(resume)线程:suspend()和resume()也是被被标注为废弃的方法

  • suspend()在导致线程暂停的同时,并不会释放任何锁资源,直到对应的线程进行了resume()操作,被挂起的线程才能继续
  • 如果resume()意外地在suspend()前执行,那么被挂起的线程很难有机会被继续执行,并且它所占用的锁也不会被释放,因此可能导致整个系统工作不正常。而且,对于被挂起的线程的状态为Runnable,严重影响我们对系统当前状态的判断 

等待线程结束(join)和谦让(yield)

  • join():阻塞当前线程,直到目标线程执行完毕。join的本质是让调用线程wait()在当前线程对象实例上,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()通知所有等待线程继续执行。因此,不要在Thread对象实例上使用类似wait()或者notify()等方法。
public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
  • Thread.yield():一旦执行,它会使当前线程让出CPU,但是当前线程还是会进行CPU资源的争夺。如果一个线程不是那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。
public static native void yied();

volatile与Java内存模型(JVM)

java内存模型是围绕原子性、可见性和有序性展开的。volatile关键字修饰一个变量:

  • 保证数据的可见性和有序性:在虚拟机的Client模式下,主线程修改变量其它线程可以发现变动;但在Server模式下,由于系统优化的结果,其它线程无法看到主线程的修改。用volatile关键字修饰变量,就可确保这个变量被修改后,应用程序范围内的所有线程都能看到这个改动。
  • volatile对于保证操作的原子性有很大的帮助,但无法保证原子性操作。

线程组

在一个系统中,如果线程数量很多,而且功能分配比较明确,就可以将相同功能的线程放置在一个线程组里。

public class MyThread implements Runnable{
    public static void main(String[] args) {
        ThreadGroup tg = new ThreadGroup("printGroup");
        Thread t1 = new Thread(tg,new MyThread(),"T1");//通过Thread的构造函数指定线程所属的线程组
        Thread t2 = new Thread(tg,new MyThread(),"T2");
        t1.start();t2.start();
        tg.activeCount();//获得活动线程总数,但是线程是动态的,这个值是一个估计值
        tg.list();//可以打印这个线程组中所有的线程信息,可帮助调试
    }

    @Override
    public void run() {
    }
}

 注:1. 不要使用stop(),他会停止线程组中的所有线程,但它会遇到和Thread.stop() 相同的问题;
2.  在创建线程组和线程的时候,尽量给它们取有意义的名字。

守护线程(Daemon)

守护线程是一种特殊的线程,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之对应的是用户线程,用户线程是系统的工作线程,它会完成这个程序应该完成的业务操作。当一个java应用内,只有守护线程时,java虚拟机会自然退出。

注意:设置守护线程必须在线程start()之前,否则会抛出异常但程序依然能正常执行,该线程会被当作用户线程。

thread.setDeamon(true); //设置thread为守护线程

线程安全和synchronized关键字

线程安全:在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。

synchronized用来实现线程之间的同步。它的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块。

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例对象的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值