《一遍文章让你快速了解JAVA---并发编程基础》

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。线程是现代操作系统调度的最小单元,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计算器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。今天主要以两个方面让大家更快的了解并发编程!

一、基本概念与方法

二、线程安全问题与解决

(一)、线程与进程

进程是CPU分配资源的最小单位,由一个或多个线程组成。

线程是CPU进行调度的最小单位,被称为轻量级线程。

一个程序至少一个进程,一个进程至少一个线程

(二)、Java中线程的三种创建方式

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

(2)实现Runnable接口,并重写run()方法

(3)实现Callable接口,并重写call()方法;此种方法有返回值,且需要使用FutureTask类进行封装

实现接口与继承Thread类的比较:

Java中只能单继承,但是可以实现多个接口;使用接口的方法更适合扩展

继承整个Thread类的方法开销过大

若想在线程执行体中(即run方法体中)访问当前线程,继承方式可以直接通过this;而接口方法要通过Thread.currrentThread()

此外实现Runnable接口创建的线程可以处理同一资源,从而实现资源的共享

(三)、线程的状态

(1)新建状态:创建后未启动

(2)就绪状态:调用start()方法后进入该状态,与其他就绪状态线程一起竞争CPU,等待CPU的调度。

(3)运行状态:就绪状态的线程获得CPU时间片,真正的执行run()方法。线程只能从就绪状态进入运行状态

(4)阻塞状态:线程由于如下所示的各种原因进入阻塞,线程挂起

该线程调用Thread.sleep()方法

等待阻塞,线程中的共享变量调用了wait()方法

I/O流方式,如read()方法,receive()方法等待数据

同步阻塞,线程因无法获得目标资源的锁而被挂起

(四)、sleep()方法和wait()方法

sleep()是Thread类中的静态方法,调用Thread.sleep(time)后线程休眠time毫秒,休眠过程中线程不会释放拥有的对象锁。如果该线程睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,该线程会在调用sleep()方法的地方抛出InterruptedException。

wait()是Object类中的方法,当线程调用一个共享变量的wait()方法是,该线程会被挂起并且释放该对象锁,进入等待此对象的等待锁定池,直到其他线程调用了该共享对象的notify()或者notifyAll()方法。其中,notify()是在等待锁定池中随机唤醒一个线程,notifyAll()是唤醒所有因该对象的wait()方法而挂起的线程。

注意:调用共享变量的wait()、notify()、notifyAll()方法,需要先获得共享变量的对象锁。被唤醒的线程不会立即执行,需要和其他线程一起竞争对象锁(由调用notify()方法的线程所释放的对象锁)。

(五)、join()方法和yield()方法

join()方法,Thread类的成员方法,插队方法,线程A的执行体中调用 B.join(),B代表线程B,则线程A会阻塞,让B线程插队。参数可以传入时间(毫秒),表示允许插队运行的时间长度。

yield()方法,Thread类的静态方法,礼让方法,线程A调用Thread.yield()方法后会让出CPU使用权,进入就绪状态,与其他处于就绪状态的线程一起竞争CPU。(实际上,调用yield()方法之后,线程调度器会从线程就绪队列中获取一个线程优先级最高的线程,而该线程的优先级会变为1)

(六)、线程中断

线程中断是线程间的一种协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

interrupt()方法,中断线程,将线程的中断标志设置为true。当线程因调用wait()、join()、sleep()等方法进入阻塞时,其他线程调用该线程的interrupt()方法,该线程会抛出InterruptedException并返回。如果调用线程的interrupt()方法后未抛出InterruptedException,则应通过interrupted()方法判断当前线程是否被中断来返回线程(如在执行体中使用该方法作为线程执行前提条件)

(七)、守护线程与用户线程
守护线程

用户线程

守护线程是服务于用户线程的,可以通过调用setDaemon(true)方法将用户线程设置为守护线程

两者可以通过JVM是否等待线程结束来区分,JVM只会等待用户线程结束;守护线程不会影响JVM的退出,不管其是否运行结束都会随着JVM的结束而结束。即用户线程全部结束时,程序终止,并杀死所有守护线程。如main函数就是一个用户线程,而垃圾回收线程就是一个守护线程。

(八)、ThreadLocal的使用

