现代操作系统在运行一个程序时, 会为其创建一个进程. 例如, 启动一个 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 问题发生.
来源: http://www.bubuko.com/infodetail-3090273.html