ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

05、Java进阶--多线程编程

2021-07-08 07:31:46  阅读:135  来源: 互联网

标签:volatile Java 进阶 int void 线程 accounts 多线程 public


多线程编程

进程和线程

进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。

在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

多线程的好处:

1、使用多线程可以减少程序的响应时间。

2、与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。

3、多CPU或者多核计算机本身就具备执行多线程的能力。

4、使用多线程能简化程序的结构,使程序便于理解和维护。

线程的状态

Java线程在运行的声明周期中可能会处于6种不同的状态,这6种线程状态分别为如下所示。

New:新创建状态。线程被创建,还没有调用 start 方法,在线程运行之前还有一些基础工作要做。

Runnable:可运行状态。一旦调用start方法,线程就处于Runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

Blocked:阻塞状态。表示线程被锁阻塞,它暂时不活动。

Waiting:等待状态。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度器重新激活它。

Timed waiting:超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的。

Terminated:终止状态。表示当前线程已经执行完毕。

导致线程终止有两种情况:

第一种就是run方法执行完毕正常退出;

第二种就是因为一个没有捕获的异常而终止了run方法,导致线程进入终止状态。

线程的状态如图:

线程创建后,调用 Thread 的 start 方法,开始进入运行状态,当线程执行wait 方法后,线程进入等待状态,进入等待状态的线程需要其他线程通知才能返回运行状态。超时等待相当于在等待状态加上了时间限制,如果超过时间限制,则线程返回运行状态。当线程调用到同步方法时,如果线程没有获得锁则进入阻塞状态,当阻塞状态的线程获取到锁时则重新回到运行状态。当线程执行完毕或者遇到意外异常终止时,都会进入终止状态。

创建线程

多线程的实现一般有以下3种方法:

1、继承Thread类,重写run()方法

public class Main {
    public static void main(String[] args) {
        Thread thread = new TestThread();
        thread.start();
    }

    public static class TestThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    }
}

2、实现Runnable接口,实现接口的run()方法

public class Main {
    public static void main(String[] args) {
        TestRunnable runnable = new TestRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }

    public static class TestRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello World");
        }
    }
}

3、实现Callable接口,重写call()方法

public class Main {
    public static void main(String[] args) {
        TestCallable callable = new TestCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(callable);
        try {
            // 等待线程结束,并返回结果
            System.out.println(future.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static class TestCallable implements Callable {
        @Override
        public String call() throws Exception {
            return "Hello World!";
        }
    }
}

一般推荐用实现Runnable接口的方式,其原因是,一个类应该在其需要加强或者修改时才会被继承。

线程中断

当线程的run方法执行完毕,或者在方法中出现没有捕获的异常时, 线程将终止。 interrupt方法可以用来请求中断线程, 当一个线程调用interrupt方法时, 线程的中断标识将被标记为true, 线程会不时检测这个中断, 以判断线程是否应该被中断。

while(!Thread.currentThread().isInterrupter()){
	doSomethingHere();
}

还可以调用Thread.interrupted()来对中断标识为进行复位。 但是如果一个线程被阻塞, 就无法检测中断状态。

如果一个线程处于阻塞状态, 那么线程在检查中断标识位时若发现中断标识位为true, 则会在阻塞方法调用出抛出InterrupetedException异常, 并且在抛出异常前将线程中断标识位标记位false,被中断的线程可以决定如何去响应中断。 如果是比较重要的线程,则不理会中断, 而大部分情况则是线程会将中断作为一个终止的请求。 另外, 不要在底层代码里捕获InterruptedException。这里有两种方式来处理:

1、在catch语句中,调用Thread.currentThread.interrupt()来设置中断状态

try{
	sleep(50);
}catch(InterruptedException e){
	Thread.currentThread.interrupt();
}

2、不使用try来捕获异常, 让方法直接抛出, 这样调用者可以捕获这个异常

void task throw InterruptedException {
    sleep(50);
}

安全终止线程

使用中断来终止线程,代码如下:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MoonRunnable runnable = new MoonRunnable();
        Thread thread = new Thread(runnable, "MoonThread");
        thread.start();
        TimeUnit.MICROSECONDS.sleep(10);
        thread.interrupt();
    }

    public static class MoonRunnable implements Runnable {
        private long i;

        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                i++;
                System.out.println("i=" + i);
            }
            System.out.println("stop");
        }
    }
}

