【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规则【八大规则】)以及三大特性:原子性、可见性、有序性
- 点赞
- 收藏
- 关注作者
评论(0)