【Java】【并发编程】入门知识

举报
huahua.Dr 发表于 2021/08/26 21:11:46 2021/08/26
【摘要】 在Java中并发就是指多线程的进程环境,进程是系统进行资源分配和调度的独立单位,每一个进程都有它的内存空间和系统资源,在同一个进程内执行的多个任务就可以看作是多个进程,线程存在于进程内,进程负责分配调度线程,线程负责执行程序,多个线程就执行多个程序

一、基本简介

  • 什么是并发

Java中并发就是指多线程的进程环境,进程是系统进行资源分配和调度的独立单位,每一个进程都有它的内存空间和系统资源,在同一个进程内执行的多个任务就可以看作是多个进程,线程存在于进程内,进程负责分配调度线程,线程负责执行程序,多个线程就执行多个程序。

实际上,Java程序天生就是一个多线程程序,包含了:

        1. 分发处理发送给JVM信号的线程
        2. 调用对象的finalize清除方法的线程
        3. 清除相互引用reference的线程
        4. main线程,也就是用户程序的入口,main线程里面还可以拥有很多的子线程
  • 为什么需要多线程

如果没有多线程,若为了使程序并发执行,那么系统需要花费大量的时间在:创建进程-->撤销进程-->进程上下文切换调度,在这一过程中,需要的空间开销也非常大,执行效率也非常低(如下图);若在一个进程中执行多个线程,则上面的空间开销和时间花费将会大大较少,何乐而不为呢,多线程提高了系统的执行效率,充分利用多核CPU的计算能力,提高应用性能。

计算机生成了可选文字:
运行
时间片0
新建
就绪
终止
等待某事件
如I/o请求
阻塞
I/O结束
或等待的事件发生

二、并发编程带来的问题

  • 频繁的上下文切换问题

正如上图中的时间片,时间片使CPU分配给各个线程的时间,因为时间非常短,所以CPU需要不断切换线程,让我们觉得多个线程是同时执行的,时间片一般是十几毫秒;每次切换都需要保存当前线程的状态,以便进行恢复先前的状态。这个切换是非常耗性能的,过于频繁就无法发挥出多线程编程的优势了。那么该怎么解决这频繁的上下文切换的问题的,目前有大概几种解决方法,后面会详细讨论:

      1. 采用无锁并发编程:JDK8以前的concurrentHashMap采用的锁分段思想,不同线程处理不同段的数据,这样在多线程环境下可以减少上下文的切换时间。
      2. 采用CAS算法:JDK8以后的concurrentHashMap采用的是无锁CAS算法;利用Atomic和乐观锁,可以减少一部分不必要的锁竞争带来的上下文切换。
      3. 尽量减少线程的使用:避免创建不需要的线程,比如任务少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
      4. 采用协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

因此,并发累加未必会比串行累加的速度快,这上下文切换的问题在实际中是需要解决的。

  • 线程安全问题(主要问题,也是我们程序开发关心的问题)

对线程编程中最难控制的就是临界区(共享内存的地方)的线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。那么怎么解决这种问题呢,解决方法如下:

      1. 避免一个线程同时获取多个锁
      2. 避免一个线程在锁内部占用多个资源,尽量保证一个锁只占用一个资源
      3. 尝试使用定时锁,如使用lock.tryLock(timeOut),当超时等待时当前线程也不会阻塞
      4. 对于数据库锁,加锁和解锁必须在同一个数据库连接里(同一个事务),否则会出现解锁失败的情况

后面还有JMM内存模型在原子性、有序性和可见性带来的问题,比如数据脏读,内存泄漏等等问题,这是又该如何保证线程安全呢,这一方面是非常重要的,后面会详细讨论。

三、并发编程的相关概念

  • 同步和异步

同步和异步通常用来形容方法的一次调用。

同步方法从被调用开始,调用者就必须等待被调用的方法结束后,调用者后面的代码才能继续执行。

异步方法指的是,调用者不管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。

  • 并发和并行

并发是指多个任务线程交替进行。

并行是指真正意义上的“同时进行”。

实际上,如果系统只有一个CPU,而使用多线程时,那么真实环境下时不能并行执行的,只能通过切换时间片的方式交替进行,完成并发执行任务,真正的并行只能出现在拥有多个CPU系统中。

  • 阻塞和非阻塞

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

阻塞是指如果一个线程占用了临界区的资源,那么其他线程需要这个资源的话就必须等待资源的释放,就会导致等待的线程挂起,这种情况就叫做阻塞。

非阻塞刚好跟阻塞相反,它强调的是没有一个线程可以阻塞其他线程,所有的线程都会尝试的向前运行。

  • 临界区

临界区用来表示一种公共资源会共享数据,可以被多个线程使用,出于线程安全问题,如果一个线程占用了临界区的资源,那么其他线程就必须等待,知道临界区的资源被释放。

  • 守护线程

守护线程是一种特殊的线程,是系统的服务线程,是专门为其他线程服务的,像垃圾回收线程就是守护线程,与之对应的是用户线程,用户线程作为系统的工作线程,守护线程的服务对象就是用户线程,当全部的用户线程执行任务完成之后,这个系统就没有什么需要服务的了,那么守护线程就没有对象需要守护了,那么守护线程就会结束,也就是说当一个java程序只有守护线程的时候,虚拟机就会退出了。

