持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
六、线程安全问题
1、什么是线程安全
线程安全是多线程访问同一段代码,不会产生不确定的结果。
2、线程安全的三大特性
2.1 原子性
原子(Atomic)就是不可分割的意思。一个操作或一组操作要么全部执行成功,要么全部失败。i++ 操作不具有原子性 ,使用原子类AtomicInteger,原子类底层实现是CAS算法。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
public static void main(String\[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
atomicInteger.addAndGet(1);
System.out.println(atomicInteger.get());
}
}
2.2 可见性
可见性是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态之后,其他线程能够看到发生的状态变化。 <------>
之所以存在可见性问题是由于Java内存模型决定的。 valatile 关键字可以解决可见性问题。
可见性问题代码示例:
public class VolatileDemo {
public static void main(String\[] args) {
ThreadV t = new ThreadV();
new Thread(t).start();
while (true){
if (t.flag){
System.out.println("----");
break;
}
}
}
}
class ThreadV implements Runnable {
// 变量,控制结束
///boolean flag = false;
//TODO:保证可见性
volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
System.out.println(e.toString());
}
flag = true;
System.out.println(Thread.currentThread().getName()+"::"+flag);
}
}
2.3 有序性
程序的执行顺序按照代码的先后顺序执行。 存在顺序性问题的原因是指令重排(是计算机底层堆执行指令的一种优化策略)。使用volatile 关键字可以禁止指令重排,它是利用了内存屏障保证特定操作的执行顺序,通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化。volatile关键字比 sychronized 更轻量级。
3、CAS
CAS ,全称Compare And Swap,即比较-替换。是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令 【比如i++操作】,用于管理对共享数据的并发访问。 CAS是一种无锁的非阻塞算法实现。包含了三个操作数:内存值V、进行比较的值A、要修改的值B。当且仅当V的值等于A时,CAS通过原子方式用新的值B来更新V的值,否则不会执行任何操作。 当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试, 当然也允许失败的线程放弃操作(由开发者自己决定)。 基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
package cn.interview\.base.thread.atomic;
public class CASCase {
public static void main(String[] args) {
}
}
class CompareAndSwap {
//内存值 v
private int value;
//获得内存值
public synchronized int get(){
return value;
}
/**
* 设置内存值
* @param expectedVale 旧的预期值 A
* @param newVale 要写入内存的值 B
* @return
*/
public synchronized boolean compareAndSet(int expectedVale,int newVale){
return expectedVale == compareAndSwap(expectedVale,newVale);
}
/**
* A V 相等判断,并写入内存
* @param expectedVale 旧的预期值 A
* @param newVale 写入内存的值 B
* @return 写入前的内存值
*/
private int compareAndSwap(int expectedVale, int newVale) {
int oldValue = value;
if (oldValue == expectedVale) {
this.value = newVale;
}
return oldValue;
}
}
3.1 使用CAS存在的问题
3.1.1 ABA
- 并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
- 可以通过AtomicStampedReference「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变 量值的版本来保证CAS的正确性。
3.1.2 循环时间长消耗资源大
自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销
3.1.3 只能保证一个共享变量的原子操作
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。 1)使用互斥锁来保证原子性; 2)将多个变量封装成对象,通过AtomicReference来保证原子性。
4、volatile
2.1 作用
1)一旦一个被volatile修饰之后,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了这个变量的值,这个变量的值对于其他线程来说是立即可见的。 2)禁止指令重排。它是利用了内存屏障保证特定操作的执行顺序,通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化。volatile关键字比 sychronized 更轻量级。 可以保证顺序性和可见性,但是无法保证原子性;
2.2 双重检查锁的单例模式
使用volatile关键字实现一个双重检查锁的单例模式。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton(){
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
5、锁机制
5.1 Synchronized
5.1.1 简介
synchronized可以把任意一个非null的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。synch根ronized根据获取的锁分类可以分为对象锁和类锁。 1)获取对象锁
- 同步代码块:synchronized(this、类实例对象)锁住的是括号中的实例对象;
- 同步非静态方法:synchronized method,锁的是对当前对象的实例对象;
2)获取类锁
- 同步代码块:synchronized(类名.class) ,锁住的是小括号中的类对象;
- 同步静态方法:synchronized static method,锁住的是当前对象的类对象;
5.2.2 底层实现原理
1)同步代码块
- synchronized 同步代码块的实现使用的是 monitor enter 和 monitor exit 指令,其中monitor enter 指向同步代码块的开始位置,monitor exit 指向同步代码块结束的位置。
- moniter 对象存在与每个Java对象的对象头中,当执行enter时,线程会尝试获取monitor的持有权,当monitor计数器为 0 是可以成功获取,获取后锁计数器设为1,当执行到 exit 执行后,将锁计数器设为0,表示锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另一个线程释放为止。
public class SynchronizedTest {
public static void main(String\[] args) {
}
public void methodBlock(){
// 同步代码块\
synchronized (this){
System.out.println("1111");
}
}
// 同步方法
public synchronized void method(){
System.out.println("2222");
}
}
2)同步方法
- synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
- 当方法调用时,调用指令将检查方法的 ACC_SYNCHRONIZED 标识是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完成后在释放monitor。在执行期间,其他任何线程都无法获得同一个monitor对象。
- 本质上两种同步方式是一致的,只不过同步方法时隐式的,无需通过字节码来完成。
5.1.3 volatile 与 synchronized 的区别
- volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程才可以访问,其他线程被阻塞住;
- volatile 仅能在变量级别使用;synchronized则可以使用在变量(以代码块的方式)、方法和类级别的;
- volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性;
- volatile不会造成线程阻塞;synchronized可能会造成线程阻塞;
- volatile标记的变量不会被编译器优化;synchronized可能会造成线程的阻塞。
变量级别的使用:
public class SynchronizedTest {
public static void main(String\[] args) {
User user = new User("Tom", "1qazse4");
synchronized (user) {
System.out.println(user.toString());
}
}
}
5.1.4 Synchronized优化
JDK 1.6 对锁的实现引入了大量的优化,偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等; 锁主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态; 锁会随着竞争的激烈而逐渐升级称之为锁膨胀,锁可以升级不可以降级,这种策略是为了提高获得锁和释放锁的效率;
5.2 Lock
Lock同步锁
JDK 1.5 前Java是靠synchronized实现锁功能。 JDK1.5 之后JUC 并发包新增了Lock接口来实现锁功能。 JUC就是java.util .concurrent工具包的简称,这是一个处理线程的工具包。在这个工具包中有 一个ReentrantLock类实现了Lock 接口,并提供了与synchronized相同的互斥性和内存可见 性。但相较于synchronized 提供了更高的处理锁的灵活性。提供了更高的处理锁的灵活性。 Condition 接口 提供了类似监视器方法,与Lock配合使用可以实现等待/通知机制。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() throws Exception {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void signal() throws Exception {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
AQS
ReentrantLock 、ReentrantReadWriteLock 底层都是基于AQS来实现的。 AQS 的全称为(AbstractQueuedSynchronizer),抽象队列同步器,这个类在java.util.concurrent.locks包下面。它是一个并发包的基础组件,用来实现各种锁、各种同步组件的。它包含了 state变量、加锁线程、等待队列等并发中的核心组件。
原理:
-
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到等待队列中。
-
AQS 使用一个 int 成员变量 state来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
//共享变量,使用volatile修饰保证线程可见性
private volatile int state;
//返回同步状态的当前值
protected final int getState() {
return state;
}
//设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
Synchronized 与 Lock的比较 ==相同点==:
- 都是用来协调多线程对共享对象、变量的访问;
- 都是可重入锁,同一个线程可以多次获得同一个锁;
- 都保证了可见性和互斥性。
==不同点==:
- ReentrantLock显示的获得、释放锁,synchronized 隐式获得释放锁
- ReentrantLock 相比 synchronized 的优势是可响应、可轮回、可中断
- ReentrantLock是API级别的同步非阻塞锁,乐观策略; synchronized是JVM级别的同步阻塞锁,悲观策略。
- ReentrantLock 可以实现公平锁 Lock lock=new ReentrantLock(boolean f);
- ReentrantLock 通过Condition可以绑定多个条件
- synchronized 在发生异常时,会自动释放线程占有的锁;而 Lock需要手动释放锁。
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。 Lock 案例 开启A B C三个线程,要求这个三个线程将自己的ID打印 10次到控制台上,保证打印结果交替显示:ABCABCABC
package cn.interview\.base.thread.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String\[] args) {
AlternateDemo a = new AlternateDemo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
a.loopA(i);
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
a.loopB(i);
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
a.loopC(i);
}
}
},"C").start();
}
}
class AlternateDemo {
// 标识 ,当前执行的线程,从第一个线程开始
private int number = 1;
//构造锁和条件
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
/**
* A线程 输出
* @param totalLoop 循环轮数
*/
public void loopA(int totalLoop) {
// 获取锁
lock.lock();
try {
if (number != 1) {
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"\t"+totalLoop);
number = 2;
condition2.signal();
} catch(Exception e) {
System.out.println(e.toString());
} finally {
// 释放锁
lock.unlock();
}
}
/**
* B线程 输出
* @param totalLoop 循环轮数
*/
public void loopB(int totalLoop) {
// 获取锁
lock.lock();
try {
if (number != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"\t"+totalLoop);
number = 3;
condition3.signal();
} catch(Exception e) {
System.out.println(e.toString());
} finally {
// 释放锁
lock.unlock();
}
}
/**
* C线程 输出
* @param totalLoop 循环轮数
*/
public void loopC(int totalLoop) {
// 获取锁
lock.lock();
try {
if (number != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"\t"+totalLoop);
number = 1;
condition1.signal();
} catch(Exception e) {
System.out.println(e);
} finally {
// 释放锁
lock.unlock();
}
}
}
5.3 锁分类
5.3.1 锁的四种状态
- 无锁状态
- 偏向锁状态:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。
- 轻量级锁状态
- 重量级锁状态
5.3.2 锁分类
ReentrantLock (可重入锁:同一个线程可以多次获得同一个锁),默认是非公平锁,(公平锁:各个线程之间是相互公平的,谁先来谁先获取锁),通过 Lock lock = new ReentrantLock(true); 实现公平锁。 ReentrantReadWriteLock (读写锁) 允许多个线程同时对某一资源进行读。 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
6、Atomic原子类
6.1 简介
Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 所以,所谓原子类说简单点就是具有原子操作特征的类。 原子类都存放在 java.util.concurrent.atomic 包下。Atomic原子类的原子性操作是基于CAS实现的。
6.2 分类
6.2.1 基本类型
使用原子的方式更新基本类型。
- AtomicInterger:整型原子类
- AtomicLog:长整型原子类
- AtomicBoolean:布尔型原子类
6.2.2 数组类型
使用原子的方式更新数组里的某个元素。
- AtomicIntergerArray:整型数组原子类
- AtomicLongArray:长整型数组原子类
- AtomicReferenceArray:引用类型数组原子类
6.2.3 引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记位的引用类型
6.2.4 对象属性修改类型
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
6.3 使用(AtomicInterger)
6.3.1 常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
6.3.2 案例
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
7、ThreadLocal
7.1 简介
- ThreadLocal 线程本地变量,主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
- 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
- 使用场景:
- 线程间数据隔离;
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束;
- 进行事务操作,用于存储线程事务信息;
- 数据库链接池,session会话管理;
7.2 使用
public class ThreadLocalExample implements Runnable{
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String\[] args) throws InterruptedException {
ThreadLocalExample obj = new ThreadLocalExample();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(obj,""+i);
Thread.sleep(new Random().nextInt(1000));
thread.start();
}
// 虽然 Thread-0 已经改变了 formatter 的值,但 Thread-1 默认格式化值与初始化值相同,其他线程也一样。
}
@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//formatter pattern is changed here by thread, but it won't reflect to other threads
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}
7.3 原理
ThreadLocal的内存结构:
- Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。也就是说每个线程都有一个属于自己的ThreadLocalMap;
- ThreadLocalMap内部维护了Entry数组,每一个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值;
- 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
7.4 内存泄漏问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉,这就会造成内存泄漏问题。
==解决方法==:
ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法释放内存空间。
七、多线程通信
1、等待通知机制
- 一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
- 可以通过 wait notfify notfiyAll join来实现等待通知机制。
等待
synchronized(obj) {
while(条件不满足) {
obj.wait();\
}
// 业务逻辑
}
通知
synchronized(obj) {
//改变条件
obj.notifyAll();
}
2、JUC包下的线程通信工具
2.1 生产者和消费者问题
生产者和消费者问题也称为有限缓冲问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程,消费者从缓冲区中消费数据。问题的关键是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据。 方案一 使用 Synchronized 、wait、notifyAll
package cn.interview.base.thread;
public class ProducerAndConsumer1 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(producer,"生产者A").start();
new Thread(producer,"生产者B").start();
new Thread(producer,"生产者C").start();
new Thread(consumer,"消费者A").start();
new Thread(consumer,"消费者B").start();
new Thread(consumer,"消费者C").start();
}
}
/**
* 店员-模拟卖货
*/
class Clerk {
//商品数量
private int product = 0;
/**
* 进货
*/
public synchronized void get(){
// TODO: 使用 synchronized 时最好使用 while,if 判断 存在虚假唤醒的问题
while (product>=1){
System.out.println("商品数量达到上限!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产商品
System.out.println(Thread.currentThread().getName()+":"+ ++product);
// 通知其他线程
this.notifyAll();
}
/**
* 卖货
*/
public synchronized void sale(){
while (product <= 0) {
System.out.println("商品缺货!");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费产品
System.out.println(Thread.currentThread().getName()+":"+ --product);
notifyAll();
}
}
/**
* 生产者
*/
class Producer implements Runnable{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.get();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
方案二
- ReentrantLock、Condition
package cn.interview.base.thread.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerAndConsumer2 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(producer,"生产者A").start();
new Thread(producer,"生产者B").start();
new Thread(producer,"生产者C").start();
new Thread(consumer,"消费者A").start();
new Thread(consumer,"消费者B").start();
new Thread(consumer,"消费者C").start();
}
}
/**
* 店员-模拟卖货
*/
class Clerk {
//商品数量
private int product = 0;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
/**
* 进货
*/
public void get(){
lock.lock();
try {
while (product>=2){
System.out.println("商品数量达到上限!");
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产商品
System.out.println(Thread.currentThread().getName()+":"+ ++product);
// 通知其他线程
condition2.signal();
} finally {
lock.unlock();
}
}
/**
* 卖货
*/
public void sale(){
lock.lock();
try {
while (product <= 0) {
System.out.println("商品缺货!");
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费产品
System.out.println(Thread.currentThread().getName()+":"+ --product);
condition1.signal();
} finally {
lock.unlock();
}
}
}
/**
* 生产者
*/
class Producer implements Runnable{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.get();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
2.2 线程通信
2.2.1 Condition
与Lock 配合使用。 5.2 Lock案例
2.2.2 CountDownLatch 闭锁
CountDownLatch一个同步辅助类,又称为闭锁。允许一个或多个线程等待其他线程完成操作。底层基于AQS实现。 以下的应用场景是可以使用闭锁来实现的:
- 闭锁可以延迟线程的进度直到其到达终止状态
- 闭锁可以用来确保某些活动直到其他活动都完成才继续执行
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行
- 确保某个服务在其依赖的所有其他服务都已经启动之后才启动
- 等待直到某个操作所有参与者都准备就绪再继续执行
代码示例:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String\[] args) {
// 等待其他的线程都执行完,在执行
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread() {
@Override
public void run() {
try {
System.out.println("任务一正在执行 ······");
Thread.sleep(300);
countDownLatch.countDown();
System.out.println("任务一执行完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
@Override
public void run() {
try {
System.out.println("任务二正在执行 ······");
Thread.sleep(200);
countDownLatch.countDown();
System.out.println("任务二执行完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
// 主线程等到2个子线程完成之后在执行
try {
countDownLatch.await();
System.out.println("到2个子线程完成之后在执行: "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2.3 CyclicBarrier 循环栅栏
CyclicBarrier(循环栅栏):CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。 CyclicBarrier 和 CountDownLatch非常类似,它也可以实现线程间的技术等待,但是它的功能更加复杂和强大。可以让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
代码示例:
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierDemo {
public static void main(String\[] args) {
/\*\*
* 参数一 触发屏障前的调用的线程数
* 参数二 屏障被触发时执行的任务
\*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("全员到齐,开始干饭!");
}
});
// 模拟三个员工
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
int user = i+1;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
System.out.println("员工:"+user+",到达餐厅,当前已经有"+(cyclicBarrier.getNumberWaiting()+1)+"人到达了");
//阻塞
cyclicBarrier.await();
Thread.sleep(3000);
System.out.println("聚餐结束,<员工"+user+">回家");
} catch (Exception e) {
e.printStackTrace();
}
}
};
executorService.execute(runnable);
}
executorService.shutdown();
}
}
CountDownLatch 与 CyclicBarrier 的区别:
- 闭锁的计数器只能使用一次;循环栅栏的计数器可以重置 (reset方法)。
- 闭锁是一个线程或多个线程等待另外n个线程完成后才能执行;而循环栅栏是n个线程相互等待,任何一个完成之前,所以线程都必须等待。
2.2.4 Semaphore 信号量
- Semaphore(信号量)-允许多个线程同时访问资源, ReentrantLock是一次只允许一个线程访问某个资源,Semaphore也可以替换ReentrantLock这个使用场景。
- Semaphore通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
- 用来控制同时访问资源的线程数量,通过协调各个线程,来保证合理的公共资源的访问。 ==应用场景:流量控制,比如数据库链接、限流等==
代码示例: 若一个工厂有 5 台机器,但是有 8 个工人,一台机器同时只能被一个工人使用,只有使用 完了其他工人才能继续使用。
package cn.interview\.base.thread.communication;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String\[] args) {
// 8个工人
int n = 8;
Semaphore semaphore = new Semaphore(5);
for (int i = 1; i <= n; i++) {
new WorkThread(i,semaphore).start();
}
}
}
class WorkThread extends Thread {
// 员工
private int num;
// 信号量
private Semaphore semaphore;
public WorkThread(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
//获取许可
try {
semaphore.acquire();
System.out.println("工人:"+this.num+"正在使用一台机器");
Thread.sleep(200);
System.out.println("工人:"+this.num+"释放机器");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
八、线程管理
1、线程组
类似于在计算机中使用文件夹管理文件,也可以使用线程组来管理线程. 在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组。
Thread 类有几个构造方法允许在创建线程时指定线程组(==ThreadGroup==),如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组。
2、线程池
2.1 概念
线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。线程池包含线程池管理器(用于创建并管理线程),工作线程(线程池中的线程),任务接口(每个任务必须实现的接口,用于工作线程调度其运行),任务队列(用于存放待处理的任务,提供一种缓冲机制) 使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2.2 线程池创建方式
2.2.1 通过构造方法实现
// 七大参数
// 保留着线程池中的线程数
// 池中最大线程数
// 空余线程等待时间
// 时间单位
// 任务队列
// 线程工厂
// 阻塞时的处理程序
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
execute 和 submit 的区别:
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
2.2.2 通过Executor 的工具类Executors 来实现
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险(OOM);
- FixedThreadPool : 该方法返回一个固定线程数量的线程池,可以进行自动线程回收。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- ScheduledThreadPool:创建一个固定大小的线程池,可以延迟或定时的执行任务。
- WorkStealingPool:内部会创建ForkJoinPool ,利用working-stealing算法,并行地处理任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/\*\*
* 通过 Executors 方法创建线程池
* @version 1.0
* /
public class ExecutorsTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(12);
MyRunnable runnable = new MyRunnable("Executors");
executorService.execute(runnable);
}
}
2.2.3 线程池案例
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class PoolTest {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
MyRunnable runnable = new MyRunnable(""+i);
executor.execute(runnable); // 执行线程
}
executor.shutdown(); //终止线程池
while (!executor.isTerminated()) {
// 所有任务在关闭后是否完成
}
System.out.println("Finished all threads");
}
}
class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
2.3 线程池工作原理
通过 executor.execute(runnable) 方法提交一个任务到线程池中去。
2.4 ForkJoinPool
就是在将一个大任务,进行拆分(fork)成若干个小任务,再将一个个的小任务运算(并行)的结果进行 join 汇总。Work-stealing算法:某个线程从其他队列里窃取任务来执行。 ForkJoin和普通的ThreadPool有什么区别?
- 一般的线程池如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。
- 在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行这种方式减少了线程的等待时间,提高了性能(Work-Stealing 算法)。