Java教程

Java并行程序基础

本文主要是介绍Java并行程序基础,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

多线程

几个概念

并行(Parallelism)、并发(Concurrency)

并行:多任务同时进行

并发:多任务快速交替进行

进程、线程

临界区

共享资源,可被多个线程使用,每一次只能有一个线程使用。

阻塞与非阻塞

阻塞:一个线程占用临界区资源,导致其他线程挂起。

非阻塞:所有线程都会尝试不断前向执行。

死锁(Deadlock),饥饿(Starvation),和活锁(Livelock)

死锁:线程互相阻塞。

饥饿:一个或多个线程无法获得所需要的资源,导致无法执行。

活锁:线程互相“谦让”,主动释放资源,导致资源不断在两个线程间跳动,没有一个线程可以同时拿到所有资源正常执行。

并发级别

根据控制并发的策略,把并发的级别分为:$\begin{cases}阻塞\无饥饿\无障碍\无锁\无等待\end{cases}$

阻塞

一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。

无饥饿(Starvation-Free)

对于非公平锁来说,系统允许高优先级的线程插队。这样有可能导致低优先级线程产生饥饿。但如果锁是公平的,按照先来后到的原则,那么饥饿就不会产生。

无障碍(Obstruction-Free)

无障碍是一种最弱的非阻塞调度。两个线程如果无障碍地执行,那么不会因为临界区的问题导致一方被挂起。如果遇到数据改坏的问题,无障碍线程就会对修改进行回滚。

无锁(Lock-Free)

无锁的并行都是无障碍的。 不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。

无等待(Wait-Free)

无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。

Java内存模型:JMM

JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。

原子性(Atomicity)

原子性是指一个操作是不可中断的。

对于32位系统来说,long型数据的读写不是原子性的(因为long型数据有64位)

可见性(Visibility)

可见性是指当一个线程修改了某一个共享变量的值时,其他线程能否立即知道这个修改。

有序性(Ordering)

有序性问题的原因是程序在执行时,可能会进行指令重排,重排后的指令与原指令顺序未必一致

指令重排

之所以需要指令重排,就是为了尽量少地中断流水线。指令重排对于提高CPU处理性能是十分必要的。虽然带来了乱序的问题,但是这点牺牲是完全值得的。

指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致。

不能重排的指令(Happen-Before)规则
  • 程序顺序原则:一个线程内保证语义的串行性。
  • volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性。
  • 锁规则:解锁必然发生在随后发生的加锁前。
  • 传递性
  • start()最前
  • 线程的终结最后
  • 线程的中断先于中断线程的代码。
  • 对象的构造函数的执行、结束先于finalize()方法。

Java并行程序基础

线程基础

线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。

线程的状态

线程的所有状态都在Thread中的State枚举中定义

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED
}
  • 如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁
  • $\begin{cases} WAITING:无时间限制的等待\TIMED_WAITING有时限的等待\end{cases}$
  • WAITING的线程正在等待一些特殊的事件,比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。

线程的基本操作

不要用 run()方法来开启新线程,它只会在当前线程中串行执行run()方法中的代码

创建线程

  1. 匿名内部类:
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello, I am t1");
    }
};
t1.start();
  1. 继承Thread类,重写run()
  2. 继承Runnable接口。

Thread类有一个非常重要的构造方法:

public Thread(Runnable target)

默认的Thread.run()方法就是直接调用内部而Runnable接口,因此,使用Runnable接口告诉线程该做什么,更为合理。

线程的优先级
/* 
 * 分时调度模型    平均分配
 * 抢占式调度模型  谁优先级高谁先执行
 * java采用的是抢占式调度模型。
 * 
 * public final int getPriority():返回线程的优先级。
 * 线程的默认优先级是5
 * 
 * public final void setPriority(int newPriority):
 * 更改线程的优先级。线程的优先级范围是:1-10。
 * 
 * 线程的优先级高,不代表一定会先执行完毕。只有在次数特别多的情况下,
 * 才能体现出来。
 */

终止线程

Thread提供了一个stop()方法。但是stop()方法被废弃而不推荐使用。原因是stop()方法过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

  • Thread.stop()方法在结束线程时,会直接终止线程,并立即释放这个线程所持有的锁,而这些锁恰恰是用来维持对象一致性的。