ThreadLocal由JDK包提供,它提供了线程本地变量,即每个访问ThreadLocal变量的线程都会有一个该变量的随机副本。线程对该变量进行操作时,实际上是对自己的本地内存里的变量进行操作,从而避免了多线程共享一个变量时的安全问题。如在封装MyBatisUtil工具包时,其中就用到了将SqlSession的实例对象存储在ThreadLocal的实例对象中,每次通过

get获取,使用完后关闭SqlSession实例对象,并set(null)将ThreadLocal清空;tl是ThreadLocal的实例对象。

二。线程安全问题与解决

(一)、Java中的线程安全问题

当多个线程对共享资源进行访问时,只有当至少有一个线程修改共享资源时才会存在线程安全问题。典型的如计数器类实现中的丢失修改问题。

(二)、共享变量的内存可见性问题

Java中所有的变量存放在主存中,而线程使用变量时会把主内存里面的变量复制到自己的工作内存中,线程读写变量时操作的是自己工作变量中的内存,然后将自己工作内存中的变量刷新到主内存中。因此,当线程A和线程B同时处理一个共享变量时,会存在内存不可见的问题。

(三)、锁的概念

(1)乐观锁与悲观锁:是从数据库概念中引入的词。悲观锁指认为数据很容易被其他线程修改,因此会在数据被处理前对数据进行加锁,使得整个处理过程中数据处于锁定状态。乐观锁则是认为数据在一般情况下不会造成冲突,因此在访问数据前不会加排它锁,只有在数据提交更新时,才会正式的对数据冲突与否进行检测。

(2)独占锁与共享锁:根据锁只能被单个线程持有还是能被多个线程持有,分为独占锁(排它锁)和共享锁。独占锁是一种悲观锁,每次访问资源前都先加上互斥锁,只允许同一时间由一个线程读取数据。而共享锁是一种乐观锁,允许多个线程同时进行读操作。

(3)公平锁与非公平锁:根据线程获取锁的抢占机制,可以分为公平锁与非公平锁。公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,即早到早得。而非公平锁则不一定先到先得。ReentrantLock提供的锁默认是非公平锁。一般来说,在没有公平性需求的前提下,尽量使用非公平锁,因为公平锁会带来性能开销。

(4)可重入锁:一个线程再次获取它自己已经获得的锁时,则称为可重入锁。可重入的原理是在锁内部维护一个线程表示,线程表示来指示该锁目前被哪个线程占有,然后关联一个计数器来表示该锁是否被线程占用,0为未被占用,1为已占用,此后每次重入则计数器+1.

(5)自旋锁:自旋锁是指线程在获取锁失败时不会马上挂起,而是在不放弃CPU使用权的情况下,多次尝试获取该锁(默认10次)。一般而言,当线程获取锁失败后,会切换到内核状态而被挂起;当该线程获取锁后又需要将其切换到内核状态而唤醒该线程,而用户状态切换到内核状态的开销是比较大的,即自旋锁是使用CPU时间换取线程阻塞与调度的开销。

(四、)synchronized的使用

synchronized是Java提供的一种原子性内置锁。是一种排它锁,同时也是非公平的。synchronized可以解决共享变量的内存可见性问题。

进入synchronized块的语义是,把块内使用的变量从线程的工作内存中清除,这样线程就会直接从主内存中去获取块内需要使用的变量。

退出synchronized块的语义是,将synchronized块内对共享变量的修改刷新到主内存中。

(五)、volatile的使用

使用锁的方式解决共享变量内存可见性的问题太过繁琐,开销太大,因此Java提供了一种弱形式的同步,即volatile关键字。

类成员变量或者类静态成员变量被volatile修饰后主要有两个特性

(1)解决不同线程对该变量进行的操作时的可见性问题。因为线程在操作volatile修饰的变量时,不会把值缓存到寄存器或者其他地方,而是直接把值刷新会主内;当其他线程获取该变量时,会从主内存中重新获取最新值,而不是使用当前线程工作内存中的值。

(2)禁止指令重排,一定程度上能保证有序性。具体情况是,写volatile变量时,写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,读之后的操作不会被编译器重排序到volatile读之前。

(六)、Java中的CAS操作

Java中使用锁来处理并发会产生线程上下文切换和重新调度的开销。而非阻塞的volatile关键字只能保证共享变量的可见性,不能解决读-改-写等原子性问题。因此JDK提供了非阻塞原子性操作,即CAS(Compare

and Swap)操作,它通过硬件保证了比较-更新操作的原子性。

