《Java编程思想》第二十一章 并发(一)

目录

前言:

1. 并发的多面性

2. 线程的机制

2.1 定义任务

2.2 使用Executor

2.3 从任务中返回值

2.4 线程休眠

2.5 线程的优先级

2.6 线程的让步和后台线程

2.7 加入一个线程

3.共享受限资源

3.1 解决共享资源竞争

3.2 原子性和易变性

总结


前言:

本系列是我本人阅读java编程思想这本书的读书笔记,主要阅读第五章到第十七章以及第二十一章的内容,今天的笔记是第二十一章

到目前为止,本书所讲到的知识都是有关于顺序编程的,也就是程序在任意时刻只执行一个步骤。但是,在很多情况下,能够并行的运行一个程序中的多个部分,则会变得非常方便和必要。

 

1. 并发的多面性

并发是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。 在一个时间段的线程代码运行时,其它线程处于挂起状。

线程和进程的概念

  • 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位) 。简单讲进程就是在某种程度上相互隔离的、独立运行的程序。
  • 同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

并发编程比较难掌握的一个主要原因就是使用并发的时候要解决的问题有多个,而实现并发的方法又有多种,而且这两者之间没有明确的界限。

用并发解决的问题大体上可以分为速度和设计可管理性两方面

并发是用多处理器编程的基本工具,为了使程序运行更快,必须要学习如何利用这些额外的处理器,而这就是并发赋予你的能力。

但是,并发通常提高的是单处理器上的程序性能。

从表面上看,并发因为有线程切换的开销,所以顺序运行程序好像开销更小一点,但有了阻塞,让问题变得有些不同。

阻塞是指,程序中的某个任务因为程序控制范围之外的某些条件而中断。“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域(synchronized)的时候,线程将进入这种状态。阻塞又分为三种情况,分别是同步阻塞,等待阻塞和其他阻塞

同步阻塞:运行线程在获得对象的同步锁时,如果该同步锁被其他线程所占用,则JVM会把该线程放入"锁池"中,等待其他线程释放锁。

等待阻塞:运行线程执行wait()方法,JVM会把该线程放入等待队列中。

其他阻塞:运行线程执行sleep()或者join(),或者发出了I/O请求时,JVM会把该线程置为阻塞状态。​​​​

实现并发的最直接方式就是使用操作系统的进程。

java实现并发采用的是在语言中提供对线程的支持,java的线程机制是抢占式的,所谓抢占式,指的是线程调度机制会周期性的中断线程,将上下文切换到下一个线程,从而为每个线程都提供时间片,使得每个线程都会被分配到合理数量的线程去执行它的任务。

2. 线程的机制

线程模型简化了单一程序中多个交织在一起的操作的处理,使用线程时,CPU将轮流给每个任务分配合理的时间片。

2.1 定义任务

java中有两种定义任务(实现多线程)的方式,分别为继承Thead和实现Runnable接口,下面就一起来看一下

//1.继承Thread类

Class MyThread extend Thread{
    private int count = 10;
    ...
    @Override
    public void run(){
        while(true){
            System.out.println("111");
            count--;
            if(count==0){break;}
        }
    }

}

//2.实现Runnable接口

Class MyThread implements Runnable{
    private int count = 10;
    ...
    @Override
    public void run(){
        while(true){
            System.out.println("111");
            count--;
            if(count==0){break;}
        }
    } 
}

//3.实现Runnable接口的第二种写法,使用静态代理的方式,把Thread类作为代理类
Class TestThread{
    Thread thread=new Thread(new MyThead()).start();
}
//4.实现Runnable接口的第三种写法,匿名内部类实现
Class TestThread{
    Thread t=new Thread(){
        private int count = 10;
        @Override
        public void run(){
           while(true){
               System.out.println("111");
               count--;
               if(count==0){break;}
           }
        }
    };
    t.start();
}



可以看到在代码中使用了start()方法来启动线程,那么能不能直接调用run()方法来启动线程呢,这里我们要搞清楚start()方法和run()方法的区别,run()方法是Runnable接口所定义的方法,是线程的主入口方法,绑定了操作系统,所有实现了Runnable的类都要重写run()方法。而start()方法是Thread类所定义的方法,Thread类又是实现Runnable接口的,调用start()方法的时候,会启动一个线程,此时线程处于就绪状态等待CPU调度,在调度过程中,会执行run()方法来完成实际的操作,但是如果直接调用run()方法,它就会被虚拟机当成一个普通的方法来调用,所以总结起来就是调用start()方法会启动一个新线程去执行run()中的操作实现了多线程,但是调用run()的话会在当前线程也就是主线程中执行run()中的操作,也就不是多线程了。

2.2 使用Executor

jdk自从1.5之后,提供了执行器(Executor)可以用来管理Thread对象,简化了并发编程。Executor允许管理异步任务的执行,无需显式的管理线程的生命周期。下面一起来看一下使用方法

public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        exec.execute(new Main());
    }
    exec.shutdown();
}

我们在上面的代码中看到了CachedThreadPool,这就又引出了一个概念,线程池,《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。

三种类型的线程池如下:

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的
    任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线
    程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会
    被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但
    若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新
    的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

2.3 从任务中返回值

Runnable执行工作的独立任务,但是它不能返回值。当你想要执行一个任务并在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。Callable是一个有类型参数的泛型,它的类型参数表示从call()方法中返回的值。并且必须使用ExecutorService.submit()方法调用它,下面来看一个示例。

class TaskWithResult implements Callable<String> {
    private int id;

    public TaskWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "result=" + id;
    }
}

public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        ArrayList<Future<String>> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(service.submit(new TaskWithResult(i)));
        }
        for (Future<String> fs : results) {
            try {
                System.out.println(fs.get());
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                //记住,打开线程池一定要在最后关闭
                service.shutdown();
            }
        }
    }
}

submit()方法会产生Future对象,Future类表示异步计算的未来结果,你可以使用isDone()方法来查询Future是否已经完成,用get()方法可以用来获取结果,也可以不调用isDone(),直接调用get(),它将阻塞,直到结果准备就绪。

2.4 线程休眠

调用sleep()可以使任务中止执行给定的时间。sleep()方法会抛出InteeruptedException异常,sleep()方法传入毫秒数告诉线程需要休眠多少时间,这一段时间内线程将进入阻塞状态,直到设定的休眠时间结束。

2.5 线程的优先级

线程的优先级将线程的重要性传递给了调度器,虽然CPU处理线程的顺序是随机的,但是它会倾向让更高优先级的线程先执行。但是这也不意位优先级低的线程就一定不会执行,只不过是得到时间片的频率比较低。java线程优先级一共分为10个等级,为1-10,java源码中有设置优先级的常量值,分别是

   /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

java中线程的默认优先级是5,最高是10,最低是1。

2.6 线程的让步和后台线程

在多线程任务中,如果已经完成了run()中一次循环中要做的工作,可以建议调度器让出自己的线程,把时间片交给别的线程使用,这个时候可以使用yield()方法,但是要记住,这仅仅只是建议,没有任何机制可以保证它一定会起作用。

后台线程(Daemon),又称精灵线程,是指程序运行中在后台提供通用服务的线程,而且后台线程并不是程序中必不可少的一部分,当程序中所有非后台线程结束时,程序也就结束了。设置后台线程的方法为setDeamon(),必须要在线程执行开始之前设置,像java中的垃圾回收线程就是后台线程。

2.7 加入一个线程

一个线程可以在其他线程上调用join()方法,比如在A线程中调用B线程的join()方法,那么A线程将会挂起,等待B线程先运行,直到B线程运行结束,A线程再重新开始运行。也可以在join()方法上加上超时参数,那么无论目标线程在超时时间达到时是否执行完成,join()方法都会返回。