线程中断

线程中断不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!

三个与线程中断有关的方法:

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

Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并处理它,当线程在sleep()休眠时,如果被中断,这个异常就会发生。

try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    System.out.println("Interrupted when sleep");
    //设置中断状态
	Thread.currentThread().interrupt();
}

Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,所以在异常处理中要再次设置中断标记位

等待(wait)和通知(notify)

wait() notify()两个方法并不是在Thread类中的,而是在Object类。这意味着任何对象都可以调用这两个方法。签名如下:

public final void wait() throws InterruptedException
public final native void notify()

Object.wait()方法不能随便调用。它必须包含在对应的synchronized语句中,无论是wait()方法或者notify()方法都需要首先获得目标对象的一个监视器

注意:Object.wait() 和Thread.sleep()方法都可以让线程等待若干时间。除wait()方法可以被唤醒外,另外一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

挂起(suspend)和继续执行(resume)线程

它们早已被标注为废弃方法。

suspend()方法在导致线程暂停的同时,并不会释放任何资源。此时其他任何线程想要访问被它占用的锁时,都会被牵连,导致无法正常继续运行。

如果resume()意外在suspend()前执行,它占用的锁不会被释放,可能会导致整个系统工作不正常。

而且对于被挂起的线程,从线程状态上看,还是Runnable,这也会严重影响我们对系统当前状态的判断。

可以利用wait()方法和notify()方法,在应用层面实现suspend()方法和resume()方法功能。

public static class ChangeObjectThread extends Thread {
        volatile boolean suspendme = false;

        public void suspendMe() {
            suspendme = true;
        }

        public void resumeMe() {
            suspendme = false;
            synchronized (this) {
                notify();
            }
        }

        @Override
        public void run() {
            while (true) {

                synchronized (this) {
                    while (suspendme)
                        try {
                            wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                }
                synchronized (u) {
                    System.out.println("inChangeObjectThread");
                }
                Thread.yield();
            }
        }
    }

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

//无限等待,会一直阻塞当前线程
public final void join() throws InterruptedException
//有等待最大时间
public final synchronized void join(long millis) throws InterruptedException
public static native void yield()//使当前线程让出CPU

volatile与Java内存模型(JMM)

当使用关键字volatile声明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点

volatile不能代替锁,它也无法保证一些复合操作的原子性。

线程组

//创建线程组
var group = new ThreadGroup("GroupName");
//创建线程并加入线程组
var t1 = new Thread(ThreadGroup group, Runnable target, String name);

线程组方法:

  1. activeCount()方法可以获得活动线程的总数,由于线程是动态的,这个值只是一个估计值,无法精确。
  2. list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助

守护线程(Daemon)

守护线程是一种特殊的线程,在后台默默完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。与之相对应的是用户线程,用户线程可以认为是系统的工作线程。

如果用户线程全部结束,则意味着这个程序实际上无事可做了。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机就会自然退出

public class DaemonDemo {

    public static void main(String[] args) throws InterruptedException {
        var t = new Thread(() -> {
            while (true) {
                System.out.println("I am alive");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.setDaemon(true);
        t.start();
        Thread.sleep(3000);
    }
}

线程优先级

在Java中,使用1到10表示线程优先级。一般可以使用内置的三个静态标量表示:

public final static int MIN_PRIORITY = 1;
public final static int NROM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

更高优先级倾向于更快完成。可能会导致低优先级线程的饥饿。

线程安全的概念与关键字synchronized

volatile 并不能真正保证线程安全。它只能确保一个线程修改了数据后,其他线程能够看到这个改动。但当两个线程同时修改某一个数据时,依然会产生冲突。

两个线程同时对i进行写入时,其中一个线程的结果会覆盖另外一个的。

关键字synchronized的作用是实现线程间的同步,它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。

关键字synchronized可以有多种用法

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法,相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁

并发下的ArrayList

ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另外一个线程访问到了不一致的内部状态,导致出现越界问题。

改进方法:使用线程安全的Vector代替ArrayList即可

并发下诡异的HashMap

JDK8中修复了死循环的问题。

贸然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方法是使用ConcurrentHashMap代替HashMap.

这篇关于Java并行程序基础的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!