一、JUC
在 Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。
二、多线程
2.1 进程和线程
- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(静态的)。
- 进程(process)是程序的一次执行过程,或是正在运行的一个程序。进程是一个动态过程,即有它自身的产生、存在和消亡的过程。每个Java程序都有一个隐含的主程序,即main方法(动态的,是线程的集合)。
- 线程(thread),线程是进程内部的一条具体的执行路径。若一个程序可同一时间执行多个线程,就是支持多线程的(一个线程是进程中的一条执行路径)。
2.2 多线程的优势
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
- 使用线程可以将耗时任务放到后台去处理,例如等待用户输入、文件读写和网络收发数据等。
2.3 线程安全
当多个线程同时共享同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
2.4 线程安全的解决方式
使用多线程之间同步锁或使用锁(lock)可以解决线程安全问题。
2.4.1 使用同步代码块
synchronized(同一个对象){
可能会发生线程冲突问题
}
在同步代码块中,多个线程必须使用的是同一把锁,即同一个对象
2.4.2 使用同步方法
class Ticket
{
private int number = 30;
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
}
}
用Thread继承的方式实现多线程,那么同步方法需要是一个静态的方法!
2.4.3 使用Lock解决线程安全
class Ticket
{
private int number = 30;
//创建锁
Lock lock = new ReentrantLock();
public void sale() {
//上锁
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} finally {
//解锁
lock.unlock();
}
}
}
- Lock实现提供更广泛的锁定操作可以比使用 synchronized获得方法和声明更好
- 他们允许更灵活的结构,可以有完全不同的特性,可以支持多个相关的 Condition对象。
- Lock提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
2.5 线程同步
多个线程共享同一个资源的环境下,每个线程工作时不会受到其他线程的干扰称之为多线程之间的同步。
三、多线程的创建方式
3.1 继承Thread类
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法启动线程,默认调用run方法。
3.2 实现Runnable接口(优先使用)
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
- 调用Thread类的start方法启动线程,其最终调用Runnable子类接口的run方法。
3.3 区别
- 继承Thread:线程代码存放Thread子类run方法中。
- 实现Runnable:线程代码存在接口的子类的run方法中。
- 实现Runnable接口避免了单继承的局限性,多个线程可以共享同一个接口子类的对象,非常适合多个相同线程来处理同一份资源。
3.4 使用Callable接口
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果。
3.4.1 Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
- 多个线程同时执行一个FutureTask,只要一个线程执行完毕,其他线程不会再执行其call()方法。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+" Come in call");
//睡5秒
TimeUnit.SECONDS.sleep(5);
//返回200的状态码
return 200;
}
}
public class CallableTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建MyThread对象
MyThread myThread = new MyThread();
//创建未来任务对象
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
//创建处理未来任务的线程
new Thread(futureTask, "这是未来任务").start();
System.out.println(Thread.currentThread().getName()+"主线程继续");
//获取未来任务的结果
Integer result = futureTask.get();
System.out.println(result);
Integer result2 = futureTask.get();
System.out.println(result2);
}
}
3.5 使用线程池
JDK 5.0起提供了
ExecutorService
和Executors
来实现线程池。
3.5.1 优势
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
3.5.2 ExecutorService
- 真正的线程池接口。常见子类
ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
- void shutdown() :关闭连接池
3.5.3 创建线程池的两种方式
- 直接通过ThreadPoolExecutor实现类new
- 通过工厂类Executors的静态方法创建,本质上也是通过1创建的线程池
public static void main(String[] args) {
//创建一个包含10个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 12; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
executorService.shutdown();
}
四、JUC工具类
4.1 ReentrantReadWriteLock
- 对共享资源有读和写的操作,且写操作没有读操作那么频繁
- 在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源
- 如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyQueue
{
//创建读写锁
ReadWriteLock rwl = new ReentrantReadWriteLock();
private Object obj;
public void readObj()
{
//上读锁
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读到的内容是:"+obj);
} finally {
//解读锁
rwl.readLock().unlock();
}
}
public void writeObj(Object obj)
{
//上写锁
rwl.writeLock().lock();
try {
this.obj = obj;
System.out.println(Thread.currentThread().getName()+"写入的内容是:"+obj);
} finally {
//解写锁
rwl.writeLock().unlock();
}
}
}
/**
*
* @Description: 一个线程写入,100个线程读取
*
*/
public class ReadWriteLockDemo{
public static void main(String[] args) throws InterruptedException{
/*
* JAVA的并发包提供了读写锁ReentrantReadWriteLock,
* 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
*
* */
//创建资源对象
MyQueue mq = new MyQueue();
//一个线程写
new Thread(()->{
mq.writeObj("快放学了");
}, "AA").start();
//100个线程读
for (int i = 1; i <= 100; i++) {
new Thread(()->{
//读内容
mq.readObj();
}, String.valueOf(i)).start();
}
}
}
4.2 CountDownLatch
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.SynchronousQueue;
/**
*
* @Description:
* *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。
*
*
* 解释:6个同学陆续离开教室后值班同学才可以关门。
*
* main主线程必须要等前面6个线程完成全部工作后,自己才能开干
*/
public class CountDownLatchDemo
{
public static void main(String[] args) throws InterruptedException
{
CountDownLatch cd = new CountDownLatch(6);
//6个同学离开教室
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开教室!");
//减少计数
cd.countDown();
}, String.valueOf(i)).start();
}
//等待
cd.await();
//班长锁门
System.out.println(Thread.currentThread().getName()+"班长锁门!");
}
}
4.3 CyclicBarrier
CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
*
* @Description: TODO(这里用一句话描述这个类的作用)
*
* CyclicBarrier
* 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
* 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
* 直到最后一个线程到达屏障时,屏障才会开门,所有
* 被屏障拦截的线程才会继续干活。
* 线程进入屏障通过CyclicBarrier的await()方法。
*
* 集齐7颗龙珠就可以召唤神龙
*/
public class CyclicBarrierDemo
{
private static final int NUMBER = 7;
public static void main(String[] args)
{
//CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cb = new CyclicBarrier(NUMBER,()->{
System.out.println("可以召唤神龙了!");
});
for (int i = 1; i <= NUMBER; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"号龙珠被收集!");
//等待
try {
cb.await();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
4.4 Semaphore
acquire
(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。release
(释放)实际上会将信号量的值加1,然后唤醒等待的线程。- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
*
* @Description: TODO(这里用一句话描述这个类的作用)
*
* 在信号量上我们定义两种操作:
* acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
* 要么一直等下去,直到有线程释放信号量,或超时。
* release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
*
* 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
*
* 情景:3个停车位,6部汽车争抢车位
*/
public class SemaphoreDemo
{
public static void main(String[] args)
{
//创建三个停车位
Semaphore sp = new Semaphore(3);
//6辆车抢夺3个停车位
for (int i = 1; i <= 6 ; i++) {
new Thread(()->{
try {
//获取资源
sp.acquire();
System.out.println(Thread.currentThread().getName()+"号车驶入停车位!");
//停车3秒
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"号车驶出停车位!");
//释放资源
sp.release();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
五、Life
愿你被世界温柔以待。
昨天越来越多,明天越来越少,这就叫人生,你之所以觉得时间一年比一年过得快,是因为时间对你一年比一年重要,别因为害怕孤单而凑合着相拥,也别因为一时的别无选择而将就的活着,总要有一段路,需要你独自走过,愿你是阳光,明媚不忧伤。
来源:CSDN
作者:SmallScorpion
链接:https://blog.csdn.net/qq_40180229/article/details/103896234