3.共享受限资源

我们直到多线程执行任务总会碰到这样的问题,两个或者多个线程同时试图访问同一个资源,比如两个线程同时访问一个银行账户,一个线程往外转账,一个线程往里存钱,这时候就会产生线程不同步的问题。

3.1 解决共享资源竞争

我们知道,类变量都是共享变量,要想避免共享资源出现竞争冲突,那就要给资源加上锁,这样的话,当一个线程在访问一个共i想资源的时候,另一个线程由于得不到这个锁,将等待阻塞,直到第一个线程执行结束交出这把锁,第二个线程才能访问到这个共享资源,java用synchronized关键字来实现锁的效果,当synchronized关键字把代码块包围起来的时候,它会检查锁是否可用,然后获取锁,执行代码,释放锁。synchronized关键字有三种用法

  • 修饰实例方法(锁当前对象实例,进入同步代码前需要获得当前锁)
  • 修饰静态方法(修饰类和修饰类一样,锁的都是当前类对象,进入同步代码前需要获得当前锁)
  • 修饰代码块(指定加锁对象,对给定的对象加锁,进入同步代码前需要获得当前锁)
//修饰实例对象
public synchronized void method(){}
//修饰静态方法
public static synchronized void method(){}
//修饰代码块
synchronized(MyClass.class){
    //给当前Class类加锁
}
synchronized(this){
    //给当前对象实例加锁
}
//给变量加锁
public void method(Object o1){
    synchronized o1{
        //操作
    }
}
synchronized(o){
    //给当前Class类加锁
}

所有的对象都有一个隐含的锁(也叫监视器)。对象上调用其任意synchronized方法的时候,该对象都会被加锁。java中的synchronized是一个可重入锁,可重入锁的意思是,一个任务可以多次获得对象的锁,假如一个线程获得对象锁之后在运行的过程中再次获得本对象上的锁,那是可以的,但是别的线程要想获得这个锁就不行,这就是可重入锁,jvm是用计数器原理来实现可重入锁的,jvm在每一个线程上关联一把锁和一个计数器,计数器从0开始,线程每获得一次锁,就在计数器上加1,如果所有的锁被完全释放,则计数器从新回到0。你也许会问,我怎么知道什么时候应该使用synchronized关键字来执行同步代码呢,书中这样写道:

如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程必须用相同的监视器同步。

java在concurrent包下还有一种显式的互斥机制。那就是Lock对象,Lock对象必须被显式的创建、锁定和释放,使用Lock对象的代码看起来是这样

private int currentValue = 0;
    Lock lock = new ReentrantLock();//和synchronized一样,也是可重入锁

    public int next() {
        lock.lock();//显式的锁定
        try {
            ++currentValue;
            return currentValue;
        } finally {
            lock.unlock();//显式的解除锁定
        }

}

Lock对象相比synchronized的好处在于,当使用synchronized关键字的某些代码执行碰到失败时,会抛出异常,而Lock要在finally里显式的释放锁的机制使得你有机会将系统维护在一个正确的状态。还有一个好处在于,在JVM的实现上,synchronized是一个重量级的锁,而Lock是一个轻量级的锁,具体的实现和Lock的更多用法在此就不再赘述。

3.2 原子性和易变性

所谓原子性,就是说一个操作不可被分割或加塞,要么全部执行,要么全不执行。原子性可以用于除“double”和“long”之外的所有基本类型的“简单操作”,java中,原子操作可以由线程机制来保证。java在concurrent包中引入了如AtomicInteger,AtomicLong,AtomicLongArray等原子操作类,它们的底层操作形式是CAS(compare and set)。

有时,你只希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,通过这种方式分离出来的代码段叫做临界区,也就是同步块。

总结

这一章,我们学习了基本的线程机制,并发的概念以及共享资源受限时候的处理方式,由于知识点太多,我把这一章节分为了两个部分,第一部分到这就结束了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值