CAS操作有个经典的ABA问题,大概意思是

线程1获取变量X的值(A),然后修改变量X的值为B,这种情况下即使使用CAS操作成,程序也不一定运行正确。因为可能存在线程2在1获取变量X后,使用CAS操作修改了X的值为B,然后又使用CAS操作修改X的值为A,这样线程1修改变量X的值是,已经是此A非彼A了。

ABA问题大概流程:1.CASget(X-A) --->2.CASset(X-B)--->2.CASset(X-A)--->1.CASset(X-B)。

ABA问题的产生是因为变量的状态值产生了环形转换,即变量值从A到B,然后再从B到A。如果规定变量的值只能朝着一个方向转换,则不会出现该问题。因此JDK中的AtomicStampedReference类给每个变量的状态值都配置了一个时间戳死,以避免ABA问题发生。

原文地址:https://blog.51cto.com/14257001/2407820

时间: 06-12

《一遍文章让你快速了解JAVA---并发编程基础》的相关文章

Java 并发编程基础导航

一.中断模型:http://ifeve.com/java-interrupt-mechanism/ 1. 协作制度, 你要我中断,只是给我一个信号,我啥时候中断,是我的事情. 比如 T1调用T2.interrupt(),    T2 如果没有写  if (Thread.intterrupted()) { return or do something}或者 if ( Thread.isintterupted()) { return or do something}  T2鸟都不鸟T1 而且 如果T

Java并发编程(一)

Java并发编程(一) 之前看<Thinking In Java>时,并发讲解的挺多的,自己算是初步了解了并发.但是其讲解的不深入,自己感觉其讲解的不够好.后来自己想再学一学并发,买了<Java并发编程实战>,看了一下讲的好基础.好多的理论,而且自我感觉讲的逻辑性不强.最后,买了本<Java并发编程的艺术>看,这本书挺好的,逻辑性非常强. 1. 概述 本篇文章主要内容来自<Java并发编程的艺术>,其讲解的比较深入,自己也有许多不懂的地方,然后自己主要把它讲

JAVA并发编程J.U.C学习总结

前言 学习了一段时间J.U.C,打算做个小结,个人感觉总结还是非常重要,要不然总感觉知识点零零散散的. 有错误也欢迎指正,大家共同进步: 另外,转载请注明链接,写篇文章不容易啊,http://www.cnblogs.com/chenpi/p/5614290.html 本文目录如下,基本上涵盖了J.U.C的主要内容: JSR 166及J.U.C Executor框架(线程池. Callable .Future) AbstractQueuedSynchronizer(AQS框架) Locks & C

Java并发编程:并发容器ConcurrentHashMap

Java并发编程:并发容器之ConcurrentHashMap(转载) 下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都 串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量严重降低.因此Java5.0开 始针对多线程并发访问设计,提供了并发性能较好的并发容器,引入

Java并发编程:Callable、Future和FutureTask(转)

Java并发编程:Callable.Future和FutureTask 在前面的文章中我们讲述了创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口. 这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果. 如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦. 而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果. 今天我们就来讨论一下Callabl

Java并发编程:进程和线程

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java并发编程学习路线

一年前由于工作需要从微软技术栈入坑Java,并陆陆续续做了一个Java后台项目,目前在搞Scala+Java混合的后台开发,一直觉得并发编程是所有后台工程师的基本功,所以也学习了小一年Java的并发工具,对整体的并发理解乃至分布式都有一定的提高,所以想和大家分享一下. 我的学习路线 首先说说学习路线,我一开始是直接上手JCIP(Java Concurrency in Practice),发现不是很好懂,把握不了那本书的主线,所以思索着从国内的作者开始先,所以便读了下方腾飞的<Java并发编程的艺

【Java并发编程】并发编程大合集-值得收藏

http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Ja

5、Java并发编程:Lock

Java并发编程:Lock 在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock. 也许有朋友会问,既然都可以通过synchronized来实现同步访问了,那么为什么还需要提供Lock?这个问题将在下面进行阐述.本文先从synchronized的缺陷讲起,然后再讲述java.util.concurrent.locks包

【转】Java并发编程:线程池的使用

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool