Java并发与多线程
1. 并发与并行
并发是指某个时间段内,多任务交替处理的能力;并行是指同时处理多任务的能力,多核CPU可以实现并行任务。
并发执行的特点:
(1)并发程序间相互制约:程序执行结果的相互依赖以及共享资源(如处理器、缓冲区)的竞争;
(2)并发程序的执行过程是断断续续的,程序需要记忆现场指令及执行点;
(3)并发数设置合理且CPU拥有足够的处理能力时,并发可以提高程序的运行效率。
2. 线程与进程
进程是系统进行资源分配和调度的基本单位,在Java中当我们启动main函数时其实就启动了一个JVM进程,main函数所在的线程就是这个进程中的一个线程,也称主线程。
线程是进程的一个实体,是进程的一个执行路径,一个进程中至少有一个线程 ,进程中多个线程共享进程的资源。由于真正占用CPU运行的是线程,所以进程是CPU分配的基本单位。
由上图可知,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域,用于保存程序的执行位置以及线程的局部变量。进程中的堆被进程中所有线程共享,主要存放new操作创建的对象实例。方法区也是线程共享的,用于存放JVM加载的类,常量及静态变量等信息。
3. 多线程与并发的基本概念
内存可见性:JVM中的内存空间,服务<-->进程,线程;
synchronized关键字:Java提供的原子性内置锁,同一时间只有一个线程能进行操作该关键字修饰的代码块
volatile关键字:解决共享变量的可见性问题,线程在写入变量时不会将值缓存在寄存器或者其他地方而是直接写入主内存;保证了值的可见性,不能保证操作的原子性,即适用于写入变量值不依赖变量当前值的场景。
原子性操作:进行一系列操作时要么全部执行,要么全部不执行
CAS(Compare And Swap)比较交换,循环比较,若一致则交换,否则循环直到一致再交换
指令重排:Java内存模型允许编译器和处理器对指令重排序以提高运行性能。对于无依赖性的代码在执行过程中可能会进行指令重排,导致在多线程中操作共享变量时可能会重排指令共享变量的执行顺序,造成结果异常,可使用volatile修饰变量,使此变量写之前的操作不会重排到写之后。
伪共享:CPU一级缓存或二级缓存同一行加载多个变量,在多线程并发执行时可能会产生多个变量的值同时改变,使缓存失效,需要退回到主内存中获取变量值,影响程序执行效率。可以使用@Contended注解解决伪共享问题,但是限制在Java核心类使用,若需在用户类路径下使用,需要添加JVM参数:-XX:RestrictContended,填充宽度默认为128,要自定义宽度可以设置-XX:ContendedPaddingWidth参数。
4. 线程的三种创建方式
a. 实现Runnable接口的run方法,可以继承其他类,任务无返回值
public class ThreadTest { //创建线程 MyThread thread = new Thread(new Runnable() { public void run() { System.out.println("I'm a child thread."); } }); thread.start(); }
b. 继承Thread类并重写run方法,不能继承其他类,任务无返回值
public static class ThreadTest { public static class MyThread extends Thread { @Override public void run() { System.out.println("I'm a child thread"); } } public static void main(String[] args) { //创建线程 MyThread thread = new MyThread(); //启动线程 thread.start(); } }
c. 使用FutureTask方式,可以继承其他类,也可以拿到返回结果
public static class CallerTask implements Callable<String> { @Override public String call() throws Exception { return "hello"; } public static void main(String[] args) throws InterruptedException { //创建异步任务 FutureTask<String> futureTask = new FutureTask<>(new CallerTask()); //启动线程 new Thread(futureTask).start(); try { //等待任务执行完毕,并返回结果 String result = futureTask.get(); System.out.println(result); } catch (ExcutionException e) { e.printStackTrace(); } } }
5. 线程的五种状态
6. 线程通知与等待
a. wait()等待:调用该方法的线程被阻塞挂起,其他线程调用了该共享对象的notify()或notifyAll()方法正常返回,若调用该线程的interrupt()方法,抛出InterruptedException异常返回
b. notify()通知:一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait()方法后被挂起的线程。
c. notifyAll()广播:唤醒阻塞在该共享变量上的所有线程。
7. 线程的其他基本方法
a. join 等待线程执行中止的方法:在线程1中使用线程2调用join方法会使线程1阻塞直到线程2执行结束后继续执行
b. sleep 调用线程会让出指定时间的CPU执行权,此时间段内线程被阻塞挂起,持有的锁并不释放。
c. yield 让出剩余时间片的CPU执行权,并未被阻塞,任处于就绪状态。
d. 线程中断
void interrupt():中断线程,线程B调用线程A的interrupt()方法设置线程A的中断标志为true,线程A并未被中断,仍继续执行,若A已被阻塞挂起则线程A会在被阻塞的地方抛出InterruptedException异常返回
boolean isInterrupted():检测当前是否被中断
boolean interrupted():检测当前线程是否被中断,若发现被中断,则会清除中断标志。
8.线程池
线程池主要解决两个问题:一是当执行大量异步任务时线程池能提供较好的性能,线程池中的线程是可复用的,减少创建和销毁线程的开销;二是线程池提供了一种资源限制和管理的手段,如限制线程的个数,动态新增线程等。
ThreadPoolExcutor:线程池提供了许多可调参数和可扩展接口,如newCachedThreadPool(线程池个数最多可达Integer.MAX_VALUE,线程自动回收),newFixedThreadPool(固定大小线程池),newSingleThreadExecutor(单个线程)。
ScheduledThreadPoolExecutor:可以指定一定延迟时间后或者定时进行任务调度的线程池。、
public static ThreadPoolTest{ //命名线程工厂 static class NamedThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; NamedThreadFactory(String name) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); if (null == name || name.isEmpty()) { name = "pool"; } namePrefix = name + "_" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon) { t.setDaemon(false); } if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } } //设置线程名称 static final String THREAD_SAVE_ACCEPT = "THREAD-ACCEPT"; static final String THREAD_SAVE_PROCESS = "THREAD-PROCESS"; //创建线程池 static ThreadPoolExecutor excutorAccept = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new NamedThreadFactory(POOL_ACCEPT); ); static ThreadPoolExecutor excutorProcess = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new NamedThreadFactory(POOL_PROCESS); ); public static void main(String[] args) { excutorAccept.execute(new Runnable(){ public void run() { System.out.println("接受用户链接线程"); } }); } public static void main(String[] args) { excutorProcess.execute(new Runnable(){ public void run() { System.out.println("处理业务线程"); } }); } excutorAccept.shutdown(); excutorProcess.shutdown(); }
ThreadLocal
ThreadLocal是一个工具类,具体存放变量的是线程的threadLocals。threadLocals是一个ThreadLocalMap类型变量,内部是一个Entry数组,Entry内部的value用来存放通过ThreadLocal的set方法传递的值。
如果在线程池中使用ThreadLocal变量,要注意调用ThreadLocal的remove方法清理线程对ThreadLocal对象及值的引用,使JVM能够回收内存。原因:线程池默认创建的线程均为用户线程,在不主动调用shutdown方法时,用户线程会一直存在,导致用户线程一直持有ThreadLocal变量,使JVM无法进行内存回收,导致内存泄露。