您的位置 首页 java

「Java」「并发编程」一看入门

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

一、基本简介

  • 什么是并发

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

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

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

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

二、并发编程带来的问题

  • 频繁的上下文切换问题

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

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

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

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

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

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

后面还有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
*  . . .
* }
* }
* </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
*  . . .
* }
* }
* </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规则【八大规则】)以及三大特性:原子性、可见性、有序性

如果感觉小编写得不错,请素质三连:点赞+转发+关注。我会努力写出更好的作品分享给大家。更多JAVA进阶学习资料小编已打包好,可以关注私信找我领取哦!

文章来源:智云一二三科技

文章标题:「Java」「并发编程」一看入门

文章地址:https://www.zhihuclub.com/183479.shtml

关于作者: 智云科技

热门文章

网站地图