一、JUC并发编程
JUC 是 Java 5.0 之后新增的一个包 java.util.concurrent ,该包提供了一套并发编程的工具类,包括原子操作、线程池、Lock、Condition 等类,方便进行多线程编程的操作。
JUC 的出现是为了解决多线程共享资源,协作完成任务时常见的问题,如同时访问共享资源、线程死锁、饥饿、并行性不足等问题。使用 JUC 提供的工具类可以简化并发程序的编写,提高程序的效率和稳定性
二、synchronized
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized
关键字的使用方式主要有下面 3 种:
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
synchronized 和 volatile 有什么区别?
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
三、LOCK锁
java.util.concurrent.locks.Lock 是一个接口,主要有三个实现:
ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(可重入读锁)
ReentrantReadWriteLock.WriteLock(可重入写锁)
(1)可重入锁
public class LockDemo {
public static void main(String[] args) {
//多线程操作
Ticket2 ticket = new Ticket2();
new Thread(()-> { for (int i = 0; i < 60; i++) ticket.sale(); },"A").start();
new Thread(()-> { for (int i = 0; i < 60; i++) ticket.sale(); },"B").start();
new Thread(()-> { for (int i = 0; i < 60; i++) ticket.sale(); },"C").start();
new Thread(()-> { for (int i = 0; i < 60; i++) ticket.sale(); },"D").start();
}
}
//资源类
class Ticket2{
private static int number=50;
Lock lock= new ReentrantLock(false);//设置true——公平锁,设置false-非公平锁,默认为非公平锁
//卖票方式
public void sale(){
//加锁
lock.lock();
try {
if (number>0){
System.out.println(Thread.currentThread().getName() + "购买了第" + (number--) + "张票,剩余票数为"+number);
}
} finally {
//解锁
lock.unlock();
}
}
}
synchronized锁与Lock锁的区别:
synchronized是内置的java关键字,而Lock是一个接口 。
synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁。
synchronized会自动释放锁,Lock必须要手动释放锁!否则会造成死锁.
synchronized 线程1(获得锁,阻塞),线程2(等待,傻傻的等);Lock锁就不会一直等下
synchronized 可重入锁 不可以中断的 非公平,Lock 可重入锁 可以判断的 非公平(可以设置)
synchronized 适合锁少量同步代码,Lock适合锁大量同步代码.
(2)公平锁
ReentrantLock 还可以实现公平锁。所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
true—创建公平锁(在锁上等待最长时间的线程有最高优先级)false—创建非公平锁
public class FairLockTest {
int ticket = 100;
private final ReentrantLock lock = new ReentrantLock(true);
public void sellTicket(){
while(true){
try{
lock.lock();
if(ticket > 0){
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " 出售了一张票,票号为:" + ticket--);
}else{
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
FairLockTest fairLockTest = new FairLockTest();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
fairLockTest.sellTicket();
}, "Thread-" + i).start();
}
}
}
(3)读写锁
在实际场景中,读操作不会改变数据,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,Java 的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为读锁,这是一种共享锁;一个是写相关的锁,称为写锁,这是一种排他锁,也叫独占锁、互斥锁。读写锁支持非公平/公平策略。
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
public class ReadWriteLockTest {
private final ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
//获得写锁
ReentrantReadWriteLock.WriteLock writeLock = rrwl.writeLock();
//获得读锁
ReentrantReadWriteLock.ReadLock readLock = rrwl.readLock();
//写操作
public void write() {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 开始写入数据...");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " 写入数据成功!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
}
//读操作
public void read() {
try {
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 开始读取数据...");
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + " 读取数据成功!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockTest readWriteLock = new ReadWriteLockTest();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
readWriteLock.write();
}, "Thread-Write" + i).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
readWriteLock.read();
}, "Thread-Read-" + i).start();
}
}
}
以上的测试结果应为每一个写操作完成后,才会开始读操作(即对应读写不可并发)。若读和写调换位置,则结果应为先读完后再写。具体测试结果如下:
四、volatile
volatile是java虚拟机提供的轻量级的同步机制。
- 保证可见性
- 不保证原子性
- 禁止指令重排
public class TestJMM{
//volatile不保证原子性
private volatile static int flag=0;
public static void add(){
flag++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+":"+flag);
}
}
以上代码运行结果不为10000,原因是不保证原子性,将变量设置为原子类AtomicInteger后即可解决该问题。
五、集合安全问题
Java 中的集合类大多数情况下都是非线程安全的,包括 ArrayList、HashMap、HashSet 等,这些集合类在多线程环境下使用时会出现并发问题。因为多个线程可能同时操作同一个对象,并发地修改数据,导致数据的不一致性。
然而,Java 中也有一些集合类是线程安全的,包括 Vector、Hashtable 和 ConcurrentHashMap 等。
线程安全的集合类采用了各种方法来保证并发时的线程安全,内部的数据结构使用了锁或者写时复制等机制,以保证多个线程并发地访问时的安全性。
而线程不安全的集合类则没有进行相应的保护机制,多个线程访问同一个集合时可能会出现以下问题:
竞争条件问题:多个线程同时向集合中添加元素,可能导致元素的重复或丢失。
视图读取问题:在一个线程遍历集合时,另一个线程修改了集合,导致并发修改异常。
为了避免这些并发问题,可以使用线程安全的集合类,或者在操作非线程安全的集合类时采用适当的同步机制,比如使用 Synchronized 或者 ReentrantLock 进行同步,或者使用并发容器 ConcurrentLinkedQueue、CopyOnWriteArrayList 等代替原有的集合类。
List<String> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 3; j++) {
// 向集合对象写入数据
list.add(UUID.randomUUID().toString().substring(0, 5));
// 打印集合对象,等于是读取数据
System.out.println(list);
}
}).start();
}
以上代码会报以下异常:java.util.ConcurrentModificationException(并发修改异常)。原因是 ArrayList 的 add 及其他方法都是线程不安全的。
以上问题解决方法:
- 使用 Vector 集合类型,不会抛异常,线程安全,不过这个类太古老(JDK 1.0),内存消耗过大。
- 使用 Collections.synchronizedList(List list),不会抛异常,线程安全,不过锁定的范围大,性能低。
ConcurrentHashMap
ConcurrentHashMap 是 Java 中的一个并发容器,它提供了线程安全的 HashMap 实现。相比于 Hashtable 和 synchronizedMap,在并发场景下有较好的性能和吞吐量。ConcurrentHashMap 的实现原理是使用分段锁(Segment)来实现多个线程之间的独立并发访问。
具体来讲,ConcurrentHashMap 将内部的数据结构划分为一定数量的段,每个段都维护着一个大小可变的散列表。在默认情况下,ConcurrentHashMap 的段数是16,每个段中都有一个锁,不同的段可以由不同的线程进行访问,从而减少了线程之间的相互干扰。这个机制即为 “分段锁”,这意味着同一时刻多个线程可以并行地访问不同的段,因此在高并发的情况下,ConcurrentHashMap 的性能和吞吐量会更好。
在 Java 8 中,ConcurrentHashMap 的实现中使用了一种更加灵活的方式,分别替换为了 数组 + 链表 和 数组 + 红黑树。
六、常用辅助类
1. CountDownLatch
CountDownLatch(计数器闭锁)是 Java 并发包中的一个工具类,它允许一个或多个线程等待其他线程执行完某些操作后再继续执行。当线程需要等待某些条件达成后再执行任务时,可以使用 CountDownLatch 来完成。
CountDownLatch 内部维护了一个计数器,通过 countDown() 方法将计数器的值减 1,调用 await() 方法的线程会被阻塞,直到计数器的值为 0 时才会被唤醒继续执行。如果计数器的值一开始就是 0,则调用 await() 方法的线程不会被阻塞,可以直接继续执行。
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException {
//总数是6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"走了");
countDownLatch.countDown();//表示数量-1
},String.valueOf(i)).start();
}
//等待计数器归零在往下执行!
countDownLatch.await();
System.out.println("关门!");
}
}
2. CyclicBarrier
CyclicBarrier(循环栅栏)是 Java 并发包中的一个工具类,它可以使一组线程互相等待,直到达到某个公共的屏障点后再一起继续执行。
不同于 CountDownLatch,CyclicBarrier 可以在多个线程之间形成一个同步点,让这些线程在这个同步点处等待,而不是让一个线程去等待其他线程。
效果:多线程在执行各自任务的时候,到达某个状态点就等待,等所有线程都到达这个状态点再继续执行后步骤。
CyclicBarrier 内部维护着一个计数器和一个屏障点状态。每当一个线程到达屏障点时,它会调用 await() 方法等待其他线程到达,直到所有等待的线程都到达屏障点后,它们才会继续执行。而且,CyclicBarrier 可以循环使用,当所有等待的线程都被释放后,CyclicBarrier 重新回到初始化状态,可以被再次使用。
public class CyclicBarrierTest {
public static void main(String[] args) {
// 创建 CyclicBarrier 实例
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println(Thread.currentThread().getName() + "过关了!");
});
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName() + "通过第 1 关");
// 当所有人通过第一关才允许进入下一关
barrier.await();
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName() + "通过第 2 关");
barrier.await();
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread().getName() + "通过第 3 关");
barrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, String.valueOf(i)).start();
}
}
}
3. Semaphore
Semaphore(信号量)是 Java 并发包中的一个工具类,用于管理一组资源的访问。Semaphore 主要用于控制同时访问某个特定资源的线程数量,也可以用于实现整体流量控制或者控制某一资源池的访问数量。
Semaphore 内部维护了一个指定数量的许可证(permit),acquire() 方法尝试获取一个许可证,如果没有许可证可用就会阻塞等待直到有许可证可用;release() 方法释放一个许可证,使得其他等待许可的线程可以获取到许可继续执行。
使用 Semaphore 可以帮助我们管理资源位;当某个线程申请资源时,由 Semaphore 检查这个资源是否可用;如果其他线程释放了这个资源,那么申请资源的线程就可以使用。
Semaphore 可以初始化一个许可数量,当许可数量已经被占用时,尝试再次获取许可的线程会被阻塞在该 Semaphore 上,直到有一个许可被释放。因此,Semaphore 可以用作简单的线程池控制器。
public class SemaphoreTest {
public static void main(String[] args) {
// 创建 Semaphore 对象,指定资源数量为 3
Semaphore semaphore = new Semaphore(3);
// 创建 10 个线程争夺这 3 个资源
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 申请资源
semaphore.acquire();
// 拿到资源执行操作
System.out.println("【" + Thread.currentThread().getName() + "】号车辆【驶入】车位");
TimeUnit.SECONDS.sleep(3);
System.out.println("【" + Thread.currentThread().getName() + "】号车辆【驶出】车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 操作完成释放资源
semaphore.release();
}
}, i + "").start();
}
}
}