JUC
一、JUC简介
在 Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。
二、多线程
1.线程和进程
程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
进程(process)是程序的一次执行过程,或是正在运行的一个程序。进程是一个动态过程,即有它自身的产生、存在和消亡的过程。每个Java程序都有一个隐含的主程序,即main方法。
线程(thread),线程是进程内部的一条具体的执行路径。若一个程序可同一时间执行多个线程,就是支持多线程的。
总结:程序是静态的,程序运行后变为一个进程,一个进程内部可以有多个线程同时执行。进程是所有线程的集合,每一个线程是进程中的一条执行路径。
并发和并行的关系:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。 并行的关键是你有同时处理多个任务的能力。
它们最关键的点就是:是否是『同时』。
2.多线程的优势
①提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
②提高计算机系统CPU的利用率
③改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
④使用线程可以将耗时任务放到后台去处理,例如等待用户输入、文件读写和网络收发数据等。
3.线程安全
3.1线程安全定义
当多个线程同时共享同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
3.2 线程安全的解决方式
使用多线程之间同步或使用锁(lock)可以解决线程安全问题。其核心在于将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
3.3 什么是多线程之间的同步
多个线程共享同一个资源的环境下,每个线程工作时不会受到其他线程的干扰称之为多线程之间的同步。
4.解决线程安全
4.1使用同步代码
synchronized(同一个对象){
可能会发生线程冲突问题
}
**注意:**在同步代码块中,多个线程必须使用的是同一把锁,即同一个对象。
一般情况下,在使用Runnable实现的线程类中,我们会使用this作为锁对象。
在使用Thread继承的线程类中,一般会使用其Class对象(Class对象在JVM中只会创建一次)。
4.2使用同步方法
class Ticket {
private int number = 400;
// 1.同步方法
public synchronized void sale() {
// 2.同步代码块
synchronized (this) {
}
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖第" + (number--) + "张票\t ,还剩" + number);
}
}
public void sale1() {
Lock lock = new ReentrantLock();
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖第" + (number--) + "张票,还剩" + number);
}
} finally {
lock.unlock();
}
}
}
/**
*
* @Description:三个卖票员卖30张票
*
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket tk = new Ticket();
// new Thread(()->{for(int i = 0;i<30;i++) tk.sale();}, "AA").start();
// new Thread(()->{for(int i = 0;i<30;i++) tk.sale();}, "BB").start();
// new Thread(()->{for(int i = 0;i<30;i++) tk.sale();}, "CC").start();
new Thread(()->{for(int i = 0;i<30;i++) tk.sale1();}, "AA").start();
new Thread(()->{for(int i = 0;i<30;i++) tk.sale1();}, "BB").start();
new Thread(()->{for(int i = 0;i<30;i++) tk.sale1();}, "CC").start();
}
}
注意:如果使用Thread继承的方式实现多线程,那么同步方法需要是一个静态的方法
4.3使用Lock解决线程安全
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。Lock实现提供更广泛的锁定操作可以比使用 synchronized获得方法和声明更好。他们允许更灵活的结构,可以有完全不同的特性,可以支持多个相关的 Condition对象。Lock提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
三、多线程的创建方式
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用四种方式来创建线程,如下所示:
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executors工具类
1.继承Thread类
①定义子类继承Thread类。
②子类中重写Thread类中的run方法。
③创建Thread子类对象,即创建了线程对象。
④调用线程对象start方法启动线程,默认调用run方法。
注意:如果只是调用run方法,则此时会在调用该方法的线程中来执行,而不是另启动一个线程。
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
//线程需要执行的任务
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建线程实例 并且调用start方法启动线程
}
}
2.实现Runnable接口创建线程
①定义子类,实现Runnable接口。
②子类中重写Runnable接口中的run方法。
③通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给
Thread类的构造方法中。
④调用Thread类的start方法启动线程,其最终调用Runnable子类接口的run方法。
public class MyThread implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//通过Thread类含参构造器创建线程对象
MyThread myThread=new MyThread();
//将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中 AA为该线程的名称
Thread thread=new Thread(myThread,"AA");
//线程启动
thread().start();
}
}
两种方式的区别:
-
继承Thread:线程代码存放Thread子类run方法中。
-
实现Runnable:线程代码存在接口的子类的run方法中。
实现Runnable接口避免了单继承的局限性,多个线程可以共享同一个接口子类的对象,非常适合多个相同线程来处理同一份资源。
3.使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
-
call()方法可以有返回值
-
call()方法可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务 V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值 V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException boolean isDone():若Callable任务完成,返回True boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
①创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
②使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
③使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
④调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
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 = new myThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
new Thread(futureTask, "未来任务").start();
System.out.println("主线程结束!");
Integer integer = futureTask.get();
System.out.println(integer);
}
}
4.使用线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。因此提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
优势:
-
提高响应速度(减少了创建新线程的时间)
-
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
-
便于线程管理
JDK 5.0起提供了ExecutorService 和 Executors来实现线程池。
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
void shutdown() :关闭连接池
创建线程池的方式:
-
直接通过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();
}
-
1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象。
-
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
-
Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。
ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较
一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。(该段话摘自《Thinking in Java》第四版)
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();
}
在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。
Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。
当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
下面给出一个Executor执行Callable任务的示例代码:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class CallableDemo{
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> resultList = new ArrayList<Future<String>>();
//创建5个任务并执行
for (int i = 0; i < 5; i++){
//使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
Future<String> future = executorService.submit(new TaskWithResult(i));
//将任务执行结果存储到List中
resultList.add(future);
}
//遍历任务的结果
for (Future<String> fs : resultList){
try{
//Future返回如果没有完成,则一直循环等待,直到Future返回完成
while(!fs.isDone);
//打印各个线程(任务)执行的结果
System.out.println(fs.get());
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}finally{
//启动一次顺序关闭,执行以前提交的任务,但不接受新任务
executorService.shutdown();
}
}
}
}
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
/**
* 任务的具体过程,一旦任务传给ExecutorService的submit方法,
* 则该方法自动在一个线程上执行
*/
public String call() throws Exception {
System.out.println("call()方法被自动调用!!!" + Thread.currentThread().getName());
//该返回结果将被Future的get方法得到
return "call()方法被自动调用,任务返回的结果是:" + id + "" + Thread.currentThread().getName();
}
}
四、JUC工具类
1. ReentrantReadWriteLock
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
当没有其他线程的写锁时,线程进入读锁。当没有其他线程的读锁和写锁时,才会进入当前线程的写锁!
class MyQueue {
private Object obj;
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void readObj() {
//上读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取的内容是:" + obj);
} finally {
//下读锁
readWriteLock.readLock().unlock();
}
}
public void writeObj(Object obj) {
//上写锁
readWriteLock.writeLock().lock();
try {
this.obj = obj;
System.out.println(Thread.currentThread().getName() + "写入的内容为:" + obj);
} finally {
//下写锁
readWriteLock.writeLock().unlock();
}
}
}
/**
*
*/
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
// 创建资源对象
MyQueue queue = new MyQueue();
// 一个线程写
new Thread(() -> {
queue.writeObj("放假了");
}, "AA").start();
//100个线程读
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
queue.readObj();
}, String.valueOf(i)).start();
}
}
}
2. CountDownLatch
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
/**
*
* @Description:六个同学都走了之后,班长才可以关门
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 六个同学离开教室
// 加锁
CountDownLatch cd = new CountDownLatch(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() + "班长锁门!");
}
}
3. CyclicBarrier
当所有线程都结束,主线程才执行
CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。
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) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
4.Semaphore
在信号量上我们定义两种操作:
acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
/**
* 在信号量上我们定义两种操作: acquire (获职)当一个线程调用acquire操作时, 它要么通过成功获取信号量(信号量减1),
* 要么一直等下去,直到有线程释放信号量,或超时。 release (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
* 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 情景: 3个停车位,6部汽车争抢车位
*
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 创建三个停车位
Semaphore sp = new Semaphore(3);
//六量车抢夺三个停车位
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
//进停车位
sp.acquire();
System.out.println(Thread.currentThread().getName() + "号车驶入停车位");
//停三秒
TimeUnit.SECONDS.sleep(3);
//驶出停车位
sp.release();
System.out.println(Thread.currentThread().getName() + "号车驶出停车位");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
总结:
以上为本文全部内容。 本人也是初次接触,如有错误,希望大佬指点一二!