四、Java中的线程Thread类

参考看一下Thread类的源码注释,了解Java中的线程:

/**

1.一个Thread类对象代表程序中的一个线程,jvm是允许多线程的

 * A <i>thread</i> is a thread of execution in a program. The Java

 * Virtual Machine allows an application to have multiple threads of

 * execution running concurrently.

 * <p>

2.每一个线程都有优先级,具有高优先级的线程优先于底优先级的线程执行,每一个线程都可以设置成一个守护线程,创建线程的时候,通过线程设置setDaemon(true)就可以设置该线程为守护线程,设置守护线程需要先于start()方法

 * Every thread has a priority. Threads with higher priority are

 * executed in preference to threads with lower priority. Each thread

 * may or may not also be marked as a daemon. When code running in

 * some thread creates a new <code>Thread</code> object, the new

 * thread has its priority initially set equal to the priority of the

 * creating thread, and is a daemon thread if and only if the

 * creating thread is a daemon.

 * <p>

2.只有当一个Java程序只存在守护线程的时候,虚拟机就会退出,让虚拟机不继续执行线程的方法有:

2.1调用system.exit方法.

2.2所有非守护线程都处于死亡状态(只有守护线程)或线程运行抛出了异常

   注意:在线程启动前可以将该线程设置为守护线程,方法是setDaemon(boolean on)

     使用守护线程最好不要在方法中使用共享资源,因为守护线程随时都可能挂掉

     在守护线程中产生的线程也是守护线程

 * When a Java Virtual Machine starts up, there is usually a single

 * non-daemon thread (which typically calls the method named

 * <code>main</code> of some designated class). The Java Virtual

 * Machine continues to execute threads until either of the following

 * occurs:

 * <ul>

 * <li>The <code>exit</code> method of class <code>Runtime</code> has been

 *     called and the security manager has permitted the exit operation

 *     to take place.

 * <li>All threads that are not daemon threads have died, either by

 *     returning from the call to the <code>run</code> method or by

 *     throwing an exception that propagates beyond the <code>run</code>

 *     method.

 * </ul>

 * <p>

3.创建线程的方式有两种(重写Runnable接口的run()方法):

3.1创建子类并继承Thread 类,同时重写run()方法(因为Thread类实现了Runnable接口)

3.2创建子类并实现Runnable接口,同时重写run()方法

下面有例子:

 * There are two ways to create a new thread of execution. One is to

 * declare a class to be a subclass of <code>Thread</code>. This

 * subclass should override the <code>run</code> method of class

 * <code>Thread</code>. An instance of the subclass can then be

 * allocated and started. For example, a thread that computes primes

 * larger than a stated value could be written as follows:

 * <hr><blockquote><pre>

 *     class PrimeThread extends Thread {

 *         long minPrime;

 *         PrimeThread(long minPrime) {

 *             this.minPrime = minPrime;

 *         }

 *

 *         public void run() {

 *             // compute primes larger than minPrime

 *             &nbsp;.&nbsp;.&nbsp;.

 *         }

 *     }

 * </pre></blockquote><hr>

 * <p>

 * The following code would then create a thread and start it running:

 * <blockquote><pre>

 *     PrimeThread p = new PrimeThread(143);

 *     p.start();

 * </pre></blockquote>

 * <p>

 * The other way to create a thread is to declare a class that

 * implements the <code>Runnable</code> interface. That class then

 * implements the <code>run</code> method. An instance of the class can

 * then be allocated, passed as an argument when creating

 * <code>Thread</code>, and started. The same example in this other

 * style looks like the following:

 * <hr><blockquote><pre>

 *     class PrimeRun implements Runnable {

 *         long minPrime;

 *         PrimeRun(long minPrime) {

 *             this.minPrime = minPrime;

 *         }

 *

 *         public void run() {

 *             // compute primes larger than minPrime

 *             &nbsp;.&nbsp;.&nbsp;.

 *         }

 *     }

 * </pre></blockquote><hr>

 * <p>

 * The following code would then create a thread and start it running:

 * <blockquote><pre>

 *     PrimeRun p = new PrimeRun(143);

 *     new Thread(p).start();

 * </pre></blockquote>

 * <p>

4.每个线程都有一个名称,如果没有会在创建的时候自动生成一个,除非指定为null

 * Every thread has a name for identification purposes. More than

 * one thread may have the same name. If a name is not specified when

 * a thread is created, a new name is generated for it.

 * <p>

 * Unless otherwise noted, passing a {@code null} argument to a constructor

 * or method in this class will cause a {@link NullPointerException} to be

 * thrown.

 */

五、总结

我们需要了解并发,为什么需要并发,还必须知道并发的优缺点,同时清楚使用并发编程之后所带来的问题:频繁上下文切换问题和线程安全问题等等,后面在并发编程的时候就朝着这些问题去编程,尝试解决这些问题,让并发编程发挥出真正的作用。

理解Java并发的关键点在于理解它的两大核心(JMM内存模型【工作内存和主内存】和happes-before规则【八大规则】)以及三大特性:原子性、可见性、有序性

 

 

 

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。