Java并发基础总结
线程基本使用
编写线程运行时执行的代码有两种方式:一种是创建Thread子类的一个实例并重写run方法,第二种是创建类的时候实现Runnable接口。当然,实现 Callable 也算是一种方式,Callable和Future结合实现可以实现在执行完任务后获取返回值,而Runnable和Thread方式是无法获取任务执行后的结果的。
public class ThreadMain { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread).start(); new MyThreas2().start(); } }// 第一种方式,实现Runable接口class MyThread implements Runnable { @Override public void run() { System.out.println("MyThread run..."); } }// 第二种方式,继承Thread类,重写run()方法class MyThreas2 extends Thread { @Override public void run() { System.out.println("MyThread2 run..."); } }
一旦线程启动后start()方法会立即返回,而不会等待run()方法执行完毕后返回,就好像run方法是在另外一个cpu上执行一样。
注意:创建并运行一个线程所犯的常见错误是调用线程的run()方法而非start()方法,如下所示:
Thread newThread = new Thread(MyRunnable()); newThread.run(); //should be start();
起初你并不会感觉到有什么不妥,因为run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行run()方法,必须调用新线程的start方法。
Callable和Future结合实现实现在执行完任务后获取返回值:
public static void main(String[] args) { ExecutorService exec = Executors.newSingleThreadExecutor(); Future<String> future = exec.submit(new CallTask()); System.out.println(future.get()); }class CallTask implements Callable { public String call() { return "hello"; } }
给线程设置线程名:
MyTask myTask = new MyTask(); Thread thread = new Thread(myTask, "myTask thread");
thread.start(); System.out.println(thread.getName());
当创建一个线程的时候,可以给线程起一个名字。它有助于我们区分不同的线程。
volatile
在多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是 轻量级的 synchronized ,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小,但是volatile不能保证变量的原子性。
volatile变量进行写操作时(汇编下有lock指令),该lock指令在多核系统下有2个作用:
将当前CPU缓存行写回系统内存。
这个写回操作会引起其他CPU缓存了改地址的数据失效。
多CPU下遵循缓存一致性原则,每个CPU通过嗅探在总线上传播的数据来检查自己的缓存值是否过期了,当发现缓存对应的内存地址被修改,将对应缓存行设置为无效状态,下次对数据操作会从系统内存重新读取。更多volatile知识请点击 深入分析Volatile的实现原理 。
synchronized
在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了。
Java中每一个对象都可以作为锁,当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
对于同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前对象的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
synchronized关键字是不能继承的,也就是说基类中的synchronized方法在子类中默认并不是synchronized的。当线程试图访问同步代码块时,必须先获得锁,退出或抛出异常时释放锁。Java中每个对象都可以作为锁,那么锁存在哪里呢?锁存在Java对象头中,如果对象是数组类型,则虚拟机用3个word(字宽) 存储对象头,如果对象是非数组类型,则用2字宽存储对象头。更多synchronized知识请点击 Java SE1.6中的Synchronized 。
线程池
线程池负责管理工作线程,包含一个等待执行的任务队列。线程池的任务队列是一个Runnable集合,工作线程负责从任务队列中取出并执行Runnable对象。
ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { executor.execute(new MyThread2()); }executor.shutdown();
Java通过Executors提供了4种线程池:
newCachedThreadPool:创建一个可缓存线程池,对于新任务如果没有空闲线程就新创建一个线程,如果空闲线程超过一定时间就会回收。
newFixedThreadPool:创建一个固定数量线程的线程池。
newSingleThreadExecutor:创建一个单线程的线程池,该线程池只用一个线程来执行任务,保证所有任务都按照FIFO顺序执行。
newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
以上几种线程池底层都是调用ThreadPoolExecutor来创建线程池的。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了*的任务队列这个参数就没什么效果。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。,可以选择的阻塞队列有以下几种:
workQueue(任务队列):用于保存等待执行的任务的阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
当提交新任务到线程池时,其处理流程如下:
1.先判断基本线程池是否已满?没满则创建一个工作线程来执行任务,满了则进入下个流程。
2.其次判断工作队列是否已满?没满则提交新任务到工作队列中,满了则进入下个流程。
3.最后判断整个线程池是否已满?没满则创建一个新的工作线程来执行任务,满了则交给饱和策略来处理这个任务。