Java并发笔记-未完待续待详解
为什么需要并行? – 业务要求 – 性能 并行计算还出于业务模型的需要 – 并不是为了提高系统性能,而是确实在业务上需要多个执行单元。 – 比如HTTP服务器,为每一个Socket连接新建一个处理线程 – 让不同线程承担不同的业务工作 – 简化任务调度 Linus Torvalds :并行计算只有在 *图像处理* 和 *服务端编程* 2个领域可以使用,并且它在这2个领域确实有着大量广泛的使用。但是在其它任何地方,并行计算毫无建树! 计算密集型 在多核时代,一般没有必要特别区分并发和并行 同步(synchronous)和异步(asynchronous) 并发(Concurrency)和并行(Parallelism) 临界区 阻塞(Blocking)和非阻塞(Non-Blocking) – 阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。 – 非阻塞允许多个线程同时进入临界区 锁(Deadlock)、饥饿(Starvation)和活锁(Livelock) 并行的级别 并发级别 – 阻塞 – 非阻塞 – 无障碍 – 无锁 – 无等待 有关并行的2个重要定律 Amdahl定律(阿姆达尔定律) Gustafson定律(古斯塔夫森) 进程切换是一个重量级的操作,需要消耗大量的计算资源 在Java中的线程会直接映射到操作系统的线程上去 Thread t1 = new Thread(); t1.start(); // 开启线程进入就绪状态 Thread t2 = new Thread(); t2.run(); // 不能开启线程,在本线程内执行run方法 继承Thread重写run方法,MyThread extends Thread new Thread(new Runnable() {}).start(); Thread.stop()不建议使用@Deprecated,太过暴力,类似于Linux强制杀死进程:kill -9 thread_id public static native void sleep(long millis) throws InterruptedException public void run() { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("Interruted!"); break; } try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("Interruted When Sleep"); // 设置中断状态,抛出异常后会清除中断标记位 ******** Thread.currentThread().interrupt(); } Thread.yield(); } } suspend()、resume() 等待线程结束(join)和谦让(yeild) join的本质: while(isAlive()) { wait(0); } 线程执行完毕后,系统会调用notifyAll(); ===> 可以不需要在线程实例上使用wait()和notifyAll() 守护线程 在后台默默地完成一些系统性的服务,比如垃圾回收线程、 JIT线程就可以理解为守护线程 当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出 Thread t = new DaemonT(); t.setDaemon(true); t.start(); 高优先级的线程更容易再竞争中获胜 基本的线程同步操作 synchronized – 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。 public void run() { for(int j = 0; j < 10000000; j++) { synchronized(instance) { i++; } } } – 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。 public synchronized void increase(){ i++; } – 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。 public static synchronized void increase(){ i++ } Object.wait() Obejct.notify() public static class T1 extends Thread { public void run() { synchronized (object) { System.out.println(System.currentTimeMillis()+":T1 start!"); try { System.out.println(System.currentTimeMillis() + ":T1 wait for object "); object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(System.currentTimeMillis()+":T1 end!"); } } } public static class T2 extends Thread { public void run() { synchronized (object) { System.out.println(System.currentTimeMillis() +":T2 start! notify one thread"); object.notify(); System.out.println(System.currentTimeMillis()+":T2 end!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } 1425224592258:T1 start! 1425224592258:T1 wait for object 1425224592258:T2 start! notify one thread 1425224592258:T2 end! 1425224594258:T1 end notify() VS notifyAll() notify()随机唤醒一个线程,notifyAll()唤醒全部等待此监视器的线程,让他们去竞争这个监视器的使用权 》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》 Java内存模型和线程安全 原子性 原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。 有序性 在并发时,程序的执行可能就会出现乱序。 一条指令的执行是可以分为很多步骤的(*数据旁路技术*&*指令重排*可以使流水线更加流畅,指令重排不能出现语义问题) – 取指 IF – 译码和取寄存器操作数 ID – 执行或者有效地址计算 EX – 存储器访问 MEM – 写回 WB 可见性 可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。 – 编译器优化 – 硬件优化(如写吸收,批操作) Java虚拟机层面的可见性(volatile) Happen-Before 程序顺序原则:一个线程内保证语义的串行性 volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前 传递性:A先于B,B先于C,那么A必然先于C 线程的start()方法先于它的每一个动作 线程的所有操作先于线程的终结(Thread.join()) 线程的中断(interrupt())先于被中断线程的代码 对象的构造函数执行结束先于finalize()方法 线程安全的概念 指某个函数、函数库在多线程环境中被调用时,能够正确地处理各个线程的局部变量,使程序功能正确完成。 ReentrantLock(重入锁 )是synchronized关键字的增强,目前两者性能不相上下。 ReentrantLock 可重入 可多次加锁 可中断 发生死锁时,可启用一个守护线程去中断某一线程从而达到解锁目的 可限时 避免死锁和长期等待锁 公平锁 公平锁虽然不会产生饥饿但是由于公平锁需要解决排队问题(先到先得),所以性能较非公平锁差,没有特殊要求没必要使用公平锁。 JDK并发包--并发容器及典型源码分析 集合包装 HashMap --> 适用小并发量,串行解决方案,非高并发解决方案 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m); } List public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); } Set public static <T> Set<T> synchronizedSet(Set<T> s) { return new SynchronizedSet<>(s); } ConcurrentHashMap ConcurrentHashMap(HashMap底层使用数组实现,因此为实现大规模高并发,可将整个数组分成N个段<Segment>,一个就可供N个线程同时写入数据,理论提高效率N倍) put()方法各个Segment有各自的锁,get()方法无锁,但是在size()方法中需要拿到所有Segment的锁后才能统计数据,但size()方法并非一个高频率调用的函数。 之所是高性能,是因为不会随随便便就加锁,而是经过自旋等待在有必要的时候才加锁。 BlockingQueue 阻塞队列,是一个接口,线程安全,不是一个高性能的容器,但是BlockingQueue是一个非常好的**在多个线程中共享数据的容器** 若为空队列,此时有线程尝试读取数据,则此读的线程会等待,直到有另外线程往队列中写入数据,则读的线程就会被唤醒并且读取数据; 若队列已经写满了,则写入的线程就会等待,直到有线程读取数据有空闲空间后才能写入队列; 因此Blocking会引起线程阻塞。 BlockingQueue作为生产者消费者容器很方便。 put()、take() 实现:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue。 ConcurrentLinkedQueue 类似ConcurrentHashMap的高性能Queue,内部使用了大量的无锁操作。offer()、poll() BlockingQueue VS ConcurrentLinkedQueue 在并发编程中我们有时候需要使用线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式:一种是使用**阻塞算法**,另一种是使用**非阻塞算法**。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现。
》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
对于我们的业务来说:线程的创建和销毁与业务无关,只关心是否执行了任务,毕竟创建销毁需要消耗计算资源,因此我们希望将更多的资源用于执行任务而不是用在辅助性质的线程创建和销毁上。
线程池限流
内置线程池的种类(Executors对应的方法)
newFiexedThreadPool 固定大小的线程池
newSingleThreadExecutor 只有一个线程的线程池
newCachedTHreadPool 弹性线程池
newScheduledTHreadPool 定时任务,执行某个线程
newFiexedThreadPool()、newSingleThreadExecutor()、newCachedTHreadPool()三个方法底层都是通过类ThreadPoolExecutor实现的,其典型构造方法(4个)为
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 超过核心线程数的空闲线程最大存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue) // 当线程池满载运行时存放新任务的阻塞队列,newCachedTHreadPool()例外,使用了SynchronousQueue
内置线程池使用:
MyThread task = new MyThread();
ExecutorService es = Executors.newFixedThreadPool(5);
for (int idx = 0; idx < 10; ++idx) {
es.submit(task);
}
ExecutorService类
shutdown()
isShutdown() : boolean
isTerminated() : boolean
submit(Callable task) : Future