JUC必要掌握(Synchronized,Lock,可重入锁ReentrantLock,可重入锁,读写锁,自旋锁,线程间通信,集合的线程安全)
本文已参与「新人创作礼」活动,一起开启掘金创作之路
1.锁(Synchronized和lock)
1.1 Synchronized
(1)Synchronized是Java内置的关键字,是Java内置的锁机制。 (2)Synchronized的作用域: 方法: 实例方法:被锁定的是类的实例对象。 静态方法:类对象,也就是Class对象。 代码块: 实例对象:锁的对象是类的实例对象。 class对象:锁的对象是Class对象。 任意实例对象object:锁的对象是实例对象。 (3)Synchronized是可重入锁,非公平锁,不可以中断,锁的释放由JVM决定。
(4)Synchronized获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时 JVM 会让线程自动释放锁。
复制代码
1.2 Lock
(1)Lock是JUC包的接口。 (2)Lock的内置方法: lock():加锁(锁不可中断)。 unlock():解锁。 tryLock():尝试获取锁。 lockInterruptibly():加锁(锁可以通过Interruptibly进行中断)。 newCondition():新建一个监视器。
Synchronized和Lock锁的区别
1、Synchronized 内置的Java关键字, Lock 是一个Java接口。
2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁。
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁。
4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去。
5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以 自己设置)。
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
复制代码
2.线程安全问题:
2.1 电影院的售票实例:
package com.example.onlyqi.juc;
/**
* @author only-qi
* @date 2022/5/30 10:16
* @description
*/
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
if (ticket > 0) {//有票 可以卖
try {
//出票操作
//使用sleep模拟一下出票时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖票:" + ticket--);
}
}
}
}
package com.example.onlyqi.juc;
/**
* @author onlyqi
* @date 2022/5/30 10:18
* @description
*/
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
复制代码
出现的问题:
3.线程同步解决线程安全问题
3.1 同步代码块:
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
复制代码
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
-
锁对象 可以是任意类型。
-
多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
public class Ticket implements Runnable {
private int ticket = 100;
Integer lock = new Integer(1);
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
synchronized (lock) {
if (ticket > 0) {//有票 可以卖
try {
//出票操作 //使用sleep模拟一下出票时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖票:" + ticket--);
}
}
}
}
}
复制代码
3.2 同步方法
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
复制代码
同步锁是谁?
对于非static方法,同步锁就是this。 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
package com.example.onlyqi.juc;
/**
* @author onlyqi
* @date 2022/5/30 10:16
* @description
*/
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
while(true) {
sellTicket();
}
}
public synchronized void sellTicket(){
//每个窗口卖票的操作
//窗口 永远开启
if (ticket > 0) {//有票 可以卖
try {
//出票操作 //使用sleep模拟一下出票时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖票:" + ticket--);
}
}
}
复制代码
3.3 Lock锁
package com.example.onlyqi.juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author onlyqi
* @date 2022/5/30 10:16
* @description
*/
public class Ticket implements Runnable {
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
lock.lock();
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
}
复制代码
4. ReentrantLock(可重入锁)
4.1 ReentrantLock默认是非公平锁
小知识点:
1、公平锁
定义:多个线程按照先到先得的策略获取锁。
优点:所有线程都有机会获得锁,不会饿死
缺点:由于所有线程都会经历阻塞态,因此唤醒阻塞线程的开销会很大。
2、非公平锁
定义:所有的线程拼运气,谁运气好,谁就获取到锁
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高
缺点:可能会有线程长时间甚至永远获取不到锁,导致饿死。
复制代码
ReentrantLock类不仅实现了Lock接口里的方法,还新增了一些其他的方法,ReenTrantLock提供的方法如下:
// 创建一个 ReentrantLock ,默认是“非公平锁”
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁
ReentrantLock(boolean fair)
// 查询当前线程保持此锁的次数
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null
protected Thread getOwner()
// 返回一个collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 尝试获取锁,仅当锁没有被其他线程获取时才能获取到锁
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()
复制代码
4.2 ReenTrantLock类常用的方法的使用案例:
(1)void lock()
package com.example.onlyqi.juc;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author onlyqi
* @date 2022/5/30 13:51
* @description
*/
public class LocklMain {
// 公共写的数据
private static List<Integer> list = new ArrayList<>();
private static Lock lock= new ReentrantLock();
public static void main(String[] args) {
final LocklMain locklMain = new LocklMain();
new Thread(new Runnable() {
@Override
public void run() {
insert();
}
},"tread01").start();
new Thread(new Runnable() {
@Override
public void run() {
insert();
}
},"tread02").start();
}
private static void insert()
{
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " 开始添加元素");
for (int i =0; i < 5; ++i)
{
list.add(i);
System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
}
System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
}
finally {
System.out.println(Thread.currentThread().getName() + " 释放了锁");
lock.unlock();
}
}
}
复制代码
例子中在main方法里初始化一个final实例和一个公共写的list,起两个线程,每个线程里都调用这个final实例的insert方法向list里add元素。由于实例方法insert()是需要同步的方法,之前可用synchronized关键字直接声明insert方法,这里在insert方法里通过调用lock.lock()和lock.unlock()对insert()方法里需要同步的部分进行加锁,从结果看达到了同步的目的:Thread01先调用lock.lock()上锁,向list add元素,add完成后调用lock.unlock()方法释放锁,接着Thread02再调用lock.lock()上锁,向list add元素,添加完后再释放锁。
注意private Lock lock=newReentrantLock();这个语句,不应该在insert方法里声明,而是全局声明(可以使用单例模式创建一个全局唯一的锁对象),不然两个线程调用insert()方法时,都会new一个lock出来,两个lock各自锁各自调用的insert()方法,到不到同步的目的。
(2) boolean tryLock()
package com.example.onlyqi.juc;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author onlyqi
* @date 2022/5/30 14:03
* @description
*/
public class TryLockMain {
// 公共写的数据
private static List<Integer> list = new ArrayList<>();
private static Lock lock= new ReentrantLock();
public static void main(String[] args) {
final TryLockMain tryLocklMain = new TryLockMain();
new Thread(new Runnable() {
@Override
public void run() {
insert();
}
},"tread01").start();
new Thread(new Runnable() {
@Override
public void run() {
insert();
}
},"tread02").start();
}
private static void insert()
{
if (lock.tryLock())
{
System.out.println(Thread.currentThread().getName() + " 获得锁成功");
try{
System.out.println(Thread.currentThread().getName() + " 开始添加元素");
for (int i =0; i < 5; ++i)
{
list.add(i);
System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
}
System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
}
finally {
System.out.println(Thread.currentThread().getName() + " 释放了锁");
lock.unlock();
}
}
else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败");
}
}
}
复制代码
运行结果:
跟上面例子很相似,不同的是在需要同步的insert()方法里用tryLock()而不是lock(),同样起两个工作线程,调用final实例对象tryLocklMain的insert()方法向list里add元素。Thread01先获得lock锁,进行for循环打印,此时Thread02也调用tryLock()方法尝试获取lock锁,由于Thread01此时持有该锁,Thread02获取锁失败,无需等待直接返回false,走else语句打印“获取锁失败”。
4.3 可重入锁
ReentrantLock和sychronized是可重入锁
可重入锁:指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。
package com.example.onlyqi.juc;
/**
* @author onlyqi
* @date 2022/5/30 14:28
* @description
*/
public class ReentrantTest implements Runnable {
public synchronized void get() {
System.out.println(Thread.currentThread().getName());
set();
}
public synchronized void set() {
System.out.println(Thread.currentThread().getName());
}
public void run() {
get();
}
public static void main(String[] args) {
ReentrantTest rt = new ReentrantTest();
for(int i=0;i<100;i++){
new Thread(rt).start();
}
}
}
复制代码
整个过程没有发生死锁的情况,输出结果如下
不可重入锁:不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。
4.4 读写锁
独占锁:指该锁一次只能被一个线程所持有。对reentrantLock和synchronized都是独占锁
共享锁:指该锁可以被多个线程所持有
复制代码
读写锁
ReentrantReadWriteLock 实现了 ReadWriteLock 接口。
对ReentrantReadWriteLock其读锁是共享,其写锁是独占
写的时候只能被一个线程持有,读的时候能被多个线程所持有
读写场景时:读的时候不应该用独占锁,影响并发性,读的时候不会造成数据不一致问题,因此可以多个人共享读
● 读-读:能共存
● 读-写:不能共存
● 写-写:不能共存
public class ReadWirteSync {
/**
* 资源类
*/
static class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// private Lock lock = null;
/**
* 定义写操作
* 满足:原子 + 独占
*
* @param key
* @param value
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
}
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
}
}
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 线程操作资源类,5个线程写
for (int i = 0; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 线程操作资源类, 5个线程读
for (int i = 0; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
复制代码
运行的结果:
在写入的时候,写入完成时被别的线程打断了。还没写完,就读取了
解决方法:
上面多线程代码是没有加锁的,在线程进行写入操作时,被其他线程频繁打断,从而不具备原子性
使用synchronized锁,又太重量级,以为读的时候可以并发读。
所以,使用读写锁
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();
//读操作的时候,转换为读锁
// 创建一个读锁
rwLock.readLock().lock();
// 读锁 释放
rwLock.readLock().unlock();
复制代码
上代码:
package com.example.onlyqi.juc;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author onlyqi
* @date 2022/5/30 14:57
* @description
*/
public class ReadWirteSync {
/**
* 资源类
*/
static class MyCache {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private volatile Map<String, Object> map = new HashMap<>();
// private Lock lock = null;
/**
* 定义写操作
* 满足:原子 + 独占
*
* @param key
* @param value
*/
public void put(String key, Object value) {
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
rwLock.writeLock().unlock();
}
public void get(String key) {
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
rwLock.readLock().unlock();
}
}
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 线程操作资源类,5个线程写
for (int i = 1; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, "写线程"+i+":").start();
}
// 线程操作资源类, 5个线程读
for (int i = 1; i < 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, "读线程"+i+":").start();
}
}
}
复制代码
运行结果:
4.5 Java自旋锁
1.自旋锁:自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
2.为什么要使用自旋锁
CAS全称呼Compare-And-Swap,它是一条CPU并发原语,俗称比较并交换!
多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。
复制代码
3. 线程自旋与线程阻塞 阻塞的缺点显而易见,线程一旦进入阻塞(Block),再被唤醒的代价比较高,性能较差。自旋的优点是线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源,所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。JVM在处理synchrized实现时就是采用了这种折中的方案,并提供了调节自旋的参数。
4.首先来对比一下互斥锁和自旋锁。 互斥锁:从等待到解锁过程,线程会从sleep状态变为running状态,过程中有线程上下文的切换,抢占CPU等开销。 自旋锁:从等待到解锁过程,线程一直处于running状态,没有上下文的切换。 5.虽然自旋锁效率比互斥锁高,但它会存在下面两个问题 自旋锁一直占用CPU,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。 试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
public class SpinLocakDemo {
/**
* 手写一个自旋锁
* 循环比较获取直到成功为止,没有类似于wait的阻塞
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
*/
public static class SpinLockDemo {
// 现在的泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in ");
// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
while (!atomicReference.compareAndSet(null, thread)) {
}
}
/**
* 解锁
*/
public void myUnLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
// 自己用完了后,把atomicReference变成null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
}
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
// 启动t1线程,开始操作
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始释放锁
spinLockDemo.myUnLock();
}, "t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒后,启动t2线程,开始占用这个锁
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
// 开始释放锁
spinLockDemo.myUnLock();
}, "t2").start();
}
}
复制代码
5. 线程间通信
5.1 线程间通信必知点
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式,
Lock锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:
• await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重 新获得锁并继续执行。
• signal()用于唤醒一个等待的线程。
注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦 获得锁成功就继续执行。
5.2 线程间通信demo
场景---两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求 用线程间通信
5.2.1 synchronized 方案
package com.example.onlyqi.juc;
/**
* @author onlyqi
* @date 2022/5/30 16:11
* @description
*/
/**
* volatile 关键字实现线程交替加减
*/
public class TestVolatile {
/**
* 交替加减
* @param args
*/
public static void main(String[] args){
DemoClass demoClass = new DemoClass();
new Thread(() ->{
for (int i = 0; i < 5; i++) {
demoClass.increment();
}
}, "线程 A").start();
new Thread(() ->{
for (int i = 0; i < 5; i++) {
demoClass.decrement();
}
}, "线程 B").start();
}
}
class DemoClass{
//加减对象
private int number = 0;
/**
* 加 1
*/
public synchronized void increment() {
try {
while (number != 0){
this.wait();
}
number++;
System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------,值为:" + number);
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 减一
*/
public synchronized void decrement(){
try {
while (number == 0){
this.wait();
}
number--;
System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------,值为:" + number);
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
}
复制代码
运行结果:
5.2.2 Lock方案
package com.example.onlyqi.juc;
/**
* @author onlyqi
* @date 2022/5/30 16:11
* @description
*/
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* volatile 关键字实现线程交替加减
*/
public class TestVolatile {
/**
* 交替加减
* @param args
*/
public static void main(String[] args){
DemoClass demoClass = new DemoClass();
new Thread(() ->{
for (int i = 0; i < 5; i++) {
demoClass.increment();
}
}, "线程 A").start();
new Thread(() ->{
for (int i = 0; i < 5; i++) {
demoClass.decrement();
}
}, "线程 B").start();
}
}
class DemoClass{
//加减对象
private int number = 0;
//声明锁
private Lock lock = new ReentrantLock();
//声明钥匙
private Condition condition = lock.newCondition();
/**
* 加 1
*/
public void increment() {
try {
lock.lock();
while (number != 0){
condition.await();
}
number++;
System.out.println("--------" + Thread.currentThread().getName() + "加一成功----------,值为:" + number);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* 减一
*/
public void decrement(){
try {
lock.lock();
while (number == 0){
condition.await();
}
number--;
System.out.println("--------" + Thread.currentThread().getName() + "减一成功----------,值为:" + number);
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
复制代码
5.3 线程间定制化通信
==问题: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照 此顺序循环 2 轮==
package com.example.onlyqi.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author onlyqi
* @date 2022/5/30 16:21
* @description
*/
public class DemoClass {
//通信对象:0--打印 A 1---打印 B 2----打印 C
private int number = 0;
//声明锁
private Lock lock = new ReentrantLock();
//声明钥匙 A
private Condition conditionA = lock.newCondition();
//声明钥匙 B
private Condition conditionB = lock.newCondition();
//声明钥匙 C
private Condition conditionC = lock.newCondition();
/**
* A 打印 5 次
*/
public void printA(int j){
try {
lock.lock();
while (number != 0){
conditionA.await();
}
System.out.println(Thread.currentThread().getName() + "输出 A,第" + j + "轮开始");
//输出 5 次 A
for (int i = 0; i < 5; i++) {
System.out.println("A");
}
//开始打印 B
number = 1;
//唤醒 B
conditionB.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* B 打印 10 次
*/
public void printB(int j){
try {
lock.lock();
while (number != 1){
conditionB.await();
}
System.out.println(Thread.currentThread().getName() + "输出 B,第" + j + "轮开始");
//输出 10 次 B
for (int i = 0; i < 10; i++) {
System.out.println("B");
}
//开始打印 C
number = 2;
//唤醒 C
conditionC.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
/**
* C 打印 15 次
*/
public void printC(int j){
try {
lock.lock();
while (number != 2){
conditionC.await();
}
System.out.println(Thread.currentThread().getName() + "输出 C,第" + j + "轮开始");
//输出 15 次 C
for (int i = 0; i < 15; i++) {
System.out.println("C");
}
System.out.println("-----------------------------------------");
//开始打印 A
number = 0;
//唤醒 A
conditionA.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
/**
* volatile 关键字实现线程交替加减
*/
class TestVolatile {
/**
* 交替加减
* @param args
*/
public static void main(String[] args){
DemoClass demoClass = new DemoClass();
new Thread(() ->{
for (int i = 1; i <= 2; i++) {
demoClass.printA(i);
}
}, "A 线程").start();
new Thread(() ->{
for (int i = 1; i <= 2; i++) {
demoClass.printB(i);
}
}, "B 线程").start();
new Thread(() ->{
for (int i = 1; i <= 2; i++) {
demoClass.printC(i);
}
}, "C 线程").start();
}
}
复制代码
运行结果:
6. 集合的线程安全
请看我之前写的一篇文章,传送门:
java集合:线程安全的实现方式与分析 传送门:
JUC必要掌握,学习第一天
JUC必要掌握(Callable&Future、JUC 三大辅助类、 阻塞队列),学习第三天