调用了sleep方法使得main线程睡眠10ms,这是为了留给MoonThread线程时间来感知中断从而结束。

除了中断,还可以采用boolean变量来控制是否需要停止线程,代码如下:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        MoonRunnable runnable = new MoonRunnable();
        Thread thread = new Thread(runnable, "MoonThread");
        thread.start();
        TimeUnit.MICROSECONDS.sleep(10);
        runnable.cancel();
    }

    public static class MoonRunnable implements Runnable {
        private long i;
        private volatile boolean on = true;
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                i++;
                System.out.println("i=" + i);
            }
            System.out.println("stop");
        }

        public void cancel(){
            on = false;
        }
    }
}

线程同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法, 这种情况通常被称为竞争条件。

重入锁与条件对象

大多数需要显式锁的情况使用synchronized非常方便,而重入锁ReentrantLock是JavaSE 5.0引入的,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。用ReentrantLock保护代码块的结构如下所示:

Lock lock = new ReentrantLock();
lock.lock();
try{
    ......
}finally{
    lock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。

如果在临界区发生了异常,锁是必须要释放的,否则其他线程将会永远被阻塞。

进入临界区时,却发现在某一个条件满足之后,它才能执行。这时可以使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又被称作条件变量。

假设一个场景需要用支付宝转账。我们首先写了支付宝的类,它的构造方法需要传入支付宝账户的数量和每个 账户的账户金额:

public class Alipay {
    private double[] accounts;
    private Lock alipayLock;
    public Alipay(int n, double money) {
        accounts = new double[n];
        alipayLock = new ReentrantLock();
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }
}

接下来我们要转账,写一个转账的方法,from是转账方,to是接收方,amount是转账金额,如下所示:

public void transfer(int from, int to, int amount){
    alipayLock.lock();
    try {
        while(accounts[from] < amount){
            // wait
        }
    }finally {
        alipayLock.unlock();
    }
}

结果我们发现转账方余额不足;如果有其他线程给这个转账方再转足够的钱,就可以转账成功了。但是这个线程已经获取了锁,它具有排他性,别的线程无法获取锁来进行存款操作,这就是我们需要引入条件对象的原因。

一个锁对象拥有多个相关的条件对象,可以用newCondition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁。

public class Alipay {
    private double[] accounts;
    private Condition condition;
    private Lock alipayLock;
    public Alipay(int n, double money) {
        accounts = new double[n];
        alipayLock = new ReentrantLock();
        //  得到条件对象
        condition = alipayLock.newCondition();
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }

    public void transfer(int from, int to, int amount) throws InterruptedException {
        alipayLock.lock();
        try {
            while(accounts[from] < amount){
                // 阻塞当前线程,并放弃锁
                condition.await();
            }
            // 转账的操作
            accounts[from] = accounts[from] - amount;
            accounts[to] = accounts[to] + amount;
            condition.signalAll();
        }finally {
            alipayLock.unlock();
        }
    }
}

当调用signalAll方法时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。

同步方法

如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

public synchronized void method(){
    ......
}

相当于重入锁代码:

lock lock = new ReentrantLock();
public void method(){
    lock.lock();
    try{
        ......
    }finally{
        lock.unlock();
    }
}

内部对象锁只有一个相关条件,wait方法将一个线程添加到等待集中,notifyAll或者notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.await(),notifyAll等价于condition.signalAll();

上面例子中的transfer方法也可以这样写:

public synchronized void transfer (int from, int to, int amount) throws InterruptedException {
    while(accounts[from] < amount){
        wait();
    }
    // 转账的操作
    accounts[from] = accounts[from] - amount;
    accounts[to] = accounts[to] + amount;
    notifyAll();
}

可以看到使用 synchronized 关键字来编写代码要简洁很多。

同步代码块

每一个Java对象都有一个锁,线程可以调用同步方法来获得锁。还有另一种机制可以获得锁,那就是使用一个同步代码块,如下所示:

synchronized(obj){
    ......
}

获得了obj的锁,obj指的是一个对象。再来看看Alipay类,我们用同步代码块进行改写。

public void transfer1(int from, int to, int amount) throws InterruptedException {
    synchronized (this){
        // 转账的操作
        accounts[from] = accounts[from] - amount;
        accounts[to] = accounts[to] + amount;
    }
}

volatile

volatile关键字为实例域的同步访问提供了免锁的机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

Java内存模型

Java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,因此,它存在内存可见性的问题。而局部变量、方法定义的参数则不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java 内存模型定义了线程和主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。

Java 内存模型的抽象示意图如图:

线程A与线程B之间若要通信的话,必须要经历下面两个步骤:

(1)线程A把线程A本地内存中更新过的共享变量刷新到主存中去。
(2)线程B到主存中去读取线程A之前已更新过的共享变量。

并发三概念

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。

volatile关键字

一个共享变量被volatile修饰之后,就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其他线程是立即可见的。

换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止使用指令重排序。

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。

下面我们来看一段代码,假设线程1先执行,线程2后执行,如下所示:

// 线程1
boolean stop = false;
while(!stop){
    // doSomething
}

// 线程2
stop = true;

这段代码不一定会将线程中断。虽说无法中断线程这个情况出现的概率很小,但是一旦发生这种情况就会造成死循环。

当stop用volatile修饰之后,当线程2进行修改时,会强制将修改的值立即写入主存,并且会导致线程1的工作内存中变量stop的缓存无效,这样线程1再次读取变量stop的值时就会去主存读取。

volatile关键字能禁止指令重排序,因此volatile能保证有序性。

volatile关键字在某些情况下的性能要优于synchronized。但是要注意volatile关键字是无法替代synchronized关键字的, 因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下两个条件:

(1)对变量的写操作不会依赖于当前值。

(2)该变量没有包含在具有其他变量的不变式中。

第一个条件就是不能是自增、自减等操作,上文已经提到volatile不保证原子性。关于第二个条件,我们来举一个例子,它包含了一个不变式:下界总是小于或等于上界,代码如下所示:

public class NumberRange {
    private volatile int lower, upper;

    public int getLower() {
        return lower;
    }

    public void setLower(int lower) {
        if (lower > this.lower){
            throw new IllegalArgumentException();
        }
        this.lower = lower;
    }

    public int getUpper() {
        return upper;
    }

    public void setUpper(int upper) {
        if (upper > this.upper){
            throw new IllegalArgumentException();
        }
        this.upper = upper;
    }
}

这种方式将lower和upper字段定义为volatile类型不能够充分实现类的线程安全。如果当两个线程在同一时间使用不一致的值执行setLower和setUpper的话,则会使范围处于不一致的状态。

例如,如果初始状态是(0,5),在同一时间内,线程A调用setLower(4)并且线程B调用setUpper(3),虽然这两个操作交叉存入的值是不符合条件的,但是这两个线程都会通过用于保护不变式的检查,使得最后的范围值是(4,3)。

因此使用 volatile 无法实现setLower和setUpper操作的原子性。

使用volatile有很多种场景,这里介绍其中的两种:

(1)状态标志

volatitle boolean shutdownRequested;
......
public void shutdown(){
    shutdownRequested = true;
}    
public void doWork(){
    while(!shutdownRequested){
        ......
    }
}

如果在另一个线程中调用 shutdown 方法,就需要执行某种同步来确保正确实现shutdownRequested 变量的可见性。

(2)双重检查模式(DCL)

public class Singleton {
    private volatitle static Singleton instance;
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(this){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

getInstance方法中对Singleton进行了两次判空,第一次是为了不必要的同步,第二次是只有在Singleton等于null的情况下才创建实例。

标签:volatile,Java,进阶,int,void,线程,accounts,多线程,public
来源: https://www.cnblogs.com/pengjingya/p/14984420.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有