初探JUC

444 阅读16分钟

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() :关闭连接池

创建线程池的方式:

  1. 直接通过ThreadPoolExecutor实现类new

  2. 通过工厂类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();
		}
	}
}

总结:

以上为本文全部内容。 本人也是初次接触,如有错误,希望大佬指点一二!